# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import import base64 import os from unittest import mock from unittest.mock import MagicMock, patch import pytest from qtpy.QtCore import QSettings, Qt, QTimer from qtpy.QtGui import QPixmap from qtpy.QtWidgets import QDialog, QMessageBox, QWidget import bec_widgets.widgets.containers.advanced_dock_area.basic_dock_area as basic_dock_module import bec_widgets.widgets.containers.advanced_dock_area.profile_utils as profile_utils from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import ( AdvancedDockArea, SaveProfileDialog, ) from bec_widgets.widgets.containers.advanced_dock_area.basic_dock_area import ( DockAreaWidget, DockSettingsDialog, ) from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import ( SETTINGS_KEYS, default_profile_path, get_profile_info, is_profile_read_only, is_quick_select, list_profiles, load_default_profile_screenshot, load_user_profile_screenshot, open_default_settings, open_user_settings, plugin_profiles_dir, read_manifest, restore_user_from_default, set_quick_select, user_profile_path, write_manifest, ) from bec_widgets.widgets.containers.advanced_dock_area.settings.dialogs import ( PreviewPanel, RestoreProfileDialog, ) from bec_widgets.widgets.containers.advanced_dock_area.settings.workspace_manager import ( WorkSpaceManager, ) from .client_mocks import mocked_client @pytest.fixture def advanced_dock_area(qtbot, mocked_client): """Create an AdvancedDockArea instance for testing.""" widget = AdvancedDockArea(client=mocked_client) qtbot.addWidget(widget) qtbot.waitExposed(widget) yield widget @pytest.fixture(autouse=True) def isolate_profile_storage(tmp_path, monkeypatch): """Ensure each test writes profiles into a unique temporary directory.""" root = tmp_path / "profiles_root" root.mkdir(parents=True, exist_ok=True) monkeypatch.setenv("BECWIDGETS_PROFILE_DIR", str(root)) yield @pytest.fixture def temp_profile_dir(): """Return the current temporary profile directory.""" return os.environ["BECWIDGETS_PROFILE_DIR"] @pytest.fixture def module_profile_factory(monkeypatch, tmp_path): """Provide a helper to create synthetic module-level (read-only) profiles.""" module_dir = tmp_path / "module_profiles" module_dir.mkdir(parents=True, exist_ok=True) monkeypatch.setattr(profile_utils, "module_profiles_dir", lambda: str(module_dir)) monkeypatch.setattr(profile_utils, "plugin_profiles_dir", lambda: None) def _create(name="readonly_profile", content="[profile]\n"): path = module_dir / f"{name}.ini" path.write_text(content) return name return _create @pytest.fixture def workspace_manager_target(): class _Signal: def __init__(self): self._slot = None def connect(self, slot): self._slot = slot def emit(self, value): if self._slot: self._slot(value) class _Combo: def __init__(self): self.current_text = "" def setCurrentText(self, text): self.current_text = text class _Action: def __init__(self, widget): self.widget = widget class _Components: def __init__(self, combo): self._combo = combo def get_action(self, name): return _Action(self._combo) class _Toolbar: def __init__(self, combo): self.components = _Components(combo) class _Target: def __init__(self): self.profile_changed = _Signal() self._combo = _Combo() self.toolbar = _Toolbar(self._combo) self._current_profile_name = None self.load_profile_calls = [] self.save_called = False self.refresh_calls = 0 def load_profile(self, name): self.load_profile_calls.append(name) self._current_profile_name = name def save_profile(self): self.save_called = True def _refresh_workspace_list(self): self.refresh_calls += 1 def _factory(): return _Target() return _factory @pytest.fixture def basic_dock_area(qtbot, mocked_client): """Create a namesake DockAreaWidget without the advanced toolbar.""" widget = DockAreaWidget(client=mocked_client, title="Test Dock Area") qtbot.addWidget(widget) qtbot.waitExposed(widget) yield widget class _NamespaceProfiles: """Helper that routes profile file helpers through a namespace.""" def __init__(self, widget: AdvancedDockArea): self.namespace = widget.profile_namespace def open_user(self, name: str): return open_user_settings(name, namespace=self.namespace) def open_default(self, name: str): return open_default_settings(name, namespace=self.namespace) def user_path(self, name: str) -> str: return user_profile_path(name, namespace=self.namespace) def default_path(self, name: str) -> str: return default_profile_path(name, namespace=self.namespace) def list_profiles(self) -> list[str]: return list_profiles(namespace=self.namespace) def set_quick_select(self, name: str, enabled: bool): set_quick_select(name, enabled, namespace=self.namespace) def is_quick_select(self, name: str) -> bool: return is_quick_select(name, namespace=self.namespace) def profile_helper(widget: AdvancedDockArea) -> _NamespaceProfiles: """Return a helper wired to the widget's profile namespace.""" return _NamespaceProfiles(widget) class TestBasicDockArea: """Focused coverage for the lightweight DockAreaWidget base.""" def test_new_widget_instance_registers_in_maps(self, basic_dock_area): panel = QWidget(parent=basic_dock_area) panel.setObjectName("basic_panel") dock = basic_dock_area.new(panel, return_dock=True) assert dock.objectName() == "basic_panel" assert basic_dock_area.dock_map()["basic_panel"] is dock assert basic_dock_area.widget_map()["basic_panel"] is panel def test_new_widget_string_creates_widget(self, basic_dock_area, qtbot): basic_dock_area.new("DarkModeButton") qtbot.waitUntil(lambda: len(basic_dock_area.dock_list()) > 0, timeout=1000) assert basic_dock_area.widget_list() def test_custom_close_handler_invoked(self, basic_dock_area, qtbot): class CloseAwareWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) self.setObjectName("closable") self.closed = False def handle_dock_close(self, dock, widget): # pragma: no cover - exercised via signal self.closed = True dock.closeDockWidget() dock.deleteDockWidget() widget = CloseAwareWidget(parent=basic_dock_area) dock = basic_dock_area.new(widget, return_dock=True) dock.closeRequested.emit() qtbot.waitUntil(lambda: widget.closed, timeout=1000) assert widget.closed is True assert "closable" not in basic_dock_area.dock_map() def test_attach_all_and_delete_all(self, basic_dock_area): first = QWidget(parent=basic_dock_area) first.setObjectName("floating_one") second = QWidget(parent=basic_dock_area) second.setObjectName("floating_two") dock_one = basic_dock_area.new(first, return_dock=True, start_floating=True) dock_two = basic_dock_area.new(second, return_dock=True, start_floating=True) assert dock_one.isFloating() and dock_two.isFloating() basic_dock_area.attach_all() assert not dock_one.isFloating() assert not dock_two.isFloating() basic_dock_area.delete_all() assert basic_dock_area.dock_list() == [] def test_manifest_serialization_includes_floating_geometry( self, basic_dock_area, qtbot, tmp_path ): anchored = QWidget(parent=basic_dock_area) anchored.setObjectName("anchored_widget") floating = QWidget(parent=basic_dock_area) floating.setObjectName("floating_widget") basic_dock_area.new(anchored, return_dock=True) dock_floating = basic_dock_area.new(floating, return_dock=True, start_floating=True) qtbot.waitUntil(lambda: dock_floating.isFloating(), timeout=2000) settings_path = tmp_path / "manifest.ini" settings = QSettings(str(settings_path), QSettings.IniFormat) write_manifest(settings, basic_dock_area.dock_list()) settings.sync() manifest_entries = read_manifest(settings) assert len(manifest_entries) == 2 assert manifest_entries[0]["object_name"] == "floating_widget" assert manifest_entries[0]["floating"] is True assert manifest_entries[0]["floating_relative"] is not None assert manifest_entries[1]["object_name"] == "anchored_widget" assert manifest_entries[1]["floating"] is False def test_splitter_weight_coercion_supports_aliases(self, basic_dock_area): weights = {"default": 0.5, "left": 2, "center": 3, "right": 4} result = basic_dock_area._coerce_weights(weights, 3, Qt.Orientation.Horizontal) assert result == [2.0, 3.0, 4.0] assert basic_dock_area._coerce_weights([0.0], 3, Qt.Orientation.Vertical) == [0.0, 1.0, 1.0] assert basic_dock_area._coerce_weights([0.0, 0.0], 2, Qt.Orientation.Vertical) == [1.0, 1.0] def test_splitter_override_keys_are_normalized(self, basic_dock_area): overrides = {0: [1, 2], (1, 0): [3, 4], "2.1": [5], " / ": [6]} normalized = basic_dock_area._normalize_override_keys(overrides) assert normalized == {(0,): [1, 2], (1, 0): [3, 4], (2, 1): [5], (): [6]} def test_schedule_splitter_weights_sets_sizes(self, basic_dock_area, monkeypatch): monkeypatch.setattr(QTimer, "singleShot", lambda *_args: _args[-1]()) class DummySplitter: def __init__(self): self._children = [object(), object(), object()] self.sizes = None self.stretch = [] def count(self): return len(self._children) def orientation(self): return Qt.Orientation.Horizontal def width(self): return 300 def height(self): return 120 def setSizes(self, sizes): self.sizes = sizes def setStretchFactor(self, idx, value): self.stretch.append((idx, value)) splitter = DummySplitter() basic_dock_area._schedule_splitter_weights(splitter, [1, 2, 1]) assert splitter.sizes == [75, 150, 75] assert splitter.stretch == [(0, 100), (1, 200), (2, 100)] def test_apply_splitter_tree_honors_overrides(self, basic_dock_area, monkeypatch): class DummySplitter: def __init__(self, orientation, children=None, label="splitter"): self._orientation = orientation self._children = list(children or []) self.label = label def count(self): return len(self._children) def orientation(self): return self._orientation def widget(self, idx): return self._children[idx] monkeypatch.setattr(basic_dock_module.QtAds, "CDockSplitter", DummySplitter) leaf = DummySplitter(Qt.Orientation.Horizontal, [], label="leaf") column_one = DummySplitter(Qt.Orientation.Vertical, [leaf], label="column_one") column_zero = DummySplitter(Qt.Orientation.Vertical, [], label="column_zero") root = DummySplitter(Qt.Orientation.Horizontal, [column_zero, column_one], label="root") calls = [] def fake_schedule(self, splitter, weights): calls.append((splitter.label, weights)) monkeypatch.setattr(DockAreaWidget, "_schedule_splitter_weights", fake_schedule) overrides = {(): ["root_override"], (0,): ["column_override"]} basic_dock_area._apply_splitter_tree( root, (), horizontal=[1, 2], vertical=[3, 4], overrides=overrides ) assert calls[0] == ("root", ["root_override"]) assert calls[1] == ("column_zero", ["column_override"]) assert calls[2] == ("column_one", [3, 4]) assert calls[3] == ("leaf", ["column_override"]) def test_set_layout_ratios_normalizes_and_applies(self, basic_dock_area, monkeypatch): class DummyContainer: def __init__(self, splitter): self._splitter = splitter def rootSplitter(self): return self._splitter root_one = object() root_two = object() containers = [DummyContainer(root_one), DummyContainer(None), DummyContainer(root_two)] monkeypatch.setattr(basic_dock_area.dock_manager, "dockContainers", lambda: containers) calls = [] def fake_apply(self, splitter, path, horizontal, vertical, overrides): calls.append((splitter, path, horizontal, vertical, overrides)) monkeypatch.setattr(DockAreaWidget, "_apply_splitter_tree", fake_apply) basic_dock_area.set_layout_ratios( horizontal=[1, 1, 1], vertical=[2, 3], splitter_overrides={"1/0": [5, 5], "": [9]} ) assert len(calls) == 2 for splitter, path, horizontal, vertical, overrides in calls: assert splitter in {root_one, root_two} assert path == () assert horizontal == [1, 1, 1] assert vertical == [2, 3] assert overrides == {(): [9], (1, 0): [5, 5]} def test_show_settings_action_defaults_disabled(self, basic_dock_area): widget = QWidget(parent=basic_dock_area) widget.setObjectName("settings_default") dock = basic_dock_area.new(widget, return_dock=True) assert dock._dock_preferences.get("show_settings_action") is False assert not hasattr(dock, "setting_action") def test_show_settings_action_can_be_enabled(self, basic_dock_area): widget = QWidget(parent=basic_dock_area) widget.setObjectName("settings_enabled") dock = basic_dock_area.new(widget, return_dock=True, show_settings_action=True) assert dock._dock_preferences.get("show_settings_action") is True assert hasattr(dock, "setting_action") assert dock.setting_action.toolTip() == "Dock settings" def test_collect_splitter_info_describes_children(self, basic_dock_area, monkeypatch): class DummyDockWidget: def __init__(self, name): self._name = name def objectName(self): return self._name class DummyDockArea: def __init__(self, dock_names): self._docks = [DummyDockWidget(name) for name in dock_names] def dockWidgets(self): return self._docks class DummySplitter: def __init__(self, orientation, children=None): self._orientation = orientation self._children = list(children or []) def orientation(self): return self._orientation def count(self): return len(self._children) def widget(self, idx): return self._children[idx] class Spacer: pass monkeypatch.setattr(basic_dock_module, "CDockSplitter", DummySplitter) monkeypatch.setattr(basic_dock_module, "CDockAreaWidget", DummyDockArea) monkeypatch.setattr(basic_dock_module, "CDockWidget", DummyDockWidget) nested_splitter = DummySplitter(Qt.Orientation.Horizontal) dock_area_child = DummyDockArea(["left", "right"]) dock_child = DummyDockWidget("solo") spacer = Spacer() root_splitter = DummySplitter( Qt.Orientation.Vertical, [nested_splitter, dock_area_child, dock_child, spacer] ) results = [] basic_dock_area._collect_splitter_info(root_splitter, (2,), results, container_index=5) assert len(results) == 2 root_entry = results[0] assert root_entry["container"] == 5 assert root_entry["path"] == (2,) assert root_entry["orientation"] == "vertical" assert root_entry["children"] == [ {"index": 0, "type": "splitter"}, {"index": 1, "type": "dock_area", "docks": ["left", "right"]}, {"index": 2, "type": "dock", "name": "solo"}, {"index": 3, "type": "Spacer"}, ] nested_entry = results[1] assert nested_entry["path"] == (2, 0) assert nested_entry["orientation"] == "horizontal" def test_describe_layout_aggregates_containers(self, basic_dock_area, monkeypatch): class DummyContainer: def __init__(self, splitter): self._splitter = splitter def rootSplitter(self): return self._splitter containers = [DummyContainer("root0"), DummyContainer(None), DummyContainer("root2")] monkeypatch.setattr(basic_dock_area.dock_manager, "dockContainers", lambda: containers) calls = [] def recorder(self, splitter, path, results, container_index): entry = {"container": container_index, "splitter": splitter, "path": path} results.append(entry) calls.append(entry) monkeypatch.setattr(DockAreaWidget, "_collect_splitter_info", recorder) info = basic_dock_area.describe_layout() assert info == calls assert [entry["splitter"] for entry in info] == ["root0", "root2"] assert [entry["container"] for entry in info] == [0, 2] assert all(entry["path"] == () for entry in info) def test_print_layout_structure_formats_output(self, basic_dock_area, monkeypatch, capsys): entries = [ { "container": 1, "path": (0,), "orientation": "horizontal", "children": [ {"index": 0, "type": "dock_area", "docks": ["alpha", "beta"]}, {"index": 1, "type": "dock", "name": "solo"}, {"index": 2, "type": "splitter"}, {"index": 3, "type": "Placeholder"}, ], } ] monkeypatch.setattr(DockAreaWidget, "describe_layout", lambda self: entries) basic_dock_area.print_layout_structure() captured = capsys.readouterr().out.strip().splitlines() assert captured == [ "container=1 path=(0,) orientation=horizontal -> " "[0:dock_area[alpha, beta], 1:dock(solo), 2:splitter, 3:Placeholder]" ] class TestAdvancedDockAreaInit: """Test initialization and basic properties.""" def test_init(self, advanced_dock_area): assert advanced_dock_area is not None assert isinstance(advanced_dock_area, AdvancedDockArea) assert advanced_dock_area.mode == "creator" assert hasattr(advanced_dock_area, "dock_manager") assert hasattr(advanced_dock_area, "toolbar") assert hasattr(advanced_dock_area, "dark_mode_button") assert hasattr(advanced_dock_area, "state_manager") def test_rpc_and_plugin_flags(self): assert AdvancedDockArea.RPC is True assert AdvancedDockArea.PLUGIN is False def test_user_access_list(self): expected_methods = [ "new", "widget_map", "widget_list", "lock_workspace", "attach_all", "delete_all", ] for method in expected_methods: assert method in AdvancedDockArea.USER_ACCESS class TestDockManagement: """Test dock creation, management, and manipulation.""" def test_new_widget_string(self, advanced_dock_area, qtbot): """Test creating a new widget from string.""" initial_count = len(advanced_dock_area.dock_list()) # Create a widget by string name widget = advanced_dock_area.new("DarkModeButton") # Wait for the dock to be created (since it's async) qtbot.wait(200) # Check that dock was actually created assert len(advanced_dock_area.dock_list()) == initial_count + 1 # Check widget was returned assert widget is not None assert hasattr(widget, "name_established") def test_new_widget_instance(self, advanced_dock_area, qtbot): """Test creating dock with existing widget instance.""" from bec_widgets.widgets.plots.waveform.waveform import Waveform initial_count = len(advanced_dock_area.dock_list()) # Create widget instance widget_instance = Waveform(parent=advanced_dock_area, client=advanced_dock_area.client) widget_instance.setObjectName("test_widget") # Add it to dock area result = advanced_dock_area.new(widget_instance) # Should return the same instance assert result == widget_instance qtbot.wait(200) assert len(advanced_dock_area.dock_list()) == initial_count + 1 def test_dock_map(self, advanced_dock_area, qtbot): """Test dock_map returns correct mapping.""" # Initially empty dock_map = advanced_dock_area.dock_map() assert isinstance(dock_map, dict) initial_count = len(dock_map) # Create a widget advanced_dock_area.new("Waveform") qtbot.wait(200) # Check dock map updated new_dock_map = advanced_dock_area.dock_map() assert len(new_dock_map) == initial_count + 1 def test_dock_list(self, advanced_dock_area, qtbot): """Test dock_list returns list of docks.""" dock_list = advanced_dock_area.dock_list() assert isinstance(dock_list, list) initial_count = len(dock_list) # Create a widget advanced_dock_area.new("Waveform") qtbot.wait(200) # Check dock list updated new_dock_list = advanced_dock_area.dock_list() assert len(new_dock_list) == initial_count + 1 def test_widget_map(self, advanced_dock_area, qtbot): """Test widget_map returns widget mapping.""" widget_map = advanced_dock_area.widget_map() assert isinstance(widget_map, dict) initial_count = len(widget_map) # Create a widget advanced_dock_area.new("DarkModeButton") qtbot.wait(200) # Check widget map updated new_widget_map = advanced_dock_area.widget_map() assert len(new_widget_map) == initial_count + 1 def test_widget_list(self, advanced_dock_area, qtbot): """Test widget_list returns list of widgets.""" widget_list = advanced_dock_area.widget_list() assert isinstance(widget_list, list) initial_count = len(widget_list) # Create a widget advanced_dock_area.new("DarkModeButton") qtbot.wait(200) # Check widget list updated new_widget_list = advanced_dock_area.widget_list() assert len(new_widget_list) == initial_count + 1 def test_delete_all(self, advanced_dock_area, qtbot): """Test delete_all functionality.""" # Create multiple widgets advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("DarkModeButton") # Wait for docks to be created qtbot.wait(200) initial_count = len(advanced_dock_area.dock_list()) assert initial_count >= 2 # Delete all advanced_dock_area.delete_all() # Wait for deletion to complete qtbot.wait(200) # Should have no docks assert len(advanced_dock_area.dock_list()) == 0 class TestAdvancedDockSettingsAction: """Ensure AdvancedDockArea exposes dock settings actions by default.""" def test_settings_action_installed_by_default(self, advanced_dock_area): widget = QWidget(parent=advanced_dock_area) widget.setObjectName("advanced_default_settings") dock = advanced_dock_area.new(widget, return_dock=True) assert hasattr(dock, "setting_action") assert dock.setting_action.toolTip() == "Dock settings" assert dock._dock_preferences.get("show_settings_action") is True def test_settings_action_can_be_disabled(self, advanced_dock_area): widget = QWidget(parent=advanced_dock_area) widget.setObjectName("advanced_settings_off") dock = advanced_dock_area.new(widget, return_dock=True, show_settings_action=False) assert not hasattr(dock, "setting_action") assert dock._dock_preferences.get("show_settings_action") is False class TestWorkspaceLocking: """Test workspace locking functionality.""" def test_lock_workspace_property_getter(self, advanced_dock_area): """Test lock_workspace property getter.""" # Initially unlocked assert advanced_dock_area.lock_workspace is False # Set locked state directly advanced_dock_area._locked = True assert advanced_dock_area.lock_workspace is True def test_lock_workspace_property_setter(self, advanced_dock_area, qtbot): """Test lock_workspace property setter.""" # Create a dock first advanced_dock_area.new("DarkModeButton") qtbot.wait(200) # Initially unlocked assert advanced_dock_area.lock_workspace is False # Lock workspace advanced_dock_area.lock_workspace = True assert advanced_dock_area._locked is True assert advanced_dock_area.lock_workspace is True # Unlock workspace advanced_dock_area.lock_workspace = False assert advanced_dock_area._locked is False assert advanced_dock_area.lock_workspace is False class TestDeveloperMode: """Test developer mode functionality.""" def test_developer_mode_toggle(self, advanced_dock_area): """Test developer mode toggle functionality.""" # Check initial state initial_editable = advanced_dock_area._editable # Toggle developer mode advanced_dock_area._on_developer_mode_toggled(True) assert advanced_dock_area._editable is True assert advanced_dock_area.lock_workspace is False advanced_dock_area._on_developer_mode_toggled(False) assert advanced_dock_area._editable is False assert advanced_dock_area.lock_workspace is True def test_set_editable(self, advanced_dock_area): """Test _set_editable functionality.""" # Test setting editable to True advanced_dock_area._set_editable(True) assert advanced_dock_area.lock_workspace is False assert advanced_dock_area._editable is True # Test setting editable to False advanced_dock_area._set_editable(False) assert advanced_dock_area.lock_workspace is True assert advanced_dock_area._editable is False class TestToolbarFunctionality: """Test toolbar setup and functionality.""" def test_toolbar_setup(self, advanced_dock_area): """Test toolbar is properly set up.""" assert hasattr(advanced_dock_area, "toolbar") assert hasattr(advanced_dock_area, "_ACTION_MAPPINGS") # Check that action mappings are properly set assert "menu_plots" in advanced_dock_area._ACTION_MAPPINGS assert "menu_devices" in advanced_dock_area._ACTION_MAPPINGS assert "menu_utils" in advanced_dock_area._ACTION_MAPPINGS def test_toolbar_plot_actions(self, advanced_dock_area): """Test plot toolbar actions trigger widget creation.""" plot_actions = [ "waveform", "scatter_waveform", "multi_waveform", "image", "motor_map", "heatmap", ] for action_name in plot_actions: with patch.object(advanced_dock_area, "new") as mock_new: menu_plots = advanced_dock_area.toolbar.components.get_action("menu_plots") action = menu_plots.actions[action_name].action # Get the expected widget type from the action mappings widget_type = advanced_dock_area._ACTION_MAPPINGS["menu_plots"][action_name][2] action.trigger() mock_new.assert_called_once_with(widget=widget_type) def test_toolbar_device_actions(self, advanced_dock_area): """Test device toolbar actions trigger widget creation.""" device_actions = ["scan_control", "positioner_box"] for action_name in device_actions: with patch.object(advanced_dock_area, "new") as mock_new: menu_devices = advanced_dock_area.toolbar.components.get_action("menu_devices") action = menu_devices.actions[action_name].action # Get the expected widget type from the action mappings widget_type = advanced_dock_area._ACTION_MAPPINGS["menu_devices"][action_name][2] action.trigger() mock_new.assert_called_once_with(widget=widget_type) def test_toolbar_utils_actions(self, advanced_dock_area): """Test utils toolbar actions trigger widget creation.""" utils_actions = ["queue", "terminal", "status", "progress_bar", "sbb_monitor"] for action_name in utils_actions: with patch.object(advanced_dock_area, "new") as mock_new: menu_utils = advanced_dock_area.toolbar.components.get_action("menu_utils") action = menu_utils.actions[action_name].action # Skip log_panel as it's disabled if action_name == "log_panel": assert not action.isEnabled() continue # Get the expected widget type from the action mappings widget_type = advanced_dock_area._ACTION_MAPPINGS["menu_utils"][action_name][2] action.trigger() if action_name == "terminal": mock_new.assert_called_once_with( widget="WebConsole", closable=True, startup_cmd=None ) else: mock_new.assert_called_once_with(widget=widget_type) def test_attach_all_action(self, advanced_dock_area, qtbot): """Test attach_all toolbar action.""" # Create floating docks advanced_dock_area.new("DarkModeButton", start_floating=True) advanced_dock_area.new("DarkModeButton", start_floating=True) qtbot.wait(200) initial_floating = len(advanced_dock_area.dock_manager.floatingWidgets()) # Trigger attach all action action = advanced_dock_area.toolbar.components.get_action("attach_all").action action.trigger() # Wait a bit for the operation qtbot.wait(200) # Should have fewer or same floating widgets final_floating = len(advanced_dock_area.dock_manager.floatingWidgets()) assert final_floating <= initial_floating def test_load_profile_restores_floating_dock(self, advanced_dock_area, qtbot): helper = profile_helper(advanced_dock_area) settings = helper.open_user("floating_profile") settings.clear() settings.setValue("profile/created_at", "2025-11-23T00:00:00Z") settings.beginWriteArray(SETTINGS_KEYS["manifest"], 2) # Floating entry settings.setArrayIndex(0) settings.setValue("object_name", "FloatingWaveform") settings.setValue("widget_class", "DarkModeButton") settings.setValue("closable", True) settings.setValue("floatable", True) settings.setValue("movable", True) settings.setValue("floating", True) settings.setValue("floating_screen", "") settings.setValue("floating_rel_x", 0.1) settings.setValue("floating_rel_y", 0.1) settings.setValue("floating_rel_w", 0.2) settings.setValue("floating_rel_h", 0.2) settings.setValue("floating_abs_x", 50) settings.setValue("floating_abs_y", 50) settings.setValue("floating_abs_w", 200) settings.setValue("floating_abs_h", 150) # Anchored entry settings.setArrayIndex(1) settings.setValue("object_name", "EmbeddedWaveform") settings.setValue("widget_class", "DarkModeButton") settings.setValue("closable", True) settings.setValue("floatable", True) settings.setValue("movable", True) settings.setValue("floating", False) settings.setValue("floating_screen", "") settings.setValue("floating_rel_x", 0.0) settings.setValue("floating_rel_y", 0.0) settings.setValue("floating_rel_w", 0.0) settings.setValue("floating_rel_h", 0.0) settings.setValue("floating_abs_x", 0) settings.setValue("floating_abs_y", 0) settings.setValue("floating_abs_w", 0) settings.setValue("floating_abs_h", 0) settings.endArray() settings.sync() advanced_dock_area.delete_all() advanced_dock_area.load_profile("floating_profile") qtbot.waitUntil(lambda: "FloatingWaveform" in advanced_dock_area.dock_map(), timeout=3000) floating_dock = advanced_dock_area.dock_map()["FloatingWaveform"] assert floating_dock.isFloating() def test_screenshot_action(self, advanced_dock_area, tmpdir): """Test screenshot toolbar action.""" # Create a test screenshot file path in tmpdir screenshot_path = tmpdir.join("test_screenshot.png") # Mock the QFileDialog.getSaveFileName to return a test filename with mock.patch("bec_widgets.utils.bec_widget.QFileDialog.getSaveFileName") as mock_dialog: mock_dialog.return_value = (str(screenshot_path), "PNG Files (*.png)") # Mock the screenshot.save method with mock.patch.object(advanced_dock_area, "grab") as mock_grab: mock_screenshot = mock.MagicMock() mock_grab.return_value = mock_screenshot # Trigger the screenshot action action = advanced_dock_area.toolbar.components.get_action("screenshot").action action.trigger() # Verify the dialog was called mock_dialog.assert_called_once() # Verify grab was called mock_grab.assert_called_once() # Verify save was called with the filename mock_screenshot.save.assert_called_once_with(str(screenshot_path)) class TestDockSettingsDialog: """Test dock settings dialog functionality.""" def test_dock_settings_dialog_init(self, advanced_dock_area): """Test DockSettingsDialog initialization.""" from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import ( DarkModeButton, ) # Create a real widget mock_widget = DarkModeButton(parent=advanced_dock_area) dialog = DockSettingsDialog(advanced_dock_area, mock_widget) assert dialog.windowTitle() == "Dock Settings" assert dialog.isModal() assert hasattr(dialog, "prop_editor") def test_open_dock_settings_dialog(self, advanced_dock_area, qtbot): """Test opening dock settings dialog.""" from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import ( DarkModeButton, ) # Create real widget and dock widget = DarkModeButton(parent=advanced_dock_area) widget.setObjectName("test_widget") # Create a real dock dock = advanced_dock_area._make_dock(widget, closable=True, floatable=True, movable=True) # Mock dialog exec to avoid blocking with patch.object(DockSettingsDialog, "exec") as mock_exec: mock_exec.return_value = QDialog.Accepted # Call the method advanced_dock_area._open_dock_settings_dialog(dock, widget) # Verify dialog was created and exec called mock_exec.assert_called_once() class TestSaveProfileDialog: """Test save profile dialog functionality.""" def test_save_profile_dialog_init(self, qtbot): """Test SaveProfileDialog initialization.""" dialog = SaveProfileDialog(None, "test_profile") qtbot.addWidget(dialog) assert dialog.windowTitle() == "Save Workspace Profile" assert dialog.isModal() assert dialog.name_edit.text() == "test_profile" def test_save_profile_dialog_get_values(self, qtbot): """Test getting values from SaveProfileDialog.""" dialog = SaveProfileDialog(None) qtbot.addWidget(dialog) dialog.name_edit.setText("my_profile") dialog.quick_select_checkbox.setChecked(True) assert dialog.get_profile_name() == "my_profile" assert dialog.is_quick_select() is True def test_save_button_enabled_state(self, qtbot): """Test save button is enabled/disabled based on name input.""" dialog = SaveProfileDialog(None) qtbot.addWidget(dialog) # Initially should be disabled (empty name) assert not dialog.save_btn.isEnabled() # Should be enabled when name is entered dialog.name_edit.setText("test") assert dialog.save_btn.isEnabled() # Should be disabled when name is cleared dialog.name_edit.setText("") assert not dialog.save_btn.isEnabled() def test_accept_blocks_empty_name(self, qtbot): dialog = SaveProfileDialog(None) qtbot.addWidget(dialog) dialog.name_edit.clear() dialog.accept() assert dialog.result() == QDialog.Rejected assert dialog.overwrite_existing is False def test_accept_readonly_suggests_unique_name(self, qtbot, monkeypatch): info_calls = [] monkeypatch.setattr( QMessageBox, "information", lambda *args, **kwargs: info_calls.append((args, kwargs)) or QMessageBox.Ok, ) dialog = SaveProfileDialog( None, name_exists=lambda name: name == "readonly_custom", profile_origin=lambda name: "module" if name == "readonly" else "unknown", origin_label=lambda name: "ModuleDefaults", ) qtbot.addWidget(dialog) dialog.name_edit.setText("readonly") dialog.accept() assert dialog.result() == QDialog.Rejected assert dialog.name_edit.text().startswith("readonly_custom") assert dialog.overwrite_checkbox.isChecked() is False assert info_calls, "Expected informational prompt for read-only profile" def test_accept_existing_profile_confirm_yes(self, qtbot, monkeypatch): monkeypatch.setattr(QMessageBox, "question", lambda *args, **kwargs: QMessageBox.Yes) dialog = SaveProfileDialog( None, current_profile_name="profile_a", name_exists=lambda name: name == "profile_a", profile_origin=lambda name: "settings" if name == "profile_a" else "unknown", ) qtbot.addWidget(dialog) dialog.name_edit.setText("profile_a") dialog.accept() assert dialog.result() == QDialog.Accepted assert dialog.overwrite_existing is True def test_accept_existing_profile_confirm_no(self, qtbot, monkeypatch): monkeypatch.setattr(QMessageBox, "question", lambda *args, **kwargs: QMessageBox.No) dialog = SaveProfileDialog( None, current_profile_name="profile_a", name_exists=lambda name: False, profile_origin=lambda name: "settings" if name == "profile_a" else "unknown", ) qtbot.addWidget(dialog) dialog.name_edit.setText("profile_a") dialog.accept() assert dialog.result() == QDialog.Rejected assert dialog.name_edit.text().startswith("profile_a_custom") assert dialog.overwrite_existing is False assert dialog.overwrite_checkbox.isChecked() is False def test_overwrite_toggle_sets_and_restores_name(self, qtbot): dialog = SaveProfileDialog( None, current_name="custom_name", current_profile_name="existing_profile" ) qtbot.addWidget(dialog) dialog.overwrite_checkbox.setChecked(True) assert dialog.name_edit.text() == "existing_profile" dialog.name_edit.setText("existing_profile") dialog.overwrite_checkbox.setChecked(False) assert dialog.name_edit.text() == "custom_name" class TestPreviewPanel: """Test preview panel scaling behavior.""" def test_preview_panel_without_pixmap(self, qtbot): panel = PreviewPanel("Current", None) qtbot.addWidget(panel) assert "No preview available" in panel.image_label.text() def test_preview_panel_with_pixmap(self, qtbot): pixmap = QPixmap(40, 20) pixmap.fill(Qt.red) panel = PreviewPanel("Current", pixmap) qtbot.addWidget(panel) assert panel.image_label.pixmap() is not None def test_preview_panel_set_pixmap_resets_placeholder(self, qtbot): panel = PreviewPanel("Current", None) qtbot.addWidget(panel) pixmap = QPixmap(30, 30) pixmap.fill(Qt.blue) panel.setPixmap(pixmap) assert panel.image_label.pixmap() is not None panel.setPixmap(None) assert panel.image_label.pixmap() is None or panel.image_label.pixmap().isNull() assert "No preview available" in panel.image_label.text() class TestRestoreProfileDialog: """Test restore dialog confirmation flow.""" def test_confirm_accepts(self, monkeypatch): monkeypatch.setattr(RestoreProfileDialog, "exec", lambda self: QDialog.Accepted) assert RestoreProfileDialog.confirm(None, QPixmap(), QPixmap()) is True def test_confirm_rejects(self, monkeypatch): monkeypatch.setattr(RestoreProfileDialog, "exec", lambda self: QDialog.Rejected) assert RestoreProfileDialog.confirm(None, QPixmap(), QPixmap()) is False class TestProfileInfoAndScreenshots: """Tests for profile utilities metadata and screenshot helpers.""" PNG_BYTES = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAAFUlEQVQYlWP8//8/A27AhEduBEsDAKXjAxHmByO3AAAAAElFTkSuQmCC" ) def _write_manifest(self, settings, count=2): settings.beginWriteArray(profile_utils.SETTINGS_KEYS["manifest"], count) for i in range(count): settings.setArrayIndex(i) settings.setValue("object_name", f"widget_{i}") settings.setValue("widget_class", "Dummy") settings.setValue("closable", True) settings.setValue("floatable", True) settings.setValue("movable", True) settings.endArray() settings.sync() def test_get_profile_info_user_origin(self, temp_profile_dir): name = "info_user" settings = open_user_settings(name) settings.setValue(profile_utils.SETTINGS_KEYS["created_at"], "2023-01-01T00:00:00Z") settings.setValue("profile/author", "Custom") set_quick_select(name, True) self._write_manifest(settings, count=3) info = get_profile_info(name) assert info.name == name assert info.origin == "settings" assert info.is_read_only is False assert info.is_quick_select is True assert info.widget_count == 3 assert info.author == "User" assert info.user_path.endswith(f"{name}.ini") assert info.size_kb >= 0 def test_get_profile_info_default_only(self, temp_profile_dir): name = "info_default" settings = open_default_settings(name) self._write_manifest(settings, count=1) user_path = user_profile_path(name) if os.path.exists(user_path): os.remove(user_path) info = get_profile_info(name) assert info.origin == "settings" assert info.user_path.endswith(f"{name}.ini") assert info.widget_count == 1 def test_get_profile_info_module_readonly(self, module_profile_factory): name = module_profile_factory("info_readonly") info = get_profile_info(name) assert info.origin == "module" assert info.is_read_only is True assert info.author == "BEC Widgets" def test_get_profile_info_unknown_profile(self): name = "nonexistent_profile" if os.path.exists(user_profile_path(name)): os.remove(user_profile_path(name)) if os.path.exists(default_profile_path(name)): os.remove(default_profile_path(name)) info = get_profile_info(name) assert info.origin == "unknown" assert info.is_read_only is False assert info.widget_count == 0 def test_load_user_profile_screenshot(self, temp_profile_dir): name = "user_screenshot" settings = open_user_settings(name) settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], self.PNG_BYTES) settings.sync() pix = load_user_profile_screenshot(name) assert pix is not None and not pix.isNull() def test_load_default_profile_screenshot(self, temp_profile_dir): name = "default_screenshot" settings = open_default_settings(name) settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], self.PNG_BYTES) settings.sync() pix = load_default_profile_screenshot(name) assert pix is not None and not pix.isNull() def test_load_screenshot_from_settings_invalid(self, temp_profile_dir): name = "invalid_screenshot" settings = open_user_settings(name) settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], "not-an-image") settings.sync() pix = profile_utils._load_screenshot_from_settings(settings) assert pix is None def test_load_screenshot_from_settings_bytes(self, temp_profile_dir): name = "bytes_screenshot" settings = open_user_settings(name) settings.setValue(profile_utils.SETTINGS_KEYS["screenshot"], self.PNG_BYTES) settings.sync() pix = profile_utils._load_screenshot_from_settings(settings) assert pix is not None and not pix.isNull() class TestWorkSpaceManager: """Test workspace manager interactions.""" @staticmethod def _create_profiles(names): for name in names: settings = open_user_settings(name) settings.setValue("meta", "value") settings.sync() def test_render_table_populates_rows(self, qtbot): profile_names = ["profile_a", "profile_b"] self._create_profiles(profile_names) manager = WorkSpaceManager(target_widget=None) qtbot.addWidget(manager) assert manager.profile_table.rowCount() >= len(profile_names) def test_switch_profile_updates_target(self, qtbot, workspace_manager_target): name = "profile_switch" self._create_profiles([name]) target = workspace_manager_target() manager = WorkSpaceManager(target_widget=target) qtbot.addWidget(manager) manager.switch_profile(name) assert target.load_profile_calls == [name] assert target._combo.current_text == name assert manager._current_selected_profile() == name def test_toggle_quick_select_updates_flag(self, qtbot, workspace_manager_target): name = "profile_toggle" self._create_profiles([name]) target = workspace_manager_target() manager = WorkSpaceManager(target_widget=target) qtbot.addWidget(manager) initial = is_quick_select(name) manager.toggle_quick_select(name) assert is_quick_select(name) is (not initial) assert target.refresh_calls >= 1 def test_save_current_as_profile_with_target(self, qtbot, workspace_manager_target): name = "profile_save" self._create_profiles([name]) target = workspace_manager_target() target._current_profile_name = name manager = WorkSpaceManager(target_widget=target) qtbot.addWidget(manager) manager.save_current_as_profile() assert target.save_called is True assert manager._current_selected_profile() == name def test_delete_profile_removes_files(self, qtbot, workspace_manager_target, monkeypatch): name = "profile_delete" self._create_profiles([name]) target = workspace_manager_target() target._current_profile_name = name manager = WorkSpaceManager(target_widget=target) qtbot.addWidget(manager) monkeypatch.setattr(QMessageBox, "question", lambda *a, **k: QMessageBox.Yes) manager.delete_profile(name) assert not os.path.exists(user_profile_path(name)) assert target.refresh_calls >= 1 def test_delete_readonly_profile_shows_message( self, qtbot, workspace_manager_target, module_profile_factory, monkeypatch ): readonly = module_profile_factory("readonly_delete") list_profiles() monkeypatch.setattr( profile_utils, "get_profile_info", lambda *a, **k: profile_utils.ProfileInfo(name=readonly, is_read_only=True), ) info_calls = [] monkeypatch.setattr( QMessageBox, "information", lambda *args, **kwargs: info_calls.append((args, kwargs)) or QMessageBox.Ok, ) manager = WorkSpaceManager(target_widget=workspace_manager_target()) qtbot.addWidget(manager) manager.delete_profile(readonly) assert info_calls, "Expected informational prompt for read-only profile" class TestAdvancedDockAreaRestoreAndDialogs: """Additional coverage for restore flows and workspace dialogs.""" def test_restore_user_profile_from_default_confirm_true(self, advanced_dock_area, monkeypatch): profile_name = "profile_restore_true" helper = profile_helper(advanced_dock_area) helper.open_default(profile_name).sync() helper.open_user(profile_name).sync() advanced_dock_area._current_profile_name = profile_name advanced_dock_area.isVisible = lambda: False pix = QPixmap(8, 8) pix.fill(Qt.red) monkeypatch.setattr( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.load_user_profile_screenshot", lambda name, namespace=None: pix, ) monkeypatch.setattr( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.load_default_profile_screenshot", lambda name, namespace=None: pix, ) monkeypatch.setattr( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.RestoreProfileDialog.confirm", lambda *args, **kwargs: True, ) with ( patch( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.restore_user_from_default" ) as mock_restore, patch.object(advanced_dock_area, "delete_all") as mock_delete_all, patch.object(advanced_dock_area, "load_profile") as mock_load_profile, ): advanced_dock_area.restore_user_profile_from_default() assert mock_restore.call_count == 1 args, kwargs = mock_restore.call_args assert args == (profile_name,) assert kwargs.get("namespace") == advanced_dock_area.profile_namespace mock_delete_all.assert_called_once() mock_load_profile.assert_called_once_with(profile_name) def test_restore_user_profile_from_default_confirm_false(self, advanced_dock_area, monkeypatch): profile_name = "profile_restore_false" helper = profile_helper(advanced_dock_area) helper.open_default(profile_name).sync() helper.open_user(profile_name).sync() advanced_dock_area._current_profile_name = profile_name advanced_dock_area.isVisible = lambda: False monkeypatch.setattr( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.load_user_profile_screenshot", lambda name: QPixmap(), ) monkeypatch.setattr( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.load_default_profile_screenshot", lambda name: QPixmap(), ) monkeypatch.setattr( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.RestoreProfileDialog.confirm", lambda *args, **kwargs: False, ) with patch( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.restore_user_from_default" ) as mock_restore: advanced_dock_area.restore_user_profile_from_default() mock_restore.assert_not_called() def test_restore_user_profile_from_default_no_target(self, advanced_dock_area, monkeypatch): advanced_dock_area._current_profile_name = None with patch( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.RestoreProfileDialog.confirm" ) as mock_confirm: advanced_dock_area.restore_user_profile_from_default() mock_confirm.assert_not_called() def test_refresh_workspace_list_with_refresh_profiles(self, advanced_dock_area): profile_name = "refresh_profile" helper = profile_helper(advanced_dock_area) helper.open_user(profile_name).sync() advanced_dock_area._current_profile_name = profile_name combo = advanced_dock_area.toolbar.components.get_action("workspace_combo").widget combo.refresh_profiles = MagicMock() advanced_dock_area._refresh_workspace_list() combo.refresh_profiles.assert_called_once_with(profile_name) def test_refresh_workspace_list_fallback(self, advanced_dock_area): class ComboStub: def __init__(self): self.items = [] self.tooltip = "" self.block_calls = [] self.cleared = False self.current_index = -1 def blockSignals(self, value): self.block_calls.append(value) def clear(self): self.items.clear() self.cleared = True def addItems(self, items): self.items.extend(items) def findText(self, text): try: return self.items.index(text) except ValueError: return -1 def setCurrentIndex(self, idx): self.current_index = idx def setToolTip(self, text): self.tooltip = text active = "active_profile" quick = "quick_profile" helper = profile_helper(advanced_dock_area) helper.open_user(active).sync() helper.open_user(quick).sync() helper.set_quick_select(quick, True) combo_stub = ComboStub() class StubAction: def __init__(self, widget): self.widget = widget with patch.object( advanced_dock_area.toolbar.components, "get_action", return_value=StubAction(combo_stub) ): advanced_dock_area._current_profile_name = active advanced_dock_area._refresh_workspace_list() assert combo_stub.block_calls == [True, False] assert combo_stub.items[0] == active assert combo_stub.tooltip == "Active profile is not in quick select" def test_show_workspace_manager_creates_dialog(self, qtbot, advanced_dock_area): action = advanced_dock_area.toolbar.components.get_action("manage_workspaces").action assert not action.isChecked() advanced_dock_area._current_profile_name = "manager_profile" helper = profile_helper(advanced_dock_area) helper.open_user("manager_profile").sync() advanced_dock_area.show_workspace_manager() assert advanced_dock_area.manage_dialog is not None assert advanced_dock_area.manage_dialog.isVisible() assert action.isChecked() assert isinstance(advanced_dock_area.manage_widget, WorkSpaceManager) advanced_dock_area.manage_dialog.close() qtbot.waitUntil(lambda: advanced_dock_area.manage_dialog is None) assert not action.isChecked() def test_manage_dialog_closed(self, advanced_dock_area): widget_mock = MagicMock() dialog_mock = MagicMock() advanced_dock_area.manage_widget = widget_mock advanced_dock_area.manage_dialog = dialog_mock action = advanced_dock_area.toolbar.components.get_action("manage_workspaces").action action.setChecked(True) advanced_dock_area._manage_dialog_closed() widget_mock.close.assert_called_once() widget_mock.deleteLater.assert_called_once() dialog_mock.deleteLater.assert_called_once() assert advanced_dock_area.manage_dialog is None assert not action.isChecked() class TestProfileManagement: """Test profile management functionality.""" def test_profile_path(self, temp_profile_dir): """Test profile path generation.""" path = user_profile_path("test_profile") expected = os.path.join(temp_profile_dir, "user", "test_profile.ini") assert path == expected default_path = default_profile_path("test_profile") expected_default = os.path.join(temp_profile_dir, "default", "test_profile.ini") assert default_path == expected_default def test_open_settings(self, temp_profile_dir): """Test opening settings for a profile.""" settings = open_user_settings("test_profile") assert isinstance(settings, QSettings) def test_list_profiles_empty(self, temp_profile_dir): """Test listing profiles when directory is empty.""" try: module_defaults = { os.path.splitext(f)[0] for f in os.listdir(profile_utils.module_profiles_dir()) if f.endswith(".ini") } except FileNotFoundError: module_defaults = set() profiles = list_profiles() assert module_defaults.issubset(set(profiles)) def test_list_profiles_with_files(self, temp_profile_dir): """Test listing profiles with existing files.""" # Create some test profile files profile_names = ["profile1", "profile2", "profile3"] for name in profile_names: settings = open_user_settings(name) settings.setValue("test", "value") settings.sync() profiles = list_profiles() for name in profile_names: assert name in profiles def test_readonly_profile_operations(self, temp_profile_dir, module_profile_factory): """Test read-only profile functionality.""" profile_name = "user_profile" # Initially should not be read-only assert not is_profile_read_only(profile_name) # Create a user profile and ensure it's writable settings = open_user_settings(profile_name) settings.setValue("test", "value") settings.sync() assert not is_profile_read_only(profile_name) # Verify a bundled module profile is detected as read-only readonly_name = module_profile_factory("module_default") assert is_profile_read_only(readonly_name) def test_write_and_read_manifest(self, temp_profile_dir, advanced_dock_area, qtbot): """Test writing and reading dock manifest.""" settings = open_user_settings("test_manifest") # Create real docks advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("DarkModeButton") # Wait for docks to be created qtbot.wait(1000) docks = advanced_dock_area.dock_list() # Write manifest write_manifest(settings, docks) settings.sync() # Read manifest items = read_manifest(settings) assert len(items) >= 3 for item in items: assert "object_name" in item assert "widget_class" in item assert "closable" in item assert "floatable" in item assert "movable" in item def test_restore_preserves_quick_select(self, temp_profile_dir): """Ensure restoring keeps the quick select flag when it was enabled.""" profile_name = "restorable_profile" default_settings = open_default_settings(profile_name) default_settings.setValue("test", "default") default_settings.sync() user_settings = open_user_settings(profile_name) user_settings.setValue("test", "user") user_settings.sync() set_quick_select(profile_name, True) assert is_quick_select(profile_name) restore_user_from_default(profile_name) assert is_quick_select(profile_name) class TestWorkspaceProfileOperations: """Test workspace profile save/load/delete operations.""" def test_save_profile_readonly_conflict( self, advanced_dock_area, temp_profile_dir, module_profile_factory ): """Test saving profile when read-only profile exists.""" profile_name = module_profile_factory("readonly_profile") new_profile = f"{profile_name}_custom" helper = profile_helper(advanced_dock_area) target_path = helper.user_path(new_profile) if os.path.exists(target_path): os.remove(target_path) class StubDialog: def __init__(self, *args, **kwargs): self.overwrite_existing = False def exec(self): return QDialog.Accepted def get_profile_name(self): return new_profile def is_quick_select(self): return False with patch( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.SaveProfileDialog", StubDialog, ): advanced_dock_area.save_profile(profile_name) assert os.path.exists(target_path) def test_load_profile_with_manifest(self, advanced_dock_area, temp_profile_dir, qtbot): """Test loading profile with widget manifest.""" profile_name = "test_load_profile" helper = profile_helper(advanced_dock_area) # Create a profile with manifest settings = helper.open_user(profile_name) settings.beginWriteArray("manifest/widgets", 1) settings.setArrayIndex(0) settings.setValue("object_name", "test_widget") settings.setValue("widget_class", "DarkModeButton") settings.setValue("closable", True) settings.setValue("floatable", True) settings.setValue("movable", True) settings.endArray() settings.sync() # Load profile advanced_dock_area.load_profile(profile_name) # Wait for widget to be created qtbot.wait(1000) # Check widget was created widget_map = advanced_dock_area.widget_map() assert "test_widget" in widget_map def test_save_as_skips_autosave_source_profile( self, advanced_dock_area, temp_profile_dir, qtbot ): """Saving a new profile avoids overwriting the source profile during the switch.""" source_profile = "autosave_source" new_profile = "autosave_new" helper = profile_helper(advanced_dock_area) settings = helper.open_user(source_profile) settings.beginWriteArray("manifest/widgets", 1) settings.setArrayIndex(0) settings.setValue("object_name", "source_widget") settings.setValue("widget_class", "DarkModeButton") settings.setValue("closable", True) settings.setValue("floatable", True) settings.setValue("movable", True) settings.endArray() settings.sync() advanced_dock_area.load_profile(source_profile) qtbot.wait(500) advanced_dock_area.new("DarkModeButton") qtbot.wait(500) class StubDialog: def __init__(self, *args, **kwargs): self.overwrite_existing = False def exec(self): return QDialog.Accepted def get_profile_name(self): return new_profile def is_quick_select(self): return False with patch( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.SaveProfileDialog", StubDialog, ): advanced_dock_area.save_profile() qtbot.wait(500) source_manifest = read_manifest(helper.open_user(source_profile)) new_manifest = read_manifest(helper.open_user(new_profile)) assert len(source_manifest) == 1 assert len(new_manifest) == 2 def test_switch_autosaves_previous_profile(self, advanced_dock_area, temp_profile_dir, qtbot): """Regular profile switches should persist the outgoing layout.""" profile_a = "autosave_keep" profile_b = "autosave_target" helper = profile_helper(advanced_dock_area) for profile in (profile_a, profile_b): settings = helper.open_user(profile) settings.beginWriteArray("manifest/widgets", 1) settings.setArrayIndex(0) settings.setValue("object_name", f"{profile}_widget") settings.setValue("widget_class", "DarkModeButton") settings.setValue("closable", True) settings.setValue("floatable", True) settings.setValue("movable", True) settings.endArray() settings.sync() advanced_dock_area.load_profile(profile_a) qtbot.wait(500) advanced_dock_area.new("DarkModeButton") qtbot.wait(500) advanced_dock_area.load_profile(profile_b) qtbot.wait(500) manifest_a = read_manifest(helper.open_user(profile_a)) assert len(manifest_a) == 2 def test_delete_profile_readonly( self, advanced_dock_area, temp_profile_dir, module_profile_factory ): """Test deleting bundled profile removes only the writable copy.""" profile_name = module_profile_factory("readonly_profile") helper = profile_helper(advanced_dock_area) helper.list_profiles() # ensure default and user copies are materialized helper.open_default(profile_name).sync() settings = helper.open_user(profile_name) settings.setValue("test", "value") settings.sync() user_path = helper.user_path(profile_name) default_path = helper.default_path(profile_name) assert os.path.exists(user_path) assert os.path.exists(default_path) with patch.object(advanced_dock_area.toolbar.components, "get_action") as mock_get_action: mock_combo = MagicMock() mock_combo.currentText.return_value = profile_name mock_get_action.return_value.widget = mock_combo with ( patch( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.QMessageBox.question", return_value=QMessageBox.Yes, ) as mock_question, patch( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.QMessageBox.information", return_value=None, ) as mock_info, ): advanced_dock_area.delete_profile() mock_question.assert_not_called() mock_info.assert_called_once() # Read-only profile should remain intact (user + default copies) assert os.path.exists(user_path) assert os.path.exists(default_path) def test_delete_profile_success(self, advanced_dock_area, temp_profile_dir): """Test successful profile deletion.""" profile_name = "deletable_profile" helper = profile_helper(advanced_dock_area) # Create regular profile settings = helper.open_user(profile_name) settings.setValue("test", "value") settings.sync() user_path = helper.user_path(profile_name) assert os.path.exists(user_path) with patch.object(advanced_dock_area.toolbar.components, "get_action") as mock_get_action: mock_combo = MagicMock() mock_combo.currentText.return_value = profile_name mock_get_action.return_value.widget = mock_combo with patch( "bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area.QMessageBox.question" ) as mock_question: mock_question.return_value = QMessageBox.Yes with patch.object(advanced_dock_area, "_refresh_workspace_list") as mock_refresh: advanced_dock_area.delete_profile() mock_question.assert_called_once() mock_refresh.assert_called_once() # Profile should be deleted assert not os.path.exists(user_path) def test_refresh_workspace_list(self, advanced_dock_area, temp_profile_dir): """Test refreshing workspace list.""" # Create some profiles helper = profile_helper(advanced_dock_area) for name in ["profile1", "profile2"]: settings = helper.open_user(name) settings.setValue("test", "value") settings.sync() with patch.object(advanced_dock_area.toolbar.components, "get_action") as mock_get_action: mock_combo = MagicMock() mock_combo.refresh_profiles = MagicMock() mock_get_action.return_value.widget = mock_combo advanced_dock_area._refresh_workspace_list() mock_combo.refresh_profiles.assert_called_once() class TestCleanupAndMisc: """Test cleanup and miscellaneous functionality.""" def test_delete_dock(self, advanced_dock_area, qtbot): """Test _delete_dock functionality.""" from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import ( DarkModeButton, ) # Create a real widget and dock widget = DarkModeButton(parent=advanced_dock_area) widget.setObjectName("test_widget") dock = advanced_dock_area._make_dock(widget, closable=True, floatable=True, movable=True) initial_count = len(advanced_dock_area.dock_list()) # Delete the dock advanced_dock_area._delete_dock(dock) # Wait for deletion to complete qtbot.wait(200) # Verify dock was removed assert len(advanced_dock_area.dock_list()) == initial_count - 1 def test_apply_dock_lock(self, advanced_dock_area, qtbot): """Test _apply_dock_lock functionality.""" # Create a dock first advanced_dock_area.new("DarkModeButton") qtbot.wait(200) # Test locking advanced_dock_area._apply_dock_lock(True) # No assertion needed - just verify it doesn't crash # Test unlocking advanced_dock_area._apply_dock_lock(False) # No assertion needed - just verify it doesn't crash def test_make_dock(self, advanced_dock_area): """Test _make_dock functionality.""" from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import ( DarkModeButton, ) # Create a real widget widget = DarkModeButton(parent=advanced_dock_area) widget.setObjectName("test_widget") initial_count = len(advanced_dock_area.dock_list()) # Create dock dock = advanced_dock_area._make_dock(widget, closable=True, floatable=True, movable=True) # Verify dock was created assert dock is not None assert len(advanced_dock_area.dock_list()) == initial_count + 1 assert dock.widget() == widget def test_install_dock_settings_action(self, advanced_dock_area): """Test _install_dock_settings_action functionality.""" from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import ( DarkModeButton, ) # Create real widget and dock widget = DarkModeButton(parent=advanced_dock_area) widget.setObjectName("test_widget") with patch.object(advanced_dock_area, "_open_dock_settings_dialog") as mock_open_dialog: dock = advanced_dock_area._make_dock( widget, closable=True, floatable=True, movable=True ) # Verify dock has settings action assert hasattr(dock, "setting_action") assert dock.setting_action is not None assert dock.setting_action.toolTip() == "Dock settings" dock.setting_action.trigger() mock_open_dialog.assert_called_once_with(dock, widget) class TestModeSwitching: """Test mode switching functionality.""" def test_mode_property_setter_valid_modes(self, advanced_dock_area): """Test setting valid modes.""" valid_modes = ["plot", "device", "utils", "creator", "user"] for mode in valid_modes: advanced_dock_area.mode = mode assert advanced_dock_area.mode == mode def test_mode_changed_signal_emission(self, advanced_dock_area, qtbot): """Test that mode_changed signal is emitted when mode changes.""" # Set up signal spy with qtbot.waitSignal(advanced_dock_area.mode_changed, timeout=1000) as blocker: advanced_dock_area.mode = "plot" # Check signal was emitted with correct argument assert blocker.args == ["plot"] class TestToolbarModeBundles: """Test toolbar bundle creation and visibility for different modes.""" def test_flat_bundles_created(self, advanced_dock_area): """Test that flat bundles are created during toolbar setup.""" # Check that flat bundles exist assert "flat_plots" in advanced_dock_area.toolbar.bundles assert "flat_devices" in advanced_dock_area.toolbar.bundles assert "flat_utils" in advanced_dock_area.toolbar.bundles def test_plot_mode_toolbar_visibility(self, advanced_dock_area): """Test toolbar bundle visibility in plot mode.""" advanced_dock_area.mode = "plot" # Should show only flat_plots bundle (and essential bundles in real implementation) shown_bundles = advanced_dock_area.toolbar.shown_bundles assert "flat_plots" in shown_bundles # Should not show other flat bundles assert "flat_devices" not in shown_bundles assert "flat_utils" not in shown_bundles # Should not show menu bundles assert "menu_plots" not in shown_bundles assert "menu_devices" not in shown_bundles assert "menu_utils" not in shown_bundles def test_device_mode_toolbar_visibility(self, advanced_dock_area): """Test toolbar bundle visibility in device mode.""" advanced_dock_area.mode = "device" shown_bundles = advanced_dock_area.toolbar.shown_bundles assert "flat_devices" in shown_bundles # Should not show other flat bundles assert "flat_plots" not in shown_bundles assert "flat_utils" not in shown_bundles def test_utils_mode_toolbar_visibility(self, advanced_dock_area): """Test toolbar bundle visibility in utils mode.""" advanced_dock_area.mode = "utils" shown_bundles = advanced_dock_area.toolbar.shown_bundles assert "flat_utils" in shown_bundles # Should not show other flat bundles assert "flat_plots" not in shown_bundles assert "flat_devices" not in shown_bundles def test_developer_mode_toolbar_visibility(self, advanced_dock_area): """Test toolbar bundle visibility in developer mode.""" advanced_dock_area.mode = "creator" shown_bundles = advanced_dock_area.toolbar.shown_bundles # Should show menu bundles assert "menu_plots" in shown_bundles assert "menu_devices" in shown_bundles assert "menu_utils" in shown_bundles # Should show essential bundles assert "spacer_bundle" in shown_bundles assert "workspace" in shown_bundles assert "dock_actions" in shown_bundles def test_user_mode_toolbar_visibility(self, advanced_dock_area): """Test toolbar bundle visibility in user mode.""" advanced_dock_area.mode = "user" shown_bundles = advanced_dock_area.toolbar.shown_bundles # Should show only essential bundles assert "spacer_bundle" in shown_bundles assert "workspace" in shown_bundles assert "dock_actions" in shown_bundles # Should not show any widget creation bundles assert "menu_plots" not in shown_bundles assert "menu_devices" not in shown_bundles assert "menu_utils" not in shown_bundles assert "flat_plots" not in shown_bundles assert "flat_devices" not in shown_bundles assert "flat_utils" not in shown_bundles class TestFlatToolbarActions: """Test flat toolbar actions functionality.""" def test_flat_plot_actions_created(self, advanced_dock_area): """Test that flat plot actions are created.""" plot_actions = [ "flat_waveform", "flat_scatter_waveform", "flat_multi_waveform", "flat_image", "flat_motor_map", "flat_heatmap", ] for action_name in plot_actions: assert advanced_dock_area.toolbar.components.exists(action_name) def test_flat_device_actions_created(self, advanced_dock_area): """Test that flat device actions are created.""" device_actions = ["flat_scan_control", "flat_positioner_box"] for action_name in device_actions: assert advanced_dock_area.toolbar.components.exists(action_name) def test_flat_utils_actions_created(self, advanced_dock_area): """Test that flat utils actions are created.""" utils_actions = [ "flat_queue", "flat_status", "flat_progress_bar", "flat_terminal", "flat_bec_shell", "flat_log_panel", "flat_sbb_monitor", ] for action_name in utils_actions: assert advanced_dock_area.toolbar.components.exists(action_name) def test_flat_plot_actions_trigger_widget_creation(self, advanced_dock_area): """Test flat plot actions trigger widget creation.""" plot_action_mapping = { "flat_waveform": "Waveform", "flat_scatter_waveform": "ScatterWaveform", "flat_multi_waveform": "MultiWaveform", "flat_image": "Image", "flat_motor_map": "MotorMap", "flat_heatmap": "Heatmap", } for action_name, widget_type in plot_action_mapping.items(): with patch.object(advanced_dock_area, "new") as mock_new: action = advanced_dock_area.toolbar.components.get_action(action_name).action action.trigger() mock_new.assert_called_once_with(widget_type) def test_flat_device_actions_trigger_widget_creation(self, advanced_dock_area): """Test flat device actions trigger widget creation.""" device_action_mapping = { "flat_scan_control": "ScanControl", "flat_positioner_box": "PositionerBox", } for action_name, widget_type in device_action_mapping.items(): with patch.object(advanced_dock_area, "new") as mock_new: action = advanced_dock_area.toolbar.components.get_action(action_name).action action.trigger() mock_new.assert_called_once_with(widget_type) def test_flat_utils_actions_trigger_widget_creation(self, advanced_dock_area): """Test flat utils actions trigger widget creation.""" utils_action_mapping = { "flat_queue": "BECQueue", "flat_status": "BECStatusBox", "flat_progress_bar": "RingProgressBar", "flat_terminal": "WebConsole", "flat_bec_shell": "WebConsole", "flat_sbb_monitor": "SBBMonitor", } for action_name, widget_type in utils_action_mapping.items(): with patch.object(advanced_dock_area, "new") as mock_new: action = advanced_dock_area.toolbar.components.get_action(action_name).action # Skip log_panel as it's disabled if action_name == "flat_log_panel": assert not action.isEnabled() continue action.trigger() mock_new.assert_called_once_with(widget_type) def test_flat_log_panel_action_disabled(self, advanced_dock_area): """Test that flat log panel action is disabled.""" action = advanced_dock_area.toolbar.components.get_action("flat_log_panel").action assert not action.isEnabled() class TestModeTransitions: """Test mode transitions and state consistency.""" def test_mode_transition_sequence(self, advanced_dock_area, qtbot): """Test sequence of mode transitions.""" modes = ["plot", "device", "utils", "creator", "user"] for mode in modes: with qtbot.waitSignal(advanced_dock_area.mode_changed, timeout=1000) as blocker: advanced_dock_area.mode = mode assert advanced_dock_area.mode == mode assert blocker.args == [mode] def test_mode_consistency_after_multiple_changes(self, advanced_dock_area): """Test mode consistency after multiple rapid changes.""" # Rapidly change modes advanced_dock_area.mode = "plot" advanced_dock_area.mode = "device" advanced_dock_area.mode = "utils" advanced_dock_area.mode = "creator" advanced_dock_area.mode = "user" # Final state should be consistent assert advanced_dock_area.mode == "user" # Toolbar should show correct bundles for user mode shown_bundles = advanced_dock_area.toolbar.shown_bundles assert "spacer_bundle" in shown_bundles assert "workspace" in shown_bundles assert "dock_actions" in shown_bundles def test_toolbar_refresh_on_mode_change(self, advanced_dock_area): """Test that toolbar is properly refreshed when mode changes.""" initial_bundles = set(advanced_dock_area.toolbar.shown_bundles) # Change to a different mode advanced_dock_area.mode = "plot" plot_bundles = set(advanced_dock_area.toolbar.shown_bundles) # Bundles should be different assert initial_bundles != plot_bundles assert "flat_plots" in plot_bundles def test_mode_switching_preserves_existing_docks(self, advanced_dock_area, qtbot): """Test that mode switching doesn't affect existing docked widgets.""" # Create some widgets advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("DarkModeButton") qtbot.wait(200) initial_dock_count = len(advanced_dock_area.dock_list()) initial_widget_count = len(advanced_dock_area.widget_list()) # Switch modes advanced_dock_area.mode = "plot" advanced_dock_area.mode = "device" advanced_dock_area.mode = "user" # Dock and widget counts should remain the same assert len(advanced_dock_area.dock_list()) == initial_dock_count assert len(advanced_dock_area.widget_list()) == initial_widget_count class TestModeProperty: """Test mode property getter and setter behavior.""" def test_mode_property_getter(self, advanced_dock_area): """Test mode property getter returns correct value.""" # Set internal mode directly and test getter advanced_dock_area._mode = "plot" assert advanced_dock_area.mode == "plot" advanced_dock_area._mode = "device" assert advanced_dock_area.mode == "device" def test_mode_property_setter_updates_internal_state(self, advanced_dock_area): """Test mode property setter updates internal state.""" advanced_dock_area.mode = "plot" assert advanced_dock_area._mode == "plot" advanced_dock_area.mode = "utils" assert advanced_dock_area._mode == "utils" def test_mode_property_setter_triggers_toolbar_update(self, advanced_dock_area): """Test mode property setter triggers toolbar update.""" with patch.object(advanced_dock_area.toolbar, "show_bundles") as mock_show_bundles: advanced_dock_area.mode = "plot" mock_show_bundles.assert_called_once() def test_multiple_mode_changes(self, advanced_dock_area, qtbot): """Test multiple rapid mode changes.""" modes = ["plot", "device", "utils", "creator", "user"] for i, mode in enumerate(modes): with qtbot.waitSignal(advanced_dock_area.mode_changed, timeout=1000) as blocker: advanced_dock_area.mode = mode assert advanced_dock_area.mode == mode assert blocker.args == [mode]