diff --git a/bec_widgets/widgets/services/beamline_states/beamline_state_manager.py b/bec_widgets/widgets/services/beamline_states/beamline_state_manager.py index 56c69cf0..bd90cd9e 100644 --- a/bec_widgets/widgets/services/beamline_states/beamline_state_manager.py +++ b/bec_widgets/widgets/services/beamline_states/beamline_state_manager.py @@ -3,10 +3,9 @@ from __future__ import annotations import sys from typing import Any -from bec_lib import bl_states +from bec_lib import bl_states, messages from bec_lib.endpoints import MessageEndpoints from bec_qthemes import material_icon -from pydantic import BaseModel from qtpy.QtCore import QAbstractListModel, QModelIndex, QSize, Qt from qtpy.QtWidgets import ( QAbstractItemView, @@ -48,7 +47,7 @@ class _BeamlineStateListModel(QAbstractListModel): super().__init__(parent) self._state_order: list[str] = [] self._state_rows: dict[str, int] = {} - self._state_configs: dict[str, dict[str, Any]] = {} + self._state_configs: dict[str, messages.BeamlineStateConfig] = {} def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: # noqa: N802 return 0 if parent.isValid() else len(self._state_order) @@ -60,7 +59,7 @@ class _BeamlineStateListModel(QAbstractListModel): if role in (Qt.ItemDataRole.DisplayRole, self.NameRole): return name if role == self.ConfigRole: - return self._state_configs.get(name, {}) + return self._state_configs.get(name) return None def flags(self, index: QModelIndex) -> Qt.ItemFlag: @@ -68,9 +67,9 @@ class _BeamlineStateListModel(QAbstractListModel): return Qt.ItemFlag.NoItemFlags return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable - def set_states(self, state_configs: list[dict[str, Any]]) -> None: - new_order = [str(state["name"]) for state in state_configs if state.get("name")] - new_configs = {str(state["name"]): state for state in state_configs if state.get("name")} + def set_states(self, state_configs: list[messages.BeamlineStateConfig]) -> None: + new_order = [state.name for state in state_configs] + new_configs = {state.name: state for state in state_configs} for row in reversed( [row for row, name in enumerate(self._state_order) if name not in new_configs] @@ -129,11 +128,8 @@ class _BeamlineStatePillDelegate(QStyledItemDelegate): self, parent: QWidget, _option: QStyleOptionViewItem, index: QModelIndex ) -> QWidget: name = index.data(_BeamlineStateListModel.NameRole) - state_config = index.data(_BeamlineStateListModel.ConfigRole) or {} - title = state_config.get("title") or name - pill = BeamlineStatePill( - parent=parent, state_name=name, title=title, client=self._manager.client - ) + state_config = index.data(_BeamlineStateListModel.ConfigRole) + pill = BeamlineStatePill(parent=parent, state_name=name, client=self._manager.client) pill.idle_card_background = self._manager.idle_card_background pill.set_state_config(state_config) pill.state_changed.connect(self._manager._on_pill_state_changed) @@ -147,9 +143,8 @@ class _BeamlineStatePillDelegate(QStyledItemDelegate): if not isinstance(editor, BeamlineStatePill): return name = index.data(_BeamlineStateListModel.NameRole) - state_config = index.data(_BeamlineStateListModel.ConfigRole) or {} - title = state_config.get("title") or name - editor.set_state_name(str(name), title=str(title)) + state_config = index.data(_BeamlineStateListModel.ConfigRole) + editor.set_state_name(str(name)) editor.idle_card_background = self._manager.idle_card_background editor.set_state_config(state_config) @@ -229,7 +224,7 @@ class BeamlineStateManager(BECWidget, QWidget): ) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self._state_pills: dict[str, BeamlineStatePill] = {} - self._state_configs: dict[str, dict[str, Any]] = {} + self._state_configs: dict[str, messages.BeamlineStateConfig] = {} self._state_order: list[str] = [] self._selected_statuses: set[str] | None = None self._selected_devices: set[str] | None = None @@ -414,14 +409,12 @@ class BeamlineStateManager(BECWidget, QWidget): ) -> None: """Update the displayed pills from ``AvailableBeamlineStatesMessage`` content.""" expanded_names = {name for name, pill in self._state_pills.items() if pill.is_expanded()} - states = content.get("states", []) - state_configs = [self._state_config_to_dict(state) for state in states] - state_configs = [state for state in state_configs if state.get("name")] + state_configs: list[messages.BeamlineStateConfig] = content.get("states", []) if state_configs == list(self._state_configs.values()): self._apply_filters() return - self._state_configs = {str(state["name"]): state for state in state_configs} - self._state_order = [str(state["name"]) for state in state_configs] + self._state_configs = {state.name: state for state in state_configs} + self._state_order = [state.name for state in state_configs] self._model.set_states(state_configs) self._open_persistent_editors(expanded_names) self._apply_filters() @@ -472,7 +465,7 @@ class BeamlineStateManager(BECWidget, QWidget): ): return False - device = self._state_device(self._state_configs.get(name, {})) + device = self._state_device(self._state_configs.get(name)) if self._selected_devices is not None and device not in self._selected_devices: return False @@ -505,14 +498,13 @@ class BeamlineStateManager(BECWidget, QWidget): ) return try: - parameters = config.model_dump(exclude={"name"}) - state_client.update_parameters(**parameters) + state_client.update_parameters(**config.model_dump(exclude={"name"})) except Exception as exc: QMessageBox.warning(self, "Cannot Update State", str(exc)) return pill = self._state_pills.get(state_name) if pill is not None: - pill.mark_current_settings_clean(config) + pill.mark_current_settings_clean() @SafeSlot(str) def _remove_state_requested(self, state_name: str) -> None: @@ -551,27 +543,10 @@ class BeamlineStateManager(BECWidget, QWidget): ) @staticmethod - def _state_device(state: dict[str, Any]) -> str | None: - parameters = state.get("parameters") - if isinstance(parameters, dict): - device = parameters.get("device") - else: - device = state.get("device") + def _state_device(state: messages.BeamlineStateConfig | None) -> str | None: + device = state.parameters.get("device") if state is not None else None return str(device) if device else None - @staticmethod - def _state_config_to_dict(state: dict[str, Any] | BaseModel) -> dict[str, Any]: - if isinstance(state, dict): - return state - state_dict = state.model_dump() - state_type = getattr(state, "state_type", None) - if state_type is not None: - state_dict.setdefault("state_type", state_type) - title = getattr(state, "title", None) - if title is not None and not state_dict.get("title"): - state_dict["title"] = title - return state_dict - def cleanup(self) -> None: self.bec_dispatcher.disconnect_slot( self.update_available_states, MessageEndpoints.available_beamline_states() diff --git a/bec_widgets/widgets/services/beamline_states/beamline_state_pill.py b/bec_widgets/widgets/services/beamline_states/beamline_state_pill.py index faa3e649..5ad1e6b7 100644 --- a/bec_widgets/widgets/services/beamline_states/beamline_state_pill.py +++ b/bec_widgets/widgets/services/beamline_states/beamline_state_pill.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any -from bec_lib import bl_states +from bec_lib import bl_states, messages from bec_lib.endpoints import MessageEndpoints from bec_qthemes import material_icon from qtpy.QtCore import Qt, Signal @@ -76,7 +76,6 @@ class BeamlineStatePill(BECWidget, QWidget): self, parent: QWidget | None = None, state_name: str | None = None, - title: str | None = None, client=None, config: ConnectionConfig | None = None, gui_id: str | None = None, @@ -89,8 +88,7 @@ class BeamlineStatePill(BECWidget, QWidget): self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self._state_name: str | None = None - self._title: str | None = None - self._state_config: dict[str, Any] = {} + self._state_config: messages.BeamlineStateConfig | None = None self._status = "unknown" self._label = "No state information available." self._expanded = False @@ -100,9 +98,9 @@ class BeamlineStatePill(BECWidget, QWidget): self._settings_dirty_fields: set[str] = set() self._settings_form_stale = True - self._init_ui(state_name, title) + self._init_ui(state_name) - def _init_ui(self, state_name: str | None = None, title: str | None = None) -> None: + def _init_ui(self, state_name: str | None = None) -> None: self._shadow = QGraphicsDropShadowEffect(self) self._shadow.setBlurRadius(18) self._shadow.setOffset(0, 2) @@ -212,7 +210,7 @@ class BeamlineStatePill(BECWidget, QWidget): self.setLayout(layout) self._settings.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - self.set_state_name(state_name, title=title) + self.set_state_name(state_name) self._update_button.setEnabled(False) self._revert_button.setEnabled(False) @@ -225,15 +223,14 @@ class BeamlineStatePill(BECWidget, QWidget): def state_name(self, state_name: str | None) -> None: self.set_state_name(state_name) - def set_state_name(self, state_name: str | None, title: str | None = None) -> None: + def set_state_name(self, state_name: str | None) -> None: """ Set the BEC beamline state this pill displays. Args: state_name: State name as published by ``AvailableBeamlineStatesMessage``. - title: Optional human-readable title for the state. """ - if state_name == self._state_name and title == self._title: + if state_name == self._state_name: return if self._state_name is not None: @@ -242,8 +239,7 @@ class BeamlineStatePill(BECWidget, QWidget): ) self._state_name = state_name - self._title = title - self._name_label.setText(title or state_name or "Beamline state") + self._name_label.setText(state_name or "Beamline state") if self._state_name is None: self._set_visual_state("unknown", "No beamline state selected.") @@ -255,13 +251,13 @@ class BeamlineStatePill(BECWidget, QWidget): self.update_state, MessageEndpoints.beamline_state(self._state_name) ) - def set_state_config(self, state_config: dict[str, Any]) -> None: + def set_state_config(self, state_config: messages.BeamlineStateConfig | None) -> None: """Set the editable BEC state configuration displayed by the expanded panel.""" self._state_config = state_config self._settings_form_stale = True if self._config_form is not None: self._populate_settings() - self._mark_settings_clean_from_current() + self.mark_current_settings_clean() @SafeProperty(bool, default=False) def idle_card_background(self) -> bool: @@ -439,13 +435,13 @@ class BeamlineStatePill(BECWidget, QWidget): def _ensure_settings_form_current(self) -> PydanticWidgetForm: if self._settings_form_stale: self._populate_settings() - self._mark_settings_clean_from_current() + self.mark_current_settings_clean() return self._ensure_config_form() def _populate_settings(self) -> None: self._populating_settings = True try: - state_type = str(self._state_config.get("state_type") or "") + state_type = self._state_config.state_type if self._state_config is not None else "" config_class = None for state_class in SUPPORTED_BEAMLINE_STATES: if state_type in {state_class.__name__, state_class.CONFIG_CLASS.state_type}: @@ -468,25 +464,8 @@ class BeamlineStatePill(BECWidget, QWidget): config = self._ensure_settings_form_current().model_instance() return config # type: ignore[return-value] - def mark_current_settings_clean( - self, config: bl_states.BeamlineStateConfig | None = None - ) -> None: + def mark_current_settings_clean(self) -> None: """Mark the current editor values as saved.""" - if config is None: - parameters = self._ensure_config_form().raw_editable_data() - else: - parameters = config.model_dump(exclude={"name"}) - if self._state_config: - state_parameters = self._state_config.get("parameters") - if isinstance(state_parameters, dict): - state_parameters.update(parameters) - else: - self._state_config.update(parameters) - if "title" in parameters: - self._state_config["title"] = parameters["title"] - self._mark_settings_clean_from_current() - - def _mark_settings_clean_from_current(self) -> None: config_form = self._ensure_config_form() self._settings_baseline = config_form.raw_editable_data() config_form.mark_clean() @@ -561,15 +540,12 @@ class BeamlineStatePill(BECWidget, QWidget): self, config_class: type[bl_states.BeamlineStateConfig] ) -> dict[str, Any]: data: dict[str, Any] = {} - parameters = self._state_config.get("parameters") - parameter_values = parameters if isinstance(parameters, dict) else {} + parameters = self._state_config.parameters if self._state_config is not None else {} for name in config_class.model_fields: if name == "name": data[name] = self._state_name - elif name in parameter_values: - data[name] = parameter_values[name] - elif name in self._state_config: - data[name] = self._state_config[name] + elif name in parameters: + data[name] = parameters[name] return data @staticmethod diff --git a/tests/unit_tests/test_beamline_state_pill.py b/tests/unit_tests/test_beamline_state_pill.py index a70e233a..2c8d775e 100644 --- a/tests/unit_tests/test_beamline_state_pill.py +++ b/tests/unit_tests/test_beamline_state_pill.py @@ -1,7 +1,7 @@ import shiboken6 -from bec_lib import bl_states +from bec_lib import bl_states, messages from qtpy.QtCore import QCoreApplication, QEvent, Qt -from qtpy.QtWidgets import QMessageBox, QStyleOptionViewItem +from qtpy.QtWidgets import QMessageBox from bec_widgets.utils.toolbars.toolbar import ModularToolBar from bec_widgets.utils.widget_io import WidgetIO @@ -15,14 +15,30 @@ from .client_mocks import mocked_client from .conftest import create_widget -def test_beamline_state_pill_updates_from_message(qtbot, mocked_client): - pill = create_widget( - qtbot, BeamlineStatePill, state_name="shutter_open", title="Shutter", client=mocked_client +def _state(name: str, state_type: str, parameters: dict | None = None): + return messages.BeamlineStateConfig( + name=name, state_type=state_type, parameters=parameters or {} ) + + +def _limits_state(name: str = "limits", **overrides): + parameters = { + "device": "samx", + "signal": "samx", + "low_limit": 0.0, + "high_limit": 10.0, + "tolerance": 0.1, + } + parameters.update(overrides) + return _state(name, "DeviceWithinLimitsState", parameters) + + +def test_beamline_state_pill_updates_from_message(qtbot, mocked_client): + pill = create_widget(qtbot, BeamlineStatePill, state_name="shutter_open", client=mocked_client) pill.update_state({"name": "shutter_open", "status": "valid", "label": "Shutter is open."}, {}) assert pill._state_name == "shutter_open" - assert pill._name_label.text() == "Shutter" + assert pill._name_label.text() == "shutter_open" assert pill._status_label.text() == "VALID" assert pill._detail_label.text() == "Shutter is open." assert not pill._icon_label.pixmap().isNull() @@ -30,9 +46,7 @@ def test_beamline_state_pill_updates_from_message(qtbot, mocked_client): def test_beamline_state_pill_ignores_other_states(qtbot, mocked_client): - pill = create_widget( - qtbot, BeamlineStatePill, state_name="shutter_open", title="Shutter", client=mocked_client - ) + pill = create_widget(qtbot, BeamlineStatePill, state_name="shutter_open", client=mocked_client) pill.update_state( {"name": "other_state", "status": "invalid", "label": "Should be ignored."}, {} ) @@ -42,25 +56,8 @@ def test_beamline_state_pill_ignores_other_states(qtbot, mocked_client): def test_beamline_state_pill_expands_and_emits_updated_limits(qtbot, mocked_client): - limits_pill = create_widget( - qtbot, BeamlineStatePill, state_name="limits", title="Limits", client=mocked_client - ) - limits_pill.set_state_config( - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": { - "name": "limits", - "title": "Limits", - "device": "samx", - "signal": "samx", - "low_limit": 0.0, - "high_limit": 10.0, - "tolerance": 0.1, - }, - } - ) + limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client) + limits_pill.set_state_config(_limits_state()) assert limits_pill._settings.isHidden() assert limits_pill._config_form is None @@ -97,9 +94,7 @@ def test_beamline_state_pill_expands_and_emits_updated_limits(qtbot, mocked_clie def test_beamline_state_pill_first_expand_uses_config_class_without_rebuild( qtbot, mocked_client, monkeypatch ): - limits_pill = create_widget( - qtbot, BeamlineStatePill, state_name="limits", title="Limits", client=mocked_client - ) + limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client) set_model_calls = [] original_set_model = pill_module.PydanticWidgetForm.set_model @@ -108,20 +103,7 @@ def test_beamline_state_pill_first_expand_uses_config_class_without_rebuild( return original_set_model(self, model, data=data) monkeypatch.setattr(pill_module.PydanticWidgetForm, "set_model", set_model_spy) - limits_pill.set_state_config( - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": { - "device": "samx", - "signal": "samx", - "low_limit": 0.0, - "high_limit": 10.0, - "tolerance": 0.1, - }, - } - ) + limits_pill.set_state_config(_limits_state()) limits_pill.set_expanded(True) assert limits_pill._config_form is not None @@ -129,23 +111,8 @@ def test_beamline_state_pill_first_expand_uses_config_class_without_rebuild( def test_beamline_state_pill_reverts_changed_settings(qtbot, mocked_client): - limits_pill = create_widget( - qtbot, BeamlineStatePill, state_name="limits", title="Limits", client=mocked_client - ) - limits_pill.set_state_config( - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": { - "device": "samx", - "signal": "samx", - "low_limit": 0.0, - "high_limit": 10.0, - "tolerance": 0.1, - }, - } - ) + limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client) + limits_pill.set_state_config(_limits_state()) limits_pill.set_expanded(True) assert limits_pill._config_form is not None @@ -166,23 +133,8 @@ def test_beamline_state_pill_reverts_changed_settings(qtbot, mocked_client): def test_beamline_state_pill_does_not_override_themed_input_controls(qtbot, mocked_client): - limits_pill = create_widget( - qtbot, BeamlineStatePill, state_name="limits", title="Limits", client=mocked_client - ) - limits_pill.set_state_config( - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": { - "device": "samx", - "signal": "samx", - "low_limit": 0.0, - "high_limit": 10.0, - "tolerance": 0.1, - }, - } - ) + limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client) + limits_pill.set_state_config(_limits_state()) limits_pill.set_expanded(True) stylesheet = limits_pill.styleSheet() @@ -197,18 +149,8 @@ def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client): beamline_state_manager.update_available_states( { "states": [ - { - "name": "shutter_open", - "title": "Shutter", - "state_type": "ShutterState", - "parameters": {}, - }, - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": {}, - }, + _state("shutter_open", "ShutterState"), + _state("limits", "DeviceWithinLimitsState"), ] }, {}, @@ -216,7 +158,7 @@ def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client): assert sorted(beamline_state_manager._state_pills) == ["limits", "shutter_open"] assert beamline_state_manager._model.rowCount() == 2 - assert beamline_state_manager._state_pills["shutter_open"]._name_label.text() == "Shutter" + assert beamline_state_manager._state_pills["shutter_open"]._name_label.text() == "shutter_open" assert not beamline_state_manager._empty_label.isVisible() beamline_state_manager._state_pills["limits"].update_state( @@ -227,17 +169,7 @@ def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client): assert summary["shutter_open"]["status"] == "unknown" beamline_state_manager.update_available_states( - { - "states": [ - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": {}, - } - ] - }, - {}, + {"states": [_state("limits", "DeviceWithinLimitsState")]}, {} ) assert sorted(beamline_state_manager._state_pills) == ["limits"] @@ -246,22 +178,7 @@ def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client): def test_beamline_state_manager_ignores_unchanged_available_states(qtbot, mocked_client): beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client) - content = { - "states": [ - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": { - "device": "samx", - "signal": "samx", - "low_limit": 0.0, - "high_limit": 10.0, - "tolerance": 0.1, - }, - } - ] - } + content = {"states": [_limits_state()]} beamline_state_manager.update_available_states(content, {}) pill = beamline_state_manager._state_pills["limits"] @@ -274,24 +191,8 @@ def test_beamline_state_manager_ignores_unchanged_available_states(qtbot, mocked def test_beamline_state_manager_adds_state_without_recreating_existing_pills(qtbot, mocked_client): beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client) - limits_state = { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": { - "device": "samx", - "signal": "samx", - "low_limit": 0.0, - "high_limit": 10.0, - "tolerance": 0.1, - }, - } - shutter_state = { - "name": "shutter_open", - "title": "Shutter", - "state_type": "ShutterState", - "parameters": {}, - } + limits_state = _limits_state() + shutter_state = _state("shutter_open", "ShutterState") beamline_state_manager.update_available_states({"states": [limits_state]}, {}) pill = beamline_state_manager._state_pills["limits"] @@ -309,17 +210,7 @@ def test_beamline_state_manager_adds_state_without_recreating_existing_pills(qtb def test_beamline_state_manager_header_click_expands_pill_once(qtbot, mocked_client): beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client) beamline_state_manager.update_available_states( - { - "states": [ - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": {"device": "samx"}, - } - ] - }, - {}, + {"states": [_state("limits", "DeviceWithinLimitsState", {"device": "samx"})]}, {} ) pill = beamline_state_manager._state_pills["limits"] @@ -332,12 +223,7 @@ def test_beamline_state_manager_header_click_expands_pill_once(qtbot, mocked_cli def test_beamline_state_manager_preserves_expanded_pill_on_refresh(qtbot, mocked_client): beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client) - state = { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": {"device": "samx", "high_limit": 10.0}, - } + state = _state("limits", "DeviceWithinLimitsState", {"device": "samx", "high_limit": 10.0}) beamline_state_manager.update_available_states({"states": [state]}, {}) beamline_state_manager._state_pills["limits"].set_expanded(True) @@ -352,17 +238,7 @@ def test_beamline_state_manager_propagates_idle_card_background(qtbot, mocked_cl qtbot, BeamlineStateManager, client=mocked_client, idle_card_background=True ) idle_card_manager.update_available_states( - { - "states": [ - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": {"device": "samx"}, - } - ] - }, - {}, + {"states": [_state("limits", "DeviceWithinLimitsState", {"device": "samx"})]}, {} ) assert idle_card_manager._state_pills["limits"]._idle_card_background is True @@ -377,18 +253,8 @@ def test_beamline_state_manager_filters_status(qtbot, mocked_client): beamline_state_manager.update_available_states( { "states": [ - { - "name": "shutter_open", - "title": "Shutter", - "state_type": "ShutterState", - "parameters": {"device": "samy"}, - }, - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": {"device": "samx"}, - }, + _state("shutter_open", "ShutterState", {"device": "samy"}), + _state("limits", "DeviceWithinLimitsState", {"device": "samx"}), ] }, {}, @@ -433,17 +299,7 @@ def test_beamline_state_manager_filters_status(qtbot, mocked_client): def test_beamline_state_manager_status_filter_reacts_to_state_changes(qtbot, mocked_client): beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client) beamline_state_manager.update_available_states( - { - "states": [ - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": {"device": "samx"}, - } - ] - }, - {}, + {"states": [_state("limits", "DeviceWithinLimitsState", {"device": "samx"})]}, {} ) beamline_state_manager._selected_statuses = {"valid"} @@ -468,18 +324,8 @@ def test_beamline_state_manager_filters_devices(qtbot, mocked_client, monkeypatc beamline_state_manager.update_available_states( { "states": [ - { - "name": "samx_limits", - "title": "samx", - "state_type": "DeviceWithinLimitsState", - "parameters": {"device": "samx"}, - }, - { - "name": "samy_limits", - "title": "samy", - "state_type": "DeviceWithinLimitsState", - "parameters": {"device": "samy"}, - }, + _state("samx_limits", "DeviceWithinLimitsState", {"device": "samx"}), + _state("samy_limits", "DeviceWithinLimitsState", {"device": "samy"}), ] }, {}, @@ -512,27 +358,28 @@ def test_beamline_state_manager_filters_devices(qtbot, mocked_client, monkeypatc assert captured["parent"] is beamline_state_manager +def test_beamline_state_manager_backend_echo_repopulates_expanded_pill(qtbot, mocked_client): + beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client) + beamline_state_manager.update_available_states({"states": [_limits_state()]}, {}) + + pill = beamline_state_manager._state_pills["limits"] + pill.set_expanded(True) + high_limit = pill._config_form.input_widget("high_limit") + high_limit.setValue(20.0) + + assert pill._update_button.isEnabled() + + beamline_state_manager.update_available_states({"states": [_limits_state(high_limit=20.0)]}, {}) + + assert pill.is_expanded() + assert high_limit.value() == 20.0 + assert not pill._update_button.isEnabled() + assert pill._config_form.dirty_fields() == set() + + def test_beamline_state_manager_updates_state_parameters(qtbot, mocked_client): beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client) - beamline_state_manager.update_available_states( - { - "states": [ - { - "name": "limits", - "title": "Limits", - "state_type": "DeviceWithinLimitsState", - "parameters": { - "device": "samx", - "signal": "samx", - "low_limit": 0.0, - "high_limit": 10.0, - "tolerance": 0.1, - }, - } - ] - }, - {}, - ) + beamline_state_manager.update_available_states({"states": [_limits_state()]}, {}) class StateClient: def __init__(self):