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/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..4c2273ee4a 100644 --- a/client/securedrop_client/gui/conversation/export/wizard.css +++ b/client/securedrop_client/gui/conversation/export/wizard.css @@ -1,8 +1,8 @@ #QWizard_export { min-width: 800px; max-width: 800px; - min-height: 300px; - max-height: 800px; + min-height: 600px; + max-height: 1000px; background-color: #fff; } diff --git a/client/tests/integration/conftest.py b/client/tests/integration/conftest.py index c00880041f..8d7134433a 100644 --- a/client/tests/integration/conftest.py +++ b/client/tests/integration/conftest.py @@ -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.PrintFileDialog(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..4c9d7507dd 100644 --- a/client/tests/integration/test_styles_sdclient.py +++ b/client/tests/integration/test_styles_sdclient.py @@ -3,6 +3,7 @@ from PyQt5.QtCore import QEvent 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): @@ -129,9 +130,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,45 +508,14 @@ 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() +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 300 == export_file_wizard.minimumSize().height() + assert 800 == export_file_wizard.maximumSize().height() + assert "#ffffff" == export_file_wizard.palette().color(QPalette.Background).name() + + window_buttons = export_file_wizard.layout().itemAt(4).widget() button_box = window_buttons.layout().itemAt(0).widget() button_box_children = button_box.findChildren(QPushButton) for c in button_box_children: @@ -554,14 +524,66 @@ def test_styles_for_export_file_dialog(export_file_dialog): 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 800 == page.minimumSize().width() + assert 800 == page.maximumSize().width() + assert 300 == page.minimumSize().height() + assert 800 == export_file_wizard.maximumSize().height() # TODO + 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 == export_file_wizard.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