From 993dca9a10ae37261181f5f3f66704eb313d8ab6 Mon Sep 17 00:00:00 2001 From: Ro Date: Tue, 15 Oct 2024 14:49:15 -0400 Subject: [PATCH] WIP: MainView tests --- client/tests/gui/test_actions.py | 4 +- client/tests/gui/test_widgets.py | 136 +++++++++++++++++++------------ 2 files changed, 87 insertions(+), 53 deletions(-) diff --git a/client/tests/gui/test_actions.py b/client/tests/gui/test_actions.py index 3a728c86e..25bb7b955 100644 --- a/client/tests/gui/test_actions.py +++ b/client/tests/gui/test_actions.py @@ -103,7 +103,9 @@ def test_deletes_source_when_dialog_accepted(self): self.action.trigger() self._controller.delete_sources.assert_called_once() - assert self._source in self._controller.delete_sources.call_args[0][0], self._controller.delete_sources.call_args[0][0] + assert ( + self._source in self._controller.delete_sources.call_args[0][0] + ), self._controller.delete_sources.call_args[0][0] def test_does_not_delete_source_when_dialog_rejected(self): # Reject the confirmation dialog from a separate thread. diff --git a/client/tests/gui/test_widgets.py b/client/tests/gui/test_widgets.py index 12317fa86..906d007d3 100644 --- a/client/tests/gui/test_widgets.py +++ b/client/tests/gui/test_widgets.py @@ -31,6 +31,8 @@ LoginButton, MainView, MessageWidget, + MultiSelectView, + NothingSelectedView, ReplyBoxWidget, ReplyTextEdit, ReplyTextEditPlaceholder, @@ -533,21 +535,19 @@ def test_MainView_show_sources_with_none_selected(mocker): # Set up SourceList so that SourceList.get_selected_source() returns a source mv.source_list = SourceList() + mv.source_list.controller = mocker.MagicMock() source_widget = SourceWidget( mocker.MagicMock(), factory.Source(uuid="stub_uuid"), mocker.MagicMock(), mocker.MagicMock() ) source_item = SourceListWidgetItem(mv.source_list) mv.source_list.setItemWidget(source_item, source_widget) mv.source_list.source_items["stub_uuid"] = source_item - mocker.patch.object(mv.source_list, "update_sources") + # mocker.patch.object(mv.source_list, "update_sources") - mv.empty_conversation_view = mocker.MagicMock() - - mv.show_sources([1, 2, 3]) + mv.show_sources([factory.Source(), factory.Source(), factory.Source()]) - mv.source_list.update_sources.assert_called_once_with([1, 2, 3]) - mv.empty_conversation_view.show_no_source_selected_message.assert_called_once_with() - mv.empty_conversation_view.show.assert_called_once_with() + # mv.source_list.update_sources.assert_called_once_with([1, 2, 3]) + assert mv.view_layout.currentIndex() == mv.NOTHING_SELECTED_INDEX def test_MainView_show_sources_from_cold_start(mocker): @@ -570,13 +570,13 @@ def test_MainView_show_sources_with_no_sources_at_all(mocker): """ mv = MainView(None) mv.source_list = mocker.MagicMock() - mv.empty_conversation_view = mocker.MagicMock() + mv._on_update_conversation_context = mocker.MagicMock(wraps=mv._on_update_conversation_context) mv.show_sources([]) mv.source_list.update_sources.assert_called_once_with([]) - mv.empty_conversation_view.show_no_sources_message.assert_called_once_with() - mv.empty_conversation_view.show.assert_called_once_with() + mv._on_update_conversation_context.assert_called() + assert mv.view_layout.currentIndex() == mv.NO_SOURCES_INDEX def test_MainView_show_sources_when_sources_are_deleted(mocker): @@ -584,18 +584,18 @@ def test_MainView_show_sources_when_sources_are_deleted(mocker): Ensure that show_sources also deletes the SourceConversationWrapper for a deleted source. """ mv = MainView(None) + sources = [factory.Source(), factory.Source(), factory.Source(), factory.Source()] mv.source_list = mocker.MagicMock() - mv.empty_conversation_view = mocker.MagicMock() mv.source_list.update_sources = mocker.MagicMock(return_value=[]) mv.delete_conversation = mocker.MagicMock() - mv.show_sources([1, 2, 3, 4]) + mv.show_sources(sources) - mv.source_list.update_sources = mocker.MagicMock(return_value=[4]) + mv.source_list.update_sources = mocker.MagicMock(return_value=[sources[-1]]) - mv.show_sources([1, 2, 3]) + mv.show_sources(sources[:-1]) - mv.delete_conversation.assert_called_once_with(4) + mv.delete_conversation.assert_called_once_with(sources[-1]) def test_MainView_delete_conversation_when_conv_wrapper_exists(mocker): @@ -638,6 +638,8 @@ def test_MainView_on_source_changed(mocker): source = factory.Source() mv.source_list = mocker.MagicMock() mv.source_list.get_selected_source = mocker.MagicMock(return_value=source) + mv.source_list.selectedItems = mocker.MagicMock(return_value=[source]) + mv.source_list.count = mocker.MagicMock(return_value=3) mv.controller = mocker.MagicMock(is_authenticated=True) mocker.patch("securedrop_client.gui.widgets.source_exists", return_value=True) # scw = SourceConversationWrapper(source, mv.controller) @@ -646,7 +648,8 @@ def test_MainView_on_source_changed(mocker): mv.on_source_changed() mv.source_list.get_selected_source.assert_called_once_with() - mv.set_conversation.assert_called_once_with(SourceConversationWrapper(source, mv.controller)) + mv.set_conversation.assert_called_once() + assert mv.set_conversation.call_args[0][0].source == source def test_MainView_on_source_changed_shows_correct_context(mocker): @@ -654,10 +657,10 @@ def test_MainView_on_source_changed_shows_correct_context(mocker): Ensure correct context presented based on number of sources selected. """ # Build sourcelist - sources = [] - for i in range(0, 10): + sources = [] + for i in range(10): sources.append(factory.Source()) - + mock_controller = mocker.MagicMock(spec=logic.Controller) mock_session = mocker.MagicMock(is_authenticated=True) mock_controller.session = mock_session @@ -668,21 +671,22 @@ def test_MainView_on_source_changed_shows_correct_context(mocker): mv.show() mv.show_sources(sources) - assert mv.empty_conversation_view.isVisible() - assert mv.empty_conversation_view.no_source_selected.isVisible() - assert mv.empty_conversation_view.no_sources.isHidden() + assert mv.view_layout.currentIndex() == mv.NOTHING_SELECTED_INDEX # Select a source, ensure the correct view context is shown mv.source_list.item(1).setSelected(True) - assert mv.empty_conversation_view.isHidden() - assert mock_controller.session.refresh.call_args[0][0] == mv.source_list.itemWidget(mv.source_list.item(1)).source + assert mv.view_layout.currentItem() == mv.CONVERSATION_INDEX + assert ( + mock_controller.session.refresh.call_args[0][0] + == mv.source_list.itemWidget(mv.source_list.item(1)).source + ) # Now ensure the "multiple sources selected" view is shown mv.source_list.selectAll() - assert mv.empty_conversation_view.isVisible() - assert mv.empty_conversation_view.multi_source_selected.isVisible() + assert mv.view_layout.currentItem() == mv.MULTI_SELECTED_INDEX + def test_MainView_on_source_changed_does_not_raise_InvalidRequestError(mocker): """ @@ -709,6 +713,7 @@ def test_MainView_on_source_changed_does_not_raise_InvalidRequestError(mocker): assert mock_logger.debug.call_count == 1, mock_logger.debug.call_args + def test_MainView_on_source_changed_when_source_no_longer_exists(mocker): """ Test that conversation for a source is cleared when the source no longer exists. @@ -716,11 +721,12 @@ def test_MainView_on_source_changed_when_source_no_longer_exists(mocker): mv = MainView(None) mv.set_conversation = mocker.MagicMock() mv.source_list = mocker.MagicMock() + mv.source_list.selectedItems = mocker.MagicMock() mv.source_list.get_selected_source = mocker.MagicMock(return_value=None) mv.on_source_changed() - mv.source_list.get_selected_source.assert_called_once_with() + assert mv.view_layout.currentIndex() == mv.NOTHING_SELECTED_INDEX mv.set_conversation.assert_not_called() @@ -729,9 +735,12 @@ def test_MainView_on_source_changed_updates_conversation_view(mocker, session): Test that the source collection is displayed in the conversation view. """ mv = MainView(None) - # mv.source_list = mocker.MagicMock() - mv.controller = mocker.MagicMock(is_authenticated=True) source = factory.Source() + + mv.source_list = mocker.MagicMock() + mv.source_list.count = mocker.MagicMock(return_value=1) + mv.source_list.selectedItems = mocker.MagicMock(return_value=[source]) + mv.controller = mocker.MagicMock(is_authenticated=True) session.add(source) file = factory.File(source=source, filename="0-mock-doc.gpg") message = factory.Message(source=source, filename="0-mock-msg.gpg") @@ -769,16 +778,20 @@ def test_MainView_on_source_changed_SourceConversationWrapper_is_preserved(mocke SourceConversationWrapper when we click away from a given source. We should create it the first time, and then it should persist. """ - mv = MainView(None) - mv.set_conversation = mocker.MagicMock() - source_selected = mocker.patch("securedrop_client.gui.widgets.SourceList.source_selected") - mv.controller = mocker.MagicMock(is_authenticated=True) source = factory.Source() source2 = factory.Source() session.add(source) session.add(source2) session.commit() + mv = MainView(None) + mv.source_list = mocker.MagicMock() + mv.source_list.count = mocker.MagicMock(return_value=2) + mv.source_list.selectedItems = mocker.MagicMock() + mv.set_conversation = mocker.MagicMock() + source_selected = mocker.patch("securedrop_client.gui.widgets.SourceList.source_selected") + mv.controller = mocker.MagicMock(is_authenticated=True) + source_conversation_init = mocker.patch( "securedrop_client.gui.widgets.SourceConversationWrapper.__init__", return_value=None ) @@ -968,31 +981,52 @@ def test_MainView_set_conversation(mocker): (i.e. that area of the screen on the right hand side). """ mv = MainView(None) - mv.view_layout = mocker.MagicMock() - mock_widget = mocker.MagicMock() - mv.set_conversation(mock_widget) + scw = SourceConversationWrapper(factory.Source(), mocker.MagicMock()) + mv.set_conversation(scw) - mv.view_layout.takeAt.assert_called_once_with(0) - mv.view_layout.addWidget.assert_called_once_with(mock_widget) + assert mv.view_layout.widget(mv.CONVERSATION_INDEX) == scw -def test_EmptyConversationView_show_no_sources_message(mocker): - ecv = EmptyConversationView() +def test_EmptyConversationView(mocker): + mv = MainView(None) + mv.source_list = mocker.MagicMock() + mv.source_list.count = mocker.MagicMock(return_value=0) + mv.source_list.selectedItems = mocker.MagicMock(return_value=[]) + mv.show() + assert mv.view_layout.count() == 4 # Sanity - are all the pages there? + mv.show_sources([]) + assert mv.view_layout.currentIndex() == mv.NO_SOURCES_INDEX + assert isinstance(mv.view_layout.widget(mv.view_layout.currentIndex()), EmptyConversationView) - ecv.show_no_sources_message() - assert not ecv.no_sources.isHidden() - assert ecv.no_source_selected.isHidden() +def test_NothingSelectedView(mocker): + mv = MainView(None) + mv.show() + mv.source_list = mocker.MagicMock() + mv.source_list.count = mocker.MagicMock(return_value=4) + mv.source_list.selectedItems = mocker.MagicMock(return_value=[]) + assert mv.view_layout.count() == 4 # Sanity - are all the pages there? + mv.show_sources([factory.Source(), factory.Source(), factory.Source()]) + assert mv.view_layout.currentIndex() == mv.NOTHING_SELECTED_INDEX + assert isinstance(mv.view_layout.widget(mv.view_layout.currentIndex()), NothingSelectedView) -def test_EmptyConversationView_show_no_source_selected_message(mocker): - ecv = EmptyConversationView() - ecv.show_no_source_selected_message() +def test_MultiSelectedView(mocker): + mv = MainView(None) + sources = [factory.Source(), factory.Source(), factory.Source()] + mv.source_list = mocker.MagicMock() + mv.source_list.count = mocker.MagicMock(return_value=len(sources)) + mv.source_list.selectedItems = mocker.MagicMock(return_value=sources[:-1]) + mv._on_update_conversation_context = mocker.MagicMock(wraps=mv._on_update_conversation_context) + mv.show_sources(sources) - assert ecv.no_sources.isHidden() - assert not ecv.no_source_selected.isHidden() + mv.show() + assert mv.view_layout.count() == 4 # Sanity - are all the pages there? + mv._on_update_conversation_context.assert_called() + assert mv.view_layout.currentIndex() == mv.MULTI_SELECTED_INDEX + assert isinstance(mv.view_layout.widget(mv.view_layout.currentIndex()), MultiSelectView) def test_SourceList_get_selected_source(mocker): @@ -3809,14 +3843,12 @@ def test_SourceConversationWrapper_on_source_deleted(mocker): mv.source_list = mocker.MagicMock() mv.source_list.get_selected_source = mocker.MagicMock(return_value=source) mv.controller = mocker.MagicMock(is_authenticated=True) - mv.show() + + # Detached sourceconversationwrapper, just for unit testing scw = SourceConversationWrapper(source, mv.controller, None) - mocker.patch("securedrop_client.gui.widgets.SourceConversationWrapper", return_value=scw) mv.on_source_changed() scw.on_source_deleted("123") - assert mv.isVisible() - assert scw.isVisible() assert not scw.conversation_title_bar.isHidden() assert not scw.reply_box.isHidden() assert not scw.reply_box.text_edit.isEnabled()