# HG changeset patch # User Goffi # Date 1757169048 -7200 # Node ID 0e11dbbcefe711ee17c57615f10bcabee3188cf2 # Parent c03297bb8d19d07250fd81908b6aa6813ed798f4 tests (browser/unit): forums; Add tests for "Forum" feature. rel 463 diff -r c03297bb8d19 -r 0e11dbbcefe7 tests/browser/unit/test_forums.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/browser/unit/test_forums.py Sat Sep 06 16:30:48 2025 +0200 @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 + +# Libervia Web +# Copyright (C) 2009-2025 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from collections import defaultdict +import sys +from libervia.backend.tools.common import data_format +import pytest +from unittest.mock import MagicMock, AsyncMock, patch + +# We mock all non-stdlib modules, so the module can be imported. +sys.modules["bridge"] = MagicMock(AsyncBridge=AsyncMock) +sys.modules["cache"] = MagicMock(cache=MagicMock()) +sys.modules["dialog"] = MagicMock() +sys.modules["editor"] = MagicMock(TagsEditor=MagicMock()) +sys.modules["file_uploader"] = MagicMock(FileUploader=MagicMock()) +sys.modules["javascript"] = MagicMock() +sys.modules["jid"] = MagicMock(JID=MagicMock()) +sys.modules["loading"] = MagicMock() +sys.modules["popup"] = MagicMock(create_popup=MagicMock()) +sys.modules["js_modules"] = MagicMock() +sys.modules["js_modules.quill"] = MagicMock(Quill=MagicMock()) +sys.modules["js_modules.quill_mention"] = MagicMock() +sys.modules["template"] = MagicMock() +sys.modules["identities"] = MagicMock() + + +class FakeElement: + + def __init__(self): + self.attrs = defaultdict(MagicMock) + + def __getattr__(self, attr) -> MagicMock: + return self.attrs[attr] + + +class FakeDocument: + + def __init__(self): + self.attrs = defaultdict(MagicMock) + self.items = defaultdict(FakeElement) + + def __getattr__(self, attr) -> MagicMock: + return self.attrs[attr] + + def __getitem__(self, item) -> FakeElement: + return self.items[item] + + def __setitem__(self, key, item) -> None: + self.items[key] = item + + +class FakeEvent: + """Fake event object with preventDefault and stopPropagation methods.""" + + def __init__(self): + self.defaultPrevented = False + self.currentTarget = None + + def preventDefault(self): + self.defaultPrevented = True + + def stopPropagation(self): + pass + + +document = FakeDocument() +browser = sys.modules["browser"] = MagicMock( + aio=MagicMock(), + console=MagicMock(), + document=document, + window=MagicMock( + service="test_service", + node="test_node", + subscribed=True, + location=MagicMock(href="http://example.com/test"), + URL=MagicMock( + new=MagicMock( + return_value=MagicMock(href="http://example.com/test?search=test") + ) + ), + RegExp=MagicMock(), + ), +) + + +class FakeTemplate(MagicMock): + + def get_elt(self, *args, **kwargs): + return FakeElement() + + +sys.modules["template"] = MagicMock(Template=FakeTemplate) + +# We can now import the actual module +from libervia.web.pages.forums.view import _browser as forums_module + + +class TestLiberviaForumView: + @pytest.fixture + def forum_view(self) -> forums_module.LiberviaForumView: + """Fixture to create a LiberviaForumView instance with mocks.""" + # Create a mock for the Quill editor + quill_mock = MagicMock() + quill_mock.getSemanticHTML.return_value = "

test content

" + quill_mock.getContents.return_value = {"ops": []} + quill_mock.getModule.return_value = MagicMock(container=MagicMock()) + + # Mock the Quill constructor + with patch("js_modules.quill.Quill.new", return_value=quill_mock): + forum_view = forums_module.LiberviaForumView() + + # Mock the tag editor + forum_view.tag_editor = MagicMock(current_tags=set()) + + return forum_view + + @pytest.fixture + def bridge(self, monkeypatch) -> AsyncMock: + bridge = AsyncMock() + monkeypatch.setattr(forums_module, "bridge", bridge) + return bridge + + @pytest.mark.asyncio + async def test_toggle_subscription_subscribed(self, forum_view, bridge) -> None: + """Toggle subscription calls unsubscribe when already subscribed.""" + with patch.object(browser.window, "subscribed", True): + button = FakeElement() + document["subscribe-button"] = button + + await forum_view.toggle_subscription() + + bridge.ps_unsubscribe.assert_awaited_once_with("test_service", "test_node") + + @pytest.mark.asyncio + async def test_toggle_subscription_not_subscribed(self, forum_view, bridge) -> None: + """Toggle subscription calls subscribe when not subscribed.""" + with patch.object(browser.window, "subscribed", False): + button = FakeElement() + document["subscribe-button"] = button + + await forum_view.toggle_subscription() + + bridge.ps_subscribe.assert_awaited_once_with("test_service", "test_node", "") + + @pytest.mark.asyncio + async def test_send_message_with_mentions(self, forum_view, bridge) -> None: + """Sending message with mentions updates ``mb_data``.""" + # Mock Quill content with mentions + quill_mock = MagicMock() + quill_mock.getSemanticHTML.return_value = "

test message

" + quill_mock.getContents.return_value = { + "ops": [{"insert": {"mention": {"id": "user@example.com"}}}] + } + forum_view.message_editor = quill_mock + + forum_view.tag_editor.current_tags = set() + + await forum_view.send_message() + + bridge.mb_send.assert_awaited() + mb_data = data_format.deserialise(bridge.mb_send.call_args[0][0]) + assert mb_data["mentions"] == ["user@example.com"] + + @pytest.mark.asyncio + async def test_send_message_error_handling(self, forum_view, bridge) -> None: + """Error in ``bridge.mb_send`` is reported.""" + bridge.mb_send.side_effect = Exception("Send failed") + + quill_mock = MagicMock() + quill_mock.getSemanticHTML.return_value = "

test message

" + quill_mock.getContents.return_value = {"ops": []} + forum_view.message_editor = quill_mock + + forum_view.tag_editor.current_tags = set() + + with patch("dialog.notification.show") as mock_show: + await forum_view.send_message() + + # Verify that error notification was shown + mock_show.assert_called_with("Can't send message: Send failed.", "error") + + def test_on_add_tag(self, forum_view) -> None: + """``tags_container`` become visible when ``on_add_tag``is called.""" + tags_container = FakeElement() + tags_container.classList = MagicMock() + document["tags_container"] = tags_container + + forum_view.on_add_tag(FakeEvent()) + + # Verify that the class list was modified + tags_container.classList.remove.assert_called_once_with("is-hidden") + + def test_on_add_attachment(self, forum_view) -> None: + """``file_input`` click is called from ``on_add_attachment``.""" + file_input = FakeElement() + document["forum-file-input"] = file_input + + file_input.click = MagicMock() + + forum_view.on_add_attachment(FakeEvent()) + + file_input.click.assert_called_once()