view 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
line wrap: on
line source

#!/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
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(Confirm=MagicMock())
sys.modules["errors"] = MagicMock()
sys.modules["file_uploader"] = MagicMock(FileUploader=MagicMock())
sys.modules["javascript"] = MagicMock()
sys.modules["jid"] = MagicMock(JID=MagicMock())
sys.modules["jid_search"] = MagicMock(JidSearch=MagicMock())
sys.modules["loading"] = MagicMock()
sys.modules["popup"] = MagicMock(create_popup=MagicMock())
sys.modules["js_modules"] = MagicMock()
sys.modules["js_modules.emoji_picker_element"] = MagicMock()
sys.modules["js_modules.tippy_js"] = MagicMock()
sys.modules["js_modules.quill"] = MagicMock(Quill=MagicMock())
sys.modules["tools"] = MagicMock()
sys.modules["interpreter"] = MagicMock(Inspector=MagicMock())
sys.modules["components"] = MagicMock(init_collapsible_cards=MagicMock())


class FakeElement:
    """FakeElement, with methods to similuate child appending and some attributes."""

    scrollHeight = 0
    scrollTop = 0
    clientHeight = 0

    def __init__(self):
        self.attrs = defaultdict(MagicMock)
        self.children = []

    def __getattr__(self, attr) -> MagicMock:
        return self.attrs[attr]

    def __le__(self, child):
        child.parent = self
        self.children.append(child)
        return True


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


document = FakeDocument()
browser = sys.modules["browser"] = MagicMock(
    aio=MagicMock(),
    console=MagicMock(),
    document=document,
    window=MagicMock(),
    navigator=MagicMock(mediaDevices=MagicMock(getDisplayMedia=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.chat import _browser as chat_module


class TestLiberviaWebChat:
    @pytest.fixture
    def chat(self) -> chat_module.LiberviaWebChat:
        """Fixture to create a LiberviaWebChat instance with mocks."""
        chat = chat_module.LiberviaWebChat()

        chat.rich_editor = MagicMock(
            getSelection=MagicMock(return_value=MagicMock(index=0, length=1)),
            getFormat=MagicMock(return_value={}),
            format=MagicMock(),
            getContents=MagicMock(return_value={"ops": []}),
        )

        return chat

    @pytest.fixture
    def bridge(self, monkeypatch) -> AsyncMock:
        bridge = AsyncMock()
        monkeypatch.setattr(chat_module, "bridge", bridge)
        return bridge

    @pytest.fixture
    def message_elt(self) -> MagicMock:
        """Fixture to create a mock message element."""
        msg_mock = MagicMock()
        msg_mock.__getitem__.side_effect = lambda key: "msg123" if key == "id" else None
        return msg_mock

    @pytest.mark.asyncio
    async def test_forward_action(self, chat, bridge, message_elt) -> None:
        """Forward action launches bridge.message_forward with correct parameters."""
        message_elt.classList.contains.return_value = True
        with patch("browser.window.prompt", return_value="recipient@example.org"):
            await chat.on_action_forward(None, message_elt)

        bridge.message_forward.assert_awaited_once_with("msg123", "recipient@example.org")

    @pytest.mark.asyncio
    async def test_rich_editor_actions(self, chat) -> None:
        """Rich editor formatting actions modify editor state correctly."""
        test_cases = [
            ("bold", ["bold", True]),
            ("italic", ["italic", True]),
            ("underline", ["underline", True]),
            ("link", ["link", "https://example.org"]),
        ]

        for action, expected_call in test_cases:
            evt = MagicMock()
            evt.currentTarget.dataset = {"action": action}

            if action == "link":
                with patch("browser.window.prompt", return_value="https://example.org"):
                    chat.on_rich_action(evt)
            else:
                chat.on_rich_action(evt)

            chat.rich_editor.format.assert_called_with(*expected_call)

    @pytest.mark.asyncio
    async def test_extra_recipients(self, monkeypatch, chat) -> None:
        """Adding extra recipient calls "insertBefore"."""
        toolbar_elt = MagicMock()
        monkeypatch.setitem(document, "rich-edit-toolbar", toolbar_elt)

        await chat.on_action_extra_recipients(selected="to")
        await chat.on_action_extra_recipients(selected="cc")

        assert toolbar_elt.parent.insertBefore.call_count == 2