diff --git a/tests/ui_tools/test_boxes.py b/tests/ui_tools/test_boxes.py index 8fc050391f..7f061f1954 100644 --- a/tests/ui_tools/test_boxes.py +++ b/tests/ui_tools/test_boxes.py @@ -1,8 +1,10 @@ import datetime from collections import OrderedDict -from typing import Any, Callable, Dict, List, Optional +from functools import reduce +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import pytest +from pytest import FixtureRequest from pytest import param as case from pytest_mock import MockerFixture from urwid import Widget @@ -11,6 +13,9 @@ TYPING_STARTED_EXPIRY_PERIOD, TYPING_STARTED_WAIT_PERIOD, TYPING_STOPPED_WAIT_PERIOD, + Composition, + PrivateComposition, + StreamComposition, ) from zulipterminal.config.keys import ( keys_for_command, @@ -33,6 +38,37 @@ WRITEBOX = MODULE + ".WriteBox" +def composition_factory( + type: str, + to: Union[str, List[int]] = "", + subject: Optional[str] = None, + read_by_sender: bool = True, +) -> Dict[str, object]: + data = { + "type": type, + "content": f"Random {type} message", + "read_by_sender": read_by_sender, + "to": to, + } + if subject: + data["subject"] = subject + return data + + +def saved_draft_factory( + draft_composition: Optional[Composition] = None, +) -> Dict[str, Union[Optional[Composition], str]]: + expected_function = ( + "view.controller.save_draft_confirmation_popup" + if draft_composition is not None + else "model.save_draft" + ) + return { + "draft_composition": draft_composition, + "expected_function": expected_function, + } + + class TestWriteBox: @pytest.fixture(autouse=True) def mock_external_classes( @@ -103,6 +139,224 @@ def test_not_calling_typing_method_without_recipients( assert not write_box.model.send_typing_status_by_user_ids.called + @pytest.fixture( + params=[ + saved_draft_factory(), + saved_draft_factory( + StreamComposition( + **composition_factory( # type: ignore[typeddict-item] + type="stream", to="Current stream", subject="Topic" + ) + ) + ), + saved_draft_factory( + PrivateComposition( + **composition_factory( # type: ignore[typeddict-item] + type="private", to=[5179] + ) + ) + ), + saved_draft_factory( + PrivateComposition( + **composition_factory( # type: ignore[typeddict-item] + type="group", to=[5140, 5179] + ) + ) + ), + ], + ids=[ + "no_saved_draft_exists", + "saved_stream_draft_exists", + "saved_private_draft_exists", + "saved_private_group_draft_exists", + ], + ) + def saved_draft( + self, request: FixtureRequest + ) -> Dict[str, Union[Optional[Composition], str]]: + return request.param + + @pytest.fixture( + params=[ + composition_factory("private", to=[5140]), + composition_factory("private", to=[5140, 5180]), + ], + ids=["private_draft", "group_draft"], + ) + def private_draft_composition( + self, request: FixtureRequest + ) -> Tuple[List[Composition], List[str]]: + return request.param + + @pytest.fixture( + params=[composition_factory("stream", to="Another stream", subject="Topic")], + ) + def stream_draft_composition( + self, request: FixtureRequest + ) -> Tuple[List[Composition], List[str]]: + return request.param + + @pytest.fixture + def private_draft_setup_fixture( + self, + mocker: MockerFixture, + private_draft_composition: PrivateComposition, + write_box: WriteBox, + ) -> Tuple[MockerFixture, WriteBox]: + mocker.patch(MODULE + ".WriteBox.update_recipients") + write_box.msg_write_box = mocker.Mock( + edit_text=private_draft_composition["content"] + ) + write_box.to_write_box = mocker.Mock() + write_box.compose_box_status = "open_with_private" + write_box.recipient_user_ids = private_draft_composition["to"] + return mocker, write_box + + @pytest.fixture + def stream_draft_setup_fixture( + self, + mocker: MockerFixture, + stream_draft_composition: StreamComposition, + write_box: WriteBox, + ) -> Tuple[MockerFixture, WriteBox]: + mocker.patch(MODULE + ".WriteBox.update_recipients") + write_box.msg_write_box = mocker.Mock( + edit_text=stream_draft_composition["content"] + ) + write_box.stream_write_box = mocker.Mock( + edit_text=stream_draft_composition["to"] + ) + write_box.title_write_box = mocker.Mock( + edit_text=stream_draft_composition["subject"] + ) + write_box.compose_box_status = "open_with_stream" + write_box.stream_id = 1 + return mocker, write_box + + @pytest.mark.parametrize("key", keys_for_command("SAVE_AS_DRAFT")) + def test_keypress_SAVE_AS_DRAFT_stream( + self, + key: str, + saved_draft: Dict[str, Union[Optional[Composition], str]], + stream_draft_composition: StreamComposition, + stream_draft_setup_fixture: Tuple[MockerFixture, WriteBox], + write_box: WriteBox, + widget_size: Callable[[Widget], urwid_Size], + ) -> None: + draft_saved_in_current_session = saved_draft["draft_composition"] + _, write_box = stream_draft_setup_fixture + assert isinstance(saved_draft["expected_function"], str) + expected_function = reduce( + getattr, saved_draft["expected_function"].split("."), write_box + ) + write_box.model.session_draft_message.return_value = ( + draft_saved_in_current_session + ) + + size = widget_size(write_box) + write_box.keypress(size, key) + + write_box.model.session_draft_message.assert_called() + expected_function.assert_called_once_with(stream_draft_composition) + + @pytest.mark.parametrize("key", keys_for_command("SAVE_AS_DRAFT")) + def test_keypress_SAVE_AS_DRAFT_private__valid_recipients( + self, + key: str, + mocker: MockerFixture, + private_draft_composition: PrivateComposition, + saved_draft: Dict[str, Union[Optional[Composition], str]], + private_draft_setup_fixture: Tuple[MockerFixture, WriteBox], + write_box: WriteBox, + widget_size: Callable[[Widget], urwid_Size], + ) -> None: + draft_saved_in_current_session = saved_draft["draft_composition"] + mocker, write_box = private_draft_setup_fixture + assert isinstance(saved_draft["expected_function"], str) + expected_function = reduce( + getattr, saved_draft["expected_function"].split("."), write_box + ) + mocker.patch( + MODULE + ".WriteBox._tidy_valid_recipients_and_notify_invalid_ones", + return_value=True, + ) + write_box.model.session_draft_message.return_value = ( + draft_saved_in_current_session + ) + + size = widget_size(write_box) + write_box.keypress(size, key) + + write_box.model.session_draft_message.assert_called() + expected_function.assert_called_once_with(private_draft_composition) + + @pytest.mark.parametrize("key", keys_for_command("SAVE_AS_DRAFT")) + def test_keypress_SAVE_AS_DRAFT_private__invalid_recipients( + self, + key: str, + mocker: MockerFixture, + saved_draft: Dict[str, Union[Optional[Composition], str]], + private_draft_setup_fixture: Tuple[MockerFixture, WriteBox], + write_box: WriteBox, + widget_size: Callable[[Widget], urwid_Size], + ) -> None: + draft_saved_in_current_session = saved_draft["draft_composition"] + mocker, write_box = private_draft_setup_fixture + mocker.patch( + MODULE + ".WriteBox._tidy_valid_recipients_and_notify_invalid_ones", + return_value=False, + ) + write_box.model.session_draft_message.return_value = ( + draft_saved_in_current_session + ) + + size = widget_size(write_box) + write_box.keypress(size, key) + + write_box.model.save_draft.assert_not_called() + write_box.view.controller.save_draft_confirmation_popup.assert_not_called() + + @pytest.mark.parametrize("key", keys_for_command("SAVE_AS_DRAFT")) + def test_keypress_SAVE_AS_DRAFT_stream__already_saved( + self, + key: str, + stream_draft_composition: StreamComposition, + stream_draft_setup_fixture: Tuple[MockerFixture, WriteBox], + write_box: WriteBox, + widget_size: Callable[[Widget], urwid_Size], + ) -> None: + write_box.model.session_draft_message.return_value = stream_draft_composition + _, write_box = stream_draft_setup_fixture + + size = widget_size(write_box) + write_box.keypress(size, key) + + write_box.model.session_draft_message.assert_called() + write_box.view.controller.save_draft_confirmation_popup.assert_not_called() + + @pytest.mark.parametrize("key", keys_for_command("SAVE_AS_DRAFT")) + def test_keypress_SAVE_AS_DRAFT_private__same_as_saved_draft( + self, + key: str, + mocker: MockerFixture, + private_draft_composition: PrivateComposition, + private_draft_setup_fixture: Tuple[MockerFixture, WriteBox], + write_box: WriteBox, + widget_size: Callable[[Widget], urwid_Size], + ) -> None: + mocker, write_box = private_draft_setup_fixture + mocker.patch( + MODULE + ".WriteBox._tidy_valid_recipients_and_notify_invalid_ones", + return_value=True, + ) + write_box.model.session_draft_message.return_value = private_draft_composition + + size = widget_size(write_box) + write_box.keypress(size, key) + + write_box.model.session_draft_message.assert_called() + write_box.view.controller.save_draft_confirmation_popup.assert_not_called() + @pytest.mark.parametrize( "text, state, is_valid_stream, required_typeahead", [