mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-12-30 18:51:19 +01:00
2264 lines
84 KiB
Python
2264 lines
84 KiB
Python
# 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]
|