diff --git a/bec_widgets/utils/forms_from_types/items.py b/bec_widgets/utils/forms_from_types/items.py index 039b35ac..dceb4a2c 100644 --- a/bec_widgets/utils/forms_from_types/items.py +++ b/bec_widgets/utils/forms_from_types/items.py @@ -430,7 +430,7 @@ class ListMetadataField(DynamicFormItem): return self._data def setValue(self, value: list): - if set(map(type, value)) != {self._types.item}: + if set(map(type, value)) | {self._types.item} != {self._types.item}: raise ValueError(f"This widget only accepts items of type {self._types.item}") self._repop(value) diff --git a/bec_widgets/widgets/editors/dict_backed_table.py b/bec_widgets/widgets/editors/dict_backed_table.py index f0cebdd3..3bb57458 100644 --- a/bec_widgets/widgets/editors/dict_backed_table.py +++ b/bec_widgets/widgets/editors/dict_backed_table.py @@ -193,8 +193,8 @@ class DictBackedTable(QWidget): def clear(self): self._table_model.replaceData({}) - def replace_data(self, data: dict): - self._table_model.replaceData(data) + def replace_data(self, data: dict | None): + self._table_model.replaceData(data or {}) def delete_selected_rows(self): """Delete rows which are part of the selection model""" diff --git a/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py b/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py index 5cc734e2..bfeec42c 100644 --- a/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py +++ b/bec_widgets/widgets/services/device_browser/device_item/device_config_dialog.py @@ -28,9 +28,13 @@ class DeviceConfigDialog(BECWidget, QDialog): RPC = False def __init__( - self, parent=None, device: str | None = None, config_helper: ConfigHelper | None = None + self, + parent=None, + device: str | None = None, + config_helper: ConfigHelper | None = None, + **kwargs, ): - super().__init__(parent=parent) + super().__init__(parent=parent, **kwargs) self._config_helper = config_helper or ConfigHelper( self.client.connector, self.client._service_name ) diff --git a/tests/unit_tests/test_device_config_form_dialog.py b/tests/unit_tests/test_device_config_form_dialog.py new file mode 100644 index 00000000..38b07200 --- /dev/null +++ b/tests/unit_tests/test_device_config_form_dialog.py @@ -0,0 +1,97 @@ +from unittest.mock import MagicMock, patch + +import pytest +from bec_lib.atlas_models import Device as DeviceConfigModel + +from bec_widgets.widgets.services.device_browser.device_item.device_config_dialog import ( + DeviceConfigDialog, +) + +_BASIC_CONFIG = { + "name": "test_device", + "enabled": True, + "deviceClass": "TestDevice", + "readoutPriority": "monitored", +} + + +@pytest.fixture +def dialog(qtbot): + """Fixture to create a DeviceConfigDialog instance.""" + mock_device = MagicMock(_config=DeviceConfigModel.model_validate(_BASIC_CONFIG).model_dump()) + mock_client = MagicMock() + mock_client.device_manager.devices = {"test_device": mock_device} + dialog = DeviceConfigDialog(device="test_device", config_helper=MagicMock(), client=mock_client) + qtbot.addWidget(dialog) + return dialog + + +def test_initialization(dialog): + assert dialog._device == "test_device" + assert dialog._container.count() == 2 + + +def test_fill_form(dialog): + with patch.object(dialog._form, "set_data") as mock_set_data: + dialog._fill_form() + mock_set_data.assert_called_once_with(DeviceConfigModel.model_validate(_BASIC_CONFIG)) + + +def test_updated_config(dialog): + """Test that updated_config returns the correct changes.""" + dialog._initial_config = {"key1": "value1", "key2": "value2"} + with patch.object( + dialog._form, "get_form_data", return_value={"key1": "value1", "key2": "new_value"} + ): + updated = dialog.updated_config() + assert updated == {"key2": "new_value"} + + +def test_apply(dialog): + with patch.object(dialog, "_process_update_action") as mock_process_update: + dialog.apply() + mock_process_update.assert_called_once() + + +def test_accept(dialog): + with ( + patch.object(dialog, "_process_update_action") as mock_process_update, + patch("qtpy.QtWidgets.QDialog.accept") as mock_parent_accept, + ): + dialog.accept() + mock_process_update.assert_called_once() + mock_parent_accept.assert_called_once() + + +def test_waiting_display(dialog, qtbot): + with ( + patch.object(dialog._spinner, "start") as mock_spinner_start, + patch.object(dialog._spinner, "stop") as mock_spinner_stop, + ): + dialog.show() + dialog._start_waiting_display() + qtbot.waitUntil(dialog._overlay_widget.isVisible, timeout=100) + mock_spinner_start.assert_called_once() + mock_spinner_stop.assert_not_called() + dialog._stop_waiting_display() + qtbot.waitUntil(lambda: not dialog._overlay_widget.isVisible(), timeout=100) + mock_spinner_stop.assert_called_once() + + +def test_update_cycle(dialog, qtbot): + update = {"enabled": False, "readoutPriority": "baseline", "deviceTags": ["tag"]} + + def _mock_send(a, c, w): + dialog.client.device_manager.devices["test_device"]._config = c["test_device"] + + dialog._config_helper.send_config_request = MagicMock(side_effect=_mock_send) + for item in dialog._form.enumerate_form_widgets(): + if (val := update.get(item.label.property("_model_field_name"))) is not None: + item.widget.setValue(val) + + assert dialog.updated_config() == update + dialog.apply() + + dialog._config_helper.send_config_request.assert_called_with( + action="update", config={"test_device": update}, wait_for_response=False + ) diff --git a/tests/unit_tests/test_pydantic_model_form.py b/tests/unit_tests/test_generated_form_form.py similarity index 99% rename from tests/unit_tests/test_pydantic_model_form.py rename to tests/unit_tests/test_generated_form_form.py index 1ddcb4d0..bc3f73d5 100644 --- a/tests/unit_tests/test_pydantic_model_form.py +++ b/tests/unit_tests/test_generated_form_form.py @@ -3,7 +3,7 @@ from decimal import Decimal import pytest from pydantic import BaseModel, Field -from bec_widgets.utils.forms_from_types.forms import PydanticModelForm +from bec_widgets.utils.forms_from_types.forms import PydanticModelForm, TypedForm from bec_widgets.utils.forms_from_types.items import ( FloatDecimalMetadataField, IntMetadataField, diff --git a/tests/unit_tests/test_generated_form_items.py b/tests/unit_tests/test_generated_form_items.py index 4cedd7c7..3cd1ec80 100644 --- a/tests/unit_tests/test_generated_form_items.py +++ b/tests/unit_tests/test_generated_form_items.py @@ -1,5 +1,5 @@ import sys -from typing import Any, Literal +from typing import Any, Literal, get_args import pytest from pydantic import ValidationError @@ -68,7 +68,7 @@ def test_form_item_spec(input, validity): {"type": list[float], "value": [0.1, 0.2, 0.3], "extra": 79.0}, ] ) -def list_metadata_field_and_values(request, qtbot): +def list_field_and_values(request, qtbot): itype, vals, extra = ( request.param.get("type"), request.param.get("value"), @@ -77,40 +77,49 @@ def list_metadata_field_and_values(request, qtbot): spec = FormItemSpec(item_type=itype, name="test_list", info=FieldInfo(annotation=itype)) (widget := ListMetadataField(parent=None, spec=spec)).setValue(vals) qtbot.addWidget(widget) - yield widget, vals, extra + yield widget, vals, extra, get_args(itype)[0] -def test_list_metadata_field(list_metadata_field_and_values: tuple[ListMetadataField, list, Any]): - list_metadata_field, vals, extra = list_metadata_field_and_values - assert list_metadata_field.getValue() == vals - assert list_metadata_field._main_widget.count() == 3 +def test_list_metadata_field(list_field_and_values: tuple[ListMetadataField, list, Any, type]): + list_field, vals, extra, _ = list_field_and_values + assert list_field.getValue() == vals + assert list_field._main_widget.count() == 3 - list_metadata_field._add_button.click() - assert len(list_metadata_field.getValue()) == 4 - assert list_metadata_field._main_widget.count() == 4 + list_field._add_button.click() + assert len(list_field.getValue()) == 4 + assert list_field._main_widget.count() == 4 - list_metadata_field._main_widget.setCurrentRow(-1) - list_metadata_field._remove_button.click() - assert len(list_metadata_field.getValue()) == 4 - assert list_metadata_field._main_widget.count() == 4 + list_field._main_widget.setCurrentRow(-1) + list_field._remove_button.click() + assert len(list_field.getValue()) == 4 + assert list_field._main_widget.count() == 4 - list_metadata_field._main_widget.setCurrentRow(2) - list_metadata_field._remove_button.click() - assert list_metadata_field.getValue() == vals[:2] + [list_metadata_field._types.default] - assert list_metadata_field._main_widget.count() == 3 + list_field._main_widget.setCurrentRow(2) + list_field._remove_button.click() + assert list_field.getValue() == vals[:2] + [list_field._types.default] + assert list_field._main_widget.count() == 3 - list_metadata_field._main_widget.setCurrentRow(1) - WidgetIO.set_value( - list_metadata_field._main_widget.itemWidget(list_metadata_field._main_widget.item(1)), extra - ) - assert list_metadata_field._main_widget.count() == 3 - assert list_metadata_field.getValue() == [vals[0], extra, list_metadata_field._types.default] + list_field._main_widget.setCurrentRow(1) + WidgetIO.set_value(list_field._main_widget.itemWidget(list_field._main_widget.item(1)), extra) + assert list_field._main_widget.count() == 3 + assert list_field.getValue() == [vals[0], extra, list_field._types.default] - list_metadata_field._add_item(extra) - assert list_metadata_field._main_widget.count() == 4 - assert list_metadata_field.getValue() == [ - vals[0], - extra, - list_metadata_field._types.default, - extra, - ] + list_field._add_item(extra) + assert list_field._main_widget.count() == 4 + assert list_field.getValue() == [vals[0], extra, list_field._types.default, extra] + + +def test_list_field_value_acceptance( + list_field_and_values: tuple[ListMetadataField, list, Any, type], +): + class _WrongType(object): ... + + list_field, _, _, t = list_field_and_values + list_field.setValue([]) + assert list_field._main_widget.count() == 0 + list_field.setValue([t(), t(), t()]) + assert list_field._main_widget.count() == 3 + with pytest.raises(ValueError) as e: + list_field.setValue([_WrongType()]) + assert list_field._main_widget.count() == 3 + assert e.match(f"This widget only accepts items of type {t}")