/
/
/
1"""Tests for the dashboard-casting helper in the Chromecast provider."""
2
3from __future__ import annotations
4
5from collections.abc import Callable
6from typing import Any
7from unittest.mock import MagicMock
8
9import pytest
10
11from music_assistant.providers.chromecast.constants import DASHBOARD_NAMESPACE, MASS_APP_ID
12from music_assistant.providers.chromecast.helpers import send_hide_dashboard, send_show_dashboard
13
14
15def test_send_show_dashboard_happy_path() -> None:
16 """Launches the receiver app and sends show_dashboard once the namespace is available."""
17 chromecast = MagicMock()
18 chromecast.name = "Living Room TV"
19 chromecast.socket_client.app_namespaces = {DASHBOARD_NAMESPACE}
20
21 def _launch_app(
22 app_id: str, *, callback_function: Callable[[bool, Any], None], **_kwargs: Any
23 ) -> None:
24 assert app_id == MASS_APP_ID
25 callback_function(True, None)
26
27 chromecast.socket_client.receiver_controller.launch_app.side_effect = _launch_app
28
29 send_show_dashboard(chromecast, "https://mass.example.com?path=%2Fparty")
30
31 chromecast.socket_client.send_app_message.assert_called_once_with(
32 DASHBOARD_NAMESPACE,
33 {"type": "show_dashboard", "url": "https://mass.example.com?path=%2Fparty"},
34 )
35 # a running session is reused by default, so the device does not chime again
36 launch_app = chromecast.socket_client.receiver_controller.launch_app
37 assert launch_app.call_args.kwargs["force_launch"] is False
38
39
40def test_send_show_dashboard_forwards_force_launch() -> None:
41 """A forced launch starts a new session even when the receiver reports the app running."""
42 chromecast = MagicMock()
43 chromecast.name = "Living Room TV"
44 chromecast.socket_client.app_namespaces = {DASHBOARD_NAMESPACE}
45
46 def _launch_app(
47 _app_id: str, *, callback_function: Callable[[bool, Any], None], **_kwargs: Any
48 ) -> None:
49 callback_function(True, None)
50
51 chromecast.socket_client.receiver_controller.launch_app.side_effect = _launch_app
52
53 send_show_dashboard(chromecast, "https://mass.example.com?path=%2Fparty", force_launch=True)
54
55 launch_app = chromecast.socket_client.receiver_controller.launch_app
56 assert launch_app.call_args.kwargs["force_launch"] is True
57
58
59def test_send_show_dashboard_launch_failure_raises() -> None:
60 """A failed launch callback raises TimeoutError without sending a message."""
61 chromecast = MagicMock()
62 chromecast.name = "Living Room TV"
63
64 def _launch_app(
65 _app_id: str, *, callback_function: Callable[[bool, Any], None], **_kwargs: Any
66 ) -> None:
67 callback_function(False, None)
68
69 chromecast.socket_client.receiver_controller.launch_app.side_effect = _launch_app
70
71 with pytest.raises(TimeoutError):
72 send_show_dashboard(chromecast, "https://mass.example.com?path=%2Fparty")
73
74 chromecast.socket_client.send_app_message.assert_not_called()
75
76
77def test_send_show_dashboard_namespace_never_appears_raises(
78 monkeypatch: pytest.MonkeyPatch,
79) -> None:
80 """Raises TimeoutError if the dashboard namespace never shows up in app_namespaces."""
81 chromecast = MagicMock()
82 chromecast.name = "Living Room TV"
83 chromecast.socket_client.app_namespaces = set()
84
85 def _launch_app(
86 _app_id: str, *, callback_function: Callable[[bool, Any], None], **_kwargs: Any
87 ) -> None:
88 callback_function(True, None)
89
90 chromecast.socket_client.receiver_controller.launch_app.side_effect = _launch_app
91
92 # fake clock that advances on every call, so the poll loop's deadline check
93 # trips deterministically without any real sleeping
94 fake_clock = [0.0]
95
96 def _fake_monotonic() -> float:
97 fake_clock[0] += 1.0
98 return fake_clock[0]
99
100 monkeypatch.setattr(
101 "music_assistant.providers.chromecast.helpers.time.monotonic", _fake_monotonic
102 )
103 monkeypatch.setattr("music_assistant.providers.chromecast.helpers.time.sleep", lambda _s: None)
104
105 with pytest.raises(TimeoutError):
106 send_show_dashboard(chromecast, "https://mass.example.com?path=%2Fparty", timeout=5.0)
107
108 chromecast.socket_client.send_app_message.assert_not_called()
109
110
111def test_send_hide_dashboard_sends_message_when_app_and_namespace_match() -> None:
112 """Sends the hide_dashboard message when our app is running with the dashboard namespace."""
113 chromecast = MagicMock()
114 chromecast.app_id = MASS_APP_ID
115 chromecast.socket_client.app_namespaces = {DASHBOARD_NAMESPACE}
116
117 result = send_hide_dashboard(chromecast)
118
119 assert result is True
120 chromecast.socket_client.send_app_message.assert_called_once_with(
121 DASHBOARD_NAMESPACE, {"type": "hide_dashboard"}
122 )
123 chromecast.socket_client.receiver_controller.launch_app.assert_not_called()
124
125
126def test_send_hide_dashboard_returns_false_when_app_differs() -> None:
127 """Nothing is sent when the receiver is not running our app."""
128 chromecast = MagicMock()
129 chromecast.app_id = "some-other-app"
130 chromecast.socket_client.app_namespaces = {DASHBOARD_NAMESPACE}
131
132 result = send_hide_dashboard(chromecast)
133
134 assert result is False
135 chromecast.socket_client.send_app_message.assert_not_called()
136
137
138def test_send_hide_dashboard_returns_false_when_namespace_missing() -> None:
139 """Nothing is sent when our app is running but the dashboard namespace isn't up yet."""
140 chromecast = MagicMock()
141 chromecast.app_id = MASS_APP_ID
142 chromecast.socket_client.app_namespaces = set()
143
144 result = send_hide_dashboard(chromecast)
145
146 assert result is False
147 chromecast.socket_client.send_app_message.assert_not_called()
148