changeset 1643:0e11dbbcefe7

tests (browser/unit): forums; Add tests for "Forum" feature. rel 463
author Goffi <goffi@goffi.org>
date Sat, 06 Sep 2025 16:30:48 +0200
parents c03297bb8d19
children 8a09eea9003f
files tests/browser/unit/test_forums.py
diffstat 1 files changed, 217 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /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 <http://www.gnu.org/licenses/>.
+
+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 = "<p>test content</p>"
+        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 = "<p>test message</p>"
+        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 = "<p>test message</p>"
+        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()