1551
|
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) |