diff --git a/securedrop_client/gui/__init__.py b/securedrop_client/gui/__init__.py index 3fd4c9d4e..77f7f60fe 100644 --- a/securedrop_client/gui/__init__.py +++ b/securedrop_client/gui/__init__.py @@ -148,6 +148,9 @@ def update_image(self, filename: str, svg_size: str = None) -> None: class SecureQLabel(QLabel): + + MAX_PREVIEW_LENGTH = 200 + def __init__( self, text: str = "", @@ -168,6 +171,7 @@ def __init__( def setText(self, text: str) -> None: text = text.strip() self.setTextFormat(Qt.PlainText) + self.preview_text = text[: self.MAX_PREVIEW_LENGTH] elided_text = self.get_elided_text(text) self.elided = True if elided_text != text else False if self.elided and self.with_tooltip: @@ -175,6 +179,9 @@ def setText(self, text: str) -> None: self.setToolTip(tooltip_label.text()) super().setText(elided_text) + def refresh_preview_text(self) -> None: + self.setText(self.preview_text) + def get_elided_text(self, full_text: str) -> str: if not self.max_length: return full_text diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index 51aca64a5..b973eb331 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -596,8 +596,8 @@ def __init__(self, parent: QObject): self.view_layout.addWidget(self.empty_conversation_view) # Add widgets to layout - self.layout.addWidget(self.source_list) - self.layout.addWidget(self.view_holder) + self.layout.addWidget(self.source_list, stretch=1) + self.layout.addWidget(self.view_holder, stretch=2) # Note: We should not delete SourceConversationWrapper when its source is unselected. This # is a temporary solution to keep copies of our objects since we do delete them. @@ -802,8 +802,10 @@ class SourceList(QListWidget): """ NUM_SOURCES_TO_ADD_AT_A_TIME = 32 + INITIAL_UPDATE_SCROLLBAR_WIDTH = 20 source_selected = pyqtSignal(str) + adjust_preview = pyqtSignal(int) def __init__(self): super().__init__() @@ -815,12 +817,19 @@ def __init__(self): layout = QVBoxLayout(self) self.setLayout(layout) + # Disable horizontal scrollbar for SourceList widget + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + # Enable ordering. self.setSortingEnabled(True) # To hold references to SourceListWidgetItem instances indexed by source UUID. self.source_items = {} + def resizeEvent(self, event): + self.adjust_preview.emit(event.size().width()) + super().resizeEvent(event) + def setup(self, controller): self.controller = controller self.controller.reply_succeeded.connect(self.set_snippet) @@ -876,13 +885,14 @@ def update(self, sources: List[Source]) -> List[str]: # Add widgets for new sources for uuid in sources_to_add: source_widget = SourceWidget( - self.controller, sources_to_add[uuid], self.source_selected + self.controller, sources_to_add[uuid], self.source_selected, self.adjust_preview ) source_item = SourceListWidgetItem(self) source_item.setSizeHint(source_widget.sizeHint()) self.insertItem(0, source_item) self.setItemWidget(source_item, source_widget) self.source_items[uuid] = source_item + self.adjust_preview.emit(self.width() - self.INITIAL_UPDATE_SCROLLBAR_WIDTH) # Re-sort SourceList to make sure the most recently-updated sources appear at the top self.sortItems(Qt.DescendingOrder) @@ -905,6 +915,7 @@ def add_source(self, sources, slice_size=1): def schedule_source_management(slice_size=slice_size): if not sources: + self.adjust_preview.emit(self.width() - self.INITIAL_UPDATE_SCROLLBAR_WIDTH) return # Process the remaining "slice_size" number of sources. @@ -912,7 +923,9 @@ def schedule_source_management(slice_size=slice_size): for source in sources_slice: try: source_uuid = source.uuid - source_widget = SourceWidget(self.controller, source, self.source_selected) + source_widget = SourceWidget( + self.controller, source, self.source_selected, self.adjust_preview + ) source_item = SourceListWidgetItem(self) source_item.setSizeHint(source_widget.sizeHint()) self.insertItem(0, source_item) @@ -975,6 +988,28 @@ def set_snippet(self, source_uuid: str, collection_item_uuid: str, content: str) source_widget.set_snippet(source_uuid, collection_item_uuid, content) +class SourcePreview(SecureQLabel): + PREVIEW_WIDTH_DIFFERENCE = 140 + + def __init__(self): + super().__init__() + + def adjust_preview(self, width): + """ + This is a workaround to the workaround for https://bugreports.qt.io/browse/QTBUG-85498. + Since QLabels containing text with long strings that cannot be wrapped have to have a fixed + width in order to fit within the scroll list widget, we have to override the normal resizing + logic. + """ + new_width = width - self.PREVIEW_WIDTH_DIFFERENCE + if self.width() == new_width: + return + + self.setFixedWidth(new_width) + self.max_length = self.width() + self.refresh_preview_text() + + class SourceWidget(QWidget): """ Used to display summary information about a source in the list view. @@ -983,9 +1018,6 @@ class SourceWidget(QWidget): TOP_MARGIN = 11 BOTTOM_MARGIN = 7 SIDE_MARGIN = 10 - PREVIEW_WIDTH = 360 - PREVIEW_WIDGET_WIDTH = 380 - PREVIEW_WIDGET_HEIGHT = 22 SPACER = 14 BOTTOM_SPACER = 11 STAR_WIDTH = 20 @@ -995,7 +1027,13 @@ class SourceWidget(QWidget): SOURCE_PREVIEW_CSS = load_css("source_preview.css") SOURCE_TIMESTAMP_CSS = load_css("source_timestamp.css") - def __init__(self, controller: Controller, source: Source, source_selected_signal: pyqtSignal): + def __init__( + self, + controller: Controller, + source: Source, + source_selected_signal: pyqtSignal, + adjust_preview: pyqtSignal, + ): super().__init__() self.controller = controller @@ -1003,15 +1041,14 @@ def __init__(self, controller: Controller, source: Source, source_selected_signa self.controller.source_deletion_failed.connect(self._on_source_deletion_failed) self.controller.authentication_state.connect(self._on_authentication_changed) source_selected_signal.connect(self._on_source_selected) + adjust_preview.connect(self._on_adjust_preview) - # Store source self.source = source self.seen = self.source.seen self.source_uuid = self.source.uuid self.last_updated = self.source.last_updated self.selected = False - # Set cursor. self.setCursor(QCursor(Qt.PointingHandCursor)) retain_space = self.sizePolicy() @@ -1021,14 +1058,10 @@ def __init__(self, controller: Controller, source: Source, source_selected_signa self.star.setFixedWidth(self.STAR_WIDTH) self.name = QLabel() self.name.setObjectName("SourceWidget_name") - self.preview = SecureQLabel(max_length=self.PREVIEW_WIDTH) + self.preview = SourcePreview() self.preview.setObjectName("SourceWidget_preview") - self.preview.setFixedSize(QSize(self.PREVIEW_WIDGET_WIDTH, self.PREVIEW_WIDGET_HEIGHT)) self.waiting_delete_confirmation = QLabel("Deletion in progress") self.waiting_delete_confirmation.setObjectName("SourceWidget_source_deleted") - self.waiting_delete_confirmation.setFixedSize( - QSize(self.PREVIEW_WIDGET_WIDTH, self.PREVIEW_WIDGET_HEIGHT) - ) self.waiting_delete_confirmation.hide() self.paperclip = SvgLabel("paperclip.svg", QSize(11, 17)) # Set to size provided in the svg self.paperclip.setObjectName("SourceWidget_paperclip") @@ -1056,9 +1089,9 @@ def __init__(self, controller: Controller, source: Source, source_selected_signa source_widget_layout.setSpacing(0) source_widget_layout.setContentsMargins(0, self.TOP_MARGIN, 0, self.BOTTOM_MARGIN) source_widget_layout.addWidget(self.star, 0, 0, 1, 1) - self.spacer_widget = QWidget() - self.spacer_widget.setFixedWidth(self.SPACER) - source_widget_layout.addWidget(self.spacer_widget, 0, 1, 1, 1) + self.spacer = QWidget() + self.spacer.setFixedWidth(self.SPACER) + source_widget_layout.addWidget(self.spacer, 0, 1, 1, 1) source_widget_layout.addWidget(self.name, 0, 2, 1, 1) source_widget_layout.addWidget(self.paperclip, 0, 3, 1, 1) source_widget_layout.addWidget(self.preview, 1, 2, 1, 1, alignment=Qt.AlignLeft) @@ -1076,6 +1109,11 @@ def __init__(self, controller: Controller, source: Source, source_selected_signa self.update() + @pyqtSlot(int) + def _on_adjust_preview(self, width): + self.setFixedWidth(width) + self.preview.adjust_preview(width) + def update(self): """ Updates the displayed values with the current values from self.source. @@ -1117,6 +1155,7 @@ def set_snippet(self, source_uuid: str, collection_uuid: str = None, content: st content = str(last_activity) self.preview.setText(content) + self.preview.adjust_preview(self.width()) def delete_source(self, event): if self.controller.api is None: @@ -2886,7 +2925,6 @@ class ConversationScrollArea(QScrollArea): def __init__(self): super().__init__() - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setWidgetResizable(True) self.setObjectName("ConversationScrollArea") diff --git a/securedrop_client/resources/css/sdclient.css b/securedrop_client/resources/css/sdclient.css index 8bb7ec9c4..667866376 100644 --- a/securedrop_client/resources/css/sdclient.css +++ b/securedrop_client/resources/css/sdclient.css @@ -182,7 +182,7 @@ QListView#SourceList { border: none; show-decoration-selected: 0; border-right: 3px solid #f3f5f9; - min-width: 500px; + min-width: 400px; max-width: 540px; } @@ -191,7 +191,7 @@ QListView#SourceList::item:selected { } QListView#SourceList::item:hover { - border: 500px solid #f9f9ff; + background-color: #f9f9ff; } #SourceWidget_container { diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index 5cba2d20b..6e684e018 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -9,8 +9,8 @@ import pytest import sqlalchemy import sqlalchemy.orm.exc -from PyQt5.QtCore import QEvent, Qt -from PyQt5.QtGui import QFocusEvent, QKeyEvent, QMovie +from PyQt5.QtCore import QEvent, QSize, Qt +from PyQt5.QtGui import QFocusEvent, QKeyEvent, QMovie, QResizeEvent from PyQt5.QtTest import QTest from PyQt5.QtWidgets import QApplication, QLineEdit, QMainWindow, QMessageBox, QVBoxLayout, QWidget from sqlalchemy.orm import attributes, scoped_session, sessionmaker @@ -46,6 +46,7 @@ SourceList, SourceListWidgetItem, SourceMenu, + SourcePreview, SourceProfileShortWidget, SourceWidget, SpeechBubble, @@ -987,7 +988,9 @@ def test_SourceList_initial_update_does_not_raise_exc_and_no_widget_created(mock sl.controller = mocker.MagicMock() mark_seen_signal = mocker.MagicMock() # Make sure SourceWidget constructor doesn't raise - source_widget = SourceWidget(sl.controller, factory.Source(), mark_seen_signal) + source_widget = SourceWidget( + sl.controller, factory.Source(), mark_seen_signal, mocker.MagicMock() + ) mocker.patch("securedrop_client.gui.widgets.SourceWidget", return_value=source_widget) source = DeletedSource() sl.initial_update([source]) @@ -1196,7 +1199,7 @@ def test_SourceList_set_snippet(mocker): sl = SourceList() mark_seen_signal = mocker.MagicMock() source_widget = SourceWidget( - mocker.MagicMock(), factory.Source(uuid="mock_uuid"), mark_seen_signal + mocker.MagicMock(), factory.Source(uuid="mock_uuid"), mark_seen_signal, mocker.MagicMock() ) source_widget.set_snippet = mocker.MagicMock() source_item = SourceListWidgetItem(sl) @@ -1236,7 +1239,7 @@ def test_SourceList_get_source_widget_if_one_exists_in_cache(mocker): sl = SourceList() mark_seen_signal = mocker.MagicMock() source_widget = SourceWidget( - mocker.MagicMock(), factory.Source(uuid="mock_uuid"), mark_seen_signal + mocker.MagicMock(), factory.Source(uuid="mock_uuid"), mark_seen_signal, mocker.MagicMock() ) source_item = SourceListWidgetItem(sl) sl.setItemWidget(source_item, source_widget) @@ -1290,7 +1293,7 @@ def test_SourceWidget_init_for_seen_source(mocker, session): session.commit() - sw = SourceWidget(controller, source, mocker.MagicMock()) + sw = SourceWidget(controller, source, mocker.MagicMock(), mocker.MagicMock()) assert sw.source == source assert sw.seen @@ -1351,7 +1354,7 @@ def test_SourceWidget_init_for_seen_source_with_legacy_data(mocker, session): session.commit() - sw = SourceWidget(controller, source, mocker.MagicMock()) + sw = SourceWidget(controller, source, mocker.MagicMock(), mocker.MagicMock()) assert sw.source == source assert sw.seen @@ -1390,7 +1393,7 @@ def test_SourceWidget_init_for_seen_source_legacy_only(mocker, session): session.commit() - sw = SourceWidget(controller, source, mocker.MagicMock()) + sw = SourceWidget(controller, source, mocker.MagicMock(), mocker.MagicMock()) assert sw.source == source assert sw.source.seen @@ -1450,7 +1453,7 @@ def test_SourceWidget_init_for_unseen_source(mocker, session): session.commit() - sw = SourceWidget(controller, source, mocker.MagicMock()) + sw = SourceWidget(controller, source, mocker.MagicMock(), mocker.MagicMock()) assert sw.source == source assert not sw.seen @@ -1496,7 +1499,7 @@ def test_SourceWidget_init_for_unseen_source_legacy_only(mocker, session): session.commit() - sw = SourceWidget(controller, source, mocker.MagicMock()) + sw = SourceWidget(controller, source, mocker.MagicMock(), mocker.MagicMock()) assert sw.source == source assert not sw.seen @@ -1512,7 +1515,7 @@ def test_SourceWidget_html_init(mocker): mock_source.journalist_designation = "foo bar baz" mark_seen_signal = mocker.MagicMock() - sw = SourceWidget(controller, mock_source, mark_seen_signal) + sw = SourceWidget(controller, mock_source, mark_seen_signal, mocker.MagicMock()) sw.name = mocker.MagicMock() sw.summary_layout = mocker.MagicMock() @@ -1522,11 +1525,43 @@ def test_SourceWidget_html_init(mocker): sw.name.setText.assert_called_once_with("foo bar baz") +def test_SourceWidget__on_adjust_preview(mocker): + """ + Ensure width of the source widget is set to the width passed into adjust_preview. + """ + sl = SourceList() + sw = SourceWidget(mocker.MagicMock(), factory.Source(), mocker.MagicMock(), mocker.MagicMock()) + sw.preview = mocker.MagicMock() + source_item = SourceListWidgetItem(sl) + sl.setItemWidget(source_item, sw) + + sw._on_adjust_preview(100) + + assert sw.width() == 100 + sw.preview.adjust_preview.assert_called_with(100) + + +def test_SourceList_resizeEvent(mocker): + sl = SourceList() + sl.adjust_preview = mocker.MagicMock() + sl.resizeEvent(QResizeEvent(QSize(100, 100), QSize(100, 100))) + sl.adjust_preview.emit.assert_called_once_with(100) + + +def test_SourcePreview_adjust_preview(mocker): + preview = SourcePreview() + preview.refresh_preview_text = mocker.MagicMock() + preview.adjust_preview(400) + preview.refresh_preview_text.assert_called_once_with() + assert preview.max_length == 400 - preview.PREVIEW_WIDTH_DIFFERENCE + assert preview.width() == 400 - preview.PREVIEW_WIDTH_DIFFERENCE + + def test_SourceWidget_update_styles_to_read(mocker): """ Ensure styles are updated so that the source widget appears read when seen is True. """ - sw = SourceWidget(mocker.MagicMock(), factory.Source(), mocker.MagicMock()) + sw = SourceWidget(mocker.MagicMock(), factory.Source(), mocker.MagicMock(), mocker.MagicMock()) sw.seen = True name = mocker.patch.object(sw, "name") timestamp = mocker.patch.object(sw, "timestamp") @@ -1544,7 +1579,7 @@ def test_SourceWidget_update_styles_to_read_selected(mocker): Ensure styles are updated so that the source widget appears read and selected when seen and selected are True. """ - sw = SourceWidget(mocker.MagicMock(), factory.Source(), mocker.MagicMock()) + sw = SourceWidget(mocker.MagicMock(), factory.Source(), mocker.MagicMock(), mocker.MagicMock()) sw.seen = True sw.selected = True name = mocker.patch.object(sw, "name") @@ -1562,7 +1597,7 @@ def test_test_SourceWidget_update_styles_to_unread(mocker): """ Ensure styles are updated so that the source widget appears unread when seen is False. """ - sw = SourceWidget(mocker.MagicMock(), factory.Source(), mocker.MagicMock()) + sw = SourceWidget(mocker.MagicMock(), factory.Source(), mocker.MagicMock(), mocker.MagicMock()) sw.seen = False name = mocker.patch.object(sw, "name") timestamp = mocker.patch.object(sw, "timestamp") @@ -1583,7 +1618,7 @@ def test_SourceWidget__on_authentication_changed(mocker): * The source widget's seen status remains unchanged when the authentication status changes to the user being online. (Seen status will be corrected in `SourceWidget.update`.) """ - sw = SourceWidget(mocker.MagicMock(), factory.Source(), mocker.MagicMock()) + sw = SourceWidget(mocker.MagicMock(), factory.Source(), mocker.MagicMock(), mocker.MagicMock()) sw.seen = False sw.update_styles = mocker.MagicMock() @@ -1607,7 +1642,7 @@ def test_SourceWidget__on_source_selected(mocker, session): controller = mocker.MagicMock() controller.authenticated_user = factory.User(id=1) source = factory.Source() - sw = SourceWidget(controller, source, mocker.MagicMock()) + sw = SourceWidget(controller, source, mocker.MagicMock(), mocker.MagicMock()) sw.seen = False sw.update_styles = mocker.MagicMock() @@ -1625,7 +1660,7 @@ def test_SourceWidget__on_source_selected_skips_op_if_uuid_does_not_match(mocker """ controller = mocker.MagicMock() source = factory.Source() - sw = SourceWidget(controller, source, mocker.MagicMock()) + sw = SourceWidget(controller, source, mocker.MagicMock(), mocker.MagicMock()) sw.seen = False sw.update_styles = mocker.MagicMock() @@ -1639,7 +1674,7 @@ def test_SourceWidget__on_source_selected_skips_op_if_uuid_does_not_match(mocker def test_SourceWidget__on_source_selected_skips_op_if_already_seen(mocker): controller = mocker.MagicMock() source = factory.Source() - sw = SourceWidget(controller, source, mocker.MagicMock()) + sw = SourceWidget(controller, source, mocker.MagicMock(), mocker.MagicMock()) sw.seen = True sw.update_styles = mocker.MagicMock() @@ -1656,7 +1691,7 @@ def test_SourceWidget_update_attachment_icon(mocker): controller = mocker.MagicMock() source = factory.Source(document_count=1) mark_seen_signal = mocker.MagicMock() - sw = SourceWidget(controller, source, mark_seen_signal) + sw = SourceWidget(controller, source, mark_seen_signal, mocker.MagicMock()) sw.update() assert not sw.paperclip.isHidden() @@ -1675,7 +1710,7 @@ def test_SourceWidget_update_does_not_raise_exception(mocker): controller = mocker.MagicMock() source = factory.Source(document_count=1) mark_seen_signal = mocker.MagicMock() - sw = SourceWidget(controller, source, mark_seen_signal) + sw = SourceWidget(controller, source, mark_seen_signal, mocker.MagicMock()) ex = sqlalchemy.exc.InvalidRequestError() controller.session.refresh.side_effect = ex mock_logger = mocker.MagicMock() @@ -1699,7 +1734,7 @@ def test_SourceWidget_set_snippet_draft_only(mocker, session_maker, session, hom session.add(reply) session.commit() - sw = SourceWidget(controller, source, mark_seen_signal) + sw = SourceWidget(controller, source, mark_seen_signal, mocker.MagicMock()) sw.set_snippet(source.uuid, reply.uuid, f.filename) assert sw.preview.text() == "File: " + f.filename @@ -1717,7 +1752,7 @@ def test_SourceWidget_set_snippet(mocker, session_maker, session, homedir): session.add(source) session.commit() - sw = SourceWidget(controller, source, mark_seen_signal) + sw = SourceWidget(controller, source, mark_seen_signal, mocker.MagicMock()) sw.set_snippet(source.uuid, "mock_file_uuid", f.filename) assert sw.preview.text() == "File: " + f.filename @@ -1743,7 +1778,7 @@ def test_SourceWidget_update_truncate_latest_msg(mocker): source.journalist_designation = "Testy McTestface" source.collection = [factory.Message(content="a" * 151)] mark_seen_signal = mocker.MagicMock() - sw = SourceWidget(controller, source, mark_seen_signal) + sw = SourceWidget(controller, source, mark_seen_signal, mocker.MagicMock()) sw.update() assert sw.preview.text().endswith("...") @@ -1757,7 +1792,7 @@ def test_SourceWidget_delete_source(mocker, session, source): ) mark_seen_signal = mocker.MagicMock() - sw = SourceWidget(mock_controller, source["source"], mark_seen_signal) + sw = SourceWidget(mock_controller, source["source"], mark_seen_signal, mocker.MagicMock()) mocker.patch("securedrop_client.gui.widgets.DeleteSourceMessageBox", mock_delete_source_message) @@ -1778,7 +1813,7 @@ def test_SourceWidget_delete_source_when_user_chooses_cancel(mocker, session, so mock_message_box_question.return_value = QMessageBox.Cancel mock_controller = mocker.MagicMock() - sw = SourceWidget(mock_controller, source, mark_seen_signal) + sw = SourceWidget(mock_controller, source, mark_seen_signal, mocker.MagicMock()) mocker.patch("securedrop_client.gui.widgets.QMessageBox.question", mock_message_box_question) sw.delete_source(None) @@ -1788,7 +1823,7 @@ def test_SourceWidget_delete_source_when_user_chooses_cancel(mocker, session, so def test_SourceWidget__on_source_deleted(mocker, session, source): controller = mocker.MagicMock() mark_seen_signal = mocker.MagicMock() - sw = SourceWidget(controller, factory.Source(uuid="123"), mark_seen_signal) + sw = SourceWidget(controller, factory.Source(uuid="123"), mark_seen_signal, mocker.MagicMock()) sw._on_source_deleted("123") assert sw.star.isHidden() assert not sw.name.isHidden() @@ -1803,7 +1838,7 @@ def test_SourceWidget__on_source_deleted_wrong_uuid(mocker, session, source): mark_seen_signal = mocker.MagicMock() source = factory.Source(uuid="123") source.document_count = mocker.MagicMock(return_value=1) - sw = SourceWidget(controller, source, mark_seen_signal) + sw = SourceWidget(controller, source, mark_seen_signal, mocker.MagicMock()) sw._on_source_deleted("321") assert not sw.star.isHidden() assert not sw.name.isHidden() @@ -1816,7 +1851,7 @@ def test_SourceWidget__on_source_deleted_wrong_uuid(mocker, session, source): def test_SourceWidget__on_source_deletion_failed(mocker, session, source): controller = mocker.MagicMock() mark_seen_signal = mocker.MagicMock() - sw = SourceWidget(controller, factory.Source(uuid="123"), mark_seen_signal) + sw = SourceWidget(controller, factory.Source(uuid="123"), mark_seen_signal, mocker.MagicMock()) sw._on_source_deleted("123") sw._on_source_deletion_failed("123") @@ -1832,7 +1867,7 @@ def test_SourceWidget__on_source_deletion_failed(mocker, session, source): def test_SourceWidget__on_source_deletion_failed_wrong_uuid(mocker, session, source): controller = mocker.MagicMock() mark_seen_signal = mocker.MagicMock() - sw = SourceWidget(controller, factory.Source(uuid="123"), mark_seen_signal) + sw = SourceWidget(controller, factory.Source(uuid="123"), mark_seen_signal, mocker.MagicMock()) sw._on_source_deleted("123") sw._on_source_deletion_failed("321") @@ -1854,7 +1889,7 @@ def test_SourceWidget_uses_SecureQLabel(mocker): source.journalist_designation = "Testy McTestface" source.collection = [factory.Message(content="a" * 121)] mark_seen_signal = mocker.MagicMock() - sw = SourceWidget(controller, source, mark_seen_signal) + sw = SourceWidget(controller, source, mark_seen_signal, mocker.MagicMock()) sw.update() assert isinstance(sw.preview, SecureQLabel) @@ -4709,7 +4744,9 @@ def test_DeleteSource_from_source_widget_when_user_is_loggedout(mocker): with mocker.patch( "securedrop_client.gui.widgets.DeleteSourceMessageBox", mock_delete_source_message_box ): - source_widget = SourceWidget(mock_controller, mock_source, mark_seen_signal) + source_widget = SourceWidget( + mock_controller, mock_source, mark_seen_signal, mocker.MagicMock() + ) source_widget.delete_source(mock_event) mock_delete_source_message_box_obj.launch.assert_not_called()