mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-08-30 05:11:16 +02:00
refactor(device_input): consolidation of device/signal combobox logic; docsrtings added
This commit is contained in:
@@ -58,6 +58,10 @@ def test_curve_setting_init(curve_setting_fixture):
|
||||
# Check that there's a curve_manager inside y_axis_box
|
||||
assert hasattr(curve_setting, "curve_manager")
|
||||
assert curve_setting.y_axis_box.layout.count() > 0
|
||||
assert not curve_setting.device_x.isEnabled()
|
||||
assert not curve_setting.signal_x.isEnabled()
|
||||
assert "red" not in curve_setting.device_x.styleSheet()
|
||||
assert "red" not in curve_setting.signal_x.styleSheet()
|
||||
|
||||
|
||||
def test_curve_setting_accept_changes(curve_setting_fixture, qtbot):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from unittest import mock
|
||||
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from bec_lib.device import Positioner, ReadoutPriority
|
||||
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import (
|
||||
BECDeviceFilter,
|
||||
@@ -55,13 +55,6 @@ def test_device_combobox_set_default_device(qtbot, mocked_client):
|
||||
assert widget.config.default == "samx"
|
||||
|
||||
|
||||
def test_device_combobox_get_filters(qtbot, mocked_client):
|
||||
widget = create_widget(qtbot=qtbot, widget=DeviceComboBox, client=mocked_client)
|
||||
|
||||
assert BECDeviceFilter.POSITIONER in widget.get_available_filters()
|
||||
assert ReadoutPriority.MONITORED in widget.get_readout_priority_filters()
|
||||
|
||||
|
||||
def test_device_combobox_properties(qtbot, mocked_client):
|
||||
widget = create_widget(qtbot=qtbot, widget=DeviceComboBox, client=mocked_client)
|
||||
|
||||
@@ -76,14 +69,35 @@ def test_device_combobox_properties(qtbot, mocked_client):
|
||||
BECDeviceFilter.SIGNAL,
|
||||
]
|
||||
|
||||
widget.readout_async = False
|
||||
assert ReadoutPriority.ASYNC not in widget.readout_filter
|
||||
|
||||
widget.readout_async = True
|
||||
widget.readout_baseline = True
|
||||
widget.readout_monitored = True
|
||||
widget.readout_on_request = True
|
||||
assert ReadoutPriority.ASYNC in widget.readout_filter
|
||||
assert ReadoutPriority.BASELINE in widget.readout_filter
|
||||
assert ReadoutPriority.MONITORED in widget.readout_filter
|
||||
assert ReadoutPriority.ON_REQUEST in widget.readout_filter
|
||||
|
||||
|
||||
def test_device_combobox_multiple_device_filters_match_main_intersection(qtbot, mocked_client):
|
||||
widget = create_widget(qtbot=qtbot, widget=DeviceComboBox, client=mocked_client)
|
||||
|
||||
widget.filter_to_device = True
|
||||
widget.filter_to_positioner = True
|
||||
|
||||
assert widget.devices
|
||||
assert all(isinstance(getattr(widget.dev, device), Positioner) for device in widget.devices)
|
||||
assert "eiger" not in widget.devices
|
||||
|
||||
|
||||
def test_device_combobox_empty_readout_filter_matches_main_empty_selection(qtbot, mocked_client):
|
||||
widget = create_widget(qtbot=qtbot, widget=DeviceComboBox, client=mocked_client)
|
||||
|
||||
widget.readout_monitored = False
|
||||
widget.readout_baseline = False
|
||||
widget.readout_async = False
|
||||
widget.readout_continuous = False
|
||||
widget.readout_on_request = False
|
||||
|
||||
assert widget.readout_filter == []
|
||||
assert widget.devices == []
|
||||
|
||||
|
||||
def test_device_combobox_signal_class_filter(qtbot, mocked_client):
|
||||
|
||||
@@ -83,6 +83,10 @@ def test_device_input_combobox_autocomplete(qtbot, mocked_client):
|
||||
widget = DeviceComboBox(client=mocked_client, autocomplete=True)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
line_edit = widget.lineEdit()
|
||||
text_changes: list[str] = []
|
||||
line_edit.setPlaceholderText("Select Device")
|
||||
line_edit.textChanged.connect(text_changes.append)
|
||||
|
||||
assert widget.autocomplete is True
|
||||
assert widget.completer() is not None
|
||||
@@ -93,6 +97,31 @@ def test_device_input_combobox_autocomplete(qtbot, mocked_client):
|
||||
|
||||
assert widget.completer() is not None
|
||||
assert widget.completer().model() == widget.model()
|
||||
assert widget.lineEdit().placeholderText() == "Select Device"
|
||||
|
||||
widget.lineEdit().setText("manual_device")
|
||||
|
||||
assert text_changes[-1] == "manual_device"
|
||||
|
||||
|
||||
def test_device_input_combobox_refresh_preserves_manual_text(device_input_combobox):
|
||||
device_input_combobox.setCurrentText("manual_device")
|
||||
|
||||
device_input_combobox.update_devices_from_filters()
|
||||
|
||||
assert device_input_combobox.currentText() == "manual_device"
|
||||
assert device_input_combobox.is_valid_input is False
|
||||
|
||||
|
||||
def test_device_input_combobox_disabled_invalid_has_neutral_border(device_input_combobox):
|
||||
device_input_combobox.setCurrentText("manual_device")
|
||||
assert "red" in device_input_combobox.styleSheet()
|
||||
|
||||
device_input_combobox.setEnabled(False)
|
||||
assert "transparent" in device_input_combobox.styleSheet()
|
||||
|
||||
device_input_combobox.setEnabled(True)
|
||||
assert "red" in device_input_combobox.styleSheet()
|
||||
|
||||
|
||||
def test_get_device_from_input_combobox_init(device_input_combobox):
|
||||
|
||||
@@ -8,7 +8,10 @@ from bec_widgets.widgets.control.device_input.device_combobox.device_combobox im
|
||||
BECDeviceFilter,
|
||||
DeviceComboBox,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import (
|
||||
SignalComboBox,
|
||||
SignalComboBoxConfig,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
@@ -18,6 +21,10 @@ class FakeSignal(Signal):
|
||||
"""Fake signal used by SignalComboBox tests."""
|
||||
|
||||
|
||||
def signal_names(signals):
|
||||
return [entry[0] if isinstance(entry, tuple) else entry for entry in signals]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_signal_combobox(qtbot, mocked_client):
|
||||
widget = create_widget(qtbot=qtbot, widget=SignalComboBox, client=mocked_client)
|
||||
@@ -49,10 +56,27 @@ def test_signal_combobox_init(device_signal_combobox):
|
||||
assert device_signal_combobox.completer().model() == device_signal_combobox.model()
|
||||
|
||||
|
||||
def test_signal_combobox_config_defaults_are_independent_lists():
|
||||
config_a = SignalComboBoxConfig(widget_class="SignalComboBox")
|
||||
config_b = SignalComboBoxConfig(widget_class="SignalComboBox")
|
||||
|
||||
config_a.signal_filter.append("hinted")
|
||||
config_a.signal_class_filter.append("AsyncSignal")
|
||||
config_a.signals.append("sig")
|
||||
|
||||
assert config_b.signal_filter == []
|
||||
assert config_b.signal_class_filter == []
|
||||
assert config_b.signals == []
|
||||
|
||||
|
||||
def test_signal_combobox_autocomplete(qtbot, mocked_client):
|
||||
widget = create_widget(
|
||||
qtbot=qtbot, widget=SignalComboBox, client=mocked_client, autocomplete=True
|
||||
)
|
||||
line_edit = widget.lineEdit()
|
||||
text_changes: list[str] = []
|
||||
line_edit.setPlaceholderText("Select Signal")
|
||||
line_edit.textChanged.connect(text_changes.append)
|
||||
|
||||
widget.set_device("samx")
|
||||
|
||||
@@ -65,6 +89,11 @@ def test_signal_combobox_autocomplete(qtbot, mocked_client):
|
||||
|
||||
assert widget.completer() is not None
|
||||
assert widget.completer().model() == widget.model()
|
||||
assert widget.lineEdit().placeholderText() == "Select Signal"
|
||||
|
||||
widget.lineEdit().setText("manual_signal")
|
||||
|
||||
assert text_changes[-1] == "manual_signal"
|
||||
|
||||
|
||||
def test_signal_combobox_qproperties(device_signal_combobox):
|
||||
@@ -82,6 +111,17 @@ def test_signal_combobox_qproperties(device_signal_combobox):
|
||||
assert device_signal_combobox._signal_filter == {Kind.config, Kind.normal}
|
||||
|
||||
|
||||
def test_signal_combobox_disabled_invalid_has_neutral_border(device_signal_combobox):
|
||||
device_signal_combobox.setCurrentText("manual_signal")
|
||||
assert "red" in device_signal_combobox.styleSheet()
|
||||
|
||||
device_signal_combobox.setEnabled(False)
|
||||
assert "transparent" in device_signal_combobox.styleSheet()
|
||||
|
||||
device_signal_combobox.setEnabled(True)
|
||||
assert "red" in device_signal_combobox.styleSheet()
|
||||
|
||||
|
||||
def test_signal_combobox(qtbot, device_signal_combobox):
|
||||
"""Test the signal_combobox"""
|
||||
container = []
|
||||
@@ -117,6 +157,34 @@ def test_signal_combobox(qtbot, device_signal_combobox):
|
||||
assert device_signal_combobox._hinted_signals == [("fake_signal", {})]
|
||||
|
||||
|
||||
def test_linked_device_combobox_updates_signal_combobox_on_each_text_change(
|
||||
qtbot, test_device_signal_combo
|
||||
):
|
||||
device, signal = test_device_signal_combo
|
||||
device.currentTextChanged.connect(signal.set_device)
|
||||
|
||||
emitted_device_texts: list[str] = []
|
||||
device.currentTextChanged.connect(emitted_device_texts.append)
|
||||
|
||||
device.setCurrentText("samx")
|
||||
assert signal.device == "samx"
|
||||
assert signal.currentText() == "samx (readback)"
|
||||
|
||||
device.setCurrentText("sa")
|
||||
|
||||
assert emitted_device_texts[-1] == "sa"
|
||||
assert signal.device == ""
|
||||
assert signal.signals == []
|
||||
assert signal.currentText() == ""
|
||||
assert signal.is_valid_input is False
|
||||
|
||||
device.setCurrentText("samx")
|
||||
|
||||
assert emitted_device_texts[-1] == "samx"
|
||||
assert signal.device == "samx"
|
||||
assert [entry[0] for entry in signal.signals] == ["samx (readback)", "setpoint", "velocity"]
|
||||
|
||||
|
||||
def test_device_signal_input_base_cleanup(qtbot, mocked_client):
|
||||
with mock.patch.object(mocked_client.callbacks, "remove"):
|
||||
widget = SignalComboBox(client=mocked_client)
|
||||
@@ -224,11 +292,107 @@ def test_signal_combobox_signal_class_filter_by_device(qtbot, mocked_client):
|
||||
device="samx",
|
||||
)
|
||||
|
||||
assert widget.signals == ["samx_readback_async"]
|
||||
assert signal_names(widget.signals) == ["samx_readback_async"]
|
||||
assert widget.signal_class_filter == ["AsyncSignal"]
|
||||
|
||||
widget.set_device("samy")
|
||||
assert widget.signals == ["samy_readback_async"]
|
||||
assert signal_names(widget.signals) == ["samy_readback_async"]
|
||||
|
||||
|
||||
def test_signal_combobox_signal_class_filter_selects_by_metadata(qtbot, mocked_client):
|
||||
"""Class-based signal lists should support obj_name/component_name lookup."""
|
||||
mocked_client.device_manager.get_bec_signals = mock.MagicMock(
|
||||
return_value=[
|
||||
(
|
||||
"eiger",
|
||||
"image",
|
||||
{
|
||||
"obj_name": "eiger_image",
|
||||
"component_name": "det.image",
|
||||
"signal_class": "PreviewSignal",
|
||||
"describe": {"signal_info": {"ndim": 2}},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
widget = create_widget(
|
||||
qtbot=qtbot,
|
||||
widget=SignalComboBox,
|
||||
client=mocked_client,
|
||||
signal_class_filter=["PreviewSignal"],
|
||||
ndim_filter=[2],
|
||||
device="eiger",
|
||||
)
|
||||
|
||||
assert widget.validate_signal("eiger_image") is True
|
||||
assert widget.validate_signal("det.image") is True
|
||||
assert widget.set_to_obj_name("eiger_image") is True
|
||||
assert widget.currentText() == "image"
|
||||
|
||||
widget.set_signal("det.image")
|
||||
|
||||
assert widget.currentText() == "image"
|
||||
|
||||
|
||||
def test_signal_combobox_signal_class_update_revalidates_selected_signal(qtbot, mocked_client):
|
||||
"""Signal-class rebuilds should validate after items and signal metadata are in sync."""
|
||||
mocked_client.device_manager.get_bec_signals = mock.MagicMock(
|
||||
return_value=[
|
||||
(
|
||||
"eiger",
|
||||
"img",
|
||||
{
|
||||
"obj_name": "img",
|
||||
"signal_class": "PreviewSignal",
|
||||
"describe": {"signal_info": {"ndim": 2}},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
widget = create_widget(
|
||||
qtbot=qtbot,
|
||||
widget=SignalComboBox,
|
||||
client=mocked_client,
|
||||
signal_class_filter=["PreviewSignal"],
|
||||
ndim_filter=[2],
|
||||
require_device=True,
|
||||
)
|
||||
|
||||
widget.set_device("eiger")
|
||||
|
||||
assert widget.currentText() == "img"
|
||||
assert widget.is_valid_input is True
|
||||
|
||||
|
||||
def test_signal_combobox_signal_class_refresh_preserves_manual_text(qtbot, mocked_client):
|
||||
mocked_client.device_manager.get_bec_signals = mock.MagicMock(
|
||||
return_value=[
|
||||
(
|
||||
"eiger",
|
||||
"img",
|
||||
{
|
||||
"obj_name": "img",
|
||||
"signal_class": "PreviewSignal",
|
||||
"describe": {"signal_info": {"ndim": 2}},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
widget = create_widget(
|
||||
qtbot=qtbot,
|
||||
widget=SignalComboBox,
|
||||
client=mocked_client,
|
||||
signal_class_filter=["PreviewSignal"],
|
||||
ndim_filter=[2],
|
||||
require_device=True,
|
||||
)
|
||||
|
||||
widget.set_device("eiger")
|
||||
widget.setCurrentText("manual_signal")
|
||||
widget.update_signals_from_signal_classes()
|
||||
|
||||
assert widget.currentText() == "manual_signal"
|
||||
assert widget.is_valid_input is False
|
||||
|
||||
|
||||
def test_signal_class_filter_setter_clears_to_kind_filters(qtbot, mocked_client):
|
||||
@@ -243,7 +407,7 @@ def test_signal_class_filter_setter_clears_to_kind_filters(qtbot, mocked_client)
|
||||
signal_class_filter=["AsyncSignal"],
|
||||
device="samx",
|
||||
)
|
||||
assert widget.signals == ["samx_readback_async"]
|
||||
assert signal_names(widget.signals) == ["samx_readback_async"]
|
||||
|
||||
widget.signal_class_filter = []
|
||||
samx = widget.dev.samx
|
||||
@@ -266,7 +430,7 @@ def test_signal_class_filter_setter_none_reverts_to_kind_filters(qtbot, mocked_c
|
||||
signal_class_filter=["AsyncSignal"],
|
||||
device="samx",
|
||||
)
|
||||
assert widget.signals == ["samx_readback_async"]
|
||||
assert signal_names(widget.signals) == ["samx_readback_async"]
|
||||
|
||||
widget.signal_class_filter = None
|
||||
samx = widget.dev.samx
|
||||
@@ -333,12 +497,12 @@ def test_signal_combobox_class_kind_ndim_filters(qtbot, mocked_client):
|
||||
)
|
||||
|
||||
# Default kinds are hinted + normal, ndim=1, device=samx
|
||||
assert widget.signals == ["sig1"]
|
||||
assert signal_names(widget.signals) == ["sig1"]
|
||||
|
||||
# Enable config kinds and widen ndim to include sig2
|
||||
widget.include_config_signals = True
|
||||
widget.ndim_filter = 2
|
||||
assert widget.signals == ["sig2"]
|
||||
assert signal_names(widget.signals) == ["sig2"]
|
||||
|
||||
|
||||
def test_signal_combobox_require_device_validation(qtbot, mocked_client):
|
||||
@@ -366,7 +530,7 @@ def test_signal_combobox_require_device_validation(qtbot, mocked_client):
|
||||
|
||||
assert widget.signals == []
|
||||
widget.set_device("samx")
|
||||
assert widget.signals == ["sig1"]
|
||||
assert signal_names(widget.signals) == ["sig1"]
|
||||
|
||||
resets: list[str] = []
|
||||
widget.signal_reset.connect(lambda: resets.append("reset"))
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
from qtpy.QtWidgets import QComboBox
|
||||
|
||||
from bec_widgets.utils.filter_io import (
|
||||
combobox_contains_text,
|
||||
get_bec_signals_for_classes,
|
||||
replace_combobox_items,
|
||||
)
|
||||
from bec_widgets.utils.filter_io import get_bec_signals_for_classes, replace_combobox_items
|
||||
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
@@ -20,8 +16,6 @@ def test_replace_combobox_items(qtbot, mocked_client):
|
||||
assert widget.itemText(0) == "testA"
|
||||
assert widget.itemText(1) == "testB"
|
||||
assert widget.itemData(1) == {"payload": True}
|
||||
assert combobox_contains_text(widget, "testA") is True
|
||||
assert combobox_contains_text(widget, "missing") is False
|
||||
|
||||
|
||||
def test_get_bec_signals_for_classes_ndim_filter(mocked_client):
|
||||
@@ -46,3 +40,19 @@ def test_replace_combobox_items_empty(qtbot):
|
||||
replace_combobox_items(widget, [])
|
||||
|
||||
assert widget.count() == 0
|
||||
|
||||
|
||||
def test_replace_combobox_items_preserves_text_and_blocks_signals(qtbot):
|
||||
widget = QComboBox()
|
||||
qtbot.addWidget(widget)
|
||||
widget.setEditable(True)
|
||||
widget.addItems(["old", "other"])
|
||||
widget.setCurrentText("typed")
|
||||
emitted: list[str] = []
|
||||
widget.currentTextChanged.connect(emitted.append)
|
||||
|
||||
replace_combobox_items(widget, ["new"], preserve_current_text=True, block_signals=True)
|
||||
|
||||
assert widget.currentText() == "typed"
|
||||
assert widget.itemText(0) == "new"
|
||||
assert emitted == []
|
||||
|
||||
Reference in New Issue
Block a user