comparison tests/browser/unit/test_calls.py @ 1551:00d04f51787e

tests: add `calls` unit tests: fix 423
author Goffi <goffi@goffi.org>
date Wed, 09 Aug 2023 00:22:18 +0200
parents
children
comparison
equal deleted inserted replaced
1550:4b0464b2a12b 1551:00d04f51787e
1 #!/usr/bin/env python3
2
3 # Libervia Web
4 # Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org)
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 import sys
20 import pytest
21 from unittest.mock import MagicMock, AsyncMock
22
23 # Mock all non-stdlib modules
24
25
26 class CancelError(Exception):
27 pass
28
29
30 sys.modules["bridge"] = MagicMock(AsyncBridge=AsyncMock)
31 browser = sys.modules["browser"] = MagicMock(
32 aio=MagicMock(), console=MagicMock(), document=MagicMock(), window=MagicMock()
33 )
34 sys.modules["cache"] = MagicMock(cache=MagicMock())
35 dialog = sys.modules["dialog"] = MagicMock()
36 dialog.CancelError = CancelError
37 confirm_mock = dialog.Confirm.return_value = MagicMock()
38 confirm_ashow = confirm_mock.ashow = AsyncMock()
39 sys.modules["jid"] = MagicMock(JID=MagicMock)
40 sys.modules["jid_search"] = MagicMock(JidSearch=MagicMock())
41 sys.modules["loading"] = MagicMock()
42 sys.modules["template"] = MagicMock(Template=MagicMock())
43 sys.modules["webrtc"] = MagicMock(WebRTC=MagicMock())
44
45 # After mocking the modules, import the actual module
46 from libervia.web.pages.calls import _browser as calls
47
48 calls.cache.fill_identities = AsyncMock()
49
50
51 class TestCallUI:
52 @pytest.fixture
53 def call_ui(self):
54 ui = calls.CallUI()
55 ui.webrtc.sid = None
56 ui.call_status_tpl = MagicMock()
57 ui.call_status_wrapper_elt = MagicMock()
58 ui.call_box_elt = MagicMock()
59 ui._update_call_button = MagicMock()
60 ui.audio_player_elt = MagicMock()
61 ui.switch_mode = MagicMock()
62 ui.incoming_call_dialog_elt = MagicMock()
63
64 ui.call_status_wrapper_elt.appended_elements = []
65
66 # Mock the `<=` operator for call_status_wrapper_elt
67 def mock_append_child(parent, child):
68 # Store the child for later assertion
69 parent.appended_elements.append(child)
70 return child
71
72 ui.call_status_wrapper_elt.__le__ = mock_append_child
73
74 return ui
75
76 def test_status(self, call_ui):
77 """Allowed values are set"""
78 # Set initial status
79 call_ui._status = None
80 assert call_ui.status == None
81
82 # Change status
83 call_ui.status = "dialing"
84 assert call_ui.status == "dialing"
85
86 def test_disallowed_status(self, call_ui):
87 """Not allowed status raises an Exception"""
88 with pytest.raises(Exception, match="this status is not allowed"):
89 call_ui.status = "disallowed_status"
90
91 def test_status_element_update(self, call_ui):
92 """A status change update the status element"""
93 call_ui._callee = "test_callee"
94 mock_cache = calls.cache
95 mock_cache.identities = {"test_callee": {"nicknames": ["Test Callee Nickname"]}}
96
97 status_elt_mock = MagicMock()
98 call_ui.call_status_tpl.get_elt.return_value = status_elt_mock
99
100 call_ui.status = "dialing"
101 call_ui.call_status_tpl.get_elt.assert_called_with(
102 {
103 "entity": "test_callee",
104 "status": "dialing",
105 "name": "Test Callee Nickname",
106 }
107 )
108 assert call_ui.call_status_wrapper_elt.appended_elements[-1] == status_elt_mock
109
110 def test_status_element_updates_with_unknown_callee(self, call_ui):
111 """A status change update the element even if callee is not known"""
112 call_ui._callee = "test_callee"
113 mock_cache = calls.cache
114 mock_cache.identities = {}
115
116 call_ui.status = "dialing"
117 call_ui.call_status_tpl.get_elt.assert_called_with(
118 {"entity": "test_callee", "status": "dialing", "name": "test_callee"}
119 )
120
121 def test_call_mode(self, call_ui):
122 call_ui.call_mode = calls.AUDIO
123 assert call_ui.call_mode == calls.AUDIO
124 call_ui._update_call_button.assert_called_once()
125 call_ui.call_box_elt.select.assert_called_with(".is-video-only")
126
127 call_ui.call_mode = calls.VIDEO
128 assert call_ui.call_mode == calls.VIDEO
129 call_ui.call_box_elt.select.assert_called_with(".is-video-only")
130
131 def test_call_mode_invalid_mode(self, call_ui):
132 with pytest.raises(ValueError, match="Invalid call mode"):
133 call_ui.call_mode = "invalid_mode"
134
135 def test_switch_call_mode(self, call_ui):
136 call_ui._call_mode = calls.AUDIO
137 call_ui.switch_call_mode(None)
138 assert call_ui.call_mode == calls.VIDEO
139
140 call_ui._call_mode = calls.VIDEO
141 call_ui.switch_call_mode(None)
142 assert call_ui.call_mode == calls.AUDIO
143
144 @pytest.mark.asyncio
145 async def test_ignore_new_call_when_call_in_progress(self, call_ui):
146 """Ignoring new incoming call when a call is already in progress."""
147 call_ui.sid = "some_sid"
148 await call_ui.on_action_new(
149 {
150 "from_jid": "test@example.org",
151 "session_id": "session123",
152 "sub_type": "audio",
153 },
154 "action_id",
155 )
156 call_ui.audio_player_elt.play.assert_not_called()
157
158 @pytest.mark.asyncio
159 async def test_action_new_fill_identities(self, call_ui):
160 """Filling identities from cache with the correct JID."""
161 calls.cache.identities = {"test@example.org": {"nicknames": ["Test User"]}}
162
163 await call_ui.on_action_new(
164 {
165 "from_jid": "test@example.org",
166 "session_id": "session123",
167 "sub_type": "audio",
168 },
169 "action_id",
170 )
171 calls.cache.fill_identities.assert_called_once_with(["test@example.org"])
172
173 @pytest.mark.asyncio
174 async def test_incoming_call_triggers_audio(self, call_ui):
175 """Incoming call triggering the play action on the audio element."""
176 await call_ui.on_action_new(
177 {
178 "from_jid": "test@example.org",
179 "session_id": "session123",
180 "sub_type": "audio",
181 },
182 "action_id",
183 )
184 call_ui.audio_player_elt.play.assert_called_once()
185
186 @pytest.mark.asyncio
187 async def test_user_accepts_call(self, call_ui):
188 """User accepting the incoming call."""
189 confirm_ashow.return_value = True
190 await call_ui.on_action_new(
191 {
192 "from_jid": "test@example.org",
193 "session_id": "session123",
194 "sub_type": "video",
195 },
196 "action_id",
197 )
198 call_ui.switch_mode.assert_called_once_with("call")
199
200 @pytest.mark.asyncio
201 async def test_user_rejects_call(self, call_ui):
202 """User rejecting the incoming call."""
203 confirm_ashow.return_value = False
204 await call_ui.on_action_new(
205 {
206 "from_jid": "test@example.org",
207 "session_id": "session123",
208 "sub_type": "video",
209 },
210 "action_id",
211 )
212 call_ui.switch_mode.assert_not_called()
213
214 @pytest.mark.asyncio
215 async def test_call_gets_cancelled(self, call_ui):
216 """Incoming call gets cancelled."""
217 confirm_ashow.side_effect = dialog.CancelError
218 await call_ui.on_action_new(
219 {
220 "from_jid": "test@example.org",
221 "session_id": "session123",
222 "sub_type": "audio",
223 },
224 "action_id",
225 )
226 assert call_ui.sid is None
227 call_ui.switch_mode.assert_not_called()
228
229 def test_toggle_fullscreen_enter(self, call_ui, monkeypatch):
230 """Entering fullscreen mode for video elements."""
231 monkeypatch.setattr(browser.document, "fullscreenElement", None)
232 monkeypatch.setattr(call_ui.call_box_elt, "requestFullscreen", MagicMock())
233
234 btn_mock_show = MagicMock()
235 monkeypatch.setattr("dialog.notification.show", btn_mock_show)
236 btn_mock_add = MagicMock()
237 btn_mock_remove = MagicMock()
238
239 full_screen_mock = MagicMock(classList=MagicMock(add=btn_mock_add))
240 exit_full_screen_mock = MagicMock(classList=MagicMock(remove=btn_mock_remove))
241
242 mock_document = {
243 "full_screen_btn": full_screen_mock,
244 "exit_full_screen_btn": exit_full_screen_mock,
245 }
246
247 getitem_mock = MagicMock()
248 monkeypatch.setattr(browser.document, "__getitem__", getitem_mock)
249 getitem_mock.side_effect = mock_document.__getitem__
250
251 call_ui.toggle_fullscreen(True)
252
253 call_ui.call_box_elt.requestFullscreen.assert_called_once()
254 btn_mock_add.assert_called_with("is-hidden")
255 btn_mock_remove.assert_called_with("is-hidden")
256 btn_mock_show.assert_not_called()
257
258 def test_toggle_fullscreen_exit(self, call_ui, monkeypatch):
259 """Exiting fullscreen mode for video elements."""
260 # As it's not None, we're in fullscreen
261 monkeypatch.setattr(browser.document, "fullscreenElement", "mocked_value")
262 monkeypatch.setattr(browser.document, "exitFullscreen", MagicMock())
263
264 btn_mock_show = MagicMock()
265 monkeypatch.setattr("dialog.notification.show", btn_mock_show)
266 btn_mock_add = MagicMock()
267 btn_mock_remove = MagicMock()
268
269 full_screen_mock = MagicMock(classList=MagicMock(remove=btn_mock_remove))
270 exit_full_screen_mock = MagicMock(classList=MagicMock(add=btn_mock_add))
271
272 mock_document = {
273 "full_screen_btn": full_screen_mock,
274 "exit_full_screen_btn": exit_full_screen_mock,
275 }
276
277 getitem_mock = MagicMock()
278 monkeypatch.setattr(browser.document, "__getitem__", getitem_mock)
279 getitem_mock.side_effect = mock_document.__getitem__
280
281 call_ui.toggle_fullscreen(False)
282
283 browser.document.exitFullscreen.assert_called_once()
284 btn_mock_add.assert_called_with("is-hidden")
285 btn_mock_remove.assert_called_with("is-hidden")
286 btn_mock_show.assert_not_called()
287
288 def test_toggle_audio_mute(self, call_ui, monkeypatch):
289 """Toggle audio mute."""
290 mock_toggle_audio_mute = MagicMock(
291 return_value=True
292 )
293 monkeypatch.setattr(call_ui.webrtc, "toggle_audio_mute", mock_toggle_audio_mute)
294
295 btn_mock_show = MagicMock()
296 monkeypatch.setattr("dialog.notification.show", btn_mock_show)
297
298 evt_mock = MagicMock(currentTarget=MagicMock(classList=MagicMock()))
299
300 call_ui.toggle_audio_mute(evt_mock)
301
302 mock_toggle_audio_mute.assert_called_once()
303 evt_mock.currentTarget.classList.remove.assert_called_with("is-success")
304 evt_mock.currentTarget.classList.add.assert_called_with("muted", "is-warning")
305 btn_mock_show.assert_called_once_with("audio is now muted", level="info", delay=2)
306
307 def test_toggle_video_mute(self, call_ui, monkeypatch):
308 """Toggle video mute."""
309 mock_toggle_video_mute = MagicMock(
310 return_value=True
311 )
312 monkeypatch.setattr(call_ui.webrtc, "toggle_video_mute", mock_toggle_video_mute)
313
314 btn_mock_show = MagicMock()
315 monkeypatch.setattr("dialog.notification.show", btn_mock_show)
316
317 evt_mock = MagicMock(currentTarget=MagicMock(classList=MagicMock()))
318
319 call_ui.toggle_video_mute(evt_mock)
320
321 mock_toggle_video_mute.assert_called_once()
322 evt_mock.currentTarget.classList.remove.assert_called_with("is-success")
323 evt_mock.currentTarget.classList.add.assert_called_with("muted", "is-warning")
324 btn_mock_show.assert_called_once_with("video is now muted", level="info", delay=2)