# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import import os from unittest import mock import pytest from bec_lib.endpoints import MessageEndpoints from qtpy.QtGui import QFontMetrics import bec_widgets from bec_widgets.applications.launch_window import LaunchWindow from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, UILaunchWindow from .client_mocks import mocked_client base_path = os.path.dirname(bec_widgets.__file__) @pytest.fixture def bec_launch_window(qtbot, mocked_client): widget = LaunchWindow(client=mocked_client) qtbot.addWidget(widget) qtbot.waitExposed(widget) yield widget def test_launch_window_initialization(bec_launch_window): assert isinstance(bec_launch_window, BECMainWindow) def test_launch_window_launch_ui_file(bec_launch_window): # Mock the file dialog to return a specific UI file path ui_file_path = os.path.join( base_path, "widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui" ) bec_launch_window._open_custom_ui_file = lambda: ui_file_path # Call the method to launch the custom UI file res = bec_launch_window.launch("custom_ui_file", ui_file=ui_file_path) assert isinstance(res, UILaunchWindow) # Check if the custom UI file was launched correctly assert res.object_name == "positioner_box" assert res.windowTitle() == "BEC - positioner_box" # We need to manually close the launched window as it is not registered with qtbot. # In real usage, the GUIServer would handle this in the sigint handler in case of a ctrl-c initiated shutdown. res.close() res.deleteLater() def test_launch_window_launch_ui_file_raises_for_qmainwindow(bec_launch_window): # Mock the file dialog to return a specific UI file path # the selected file must contain a QMainWindow widget but can be any file ui_file_path = os.path.join(base_path, "examples/general_app/general_app.ui") # Call the method to launch the custom UI file with pytest.raises(ValueError) as excinfo: bec_launch_window.launch("custom_ui_file", ui_file=ui_file_path) assert "Loading a QMainWindow from a UI file is currently not supported." in str(excinfo.value) def test_launch_window_launch_default_auto_update(bec_launch_window): # Mock the auto update selection bec_launch_window.tiles["auto_update"].selector.setCurrentText("Default") # Call the method to launch the auto update res = bec_launch_window._open_auto_update() assert isinstance(res, AutoUpdates) assert res.windowTitle() == "BEC - AutoUpdates" # We need to manually close the launched window as it is not registered with qtbot. # In real usage, the GUIServer would handle this in the sigint handler in case of a ctrl-c initiated shutdown. res.close() res.deleteLater() def test_launch_window_launch_plugin_auto_update(bec_launch_window): class PluginAutoUpdate(AutoUpdates): ... bec_launch_window.available_auto_updates = {"PluginAutoUpdate": PluginAutoUpdate} bec_launch_window.tiles["auto_update"].selector.clear() bec_launch_window.tiles["auto_update"].selector.addItems( list(bec_launch_window.available_auto_updates.keys()) + ["Default"] ) bec_launch_window.tiles["auto_update"].selector.setCurrentText("PluginAutoUpdate") res = bec_launch_window._open_auto_update() assert isinstance(res, PluginAutoUpdate) assert res.windowTitle() == "BEC - PluginAutoUpdate" # We need to manually close the launched window as it is not registered with qtbot. # In real usage, the GUIServer would handle this in the sigint handler in case of a ctrl-c initiated shutdown. res.close() res.deleteLater() @pytest.mark.parametrize( "connections, hide", [ ({}, False), ({"launcher": mock.MagicMock()}, False), ({"launcher": mock.MagicMock(), "dock_area": mock.MagicMock()}, False), ( { "launcher": mock.MagicMock(), "dock_area": mock.MagicMock(), "scan_progress": mock.MagicMock(), }, False, ), ( { "launcher": mock.MagicMock(), "dock_area": mock.MagicMock(), "scan_progress_simple": mock.MagicMock(), "scan_progress_full": mock.MagicMock(), }, False, ), ( { "launcher": mock.MagicMock(), "dock_area": mock.MagicMock(), "scan_progress_simple": mock.MagicMock(), "scan_progress_full": mock.MagicMock(), "hover_widget": mock.MagicMock(), }, True, ), ], ) def test_gui_server_turns_off_the_lights(bec_launch_window, connections, hide): with ( mock.patch.object(bec_launch_window, "show") as mock_show, mock.patch.object(bec_launch_window, "activateWindow") as mock_activate_window, mock.patch.object(bec_launch_window, "raise_") as mock_raise, mock.patch.object(bec_launch_window, "hide") as mock_hide, mock.patch.object( bec_launch_window.app, "setQuitOnLastWindowClosed" ) as mock_set_quit_on_last_window_closed, ): bec_launch_window._turn_off_the_lights(connections) if hide: mock_hide.assert_called_once() mock_set_quit_on_last_window_closed.assert_called_once_with(False) else: mock_show.assert_called_once() mock_activate_window.assert_called_once() mock_raise.assert_called_once() mock_set_quit_on_last_window_closed.assert_called_once_with(True) @pytest.mark.parametrize( "connections, close_called", [ ({}, True), ({"launcher": mock.MagicMock()}, True), ({"launcher": mock.MagicMock(), "dock_area": mock.MagicMock()}, True), ( { "launcher": mock.MagicMock(), "dock_area": mock.MagicMock(), "scan_progress": mock.MagicMock(), }, True, ), ( { "launcher": mock.MagicMock(), "dock_area": mock.MagicMock(), "scan_progress_simple": mock.MagicMock(), "scan_progress_full": mock.MagicMock(), }, True, ), ( { "launcher": mock.MagicMock(), "dock_area": mock.MagicMock(), "scan_progress_simple": mock.MagicMock(), "scan_progress_full": mock.MagicMock(), "hover_widget": mock.MagicMock(), }, False, ), ], ) def test_launch_window_closes(bec_launch_window, connections, close_called): """ Test that the close event is handled correctly based on the connections. If there are no connections or only the launcher connection, the window should close. If there are other connections, the window should hide instead of closing. """ close_event = mock.MagicMock() with mock.patch.object( bec_launch_window.register, "list_all_connections", return_value=connections ): with mock.patch.object(bec_launch_window, "hide") as mock_hide: bec_launch_window.closeEvent(close_event) if close_called: mock_hide.assert_not_called() close_event.accept.assert_called_once() else: mock_hide.assert_called_once() close_event.accept.assert_not_called() close_event.ignore.assert_called_once() def test_main_label_fits_tile_width(bec_launch_window, qtbot): """ Every tile’s main label must render in a single line and its text width must not exceed the usable width of the tile. """ for name, tile in bec_launch_window.tiles.items(): label = tile.main_label qtbot.waitUntil(lambda: label.isVisible()) metrics = QFontMetrics(label.font()) text_width = metrics.horizontalAdvance(label.text()) content_width = ( tile.tile_size[0] - tile.layout.contentsMargins().left() - tile.layout.contentsMargins().right() ) assert text_width <= content_width, f"{name} main label exceeds tile width" # _fit_label_to_width disables wrapping, so confirm that: assert not label.wordWrap(), f"{name} main label is wrapped" def test_main_label_point_size_uniform(bec_launch_window): """ The launcher should unify all main-label font sizes to the smallest needed size, so every tile shares the same point size. """ point_sizes = {tile.main_label.font().pointSize() for tile in bec_launch_window.tiles.values()} assert len(point_sizes) == 1, f"Non-uniform main-label point sizes: {point_sizes}"