comparison tests/browser/unit/test_chat.py @ 1637:29dd52585984 default tip

tests (browser/unit/chat): Add tests for forwarding, rich editing and extra recipients: fix 461
author Goffi <goffi@goffi.org>
date Fri, 04 Jul 2025 18:15:48 +0200
parents
children
comparison
equal deleted inserted replaced
1636:691f6c8afb31 1637:29dd52585984
1 #!/usr/bin/env python3
2
3 # Libervia Web
4 # Copyright (C) 2009-2025 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 from collections import defaultdict
20 import sys
21 import pytest
22 from unittest.mock import MagicMock, AsyncMock, patch
23
24 # We mock all non-stdlib modules, so the module can be imported.
25 sys.modules["bridge"] = MagicMock(AsyncBridge=AsyncMock)
26 sys.modules["cache"] = MagicMock(cache=MagicMock())
27 sys.modules["dialog"] = MagicMock(Confirm=MagicMock())
28 sys.modules["errors"] = MagicMock()
29 sys.modules["file_uploader"] = MagicMock(FileUploader=MagicMock())
30 sys.modules["javascript"] = MagicMock()
31 sys.modules["jid"] = MagicMock(JID=MagicMock())
32 sys.modules["jid_search"] = MagicMock(JidSearch=MagicMock())
33 sys.modules["loading"] = MagicMock()
34 sys.modules["popup"] = MagicMock(create_popup=MagicMock())
35 sys.modules["js_modules"] = MagicMock()
36 sys.modules["js_modules.emoji_picker_element"] = MagicMock()
37 sys.modules["js_modules.tippy_js"] = MagicMock()
38 sys.modules["js_modules.quill"] = MagicMock(Quill=MagicMock())
39 sys.modules["tools"] = MagicMock()
40 sys.modules["interpreter"] = MagicMock(Inspector=MagicMock())
41 sys.modules["components"] = MagicMock(init_collapsible_cards=MagicMock())
42
43
44 class FakeElement:
45 """FakeElement, with methods to similuate child appending and some attributes."""
46
47 scrollHeight = 0
48 scrollTop = 0
49 clientHeight = 0
50
51 def __init__(self):
52 self.attrs = defaultdict(MagicMock)
53 self.children = []
54
55 def __getattr__(self, attr) -> MagicMock:
56 return self.attrs[attr]
57
58 def __le__(self, child):
59 child.parent = self
60 self.children.append(child)
61 return True
62
63
64 class FakeDocument:
65
66 def __init__(self):
67 self.attrs = defaultdict(MagicMock)
68 self.items = defaultdict(FakeElement)
69
70 def __getattr__(self, attr) -> MagicMock:
71 return self.attrs[attr]
72
73 def __getitem__(self, item) -> FakeElement:
74 return self.items[item]
75
76 def __setitem__(self, key, item) -> None:
77 self.items[key] = item
78
79
80 document = FakeDocument()
81 browser = sys.modules["browser"] = MagicMock(
82 aio=MagicMock(),
83 console=MagicMock(),
84 document=document,
85 window=MagicMock(),
86 navigator=MagicMock(mediaDevices=MagicMock(getDisplayMedia=MagicMock())),
87 )
88
89
90 class FakeTemplate(MagicMock):
91
92 def get_elt(self, *args, **kwargs):
93 return FakeElement()
94
95
96 sys.modules["template"] = MagicMock(Template=FakeTemplate)
97
98 # We can now import the actual module
99 from libervia.web.pages.chat import _browser as chat_module
100
101
102 class TestLiberviaWebChat:
103 @pytest.fixture
104 def chat(self) -> chat_module.LiberviaWebChat:
105 """Fixture to create a LiberviaWebChat instance with mocks."""
106 chat = chat_module.LiberviaWebChat()
107
108 chat.rich_editor = MagicMock(
109 getSelection=MagicMock(return_value=MagicMock(index=0, length=1)),
110 getFormat=MagicMock(return_value={}),
111 format=MagicMock(),
112 getContents=MagicMock(return_value={"ops": []}),
113 )
114
115 return chat
116
117 @pytest.fixture
118 def bridge(self, monkeypatch) -> AsyncMock:
119 bridge = AsyncMock()
120 monkeypatch.setattr(chat_module, "bridge", bridge)
121 return bridge
122
123 @pytest.fixture
124 def message_elt(self) -> MagicMock:
125 """Fixture to create a mock message element."""
126 msg_mock = MagicMock()
127 msg_mock.__getitem__.side_effect = lambda key: "msg123" if key == "id" else None
128 return msg_mock
129
130 @pytest.mark.asyncio
131 async def test_forward_action(self, chat, bridge, message_elt) -> None:
132 """Forward action launches bridge.message_forward with correct parameters."""
133 message_elt.classList.contains.return_value = True
134 with patch("browser.window.prompt", return_value="recipient@example.org"):
135 await chat.on_action_forward(None, message_elt)
136
137 bridge.message_forward.assert_awaited_once_with("msg123", "recipient@example.org")
138
139 @pytest.mark.asyncio
140 async def test_rich_editor_actions(self, chat) -> None:
141 """Rich editor formatting actions modify editor state correctly."""
142 test_cases = [
143 ("bold", ["bold", True]),
144 ("italic", ["italic", True]),
145 ("underline", ["underline", True]),
146 ("link", ["link", "https://example.org"]),
147 ]
148
149 for action, expected_call in test_cases:
150 evt = MagicMock()
151 evt.currentTarget.dataset = {"action": action}
152
153 if action == "link":
154 with patch("browser.window.prompt", return_value="https://example.org"):
155 chat.on_rich_action(evt)
156 else:
157 chat.on_rich_action(evt)
158
159 chat.rich_editor.format.assert_called_with(*expected_call)
160
161 @pytest.mark.asyncio
162 async def test_extra_recipients(self, monkeypatch, chat) -> None:
163 """Adding extra recipient calls "insertBefore"."""
164 toolbar_elt = MagicMock()
165 monkeypatch.setitem(document, "rich-edit-toolbar", toolbar_elt)
166
167 await chat.on_action_extra_recipients(selected="to")
168 await chat.on_action_extra_recipients(selected="cc")
169
170 assert toolbar_elt.parent.insertBefore.call_count == 2