diff --git a/client/securedrop_client/export_status.py b/client/securedrop_client/export_status.py index da475c3fa3..65a0c43e02 100644 --- a/client/securedrop_client/export_status.py +++ b/client/securedrop_client/export_status.py @@ -31,8 +31,9 @@ class ExportStatus(Enum): SUCCESS_EXPORT = "SUCCESS_EXPORT" ERROR_EXPORT = "ERROR_EXPORT" # Could not write to disk - # Export succeeds but drives were not properly unmounted + # Export succeeds but drives were not properly closed ERROR_EXPORT_CLEANUP = "ERROR_EXPORT_CLEANUP" + ERROR_UNMOUNT_VOLUME_BUSY = "ERROR_UNMOUNT_VOLUME_BUSY" DEVICE_ERROR = "DEVICE_ERROR" # Something went wrong while trying to check the device diff --git a/client/securedrop_client/gui/actions.py b/client/securedrop_client/gui/actions.py index ff189f086a..1a0c2f6f20 100644 --- a/client/securedrop_client/gui/actions.py +++ b/client/securedrop_client/gui/actions.py @@ -15,8 +15,8 @@ from securedrop_client import state from securedrop_client.conversation import Transcript as ConversationTranscript from securedrop_client.db import Source +from securedrop_client.export import Export from securedrop_client.gui.base import ModalDialog -from securedrop_client.gui.conversation import ExportDevice from securedrop_client.gui.conversation import ( PrintTranscriptDialog as PrintConversationTranscriptDialog, ) @@ -184,7 +184,7 @@ def _on_triggered(self) -> None: # out of scope, any pending file removal will be performed # by the operating system. with open(file_path, "r") as f: - export = ExportDevice() + export = Export() dialog = PrintConversationTranscriptDialog( export, TRANSCRIPT_FILENAME, [str(file_path)] ) @@ -234,7 +234,7 @@ def _on_triggered(self) -> None: # out of scope, any pending file removal will be performed # by the operating system. with open(file_path, "r") as f: - export_device = ExportDevice() + export_device = Export() wizard = ExportWizard(export_device, TRANSCRIPT_FILENAME, [str(file_path)]) wizard.exec() @@ -320,7 +320,7 @@ def _prepare_to_export(self) -> None: # out of scope, any pending file removal will be performed # by the operating system. with ExitStack() as stack: - export_device = ExportDevice() + export_device = Export() files = [ stack.enter_context(open(file_location, "r")) for file_location in file_locations ] diff --git a/client/securedrop_client/gui/conversation/__init__.py b/client/securedrop_client/gui/conversation/__init__.py index 219c004655..c9db19eec9 100644 --- a/client/securedrop_client/gui/conversation/__init__.py +++ b/client/securedrop_client/gui/conversation/__init__.py @@ -3,7 +3,6 @@ """ # Import classes here to make possible to import them from securedrop_client.gui.conversation from .delete import DeleteConversationDialog # noqa: F401 -from .export import Export as ExportDevice # noqa: F401 from .export import ExportWizard as ExportWizard # noqa: F401 -from .export import PrintDialog as PrintFileDialog # noqa: F401 +from .export import PrintDialog # noqa: F401 from .export import PrintTranscriptDialog # noqa: F401 diff --git a/client/securedrop_client/gui/conversation/export/__init__.py b/client/securedrop_client/gui/conversation/export/__init__.py index 328c19e436..29f7a78c29 100644 --- a/client/securedrop_client/gui/conversation/export/__init__.py +++ b/client/securedrop_client/gui/conversation/export/__init__.py @@ -1,4 +1,3 @@ -from ....export import Export # noqa: F401 from .export_wizard import ExportWizard # noqa: F401 from .print_dialog import PrintDialog # noqa: F401 from .print_transcript_dialog import PrintTranscriptDialog # noqa: F401 diff --git a/client/securedrop_client/gui/conversation/export/export_wizard_page.py b/client/securedrop_client/gui/conversation/export/export_wizard_page.py index 712f2e40d4..2229a42feb 100644 --- a/client/securedrop_client/gui/conversation/export/export_wizard_page.py +++ b/client/securedrop_client/gui/conversation/export/export_wizard_page.py @@ -85,6 +85,7 @@ def _build_layout(self) -> QVBoxLayout: """ Create parent layout, draw elements, return parent layout """ + self.setObjectName("QWizard_export_page") self.setStyleSheet(self.WIZARD_CSS) parent_layout = QVBoxLayout(self) parent_layout.setContentsMargins(self.MARGIN, self.MARGIN, self.MARGIN, self.MARGIN) diff --git a/client/securedrop_client/gui/conversation/export/print_dialog.py b/client/securedrop_client/gui/conversation/export/print_dialog.py index 40eaa7c887..012a93a35d 100644 --- a/client/securedrop_client/gui/conversation/export/print_dialog.py +++ b/client/securedrop_client/gui/conversation/export/print_dialog.py @@ -97,7 +97,7 @@ def _print_file(self) -> None: self._device.print(self.filepaths) self.close() - @pyqtSlot() + @pyqtSlot(object) def _on_print_preflight_check_succeeded(self, status: ExportStatus) -> None: # We don't use the ExportStatus for now for "success" status, # but in future work we will migrate towards a wizard-style dialog, where diff --git a/client/securedrop_client/gui/conversation/export/wizard.css b/client/securedrop_client/gui/conversation/export/wizard.css index 958cf02292..5ff21542ad 100644 --- a/client/securedrop_client/gui/conversation/export/wizard.css +++ b/client/securedrop_client/gui/conversation/export/wizard.css @@ -1,9 +1,13 @@ #QWizard_export { min-width: 800px; max-width: 800px; - min-height: 300px; + min-height: 450px; max-height: 800px; - background-color: #fff; + background: #ffffff; +} + +#QWizard_export_page { + background: #ffffff; } #QWizard_header_icon, #QWizard_header_spinner { diff --git a/client/securedrop_client/gui/widgets.py b/client/securedrop_client/gui/widgets.py index 19b2f789ca..ab39824464 100644 --- a/client/securedrop_client/gui/widgets.py +++ b/client/securedrop_client/gui/widgets.py @@ -70,6 +70,7 @@ Source, User, ) +from securedrop_client.export import Export from securedrop_client.gui import conversation from securedrop_client.gui.actions import ( DeleteConversationAction, @@ -2460,7 +2461,7 @@ def _on_export_clicked(self) -> None: logger.debug("Clicked export but file not downloaded") return - export_device = conversation.ExportDevice() + export_device = Export() self.export_wizard = ExportWizard(export_device, self.file.filename, [file_location]) self.export_wizard.show() @@ -2476,9 +2477,9 @@ def _on_print_clicked(self) -> None: filepath = self.file.location(self.controller.data_dir) - export_device = conversation.ExportDevice() + export_device = Export() - dialog = conversation.PrintFileDialog(export_device, self.file.filename, [filepath]) + dialog = conversation.PrintDialog(export_device, self.file.filename, [filepath]) dialog.exec() def _on_left_click(self) -> None: diff --git a/client/tests/conftest.py b/client/tests/conftest.py index b161d25fec..37c1e7ef65 100644 --- a/client/tests/conftest.py +++ b/client/tests/conftest.py @@ -23,6 +23,7 @@ Source, make_session_maker, ) +from securedrop_client.export import Export from securedrop_client.export_status import ExportStatus from securedrop_client.gui import conversation from securedrop_client.gui.main import Window @@ -78,9 +79,9 @@ def lang(request): def print_dialog(mocker, homedir): mocker.patch("PyQt5.QtWidgets.QApplication.activeWindow", return_value=QMainWindow()) - export_device = mocker.MagicMock(spec=conversation.ExportDevice) + export_device = mocker.MagicMock(spec=Export) - dialog = conversation.PrintFileDialog(export_device, "file123.jpg", ["/mock/path/to/file"]) + dialog = conversation.PrintDialog(export_device, "file123.jpg", ["/mock/path/to/file"]) yield dialog @@ -89,7 +90,7 @@ def print_dialog(mocker, homedir): def print_transcript_dialog(mocker, homedir): mocker.patch("PyQt5.QtWidgets.QApplication.activeWindow", return_value=QMainWindow()) - export_device = mocker.MagicMock(spec=conversation.ExportDevice) + export_device = mocker.MagicMock(spec=Export) dialog = conversation.PrintTranscriptDialog( export_device, "transcript.txt", ["some/path/transcript.txt"] @@ -102,7 +103,7 @@ def print_transcript_dialog(mocker, homedir): def export_wizard_multifile(mocker, homedir): mocker.patch("PyQt5.QtWidgets.QApplication.activeWindow", return_value=QMainWindow()) - export_device = mocker.MagicMock(spec=conversation.ExportDevice) + export_device = mocker.MagicMock(spec=Export) wizard = conversation.ExportWizard( export_device, @@ -117,7 +118,7 @@ def export_wizard_multifile(mocker, homedir): def export_wizard(mocker, homedir): mocker.patch("PyQt5.QtWidgets.QApplication.activeWindow", return_value=QMainWindow()) - export_device = mocker.MagicMock(spec=conversation.ExportDevice) + export_device = mocker.MagicMock(spec=Export) dialog = conversation.ExportWizard(export_device, "file123.jpg", ["/mock/path/to/file"]) @@ -128,7 +129,7 @@ def export_wizard(mocker, homedir): def export_transcript_wizard(mocker, homedir): mocker.patch("PyQt5.QtWidgets.QApplication.activeWindow", return_value=QMainWindow()) - export_device = mocker.MagicMock(spec=conversation.ExportDevice) + export_device = mocker.MagicMock(spec=Export) dialog = conversation.ExportWizard( export_device, "transcript.txt", ["/some/path/transcript.txt"] @@ -177,7 +178,7 @@ def mock_export_locked(): * "Export" clicked, export wizard launched * Passphrase successfully entered on first attempt (and export suceeeds) """ - device = conversation.ExportDevice() + device = mock.MagicMock(spec=Export) device.run_export_preflight_checks = lambda: device.export_state_changed.emit( ExportStatus.NO_DEVICE_DETECTED @@ -201,7 +202,7 @@ def mock_export_unlocked(): * Export wizard launched * Export succeeds """ - device = conversation.ExportDevice() + device = mock.MagicMock(spec=Export) device.run_export_preflight_checks = lambda: device.export_state_changed.emit( ExportStatus.DEVICE_WRITABLE @@ -225,7 +226,7 @@ def mock_export_no_usb_then_bad_passphrase(): * Correct passphrase * Export succeeds """ - device = conversation.ExportDevice() + device = mock.MagicMock(spec=Export) device.run_export_preflight_checks = lambda: device.export_state_changed.emit( ExportStatus.NO_DEVICE_DETECTED @@ -256,7 +257,7 @@ def mock_export_fail_early(): * Unrecoverable error before export happens (eg, mount error) """ - device = conversation.ExportDevice() + device = mock.MagicMock(spec=Export) device.run_export_preflight_checks = lambda: device.export_state_changed.emit( ExportStatus.DEVICE_LOCKED diff --git a/client/tests/gui/conversation/export/test_export_wizard.py b/client/tests/gui/conversation/export/test_export_wizard.py index 24124e72d7..2b027a33a2 100644 --- a/client/tests/gui/conversation/export/test_export_wizard.py +++ b/client/tests/gui/conversation/export/test_export_wizard.py @@ -1,7 +1,8 @@ from unittest import mock +from securedrop_client.export import Export from securedrop_client.export_status import ExportStatus -from securedrop_client.gui.conversation.export import Export, ExportWizard +from securedrop_client.gui.conversation.export import ExportWizard from securedrop_client.gui.conversation.export.export_wizard_constants import STATUS_MESSAGES, Pages from securedrop_client.gui.conversation.export.export_wizard_page import ( ErrorPage, diff --git a/client/tests/gui/conversation/export/test_print_dialog.py b/client/tests/gui/conversation/export/test_print_dialog.py index 0bd4836f8e..238a9b6074 100644 --- a/client/tests/gui/conversation/export/test_print_dialog.py +++ b/client/tests/gui/conversation/export/test_print_dialog.py @@ -1,30 +1,30 @@ from securedrop_client.export_status import ExportError, ExportStatus -from securedrop_client.gui.conversation import PrintFileDialog +from securedrop_client.gui.conversation import PrintDialog from tests.helper import app # noqa: F401 -def test_PrintFileDialog_init(mocker): +def test_PrintDialog_init(mocker): _show_starting_instructions_fn = mocker.patch( - "securedrop_client.gui.conversation.PrintFileDialog._show_starting_instructions" + "securedrop_client.gui.conversation.PrintDialog._show_starting_instructions" ) - PrintFileDialog(mocker.MagicMock(), "mock.jpg", ["/mock/path/to/file"]) + PrintDialog(mocker.MagicMock(), "mock.jpg", ["/mock/path/to/file"]) _show_starting_instructions_fn.assert_called_once_with() -def test_PrintFileDialog_init_sanitizes_filename(mocker): +def test_PrintDialog_init_sanitizes_filename(mocker): secure_qlabel = mocker.patch( "securedrop_client.gui.conversation.export.print_dialog.SecureQLabel" ) filename = '' - PrintFileDialog(mocker.MagicMock(), filename, ["/mock/path/to/file"]) + PrintDialog(mocker.MagicMock(), filename, ["/mock/path/to/file"]) secure_qlabel.assert_any_call(filename, wordwrap=False, max_length=260) -def test_PrintFileDialog__show_starting_instructions(mocker, print_dialog): +def test_PrintDialog__show_starting_instructions(mocker, print_dialog): print_dialog._show_starting_instructions() # file123.jpg comes from the print_dialog fixture @@ -55,7 +55,7 @@ def test_PrintFileDialog__show_starting_instructions(mocker, print_dialog): assert not print_dialog.cancel_button.isHidden() -def test_PrintFileDialog__show_insert_usb_message(mocker, print_dialog): +def test_PrintDialog__show_insert_usb_message(mocker, print_dialog): print_dialog._show_insert_usb_message() assert print_dialog.header.text() == "Connect USB printer" @@ -68,7 +68,7 @@ def test_PrintFileDialog__show_insert_usb_message(mocker, print_dialog): assert not print_dialog.cancel_button.isHidden() -def test_PrintFileDialog__show_generic_error_message(mocker, print_dialog): +def test_PrintDialog__show_generic_error_message(mocker, print_dialog): print_dialog.error_status = "mock_error_status" print_dialog._show_generic_error_message() @@ -83,7 +83,7 @@ def test_PrintFileDialog__show_generic_error_message(mocker, print_dialog): assert not print_dialog.cancel_button.isHidden() -def test_PrintFileDialog__print_file(mocker, print_dialog): +def test_PrintDialog__print_file(mocker, print_dialog): print_dialog.close = mocker.MagicMock() print_dialog._print_file() @@ -91,7 +91,7 @@ def test_PrintFileDialog__print_file(mocker, print_dialog): print_dialog.close.assert_called_once_with() -def test_PrintFileDialog__on_print_preflight_check_succeeded(mocker, print_dialog): +def test_PrintDialog__on_print_preflight_check_succeeded(mocker, print_dialog): print_dialog._print_file = mocker.MagicMock() print_dialog.continue_button = mocker.MagicMock() print_dialog.continue_button.clicked = mocker.MagicMock() @@ -103,7 +103,7 @@ def test_PrintFileDialog__on_print_preflight_check_succeeded(mocker, print_dialo print_dialog.continue_button.clicked.connect.assert_called_once_with(print_dialog._print_file) -def test_PrintFileDialog__on_print_preflight_check_succeeded_when_continue_enabled( +def test_PrintDialog__on_print_preflight_check_succeeded_when_continue_enabled( mocker, print_dialog ): print_dialog._print_file = mocker.MagicMock() @@ -114,7 +114,7 @@ def test_PrintFileDialog__on_print_preflight_check_succeeded_when_continue_enabl print_dialog._print_file.assert_called_once_with() -def test_PrintFileDialog__on_print_preflight_check_succeeded_enabled_after_preflight_success( +def test_PrintDialog__on_print_preflight_check_succeeded_enabled_after_preflight_success( mocker, print_dialog ): assert not print_dialog.continue_button.isEnabled() @@ -122,7 +122,7 @@ def test_PrintFileDialog__on_print_preflight_check_succeeded_enabled_after_prefl assert print_dialog.continue_button.isEnabled() -def test_PrintFileDialog__on_print_preflight_check_succeeded_enabled_after_preflight_failure( +def test_PrintDialog__on_print_preflight_check_succeeded_enabled_after_preflight_failure( mocker, print_dialog ): assert not print_dialog.continue_button.isEnabled() @@ -130,7 +130,7 @@ def test_PrintFileDialog__on_print_preflight_check_succeeded_enabled_after_prefl assert print_dialog.continue_button.isEnabled() -def test_PrintFileDialog__on_print_preflight_check_failed_when_status_is_PRINTER_NOT_FOUND( +def test_PrintDialog__on_print_preflight_check_failed_when_status_is_PRINTER_NOT_FOUND( mocker, print_dialog ): print_dialog._show_insert_usb_message = mocker.MagicMock() @@ -150,7 +150,7 @@ def test_PrintFileDialog__on_print_preflight_check_failed_when_status_is_PRINTER print_dialog._show_insert_usb_message.assert_called_once_with() -def test_PrintFileDialog__on_print_preflight_check_failed_when_status_is_ERROR_PRINTER_URI( +def test_PrintDialog__on_print_preflight_check_failed_when_status_is_ERROR_PRINTER_URI( mocker, print_dialog ): print_dialog._show_generic_error_message = mocker.MagicMock() @@ -172,7 +172,7 @@ def test_PrintFileDialog__on_print_preflight_check_failed_when_status_is_ERROR_P assert print_dialog.error_status == ExportStatus.ERROR_PRINTER_URI -def test_PrintFileDialog__on_print_preflight_check_failed_when_status_is_CALLED_PROCESS_ERROR( +def test_PrintDialog__on_print_preflight_check_failed_when_status_is_CALLED_PROCESS_ERROR( mocker, print_dialog ): print_dialog._show_generic_error_message = mocker.MagicMock() @@ -194,9 +194,7 @@ def test_PrintFileDialog__on_print_preflight_check_failed_when_status_is_CALLED_ assert print_dialog.error_status == ExportStatus.CALLED_PROCESS_ERROR -def test_PrintFileDialog__on_print_preflight_check_failed_when_status_is_unknown( - mocker, print_dialog -): +def test_PrintDialog__on_print_preflight_check_failed_when_status_is_unknown(mocker, print_dialog): print_dialog._show_generic_error_message = mocker.MagicMock() print_dialog.continue_button = mocker.MagicMock() print_dialog.continue_button.clicked = mocker.MagicMock() diff --git a/client/tests/gui/test_widgets.py b/client/tests/gui/test_widgets.py index 0c3d49b8cc..e2dcbc4560 100644 --- a/client/tests/gui/test_widgets.py +++ b/client/tests/gui/test_widgets.py @@ -3589,8 +3589,7 @@ def test_FileWidget__on_export_clicked(mocker, session, source): controller = mocker.MagicMock(get_file=get_file) file_location = file.location(controller.data_dir) - # It doesn't live here, but see __init__.py - export_device = mocker.patch("securedrop_client.gui.conversation.ExportDevice") + export_device = mocker.patch("securedrop_client.export.Export") fw = FileWidget( file.uuid, controller, mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock(), 0, 123 @@ -3600,9 +3599,10 @@ def test_FileWidget__on_export_clicked(mocker, session, source): controller.run_export_preflight_checks = mocker.MagicMock() controller.downloaded_file_exists = mocker.MagicMock(return_value=True) - wizard = mocker.patch("securedrop_client.gui.conversation.export.ExportWizard") + wizard = mocker.patch("securedrop_client.gui.conversation.ExportWizard") fw._on_export_clicked() + wizard.assert_called_once() wizard.assert_called_once_with( export_device(), file.filename, [file_location] ), f"{wizard.call_args}" @@ -3650,7 +3650,7 @@ def test_FileWidget__on_print_clicked(mocker, session, source): get_file = mocker.MagicMock(return_value=file) controller = mocker.MagicMock(get_file=get_file) - export_device = mocker.patch("securedrop_client.gui.conversation.ExportDevice") + export_device = mocker.patch("securedrop_client.export.Export") file_location = file.location(controller.data_dir) fw = FileWidget( @@ -3667,7 +3667,7 @@ def test_FileWidget__on_print_clicked(mocker, session, source): controller.print_file = mocker.MagicMock() controller.downloaded_file_exists = mocker.MagicMock(return_value=True) - dialog = mocker.patch("securedrop_client.gui.conversation.PrintFileDialog") + dialog = mocker.patch("securedrop_client.gui.conversation.PrintDialog") fw._on_print_clicked() @@ -3698,7 +3698,7 @@ def test_FileWidget__on_print_clicked_missing_file(mocker, session, source): mocker.patch("PyQt5.QtWidgets.QDialog.exec") controller.print_file = mocker.MagicMock() controller.downloaded_file_exists = mocker.MagicMock(return_value=False) - dialog = mocker.patch("securedrop_client.gui.conversation.PrintFileDialog") + dialog = mocker.patch("securedrop_client.gui.conversation.PrintDialog") fw._on_print_clicked() diff --git a/client/tests/integration/conftest.py b/client/tests/integration/conftest.py index c00880041f..ae0b9188af 100644 --- a/client/tests/integration/conftest.py +++ b/client/tests/integration/conftest.py @@ -2,10 +2,10 @@ from PyQt5.QtWidgets import QApplication from securedrop_client.app import threads +from securedrop_client.export import Export from securedrop_client.export_status import ExportStatus from securedrop_client.gui import conversation from securedrop_client.gui.base import ModalDialog -from securedrop_client.gui.conversation.export import Export from securedrop_client.gui.main import Window from securedrop_client.logic import Controller from tests import factory @@ -148,14 +148,14 @@ def modal_dialog(mocker, homedir): @pytest.fixture(scope="function") def mock_export(mocker): - device = Export() + export = mocker.MagicMock(spec=Export) """A export that assumes the Qubes RPC calls are successful and skips them.""" - device.run_preflight_checks = lambda: ExportStatus.DEVICE_LOCKED - device.send_file_to_usb_device = lambda paths, passphrase: ExportStatus.SUCCESS_EXPORT - device.run_printer_preflight = lambda: ExportStatus.PRINT_PREFLIGHT_SUCCESS - device.run_print = lambda paths: ExportStatus.PRINT_SUCCESS - return device + export.run_export_preflight_checks = lambda: ExportStatus.DEVICE_LOCKED + export.export = lambda paths, passphrase: ExportStatus.SUCCESS_EXPORT + export.run_printer_preflight_checks = lambda: ExportStatus.PRINT_PREFLIGHT_SUCCESS + export.print = lambda paths: ExportStatus.PRINT_SUCCESS + return export @pytest.fixture(scope="function") @@ -184,8 +184,8 @@ def print_dialog(mocker, homedir): controller.qubes = False gui.setup(controller) gui.login_dialog.close() - export_device = conversation.ExportDevice() - dialog = conversation.PrintFileDialog(export_device, "file_name", ["/mock/export/file"]) + export = Export() + dialog = conversation.PrintDialog(export, "file_name", ["/mock/export/file"]) yield dialog @@ -195,8 +195,9 @@ def print_dialog(mocker, homedir): @pytest.fixture(scope="function") -def export_file_dialog(mocker, homedir): +def export_file_wizard(mocker, homedir): mocker.patch("securedrop_client.export.Export", return_value=mock_export) + export = Export() app = QApplication([]) gui = Window() app.setActiveWindow(gui) @@ -217,8 +218,7 @@ def export_file_dialog(mocker, homedir): controller.qubes = False gui.setup(controller) gui.login_dialog.close() - export_device = conversation.ExportDevice() - dialog = conversation.ExportDialog(export_device, "file_name", ["/mock/export/filepath"]) + dialog = conversation.ExportWizard(export, "file_name", ["/mock/export/filepath"]) dialog.show() yield dialog diff --git a/client/tests/integration/test_styles_sdclient.py b/client/tests/integration/test_styles_sdclient.py index fa5d0484ab..177cb54ad6 100644 --- a/client/tests/integration/test_styles_sdclient.py +++ b/client/tests/integration/test_styles_sdclient.py @@ -4,6 +4,11 @@ from PyQt5.QtGui import QFont, QPalette from PyQt5.QtWidgets import QLabel, QLineEdit, QPushButton, QWidget +from securedrop_client.gui.conversation.export.export_wizard_page import ( + PassphraseWizardPage, + PreflightPage, +) + def test_css(main_window): login_dialog = main_window.login_dialog @@ -129,9 +134,9 @@ def test_class_name_matches_css_object_name_for_print_dialog(print_dialog): assert "PrintDialog" == print_dialog.__class__.__name__ -def test_class_name_matches_css_object_name_for_export_file_dialog(export_file_dialog): - assert "ExportDialog" == export_file_dialog.__class__.__name__ - assert "ExportDialog" in export_file_dialog.passphrase_form.objectName() +def test_class_name_matches_css_object_name_for_export_file_dialog(export_file_wizard): + assert "ExportWizard" == export_file_wizard.__class__.__name__ + assert "QWizard_export" in export_file_wizard.objectName() def test_class_name_matches_css_object_name_for_modal_dialog(modal_dialog): @@ -507,61 +512,76 @@ def test_styles_for_print_dialog(print_dialog): assert 15 == c.font().pixelSize() -def test_styles_for_export_file_dialog(export_file_dialog): - assert 800 == export_file_dialog.minimumSize().width() - assert 800 == export_file_dialog.maximumSize().width() - assert 300 == export_file_dialog.minimumSize().height() - assert 800 == export_file_dialog.maximumSize().height() - assert "#ffffff" == export_file_dialog.palette().color(QPalette.Background).name() - assert 110 == export_file_dialog.header_icon.minimumSize().width() # 80px + 30px margin - assert 110 == export_file_dialog.header_icon.maximumSize().width() # 80px + 30px margin - assert 64 == export_file_dialog.header_icon.minimumSize().height() # 64px + 0px margin - assert 64 == export_file_dialog.header_icon.maximumSize().height() # 64px + 0px margin - assert ( - 110 == export_file_dialog.header_spinner_label.minimumSize().width() - ) # 80px + 30px margin - assert ( - 110 == export_file_dialog.header_spinner_label.maximumSize().width() - ) # 80px + 30px margin - assert 64 == export_file_dialog.header_spinner_label.minimumSize().height() # 64px + 0px margin - assert 64 == export_file_dialog.header_spinner_label.maximumSize().height() # 64px + 0px margin - assert 68 == export_file_dialog.header.minimumSize().height() # 68px + 0px margin - assert 68 == export_file_dialog.header.maximumSize().height() # 68px + 0px margin - assert "Montserrat" == export_file_dialog.header.font().family() - assert QFont.Bold == export_file_dialog.header.font().weight() - assert 24 == export_file_dialog.header.font().pixelSize() - assert "#2a319d" == export_file_dialog.header.palette().color(QPalette.Foreground).name() - assert (0, 0, 0, 0) == export_file_dialog.header.getContentsMargins() - assert 2 == export_file_dialog.header_line.minimumSize().height() # 2px + 20px margin - assert 2 == export_file_dialog.header_line.maximumSize().height() # 2px + 20px margin - assert 38 == math.floor(255 * 0.15) # sanity check - assert ( - 38 == export_file_dialog.header_line.palette().color(QPalette.Background).rgba64().alpha8() - ) - assert 42 == export_file_dialog.header_line.palette().color(QPalette.Background).red() - assert 49 == export_file_dialog.header_line.palette().color(QPalette.Background).green() - assert 157 == export_file_dialog.header_line.palette().color(QPalette.Background).blue() - - assert "Montserrat" == export_file_dialog.body.font().family() - assert 16 == export_file_dialog.body.font().pixelSize() - assert "#302aa3" == export_file_dialog.body.palette().color(QPalette.Foreground).name() - window_buttons = export_file_dialog.layout().itemAt(4).widget() - button_box = window_buttons.layout().itemAt(0).widget() - button_box_children = button_box.findChildren(QPushButton) +def test_styles_for_export_file_wizard(export_file_wizard): + assert 800 == export_file_wizard.minimumSize().width() + assert 800 == export_file_wizard.maximumSize().width() + assert 450 == export_file_wizard.minimumSize().height() + assert 800 == export_file_wizard.maximumSize().height() + assert "#ffffff" == export_file_wizard.palette().color(QPalette.Background).name() + + button_box_children = [ + export_file_wizard.next_button, + export_file_wizard.back_button, + export_file_wizard.finish_button, + export_file_wizard.cancel_button, + ] + for c in button_box_children: - assert 44 == c.height() # 40px + 4px of border + assert 40 == c.height() assert "Montserrat" == c.font().family() assert QFont.DemiBold - 1 == c.font().weight() assert 15 == c.font().pixelSize() - passphrase_children_qlabel = export_file_dialog.passphrase_form.findChildren(QLabel) + +def test_styles_for_export_file_wizard_page(export_file_wizard): + page = export_file_wizard.currentPage() + assert isinstance(page, PreflightPage) + assert "#ffffff" == page.palette().color(QPalette.Background).name() + assert 110 == page.header_icon.minimumSize().width() # 80px + 30px margin + assert 110 == page.header_icon.maximumSize().width() # 80px + 30px margin + assert 64 == page.header_icon.minimumSize().height() # 64px + 0px margin + assert 64 == page.header_icon.maximumSize().height() # 64px + 0px margin + assert 110 == page.header_spinner_label.minimumSize().width() # 80px + 30px margin + assert 110 == page.header_spinner_label.maximumSize().width() # 80px + 30px margin + assert 64 == page.header_spinner_label.minimumSize().height() # 64px + 0px margin + assert 64 == page.header_spinner_label.maximumSize().height() # 64px + 0px margin + assert 68 == page.header.minimumSize().height() # 68px + 0px margin + assert 68 == page.header.maximumSize().height() # 68px + 0px margin + assert "Montserrat" == page.header.font().family() + assert QFont.Bold == page.header.font().weight() + assert 24 == page.header.font().pixelSize() + assert "#2a319d" == page.header.palette().color(QPalette.Foreground).name() + assert (0, 0, 0, 0) == page.header.getContentsMargins() + assert 2 == page.header_line.minimumSize().height() # 2px + 20px margin + assert 2 == page.header_line.maximumSize().height() # 2px + 20px margin + assert 38 == math.floor(255 * 0.15) # sanity check + assert 38 == page.header_line.palette().color(QPalette.Background).rgba64().alpha8() + assert 42 == page.header_line.palette().color(QPalette.Background).red() + assert 49 == page.header_line.palette().color(QPalette.Background).green() + assert 157 == page.header_line.palette().color(QPalette.Background).blue() + + assert "Montserrat" == page.body.font().family() + assert 16 == page.body.font().pixelSize() + assert "#302aa3" == page.body.palette().color(QPalette.Foreground).name() + + +def test_style_passphrase_wizard_page(export_file_wizard): + page = export_file_wizard.currentPage() + assert isinstance(page, PreflightPage) + export_file_wizard.next() + + # the mock_export fixture starts with a device inserted, so the next page will be + # the passphrase prompt + assert isinstance(page, PassphraseWizardPage) + + passphrase_children_qlabel = page.passphrase_form.findChildren(QLabel) for c in passphrase_children_qlabel: assert "Montserrat" == c.font().family() or "Source Sans Pro" == c.font().family() assert QFont.DemiBold - 1 == c.font().weight() assert 12 == c.font().pixelSize() assert "#2a319d" == c.palette().color(QPalette.Foreground).name() - form_children_qlineedit = export_file_dialog.passphrase_form.findChildren(QLineEdit) + form_children_qlineedit = page.passphrase_form.findChildren(QLineEdit) for c in form_children_qlineedit: assert 32 == c.minimumSize().height() # 30px + 2px padding-bottom assert 32 == c.maximumSize().height() # 30px + 2px padding-bottom diff --git a/client/tests/test_export.py b/client/tests/test_export.py index 0244f2bdba..b3f7c7fa58 100644 --- a/client/tests/test_export.py +++ b/client/tests/test_export.py @@ -6,8 +6,8 @@ import pytest from PyQt5.QtTest import QSignalSpy +from securedrop_client.export import Export from securedrop_client.export_status import ExportError, ExportStatus -from securedrop_client.gui.conversation.export import Export from tests import factory _PATH_TO_PRETEND_ARCHIVE = "/tmp/archive-pretend"