mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
186 lines
7.2 KiB
Python
186 lines
7.2 KiB
Python
# 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()}, 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()}, 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}"
|