From 2b5b7360aed3e8f4246d87e6f7fc567bd2dbe78f Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 16 Jan 2026 19:56:55 +0100 Subject: [PATCH] test(device-manager-view): improve test coverage for device-manager-view --- .../device_manager_display_widget.py | 48 +++-------- tests/unit_tests/test_device_manager_view.py | 86 +++++++++++++++++++ 2 files changed, 100 insertions(+), 34 deletions(-) diff --git a/bec_widgets/applications/views/device_manager_view/device_manager_display_widget.py b/bec_widgets/applications/views/device_manager_view/device_manager_display_widget.py index b414090d..640f6b20 100644 --- a/bec_widgets/applications/views/device_manager_view/device_manager_display_widget.py +++ b/bec_widgets/applications/views/device_manager_view/device_manager_display_widget.py @@ -77,27 +77,27 @@ class CustomBusyWidget(QWidget): super().__init__(parent=parent) # Widgets - progress = DeviceInitializationProgressBar(parent=self, client=client) - progress.setMinimumWidth(320) + self.progress = DeviceInitializationProgressBar(parent=self, client=client) + self.progress.setMinimumWidth(320) # Spinner - spinner = SpinnerWidget(parent=self) + self.spinner = SpinnerWidget(parent=self) scale = self._ui_scale() spinner_size = int(scale * 0.12) if scale else 1 spinner_size = max(32, min(spinner_size, 96)) - spinner.setFixedSize(spinner_size, spinner_size) + self.spinner.setFixedSize(spinner_size, spinner_size) # Cancel button - cancel_button = QPushButton("Cancel Upload", parent=self) - cancel_button.setIcon(material_icon("cancel")) - cancel_button.clicked.connect(self.cancel_requested.emit) + self.cancel_button = QPushButton("Cancel Upload", parent=self) + self.cancel_button.setIcon(material_icon("cancel")) + self.cancel_button.clicked.connect(self.cancel_requested.emit) button_height = int(spinner_size * 0.9) button_height = max(36, min(button_height, 72)) aspect_ratio = 3.8 # width / height, visually stable for text buttons button_width = int(button_height * aspect_ratio) - cancel_button.setFixedSize(button_width, button_height) + self.cancel_button.setFixedSize(button_width, button_height) color = get_accent_colors() - cancel_button.setStyleSheet( + self.cancel_button.setStyleSheet( f""" QPushButton {{ background-color: {color.emergency.name()}; @@ -113,10 +113,10 @@ class CustomBusyWidget(QWidget): content_layout.setContentsMargins(24, 24, 24, 24) content_layout.setSpacing(16) content_layout.addStretch() - content_layout.addWidget(spinner, 0, Qt.AlignmentFlag.AlignHCenter) - content_layout.addWidget(progress, 0, Qt.AlignmentFlag.AlignHCenter) + content_layout.addWidget(self.spinner, 0, Qt.AlignmentFlag.AlignHCenter) + content_layout.addWidget(self.progress, 0, Qt.AlignmentFlag.AlignHCenter) content_layout.addStretch() - content_layout.addWidget(cancel_button, 0, Qt.AlignmentFlag.AlignHCenter) + content_layout.addWidget(self.cancel_button, 0, Qt.AlignmentFlag.AlignHCenter) if hasattr(color, "_colors"): bg_color = color._colors.get("BG", None) @@ -138,14 +138,12 @@ class CustomBusyWidget(QWidget): def showEvent(self, event): """Show event to start the spinner.""" super().showEvent(event) - for child in self.findChildren(SpinnerWidget): - child.start() + self.spinner.start() def hideEvent(self, event): """Hide event to stop the spinner.""" super().hideEvent(event) - for child in self.findChildren(SpinnerWidget): - child.stop() + self.spinner.stop() class DeviceManagerDisplayWidget(DockAreaWidget): @@ -171,9 +169,6 @@ class DeviceManagerDisplayWidget(DockAreaWidget): self._config_helper = config_helper.ConfigHelper(self.client.connector) self._shared_selection = SharedSelectionSignal() - # Custom upload widget for busy overlay - self._custom_overlay_widget: QWidget | None = None - # Device Table View widget self.device_table_view = DeviceTable(self) @@ -687,20 +682,9 @@ class DeviceManagerDisplayWidget(DockAreaWidget): # Config is in sync with BEC, so we update the state self.device_table_view.device_config_in_sync_with_redis.emit(False) - # Cleanup custom overlay widget - if self._custom_overlay_widget is not None: - self._custom_overlay_widget.close() - self._custom_overlay_widget.deleteLater() - self._custom_overlay_widget = None - def _handle_push_complete_to_communicator(self): """Handle completion of the config push to Redis.""" self._set_busy_wrapper(enabled=False) - # Cleanup custom overlay widget - if self._custom_overlay_widget is not None: - self._custom_overlay_widget.close() - self._custom_overlay_widget.deleteLater() - self._custom_overlay_widget = None def _handle_exception_from_communicator(self, exception: Exception): """Handle exceptions from the config communicator.""" @@ -710,10 +694,6 @@ class DeviceManagerDisplayWidget(DockAreaWidget): f"An error occurred while uploading the configuration to BEC Server:\n{str(exception)}", ) self._set_busy_wrapper(enabled=False) - if self._custom_overlay_widget is not None: - self._custom_overlay_widget.close() - self._custom_overlay_widget.deleteLater() - self._custom_overlay_widget = None @SafeSlot() def _save_to_disk_action(self): diff --git a/tests/unit_tests/test_device_manager_view.py b/tests/unit_tests/test_device_manager_view.py index 0691b6df..e7326c53 100644 --- a/tests/unit_tests/test_device_manager_view.py +++ b/tests/unit_tests/test_device_manager_view.py @@ -30,6 +30,7 @@ from bec_widgets.applications.views.device_manager_view.device_manager_view impo DeviceManagerView, DeviceManagerWidget, ) +from bec_widgets.utils.colors import get_accent_colors from bec_widgets.widgets.control.device_manager.components import ( DeviceTable, DMConfigView, @@ -612,6 +613,34 @@ class TestDeviceManagerView: cfg_iter.append(dev_config_copy) return cfg_iter + def test_custom_busy_widget(self, custom_busy: CustomBusyWidget, qtbot): + """Test the CustomBusyWidget functionality.""" + + # Check layout + assert custom_busy.progress is not None + assert custom_busy.spinner is not None + assert custom_busy.spinner._started is False + + # Check background + color = get_accent_colors() + bg = color._colors["BG"] + sheet = custom_busy.styleSheet() + assert bg.name() in sheet + assert "border-radius: 12px" in sheet + + # Show event should start spinner + custom_busy.showEvent(None) + assert custom_busy.spinner._started is True + + with qtbot.waitSignal(custom_busy.cancel_requested) as sig_blocker: + qtbot.mouseClick(custom_busy.cancel_button, QtCore.Qt.LeftButton) + # Check that the signal was emitted + assert sig_blocker.signal_triggered is True + + # Hide should + custom_busy.hideEvent(None) + assert custom_busy.spinner._started is False + def test_device_manager_view_add_remove_device( self, device_manager_display_widget: DeviceManagerDisplayWidget, device_config ): @@ -750,3 +779,60 @@ class TestDeviceManagerView: "rerun_validation" ].action.action.triggered.emit() assert len(mock_change_configs.call_args[0][0]) == 1 + + def test_handle_cancel_config_upload_failed( + self, device_manager_display_widget: DeviceManagerDisplayWidget, qtbot + ): + """Test handling cancel during config upload failure.""" + dm_view = device_manager_display_widget + validation_results = { + "Device_1": ( + {"name": "Device_1"}, + ConfigStatus.VALID.value, + ConnectionStatus.CANNOT_CONNECT.value, + ), + "Device_2": ( + {"name": "Device_2"}, + ConfigStatus.INVALID.value, + ConnectionStatus.UNKNOWN.value, + ), + } + with mock.patch.object( + dm_view.device_table_view, "get_validation_results", return_value=validation_results + ): + with ( + mock.patch.object( + dm_view.device_table_view, "update_multiple_device_validations" + ) as mock_update, + mock.patch.object( + dm_view.ophyd_test_view, "change_device_configs" + ) as mock_change_configs, + ): + with qtbot.waitSignal( + dm_view.device_table_view.device_config_in_sync_with_redis + ) as sig_blocker: + dm_view._handle_cancel_config_upload_failed( + exception=Exception("Test Exception") + ) + assert sig_blocker.signal_triggered is True + mock_change_configs.assert_called_once_with( + [validation_results["Device_1"][0], validation_results["Device_2"][0]], + added=True, + skip_validation=False, + ) + mock_update.assert_called_once_with( + [ + ( + validation_results["Device_1"][0], + validation_results["Device_1"][1], + ConnectionStatus.UNKNOWN.value, + "Upload Cancelled", + ), + ( + validation_results["Device_2"][0], + validation_results["Device_2"][1], + ConnectionStatus.UNKNOWN.value, + "Upload Cancelled", + ), + ] + )