/
/
/
1"""Tests for hiding the reconfigure action when a setup flow has nothing to offer."""
2
3from __future__ import annotations
4
5from contextlib import contextmanager
6from types import SimpleNamespace
7from typing import TYPE_CHECKING
8from unittest.mock import MagicMock, PropertyMock, patch
9
10from music_assistant.models.player import Player
11
12if TYPE_CHECKING:
13 from collections.abc import Iterator
14
15 from music_assistant.models.setup_flow import SetupSession
16
17
18class _FlowPlayer(Player):
19 """A player implementing a flow, so ``implements_setup_flow`` is True."""
20
21 async def run_setup_flow(self, session: SetupSession) -> None:
22 """Never runs; its presence is what marks the class as implementing a flow."""
23
24
25class _UnavailableFlowPlayer(_FlowPlayer):
26 """A player whose flow currently has nothing left to set up."""
27
28 @property
29 def setup_flow_available(self) -> bool:
30 """Report the flow as having nothing to offer."""
31 return False
32
33
34def test_a_player_implementing_a_flow_offers_it_by_default() -> None:
35 """The base override answers True, so existing providers are unaffected."""
36 player = _bare(_FlowPlayer)
37 assert player.implements_setup_flow is True
38 assert player.has_setup_flow is True
39
40
41def test_a_player_can_report_its_flow_as_unavailable() -> None:
42 """Reconfigure is hidden rather than opening a flow that can only abort."""
43 player = _bare(_UnavailableFlowPlayer)
44 assert player.implements_setup_flow is True
45 assert player.has_setup_flow is False
46
47
48def test_an_unavailable_child_flow_is_not_offered_through_its_wrapper() -> None:
49 """A wrapper delegates to its protocol child, so it must respect the child's answer."""
50 with _wrapper_over(_bare(_UnavailableFlowPlayer)) as wrapper:
51 assert wrapper.has_setup_flow is False
52
53
54def test_an_available_child_flow_is_offered_through_its_wrapper() -> None:
55 """The delegation still works for a child that does have something to set up."""
56 with _wrapper_over(_bare(_FlowPlayer)) as wrapper:
57 assert wrapper.has_setup_flow is True
58
59
60def _bare(cls: type[Player]) -> Player:
61 """Construct a player without the full provider/mass wiring these reads do not need."""
62 player = object.__new__(cls)
63 player._cache = {}
64 return player
65
66
67@contextmanager
68def _wrapper_over(child: Player) -> Iterator[Player]:
69 """
70 Yield a player wrapping ``child`` as its single non-native output protocol.
71
72 ``output_protocols`` derives its links from live config and player state, which is far
73 more wiring than these reads need, so it is patched down to the one link under test.
74 """
75 wrapper = object.__new__(Player)
76 wrapper._cache = {}
77 mass = MagicMock()
78 mass.players.get_player = MagicMock(return_value=child)
79 wrapper.mass = mass
80 links = [SimpleNamespace(is_native=False, output_protocol_id="child-1")]
81 with patch.object(Player, "output_protocols", PropertyMock(return_value=links)):
82 yield wrapper
83