mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-09 02:00:56 +02:00
Compare commits
3 Commits
feature/fe
...
test/move_
| Author | SHA1 | Date | |
|---|---|---|---|
| a94eaede46 | |||
| 1b9a56f4d5 | |||
| a3794a22b3 |
@@ -189,7 +189,6 @@ class LaunchTile(RoundedFrame):
|
||||
|
||||
class LaunchWindow(BECMainWindow):
|
||||
RPC = True
|
||||
PLUGIN = False
|
||||
TILE_SIZE = (250, 300)
|
||||
DEFAULT_LAUNCH_SIZE = (800, 600)
|
||||
USER_ACCESS = ["show_launcher", "hide_launcher"]
|
||||
|
||||
@@ -5,7 +5,6 @@ from bec_widgets.applications.navigation_centre.side_bar import SideBar
|
||||
from bec_widgets.applications.navigation_centre.side_bar_components import NavigationItem
|
||||
from bec_widgets.applications.views.developer_view.developer_view import DeveloperView
|
||||
from bec_widgets.applications.views.device_manager_view.device_manager_view import DeviceManagerView
|
||||
from bec_widgets.applications.views.dock_area_view.dock_area_view import DockAreaView
|
||||
from bec_widgets.applications.views.view import ViewBase, WaveformViewInline, WaveformViewPopup
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.utils.screen_utils import (
|
||||
@@ -13,12 +12,11 @@ from bec_widgets.utils.screen_utils import (
|
||||
available_screen_geometry,
|
||||
main_app_size_for_screen,
|
||||
)
|
||||
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
|
||||
|
||||
class BECMainApp(BECMainWindow):
|
||||
RPC = False
|
||||
PLUGIN = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -52,16 +50,13 @@ class BECMainApp(BECMainWindow):
|
||||
|
||||
def _add_views(self):
|
||||
self.add_section("BEC Applications", "bec_apps")
|
||||
self.dock_area = DockAreaView(self)
|
||||
self.ads = BECDockArea(self, profile_namespace="bec", auto_profile_namespace=False)
|
||||
self.ads.setObjectName("MainWorkspace")
|
||||
self.device_manager = DeviceManagerView(self)
|
||||
self.developer_view = DeveloperView(self)
|
||||
|
||||
self.add_view(
|
||||
icon="widgets",
|
||||
title="Dock Area",
|
||||
id="dock_area",
|
||||
widget=self.dock_area,
|
||||
mini_text="Docks",
|
||||
icon="widgets", title="Dock Area", id="dock_area", widget=self.ads, mini_text="Docks"
|
||||
)
|
||||
self.add_view(
|
||||
icon="display_settings",
|
||||
|
||||
@@ -79,8 +79,6 @@ def markdown_to_html(md_text: str) -> str:
|
||||
|
||||
|
||||
class DeveloperWidget(DockAreaWidget):
|
||||
RPC = False
|
||||
PLUGIN = False
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, variant="compact", **kwargs)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.applications.views.view import ViewBase
|
||||
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
|
||||
|
||||
|
||||
class DockAreaView(ViewBase):
|
||||
"""
|
||||
Modular dock area view for arranging and managing multiple dockable widgets.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
content: QWidget | None = None,
|
||||
*,
|
||||
id: str | None = None,
|
||||
title: str | None = None,
|
||||
):
|
||||
super().__init__(parent=parent, content=content, id=id, title=title)
|
||||
self.dock_area = BECDockArea(
|
||||
self, profile_namespace="bec", auto_profile_namespace=False, object_name="DockArea"
|
||||
)
|
||||
self.set_content(self.dock_area)
|
||||
@@ -2848,20 +2848,6 @@ class ImageItem(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class LaunchWindow(RPCBase):
|
||||
@rpc_call
|
||||
def show_launcher(self):
|
||||
"""
|
||||
Show the launcher window.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def hide_launcher(self):
|
||||
"""
|
||||
Hide the launcher window.
|
||||
"""
|
||||
|
||||
|
||||
class LogPanel(RPCBase):
|
||||
"""Displays a log panel"""
|
||||
|
||||
|
||||
@@ -291,8 +291,7 @@ def main():
|
||||
|
||||
client_path = module_dir / client_subdir / "client.py"
|
||||
|
||||
packages = ("widgets", "applications") if module_name == "bec_widgets" else ("widgets",)
|
||||
rpc_classes = get_custom_classes(module_name, packages=packages)
|
||||
rpc_classes = get_custom_classes(module_name)
|
||||
logger.info(f"Obtained classes with RPC objects: {rpc_classes!r}")
|
||||
|
||||
generator = ClientGenerator(base=module_name == "bec_widgets")
|
||||
|
||||
@@ -32,8 +32,7 @@ class RPCWidgetHandler:
|
||||
None
|
||||
"""
|
||||
self._widget_classes = (
|
||||
get_custom_classes("bec_widgets", packages=("widgets", "applications"))
|
||||
+ get_all_plugin_widgets()
|
||||
get_custom_classes("bec_widgets") + get_all_plugin_widgets()
|
||||
).as_dict(IGNORE_WIDGETS)
|
||||
|
||||
def create_widget(self, widget_type, **kwargs) -> BECWidget:
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
# pylint: skip-file
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import h5py
|
||||
from bec_lib import messages
|
||||
from bec_lib.bec_service import messages
|
||||
from bec_lib.config_helper import ConfigHelper
|
||||
from bec_lib.device import Device as BECDevice
|
||||
from bec_lib.device import Positioner as BECPositioner
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from bec_lib.devicemanager import DeviceContainer
|
||||
from bec_lib.messages import _StoredDataInfo
|
||||
from bec_lib.scan_history import ScanHistory
|
||||
from qtpy.QtCore import QEvent, QEventLoop
|
||||
|
||||
|
||||
class FakeDevice(BECDevice):
|
||||
@@ -219,7 +228,9 @@ class Device(FakeDevice):
|
||||
|
||||
|
||||
class DMMock:
|
||||
def __init__(self):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._service = args[0]
|
||||
self.config_helper = ConfigHelper(self._service.connector, self._service._service_name)
|
||||
self.devices = DeviceContainer()
|
||||
self.enabled_devices = [device for device in self.devices if device.enabled]
|
||||
|
||||
@@ -273,6 +284,10 @@ class DMMock:
|
||||
configs.append(device._config)
|
||||
return configs
|
||||
|
||||
def initialize(*_): ...
|
||||
|
||||
def shutdown(self): ...
|
||||
|
||||
|
||||
DEVICES = [
|
||||
FakePositioner("samx", limits=[-10, 10], read_value=2.0),
|
||||
@@ -301,3 +316,157 @@ def check_remote_data_size(widget, plot_name, num_elements):
|
||||
Used in the qtbot.waitUntil function.
|
||||
"""
|
||||
return len(widget.get_all_data()[plot_name]["x"]) == num_elements
|
||||
|
||||
|
||||
class DummyData:
|
||||
def __init__(self, val, timestamps):
|
||||
self.val = val
|
||||
self.timestamps = timestamps
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key == "val":
|
||||
return self.val
|
||||
return default
|
||||
|
||||
|
||||
def create_dummy_scan_item():
|
||||
"""
|
||||
Helper to create a dummy scan item with both live_data and metadata/status_message info.
|
||||
"""
|
||||
dummy_live_data = {
|
||||
"samx": {"samx": DummyData(val=[10, 20, 30], timestamps=[100, 200, 300])},
|
||||
"samy": {"samy": DummyData(val=[5, 10, 15], timestamps=[100, 200, 300])},
|
||||
"bpm4i": {"bpm4i": DummyData(val=[5, 6, 7], timestamps=[101, 201, 301])},
|
||||
"async_device": {"async_device": DummyData(val=[1, 2, 3], timestamps=[11, 21, 31])},
|
||||
}
|
||||
dummy_scan = MagicMock()
|
||||
dummy_scan.live_data = dummy_live_data
|
||||
dummy_scan.metadata = {
|
||||
"bec": {
|
||||
"scan_id": "dummy",
|
||||
"scan_report_devices": ["samx"],
|
||||
"readout_priority": {"monitored": ["bpm4i"], "async": ["async_device"]},
|
||||
}
|
||||
}
|
||||
dummy_scan.status_message.info = {
|
||||
"readout_priority": {"monitored": ["bpm4i"], "async": ["async_device"]},
|
||||
"scan_report_devices": ["samx"],
|
||||
}
|
||||
return dummy_scan
|
||||
|
||||
|
||||
def inject_scan_history(widget, scan_history_factory, *history_args):
|
||||
"""
|
||||
Helper to inject scan history messages into client history.
|
||||
"""
|
||||
history_msgs = []
|
||||
for scan_id, scan_number in history_args:
|
||||
history_msgs.append(scan_history_factory(scan_id=scan_id, scan_number=scan_number))
|
||||
widget.client.history = ScanHistory(widget.client, False)
|
||||
for msg in history_msgs:
|
||||
widget.client.history._scan_data[msg.scan_id] = msg
|
||||
widget.client.history._scan_ids.append(msg.scan_id)
|
||||
widget.client.queue.scan_storage.current_scan = None
|
||||
return history_msgs
|
||||
|
||||
|
||||
def create_history_file(file_path, data: dict, metadata: dict) -> messages.ScanHistoryMessage:
|
||||
"""
|
||||
Helper to create a history file with the given data.
|
||||
The data should contain readout groups, e.g.
|
||||
{
|
||||
"baseline": {"samx": {"samx": {"value": [1, 2, 3], "timestamp": [100, 200, 300]}},
|
||||
"monitored": {"bpm4i": {"bpm4i": {"value": [5, 6, 7], "timestamp": [101, 201, 301]}}},
|
||||
"async": {"async_device": {"async_device": {"value": [1, 2, 3], "timestamp": [11, 21, 31]}}},
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
with h5py.File(file_path, "w") as f:
|
||||
_metadata = f.create_group("entry/collection/metadata")
|
||||
_metadata.create_dataset("sample_name", data="test_sample")
|
||||
metadata_bec = f.create_group("entry/collection/metadata/bec")
|
||||
for key, value in metadata.items():
|
||||
if isinstance(value, dict):
|
||||
metadata_bec.create_group(key)
|
||||
for sub_key, sub_value in value.items():
|
||||
if isinstance(sub_value, list):
|
||||
sub_value = json.dumps(sub_value)
|
||||
metadata_bec[key].create_dataset(sub_key, data=sub_value)
|
||||
elif isinstance(sub_value, dict):
|
||||
for sub_sub_key, sub_sub_value in sub_value.items():
|
||||
sub_sub_group = metadata_bec[key].create_group(sub_key)
|
||||
# Handle _StoredDataInfo objects
|
||||
if isinstance(sub_sub_value, _StoredDataInfo):
|
||||
# Store the numeric shape
|
||||
sub_sub_group.create_dataset("shape", data=sub_sub_value.shape)
|
||||
# Store the dtype as a UTF-8 string
|
||||
dt = sub_sub_value.dtype or ""
|
||||
sub_sub_group.create_dataset(
|
||||
"dtype", data=dt, dtype=h5py.string_dtype(encoding="utf-8")
|
||||
)
|
||||
continue
|
||||
if isinstance(sub_sub_value, list):
|
||||
json_val = json.dumps(sub_sub_value)
|
||||
sub_sub_group.create_dataset(sub_sub_key, data=json_val)
|
||||
elif isinstance(sub_sub_value, dict):
|
||||
for k2, v2 in sub_sub_value.items():
|
||||
val = json.dumps(v2) if isinstance(v2, list) else v2
|
||||
sub_sub_group.create_dataset(k2, data=val)
|
||||
else:
|
||||
sub_sub_group.create_dataset(sub_sub_key, data=sub_sub_value)
|
||||
else:
|
||||
metadata_bec[key].create_dataset(sub_key, data=sub_value)
|
||||
else:
|
||||
metadata_bec.create_dataset(key, data=value)
|
||||
for group, devices in data.items():
|
||||
readout_group = f.create_group(f"entry/collection/readout_groups/{group}")
|
||||
|
||||
for device, device_data in devices.items():
|
||||
dev_group = f.create_group(f"entry/collection/devices/{device}")
|
||||
for signal, signal_data in device_data.items():
|
||||
signal_group = dev_group.create_group(signal)
|
||||
for signal_key, signal_values in signal_data.items():
|
||||
signal_group.create_dataset(signal_key, data=signal_values)
|
||||
|
||||
readout_group[device] = h5py.SoftLink(f"/entry/collection/devices/{device}")
|
||||
msg = messages.ScanHistoryMessage(
|
||||
scan_id=metadata["scan_id"],
|
||||
scan_name=metadata["scan_name"],
|
||||
exit_status=metadata["exit_status"],
|
||||
file_path=file_path,
|
||||
scan_number=metadata["scan_number"],
|
||||
dataset_number=metadata["dataset_number"],
|
||||
start_time=time.time(),
|
||||
end_time=time.time(),
|
||||
num_points=metadata["num_points"],
|
||||
request_inputs=metadata["request_inputs"],
|
||||
stored_data_info=metadata.get("stored_data_info"),
|
||||
metadata={"scan_report_devices": metadata.get("scan_report_devices")},
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
def create_widget(qtbot, widget, *args, **kwargs):
|
||||
"""
|
||||
Create a widget and add it to the qtbot for testing. This is a helper function that
|
||||
should be used in all tests that require a widget to be created.
|
||||
|
||||
Args:
|
||||
qtbot (fixture): pytest-qt fixture
|
||||
widget (QWidget): widget class to be created
|
||||
*args: positional arguments for the widget
|
||||
**kwargs: keyword arguments for the widget
|
||||
|
||||
Returns:
|
||||
QWidget: the created widget
|
||||
"""
|
||||
widget = widget(*args, **kwargs)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
|
||||
|
||||
def process_all_deferred_deletes(qapp):
|
||||
qapp.sendPostedEvents(None, QEvent.Type.DeferredDelete)
|
||||
qapp.processEvents(QEventLoop.ProcessEventsFlag.AllEvents)
|
||||
|
||||
@@ -12,7 +12,7 @@ import shiboken6 as shb
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.utils.import_utils import lazy_import_from
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy.QtCore import Property, QObject, QRunnable, QThreadPool, Signal
|
||||
from qtpy.QtCore import Property, QObject, QRunnable, QThreadPool, QTimer, Signal
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
@@ -23,6 +23,7 @@ from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, s
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.widgets.containers.dock import BECDock
|
||||
else:
|
||||
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
|
||||
|
||||
@@ -272,8 +273,6 @@ class BECConnector:
|
||||
Args:
|
||||
name (str): The new object name.
|
||||
"""
|
||||
# sanitize before setting to avoid issues with Qt object names and RPC namespaces
|
||||
name = sanitize_namespace(name)
|
||||
super().setObjectName(name)
|
||||
self.object_name = name
|
||||
if self.rpc_register.object_is_registered(self):
|
||||
|
||||
@@ -123,17 +123,16 @@ class BECDispatcher:
|
||||
self._registered_slots: DefaultDict[Hashable, QtThreadSafeCallback] = (
|
||||
collections.defaultdict()
|
||||
)
|
||||
self.client = client
|
||||
|
||||
if self.client is None:
|
||||
if config is not None:
|
||||
if not isinstance(config, ServiceConfig):
|
||||
# config is supposed to be a path
|
||||
config = ServiceConfig(config)
|
||||
if client is None:
|
||||
if config is not None and not isinstance(config, ServiceConfig):
|
||||
# config is supposed to be a path
|
||||
config = ServiceConfig(config)
|
||||
self.client = BECClient(
|
||||
config=config, connector_cls=QtRedisConnector, name="BECWidgets"
|
||||
)
|
||||
else:
|
||||
self.client = client
|
||||
if self.client.started:
|
||||
# have to reinitialize client to use proper connector
|
||||
logger.info("Shutting down BECClient to switch to QtRedisConnector")
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
"""
|
||||
Login dialog for user authentication.
|
||||
The Login Widget is styled in a Material Design style and emits
|
||||
the entered credentials through a signal for further processing.
|
||||
"""
|
||||
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtWidgets import QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class BECLogin(QWidget):
|
||||
"""Login dialog for user authentication in Material Design style."""
|
||||
|
||||
credentials_entered = Signal(str, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
# Only displayed if this widget as standalone widget, and not embedded in another widget
|
||||
self.setWindowTitle("Login")
|
||||
|
||||
title = QLabel("Sign in", parent=self)
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title.setStyleSheet(
|
||||
"""
|
||||
#QLabel
|
||||
{
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
self.username = QLineEdit(parent=self)
|
||||
self.username.setPlaceholderText("Username")
|
||||
|
||||
self.password = QLineEdit(parent=self)
|
||||
self.password.setPlaceholderText("Password")
|
||||
self.password.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
|
||||
self.ok_btn = QPushButton("Sign in", parent=self)
|
||||
self.ok_btn.setDefault(True)
|
||||
self.ok_btn.clicked.connect(self._emit_credentials)
|
||||
# If the user presses Enter in the password field, trigger the OK button click
|
||||
self.password.returnPressed.connect(self.ok_btn.click)
|
||||
|
||||
# Build Layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(32, 32, 32, 32)
|
||||
layout.setSpacing(16)
|
||||
|
||||
layout.addWidget(title)
|
||||
layout.addSpacing(8)
|
||||
layout.addWidget(self.username)
|
||||
layout.addWidget(self.password)
|
||||
layout.addSpacing(12)
|
||||
layout.addWidget(self.ok_btn)
|
||||
|
||||
self.username.setFocus()
|
||||
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QLineEdit {
|
||||
padding: 8px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def _clear_password(self):
|
||||
"""Clear the password field."""
|
||||
self.password.clear()
|
||||
|
||||
def _emit_credentials(self):
|
||||
"""Emit credentials and clear the password field."""
|
||||
self.credentials_entered.emit(self.username.text().strip(), self.password.text())
|
||||
self._clear_password()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from bec_qthemes import apply_theme
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("light")
|
||||
|
||||
dialog = BECLogin()
|
||||
|
||||
dialog.credentials_entered.connect(lambda u, p: print(f"Username: {u}, Password: {p}"))
|
||||
dialog.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Iterable
|
||||
|
||||
from bec_lib.plugin_helper import _get_available_plugins
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from qtpy.QtWidgets import QGraphicsWidget, QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
@@ -166,17 +166,18 @@ class BECClassContainer:
|
||||
return [info.obj for info in self.collection]
|
||||
|
||||
|
||||
def _collect_classes_from_package(repo_name: str, package: str) -> BECClassContainer:
|
||||
"""Collect classes from a package subtree (for example ``widgets`` or ``applications``)."""
|
||||
collection = BECClassContainer()
|
||||
try:
|
||||
anchor_module = importlib.import_module(f"{repo_name}.{package}")
|
||||
except ModuleNotFoundError as exc:
|
||||
# Some plugin repositories expose only one subtree. Skip gracefully if it does not exist.
|
||||
if exc.name == f"{repo_name}.{package}":
|
||||
return collection
|
||||
raise
|
||||
def get_custom_classes(repo_name: str) -> BECClassContainer:
|
||||
"""
|
||||
Get all RPC-enabled classes in the specified repository.
|
||||
|
||||
Args:
|
||||
repo_name(str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
|
||||
"""
|
||||
collection = BECClassContainer()
|
||||
anchor_module = importlib.import_module(f"{repo_name}.widgets")
|
||||
directory = os.path.dirname(anchor_module.__file__)
|
||||
for root, _, files in sorted(os.walk(directory)):
|
||||
for file in files:
|
||||
@@ -184,13 +185,13 @@ def _collect_classes_from_package(repo_name: str, package: str) -> BECClassConta
|
||||
continue
|
||||
|
||||
path = os.path.join(root, file)
|
||||
rel_dir = os.path.dirname(os.path.relpath(path, directory))
|
||||
if rel_dir in ("", "."):
|
||||
subs = os.path.dirname(os.path.relpath(path, directory)).split("/")
|
||||
if len(subs) == 1 and not subs[0]:
|
||||
module_name = file.split(".")[0]
|
||||
else:
|
||||
module_name = ".".join(rel_dir.split(os.sep) + [file.split(".")[0]])
|
||||
module_name = ".".join(subs + [file.split(".")[0]])
|
||||
|
||||
module = importlib.import_module(f"{repo_name}.{package}.{module_name}")
|
||||
module = importlib.import_module(f"{repo_name}.widgets.{module_name}")
|
||||
|
||||
for name in dir(module):
|
||||
obj = getattr(module, name)
|
||||
@@ -202,30 +203,12 @@ def _collect_classes_from_package(repo_name: str, package: str) -> BECClassConta
|
||||
class_info.is_connector = True
|
||||
if issubclass(obj, QWidget) or issubclass(obj, BECWidget):
|
||||
class_info.is_widget = True
|
||||
if len(subs) == 1 and (
|
||||
issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)
|
||||
):
|
||||
class_info.is_top_level = True
|
||||
if hasattr(obj, "PLUGIN") and obj.PLUGIN:
|
||||
class_info.is_plugin = True
|
||||
collection.add_class(class_info)
|
||||
return collection
|
||||
|
||||
|
||||
def get_custom_classes(
|
||||
repo_name: str, packages: tuple[str, ...] | None = None
|
||||
) -> BECClassContainer:
|
||||
"""
|
||||
Get all relevant classes for RPC/CLI in the specified repository.
|
||||
|
||||
By default, discovery is limited to ``<repo>.widgets`` for backward compatibility.
|
||||
Additional package subtrees (for example ``applications``) can be included explicitly.
|
||||
|
||||
Args:
|
||||
repo_name(str): The name of the repository.
|
||||
packages(tuple[str, ...] | None): Optional tuple of package names to scan. Defaults to ("widgets",) for backward compatibility.
|
||||
|
||||
Returns:
|
||||
BECClassContainer: Container with collected class information.
|
||||
"""
|
||||
selected_packages = packages or ("widgets",)
|
||||
collection = BECClassContainer()
|
||||
for package in selected_packages:
|
||||
collection += _collect_classes_from_package(repo_name, package)
|
||||
|
||||
return collection
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtCore import QEvent, QSize, Qt, QTimer
|
||||
from qtpy.QtGui import QAction, QActionGroup, QIcon
|
||||
@@ -32,7 +31,6 @@ from bec_widgets.widgets.containers.main_window.addons.notification_center.notif
|
||||
from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
|
||||
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
|
||||
from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
|
||||
from bec_widgets.widgets.utility.feedback_dialog.feedback_dialog import FeedbackDialog
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
@@ -344,34 +342,6 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
help_menu.addAction(widgets_docs)
|
||||
help_menu.addAction(bug_report)
|
||||
|
||||
# Add separator before feedback
|
||||
help_menu.addSeparator()
|
||||
|
||||
# Feedback action
|
||||
feedback_icon = QApplication.style().standardIcon(
|
||||
QStyle.StandardPixmap.SP_MessageBoxQuestion
|
||||
)
|
||||
feedback_action = QAction("Feedback", self)
|
||||
feedback_action.setIcon(feedback_icon)
|
||||
feedback_action.triggered.connect(self._show_feedback_dialog)
|
||||
help_menu.addAction(feedback_action)
|
||||
|
||||
def _show_feedback_dialog(self):
|
||||
"""Show the feedback dialog and handle the submitted feedback."""
|
||||
dialog = FeedbackDialog(self)
|
||||
|
||||
def on_feedback_submitted(rating: int, comment: str, email: str):
|
||||
rating = max(1, min(rating, 5)) # Ensure rating is between 1 and 5
|
||||
username = os.getlogin()
|
||||
|
||||
message = messages.FeedbackMessage(
|
||||
feedback=comment, rating=rating, contact=email, username=username
|
||||
)
|
||||
self.bec_dispatcher.client.connector.send(MessageEndpoints.submit_feedback(), message)
|
||||
|
||||
dialog.feedback_submitted.connect(on_feedback_submitted)
|
||||
dialog.exec()
|
||||
|
||||
################################################################################
|
||||
# Status Bar Addons
|
||||
################################################################################
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtGui import QColor, QFont
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QTextEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeConnect, SafeSlot
|
||||
|
||||
|
||||
class StarRating(QWidget):
|
||||
"""
|
||||
A star rating widget that allows users to rate from 1 to 5 stars.
|
||||
"""
|
||||
|
||||
rating_changed = Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._rating = 0
|
||||
self._hovered_star = 0
|
||||
self._star_buttons = []
|
||||
|
||||
# Get theme colors
|
||||
theme = getattr(QApplication.instance(), "theme", None)
|
||||
if theme:
|
||||
SafeConnect(self, theme.theme_changed, self._update_theme_colors)
|
||||
self._update_theme_colors()
|
||||
|
||||
# Enable mouse tracking to handle hover across the entire widget
|
||||
self.setMouseTracking(True)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(5)
|
||||
|
||||
for i in range(5):
|
||||
btn = QPushButton("★")
|
||||
btn.setFixedSize(30, 30)
|
||||
btn.setFlat(True)
|
||||
btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
btn.clicked.connect(lambda checked=False, idx=i + 1: self._set_rating(idx))
|
||||
layout.addWidget(btn)
|
||||
self._star_buttons.append(btn)
|
||||
|
||||
self.setLayout(layout)
|
||||
self._update_display()
|
||||
|
||||
@SafeSlot(str)
|
||||
def _update_theme_colors(self, _theme: str | None = None):
|
||||
"""Update colors based on theme."""
|
||||
theme = getattr(QApplication.instance(), "theme", None)
|
||||
colors = theme.colors if theme else {}
|
||||
|
||||
self._inactive_color = colors.get("SEPARATOR", QColor(200, 200, 200))
|
||||
self._active_color = colors.get("ACCENT_WARNING", QColor(255, 193, 7))
|
||||
|
||||
# Update display if already initialized
|
||||
if hasattr(self, "_star_buttons") and self._star_buttons:
|
||||
self._update_display()
|
||||
|
||||
def _set_rating(self, rating: int):
|
||||
"""Set the rating and emit the signal."""
|
||||
if self._rating != rating:
|
||||
self._rating = rating
|
||||
self.rating_changed.emit(rating)
|
||||
self._update_display()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""Handle mouse movement to update hovered star."""
|
||||
# Calculate which star is being hovered based on mouse position
|
||||
x_pos = event.pos().x()
|
||||
star_idx = 0
|
||||
|
||||
# Find which star region we're in (including gaps between stars)
|
||||
for i, btn in enumerate(self._star_buttons):
|
||||
btn_geometry = btn.geometry()
|
||||
# If we're to the right of this button's left edge, this is the current star
|
||||
# (including the gap before the next button)
|
||||
if x_pos >= btn_geometry.left():
|
||||
star_idx = i + 1
|
||||
else:
|
||||
break
|
||||
|
||||
if star_idx != self._hovered_star:
|
||||
self._hovered_star = star_idx
|
||||
self._update_display()
|
||||
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
"""Handle mouse leaving the widget."""
|
||||
self._hovered_star = 0
|
||||
self._update_display()
|
||||
super().leaveEvent(event)
|
||||
|
||||
def _update_display(self):
|
||||
"""Update the visual display of stars."""
|
||||
display_rating = self._hovered_star if self._hovered_star > 0 else self._rating
|
||||
inactive_color_name = self._inactive_color.name()
|
||||
active_color_name = self._active_color.name()
|
||||
|
||||
for i, btn in enumerate(self._star_buttons):
|
||||
if i < display_rating:
|
||||
btn.setStyleSheet(
|
||||
f"""
|
||||
QPushButton {{
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 24px;
|
||||
color: {active_color_name};
|
||||
}}
|
||||
"""
|
||||
)
|
||||
else:
|
||||
btn.setStyleSheet(
|
||||
f"""
|
||||
QPushButton {{
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 24px;
|
||||
color: {inactive_color_name};
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
color: {active_color_name};
|
||||
}}
|
||||
"""
|
||||
)
|
||||
|
||||
def rating(self) -> int:
|
||||
"""Get the current rating."""
|
||||
return self._rating
|
||||
|
||||
def set_rating(self, rating: int):
|
||||
"""Set the rating programmatically."""
|
||||
if 0 <= rating <= 5:
|
||||
self._set_rating(rating)
|
||||
|
||||
|
||||
class FeedbackDialog(QDialog):
|
||||
"""
|
||||
A feedback dialog widget containing a comment field, star rating, and optional email field.
|
||||
|
||||
Signals:
|
||||
feedbackSubmitted: Emitted when feedback is submitted (rating: int, comment: str, email: str)
|
||||
"""
|
||||
|
||||
feedback_submitted = Signal(int, str, str)
|
||||
ICON_NAME = "feedback"
|
||||
PLUGIN = True
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Feedback")
|
||||
self.setModal(True)
|
||||
self.setMinimumWidth(400)
|
||||
self.setMinimumHeight(300)
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the user interface."""
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(15)
|
||||
|
||||
# Title
|
||||
title_label = QLabel("We'd love to hear your feedback!")
|
||||
title_font = QFont()
|
||||
title_font.setPointSize(12)
|
||||
title_font.setBold(True)
|
||||
title_label.setFont(title_font)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# Star rating section
|
||||
rating_layout = QVBoxLayout()
|
||||
rating_label = QLabel("Rating:")
|
||||
rating_layout.addWidget(rating_label)
|
||||
|
||||
self._star_rating = StarRating()
|
||||
rating_layout.addWidget(self._star_rating)
|
||||
layout.addLayout(rating_layout)
|
||||
|
||||
# Comment section
|
||||
comment_label = QLabel("Comments:")
|
||||
layout.addWidget(comment_label)
|
||||
|
||||
self._comment_field = QTextEdit()
|
||||
self._comment_field.setPlaceholderText("Please share your thoughts...")
|
||||
self._comment_field.setMaximumHeight(150)
|
||||
layout.addWidget(self._comment_field)
|
||||
|
||||
# Email section (optional)
|
||||
email_label = QLabel("Email (optional, for follow-up):")
|
||||
layout.addWidget(email_label)
|
||||
|
||||
self._email_field = QLineEdit()
|
||||
self._email_field.setPlaceholderText("your.email@example.com")
|
||||
layout.addWidget(self._email_field)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.addStretch()
|
||||
|
||||
self._cancel_button = QPushButton("Cancel")
|
||||
self._cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(self._cancel_button)
|
||||
|
||||
self._submit_button = QPushButton("Submit")
|
||||
self._submit_button.setDefault(True)
|
||||
self._submit_button.clicked.connect(self._on_submit)
|
||||
button_layout.addWidget(self._submit_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def _on_submit(self):
|
||||
"""Handle submit button click."""
|
||||
rating = self._star_rating.rating()
|
||||
comment = self._comment_field.toPlainText().strip()
|
||||
email = self._email_field.text().strip()
|
||||
|
||||
# Emit the feedback signal
|
||||
self.feedback_submitted.emit(rating, comment, email)
|
||||
|
||||
# Accept the dialog
|
||||
self.accept()
|
||||
|
||||
def get_feedback(self) -> tuple[int, str, str]:
|
||||
"""
|
||||
Get the current feedback values.
|
||||
|
||||
Returns:
|
||||
tuple: (rating, comment, email)
|
||||
"""
|
||||
return (
|
||||
self._star_rating.rating(),
|
||||
self._comment_field.toPlainText().strip(),
|
||||
self._email_field.text().strip(),
|
||||
)
|
||||
|
||||
def set_rating(self, rating: int):
|
||||
"""Set the star rating."""
|
||||
self._star_rating.set_rating(rating)
|
||||
|
||||
def set_comment(self, comment: str):
|
||||
"""Set the comment text."""
|
||||
self._comment_field.setPlainText(comment)
|
||||
|
||||
def set_email(self, email: str):
|
||||
"""Set the email text."""
|
||||
self._email_field.setText(email)
|
||||
|
||||
@staticmethod
|
||||
def show_feedback_dialog(parent=None) -> tuple[int, str, str] | None:
|
||||
"""
|
||||
Show the feedback dialog and return the feedback if submitted.
|
||||
|
||||
Args:
|
||||
parent: Parent widget
|
||||
|
||||
Returns:
|
||||
tuple: (rating, comment, email) if submitted, None if cancelled
|
||||
"""
|
||||
dialog = FeedbackDialog(parent)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
return dialog.get_feedback()
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("dark")
|
||||
dialog = FeedbackDialog()
|
||||
|
||||
def on_feedback(rating, comment, email):
|
||||
print(f"Rating: {rating}")
|
||||
print(f"Comment: {comment}")
|
||||
print(f"Email: {email}")
|
||||
|
||||
dialog.feedback_submitted.connect(on_feedback)
|
||||
dialog.exec()
|
||||
sys.exit(app.exec())
|
||||
@@ -1,256 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
from math import inf
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import fakeredis
|
||||
import pytest
|
||||
from bec_lib.bec_service import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.redis_connector import RedisConnector
|
||||
from bec_lib.scan_history import ScanHistory
|
||||
|
||||
from bec_widgets.tests.utils import DEVICES, DMMock, FakePositioner, Positioner
|
||||
|
||||
|
||||
def fake_redis_server(host, port, **kwargs):
|
||||
redis = fakeredis.FakeRedis()
|
||||
return redis
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_client(bec_dispatcher):
|
||||
connector = RedisConnector("localhost:1", redis_cls=fake_redis_server)
|
||||
# Create a MagicMock object
|
||||
client = MagicMock() # TODO change to real BECClient
|
||||
|
||||
# Shutdown the original client
|
||||
bec_dispatcher.client.shutdown()
|
||||
# Mock the connector attribute
|
||||
bec_dispatcher.client = client
|
||||
|
||||
# Mock the device_manager.devices attribute
|
||||
client.connector = connector
|
||||
client.device_manager = DMMock()
|
||||
client.device_manager.add_devices(DEVICES)
|
||||
|
||||
def mock_mv(*args, relative=False):
|
||||
# Extracting motor and value pairs
|
||||
for i in range(0, len(args), 2):
|
||||
motor = args[i]
|
||||
value = args[i + 1]
|
||||
motor.move(value, relative=relative)
|
||||
return MagicMock(wait=MagicMock())
|
||||
|
||||
client.scans = MagicMock(mv=mock_mv)
|
||||
|
||||
# Ensure isinstance check for Positioner passes
|
||||
original_isinstance = isinstance
|
||||
|
||||
def isinstance_mock(obj, class_info):
|
||||
if class_info == Positioner and isinstance(obj, FakePositioner):
|
||||
return True
|
||||
return original_isinstance(obj, class_info)
|
||||
|
||||
with patch("builtins.isinstance", new=isinstance_mock):
|
||||
yield client
|
||||
connector.shutdown() # TODO change to real BECClient
|
||||
|
||||
|
||||
##################################################
|
||||
# Client Fixture with DAP
|
||||
##################################################
|
||||
@pytest.fixture(scope="function")
|
||||
def dap_plugin_message():
|
||||
msg = messages.AvailableResourceMessage(
|
||||
**{
|
||||
"resource": {
|
||||
"GaussianModel": {
|
||||
"class": "LmfitService1D",
|
||||
"user_friendly_name": "GaussianModel",
|
||||
"class_doc": "A model based on a Gaussian or normal distribution lineshape.\n\n The model has three Parameters: `amplitude`, `center`, and `sigma`.\n In addition, parameters `fwhm` and `height` are included as\n constraints to report full width at half maximum and maximum peak\n height, respectively.\n\n .. math::\n\n f(x; A, \\mu, \\sigma) = \\frac{A}{\\sigma\\sqrt{2\\pi}} e^{[{-{(x-\\mu)^2}/{{2\\sigma}^2}}]}\n\n where the parameter `amplitude` corresponds to :math:`A`, `center` to\n :math:`\\mu`, and `sigma` to :math:`\\sigma`. The full width at half\n maximum is :math:`2\\sigma\\sqrt{2\\ln{2}}`, approximately\n :math:`2.3548\\sigma`.\n\n For more information, see: https://en.wikipedia.org/wiki/Normal_distribution\n\n ",
|
||||
"run_doc": "A model based on a Gaussian or normal distribution lineshape.\n\n The model has three Parameters: `amplitude`, `center`, and `sigma`.\n In addition, parameters `fwhm` and `height` are included as\n constraints to report full width at half maximum and maximum peak\n height, respectively.\n\n .. math::\n\n f(x; A, \\mu, \\sigma) = \\frac{A}{\\sigma\\sqrt{2\\pi}} e^{[{-{(x-\\mu)^2}/{{2\\sigma}^2}}]}\n\n where the parameter `amplitude` corresponds to :math:`A`, `center` to\n :math:`\\mu`, and `sigma` to :math:`\\sigma`. The full width at half\n maximum is :math:`2\\sigma\\sqrt{2\\ln{2}}`, approximately\n :math:`2.3548\\sigma`.\n\n For more information, see: https://en.wikipedia.org/wiki/Normal_distribution\n\n \n Args:\n scan_item (ScanItem): Scan item or scan ID\n device_x (DeviceBase | str): Device name for x\n signal_x (DeviceBase | str): Signal name for x\n device_y (DeviceBase | str): Device name for y\n signal_y (DeviceBase | str): Signal name for y\n parameters (dict): Fit parameters\n ",
|
||||
"run_name": "fit",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "scan_item",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "ScanItem | str",
|
||||
},
|
||||
{
|
||||
"name": "device_x",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceBase | str",
|
||||
},
|
||||
{
|
||||
"name": "signal_x",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceBase | str",
|
||||
},
|
||||
{
|
||||
"name": "device_y",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceBase | str",
|
||||
},
|
||||
{
|
||||
"name": "signal_y",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceBase | str",
|
||||
},
|
||||
{
|
||||
"name": "parameters",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
"auto_fit_supported": True,
|
||||
"params": {
|
||||
"amplitude": {
|
||||
"name": "amplitude",
|
||||
"value": 1.0,
|
||||
"vary": True,
|
||||
"min": -inf,
|
||||
"max": inf,
|
||||
"expr": None,
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
"center": {
|
||||
"name": "center",
|
||||
"value": 0.0,
|
||||
"vary": True,
|
||||
"min": -inf,
|
||||
"max": inf,
|
||||
"expr": None,
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
"sigma": {
|
||||
"name": "sigma",
|
||||
"value": 1.0,
|
||||
"vary": True,
|
||||
"min": 0,
|
||||
"max": inf,
|
||||
"expr": None,
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
"fwhm": {
|
||||
"name": "fwhm",
|
||||
"value": 2.35482,
|
||||
"vary": False,
|
||||
"min": -inf,
|
||||
"max": inf,
|
||||
"expr": "2.3548200*sigma",
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
"height": {
|
||||
"name": "height",
|
||||
"value": 0.3989423,
|
||||
"vary": False,
|
||||
"min": -inf,
|
||||
"max": inf,
|
||||
"expr": "0.3989423*amplitude/max(1e-15, sigma)",
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
},
|
||||
"class_args": [],
|
||||
"class_kwargs": {"model": "GaussianModel"},
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
yield msg
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_client_with_dap(mocked_client, dap_plugin_message):
|
||||
dap_services = {
|
||||
"BECClient": messages.StatusMessage(name="BECClient", status=1, info={}),
|
||||
"DAPServer/LmfitService1D": messages.StatusMessage(
|
||||
name="LmfitService1D", status=1, info={}
|
||||
),
|
||||
}
|
||||
client = mocked_client
|
||||
client.service_status = dap_services
|
||||
client.connector.set(
|
||||
topic=MessageEndpoints.dap_available_plugins("dap"), msg=dap_plugin_message
|
||||
)
|
||||
|
||||
# Patch the client's DAP attribute so that the available models include "GaussianModel"
|
||||
patched_models = {"GaussianModel": {}, "LorentzModel": {}, "SineModel": {}}
|
||||
client.dap._available_dap_plugins = patched_models
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
class DummyData:
|
||||
def __init__(self, val, timestamps):
|
||||
self.val = val
|
||||
self.timestamps = timestamps
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key == "val":
|
||||
return self.val
|
||||
return default
|
||||
|
||||
|
||||
def create_dummy_scan_item():
|
||||
"""
|
||||
Helper to create a dummy scan item with both live_data and metadata/status_message info.
|
||||
"""
|
||||
dummy_live_data = {
|
||||
"samx": {"samx": DummyData(val=[10, 20, 30], timestamps=[100, 200, 300])},
|
||||
"samy": {"samy": DummyData(val=[5, 10, 15], timestamps=[100, 200, 300])},
|
||||
"bpm4i": {"bpm4i": DummyData(val=[5, 6, 7], timestamps=[101, 201, 301])},
|
||||
"async_device": {"async_device": DummyData(val=[1, 2, 3], timestamps=[11, 21, 31])},
|
||||
}
|
||||
dummy_scan = MagicMock()
|
||||
dummy_scan.live_data = dummy_live_data
|
||||
dummy_scan.metadata = {
|
||||
"bec": {
|
||||
"scan_id": "dummy",
|
||||
"scan_report_devices": ["samx"],
|
||||
"readout_priority": {"monitored": ["bpm4i"], "async": ["async_device"]},
|
||||
}
|
||||
}
|
||||
dummy_scan.status_message = MagicMock()
|
||||
dummy_scan.status_message.info = {
|
||||
"readout_priority": {"monitored": ["bpm4i"], "async": ["async_device"]},
|
||||
"scan_report_devices": ["samx"],
|
||||
}
|
||||
return dummy_scan
|
||||
|
||||
|
||||
def inject_scan_history(widget, scan_history_factory, *history_args):
|
||||
"""
|
||||
Helper to inject scan history messages into client history.
|
||||
"""
|
||||
history_msgs = []
|
||||
for scan_id, scan_number in history_args:
|
||||
history_msgs.append(scan_history_factory(scan_id=scan_id, scan_number=scan_number))
|
||||
widget.client.history = ScanHistory(widget.client, False)
|
||||
for msg in history_msgs:
|
||||
widget.client.history._scan_data[msg.scan_id] = msg
|
||||
widget.client.history._scan_ids.append(msg.scan_id)
|
||||
widget.client.queue.scan_storage.current_scan = None
|
||||
return history_msgs
|
||||
@@ -1,19 +1,37 @@
|
||||
import json
|
||||
import time
|
||||
from math import inf
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
import fakeredis
|
||||
import h5py
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_lib import messages
|
||||
from bec_lib import messages, service_config
|
||||
from bec_lib.bec_service import messages
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.messages import _StoredDataInfo
|
||||
from bec_lib.scan_history import ScanHistory
|
||||
from bec_qthemes import apply_theme
|
||||
from ophyd._pyepics_shim import _dispatcher
|
||||
from pytestqt.exceptions import TimeoutError as QtBotTimeoutError
|
||||
from qtpy.QtCore import QEvent, QEventLoop
|
||||
from qtpy.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.tests.utils import (
|
||||
DEVICES,
|
||||
DMMock,
|
||||
FakePositioner,
|
||||
Positioner,
|
||||
create_history_file,
|
||||
process_all_deferred_deletes,
|
||||
)
|
||||
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
|
||||
from bec_widgets.utils import error_popups
|
||||
from bec_widgets.utils.bec_dispatcher import QtRedisConnector
|
||||
|
||||
# Patch to set default RAISE_ERROR_DEFAULT to True for tests
|
||||
# This means that by default, error popups will raise exceptions during tests
|
||||
@@ -29,11 +47,6 @@ def pytest_runtest_makereport(item, call):
|
||||
item.stash["failed"] = rep.failed
|
||||
|
||||
|
||||
def process_all_deferred_deletes(qapp):
|
||||
qapp.sendPostedEvents(None, QEvent.DeferredDelete)
|
||||
qapp.processEvents(QEventLoop.AllEvents)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def qapplication(qtbot, request, testable_qtimer_class): # pylint: disable=unused-argument
|
||||
qapp = QApplication.instance()
|
||||
@@ -46,7 +59,6 @@ def qapplication(qtbot, request, testable_qtimer_class): # pylint: disable=unus
|
||||
# if the test failed, we don't want to check for open widgets as
|
||||
# it simply pollutes the output
|
||||
# stop pyepics dispatcher for leaking tests
|
||||
from ophyd._pyepics_shim import _dispatcher
|
||||
|
||||
_dispatcher.stop()
|
||||
if request.node.stash._storage.get("failed"):
|
||||
@@ -71,9 +83,36 @@ def rpc_register():
|
||||
RPCRegister.reset_singleton()
|
||||
|
||||
|
||||
_REDIS_CONN: QtRedisConnector | None = None
|
||||
|
||||
|
||||
def global_mock_qt_redis_connector(*_, **__):
|
||||
global _REDIS_CONN
|
||||
if _REDIS_CONN is None:
|
||||
_REDIS_CONN = QtRedisConnector(bootstrap="localhost:1", redis_cls=fakeredis.FakeRedis)
|
||||
return _REDIS_CONN
|
||||
|
||||
|
||||
def mock_client(*_, **__):
|
||||
with (
|
||||
patch("bec_lib.client.DeviceManagerBase", DMMock),
|
||||
patch("bec_lib.client.DAPPlugins"),
|
||||
patch("bec_lib.client.Scans"),
|
||||
patch("bec_lib.client.ScanManager"),
|
||||
patch("bec_lib.bec_service.BECAccess"),
|
||||
):
|
||||
client = BECClient(
|
||||
config=service_config.ServiceConfig(config={"redis": {"host": "localhost", "port": 1}}),
|
||||
connector_cls=global_mock_qt_redis_connector,
|
||||
)
|
||||
client.start()
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def bec_dispatcher(threads_check): # pylint: disable=unused-argument
|
||||
bec_dispatcher = bec_dispatcher_module.BECDispatcher()
|
||||
with mock.patch.object(bec_dispatcher_module, "BECClient", mock_client):
|
||||
bec_dispatcher = bec_dispatcher_module.BECDispatcher()
|
||||
yield bec_dispatcher
|
||||
bec_dispatcher.disconnect_all()
|
||||
# clean BEC client
|
||||
@@ -97,103 +136,6 @@ def suppress_message_box(monkeypatch):
|
||||
monkeypatch.setattr(QMessageBox, "exec_", lambda *args, **kwargs: QMessageBox.Ok)
|
||||
|
||||
|
||||
def create_widget(qtbot, widget, *args, **kwargs):
|
||||
"""
|
||||
Create a widget and add it to the qtbot for testing. This is a helper function that
|
||||
should be used in all tests that require a widget to be created.
|
||||
|
||||
Args:
|
||||
qtbot (fixture): pytest-qt fixture
|
||||
widget (QWidget): widget class to be created
|
||||
*args: positional arguments for the widget
|
||||
**kwargs: keyword arguments for the widget
|
||||
|
||||
Returns:
|
||||
QWidget: the created widget
|
||||
"""
|
||||
widget = widget(*args, **kwargs)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
return widget
|
||||
|
||||
|
||||
def create_history_file(file_path, data: dict, metadata: dict) -> messages.ScanHistoryMessage:
|
||||
"""
|
||||
Helper to create a history file with the given data.
|
||||
The data should contain readout groups, e.g.
|
||||
{
|
||||
"baseline": {"samx": {"samx": {"value": [1, 2, 3], "timestamp": [100, 200, 300]}},
|
||||
"monitored": {"bpm4i": {"bpm4i": {"value": [5, 6, 7], "timestamp": [101, 201, 301]}}},
|
||||
"async": {"async_device": {"async_device": {"value": [1, 2, 3], "timestamp": [11, 21, 31]}}},
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
with h5py.File(file_path, "w") as f:
|
||||
_metadata = f.create_group("entry/collection/metadata")
|
||||
_metadata.create_dataset("sample_name", data="test_sample")
|
||||
metadata_bec = f.create_group("entry/collection/metadata/bec")
|
||||
for key, value in metadata.items():
|
||||
if isinstance(value, dict):
|
||||
metadata_bec.create_group(key)
|
||||
for sub_key, sub_value in value.items():
|
||||
if isinstance(sub_value, list):
|
||||
sub_value = json.dumps(sub_value)
|
||||
metadata_bec[key].create_dataset(sub_key, data=sub_value)
|
||||
elif isinstance(sub_value, dict):
|
||||
for sub_sub_key, sub_sub_value in sub_value.items():
|
||||
sub_sub_group = metadata_bec[key].create_group(sub_key)
|
||||
# Handle _StoredDataInfo objects
|
||||
if isinstance(sub_sub_value, _StoredDataInfo):
|
||||
# Store the numeric shape
|
||||
sub_sub_group.create_dataset("shape", data=sub_sub_value.shape)
|
||||
# Store the dtype as a UTF-8 string
|
||||
dt = sub_sub_value.dtype or ""
|
||||
sub_sub_group.create_dataset(
|
||||
"dtype", data=dt, dtype=h5py.string_dtype(encoding="utf-8")
|
||||
)
|
||||
continue
|
||||
if isinstance(sub_sub_value, list):
|
||||
json_val = json.dumps(sub_sub_value)
|
||||
sub_sub_group.create_dataset(sub_sub_key, data=json_val)
|
||||
elif isinstance(sub_sub_value, dict):
|
||||
for k2, v2 in sub_sub_value.items():
|
||||
val = json.dumps(v2) if isinstance(v2, list) else v2
|
||||
sub_sub_group.create_dataset(k2, data=val)
|
||||
else:
|
||||
sub_sub_group.create_dataset(sub_sub_key, data=sub_sub_value)
|
||||
else:
|
||||
metadata_bec[key].create_dataset(sub_key, data=sub_value)
|
||||
else:
|
||||
metadata_bec.create_dataset(key, data=value)
|
||||
for group, devices in data.items():
|
||||
readout_group = f.create_group(f"entry/collection/readout_groups/{group}")
|
||||
|
||||
for device, device_data in devices.items():
|
||||
dev_group = f.create_group(f"entry/collection/devices/{device}")
|
||||
for signal, signal_data in device_data.items():
|
||||
signal_group = dev_group.create_group(signal)
|
||||
for signal_key, signal_values in signal_data.items():
|
||||
signal_group.create_dataset(signal_key, data=signal_values)
|
||||
|
||||
readout_group[device] = h5py.SoftLink(f"/entry/collection/devices/{device}")
|
||||
msg = messages.ScanHistoryMessage(
|
||||
scan_id=metadata["scan_id"],
|
||||
scan_name=metadata["scan_name"],
|
||||
exit_status=metadata["exit_status"],
|
||||
file_path=file_path,
|
||||
scan_number=metadata["scan_number"],
|
||||
dataset_number=metadata["dataset_number"],
|
||||
start_time=time.time(),
|
||||
end_time=time.time(),
|
||||
num_points=metadata["num_points"],
|
||||
request_inputs=metadata["request_inputs"],
|
||||
stored_data_info=metadata.get("stored_data_info"),
|
||||
metadata={"scan_report_devices": metadata.get("scan_report_devices")},
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def grid_scan_history_msg(tmpdir):
|
||||
x_grid, y_grid = np.meshgrid(np.linspace(-5, 5, 10), np.linspace(-5, 5, 10))
|
||||
@@ -339,3 +281,172 @@ def scan_history_factory(tmpdir):
|
||||
return create_history_file(file_path, data, metadata)
|
||||
|
||||
return _factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_client(bec_dispatcher):
|
||||
|
||||
# Ensure isinstance check for Positioner passes
|
||||
original_isinstance = isinstance
|
||||
|
||||
def isinstance_mock(obj, class_info):
|
||||
if class_info == Positioner and isinstance(obj, FakePositioner):
|
||||
return True
|
||||
return original_isinstance(obj, class_info)
|
||||
|
||||
with patch("builtins.isinstance", new=isinstance_mock):
|
||||
yield bec_dispatcher.client
|
||||
bec_dispatcher.client.connector.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_client_w_devices(mocked_client):
|
||||
mocked_client.device_manager.add_devices(DEVICES)
|
||||
yield mocked_client
|
||||
|
||||
|
||||
##################################################
|
||||
# Client Fixture with DAP
|
||||
##################################################
|
||||
@pytest.fixture(scope="function")
|
||||
def dap_plugin_message():
|
||||
msg = messages.AvailableResourceMessage(
|
||||
**{
|
||||
"resource": {
|
||||
"GaussianModel": {
|
||||
"class": "LmfitService1D",
|
||||
"user_friendly_name": "GaussianModel",
|
||||
"class_doc": "A model based on a Gaussian or normal distribution lineshape.\n\n The model has three Parameters: `amplitude`, `center`, and `sigma`.\n In addition, parameters `fwhm` and `height` are included as\n constraints to report full width at half maximum and maximum peak\n height, respectively.\n\n .. math::\n\n f(x; A, \\mu, \\sigma) = \\frac{A}{\\sigma\\sqrt{2\\pi}} e^{[{-{(x-\\mu)^2}/{{2\\sigma}^2}}]}\n\n where the parameter `amplitude` corresponds to :math:`A`, `center` to\n :math:`\\mu`, and `sigma` to :math:`\\sigma`. The full width at half\n maximum is :math:`2\\sigma\\sqrt{2\\ln{2}}`, approximately\n :math:`2.3548\\sigma`.\n\n For more information, see: https://en.wikipedia.org/wiki/Normal_distribution\n\n ",
|
||||
"run_doc": "A model based on a Gaussian or normal distribution lineshape.\n\n The model has three Parameters: `amplitude`, `center`, and `sigma`.\n In addition, parameters `fwhm` and `height` are included as\n constraints to report full width at half maximum and maximum peak\n height, respectively.\n\n .. math::\n\n f(x; A, \\mu, \\sigma) = \\frac{A}{\\sigma\\sqrt{2\\pi}} e^{[{-{(x-\\mu)^2}/{{2\\sigma}^2}}]}\n\n where the parameter `amplitude` corresponds to :math:`A`, `center` to\n :math:`\\mu`, and `sigma` to :math:`\\sigma`. The full width at half\n maximum is :math:`2\\sigma\\sqrt{2\\ln{2}}`, approximately\n :math:`2.3548\\sigma`.\n\n For more information, see: https://en.wikipedia.org/wiki/Normal_distribution\n\n \n Args:\n scan_item (ScanItem): Scan item or scan ID\n device_x (DeviceBase | str): Device name for x\n signal_x (DeviceBase | str): Signal name for x\n device_y (DeviceBase | str): Device name for y\n signal_y (DeviceBase | str): Signal name for y\n parameters (dict): Fit parameters\n ",
|
||||
"run_name": "fit",
|
||||
"signature": [
|
||||
{
|
||||
"name": "args",
|
||||
"kind": "VAR_POSITIONAL",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
{
|
||||
"name": "scan_item",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "ScanItem | str",
|
||||
},
|
||||
{
|
||||
"name": "device_x",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceBase | str",
|
||||
},
|
||||
{
|
||||
"name": "signal_x",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceBase | str",
|
||||
},
|
||||
{
|
||||
"name": "device_y",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceBase | str",
|
||||
},
|
||||
{
|
||||
"name": "signal_y",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "DeviceBase | str",
|
||||
},
|
||||
{
|
||||
"name": "parameters",
|
||||
"kind": "KEYWORD_ONLY",
|
||||
"default": None,
|
||||
"annotation": "dict",
|
||||
},
|
||||
{
|
||||
"name": "kwargs",
|
||||
"kind": "VAR_KEYWORD",
|
||||
"default": "_empty",
|
||||
"annotation": "_empty",
|
||||
},
|
||||
],
|
||||
"auto_fit_supported": True,
|
||||
"params": {
|
||||
"amplitude": {
|
||||
"name": "amplitude",
|
||||
"value": 1.0,
|
||||
"vary": True,
|
||||
"min": -inf,
|
||||
"max": inf,
|
||||
"expr": None,
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
"center": {
|
||||
"name": "center",
|
||||
"value": 0.0,
|
||||
"vary": True,
|
||||
"min": -inf,
|
||||
"max": inf,
|
||||
"expr": None,
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
"sigma": {
|
||||
"name": "sigma",
|
||||
"value": 1.0,
|
||||
"vary": True,
|
||||
"min": 0,
|
||||
"max": inf,
|
||||
"expr": None,
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
"fwhm": {
|
||||
"name": "fwhm",
|
||||
"value": 2.35482,
|
||||
"vary": False,
|
||||
"min": -inf,
|
||||
"max": inf,
|
||||
"expr": "2.3548200*sigma",
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
"height": {
|
||||
"name": "height",
|
||||
"value": 0.3989423,
|
||||
"vary": False,
|
||||
"min": -inf,
|
||||
"max": inf,
|
||||
"expr": "0.3989423*amplitude/max(1e-15, sigma)",
|
||||
"brute_step": None,
|
||||
"user_data": None,
|
||||
},
|
||||
},
|
||||
"class_args": [],
|
||||
"class_kwargs": {"model": "GaussianModel"},
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
yield msg
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_client_with_dap(mocked_client, dap_plugin_message):
|
||||
mocked_client.device_manager.add_devices(DEVICES)
|
||||
dap_services = {
|
||||
"BECClient": messages.StatusMessage(name="BECClient", status=1, info={}),
|
||||
"DAPServer/LmfitService1D": messages.StatusMessage(
|
||||
name="LmfitService1D", status=1, info={}
|
||||
),
|
||||
}
|
||||
type(mocked_client).service_status = PropertyMock(return_value=dap_services)
|
||||
mocked_client.connector.set(
|
||||
topic=MessageEndpoints.dap_available_plugins("dap"), msg=dap_plugin_message
|
||||
)
|
||||
|
||||
# Patch the client's DAP attribute so that the available models include "GaussianModel"
|
||||
patched_models = {"GaussianModel": {}, "LorentzModel": {}, "SineModel": {}}
|
||||
mocked_client.dap._available_dap_plugins = patched_models
|
||||
|
||||
yield mocked_client
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.control.buttons.button_abort.button_abort import AbortButton
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def abort_button(qtbot, mocked_client):
|
||||
widget = AbortButton(client=mocked_client)
|
||||
widget.queue = MagicMock()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import pytest
|
||||
from qtpy.QtWidgets import QDoubleSpinBox, QLineEdit
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||
from bec_widgets.widgets.plots.setting_menus.axis_settings import AxisSettings
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -9,8 +9,6 @@ from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.utils.error_popups import SafeProperty
|
||||
from bec_widgets.utils.error_popups import SafeSlot as Slot
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
class BECConnectorQObject(BECConnector, QObject): ...
|
||||
|
||||
@@ -39,18 +37,6 @@ def test_bec_connector_set_gui_id(bec_connector):
|
||||
assert bec_connector.config.gui_id == "test_gui_id"
|
||||
|
||||
|
||||
def test_bec_connector_sanitize_names(mocked_client):
|
||||
class MyWidget(BECConnector, QWidget):
|
||||
def __init__(self, parent=None, client=None, **kwargs):
|
||||
super().__init__(parent=parent, client=client, **kwargs)
|
||||
|
||||
widget = MyWidget(client=mocked_client)
|
||||
widget.setObjectName("Test Name With Spaces")
|
||||
assert widget.objectName() == "Test_Name_With_Spaces"
|
||||
widget.setObjectName("Test@Name#With$Special%Characters!")
|
||||
assert widget.objectName() == "Test_Name_With_Special_Characters_"
|
||||
|
||||
|
||||
def test_bec_connector_change_config(bec_connector):
|
||||
bec_connector.on_config_update({"gui_id": "test_gui_id"})
|
||||
assert bec_connector.config.gui_id == "test_gui_id"
|
||||
@@ -146,7 +132,7 @@ def test_bec_connector_change_object_name(bec_connector):
|
||||
assert not any(obj.objectName() == previous_name for obj in all_objects)
|
||||
|
||||
|
||||
def test_bec_connector_export_settings():
|
||||
def test_bec_connector_export_settings(mocked_client):
|
||||
|
||||
class MyWidget(BECConnector, QWidget):
|
||||
def __init__(self, parent=None, client=None, **kwargs):
|
||||
|
||||
@@ -4,10 +4,45 @@ import time
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from bec_lib import service_config
|
||||
from bec_lib.messages import ScanMessage
|
||||
from bec_lib.serialization import MsgpackSerialization
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import QtRedisConnector, QtThreadSafeCallback
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher, QtRedisConnector, QtThreadSafeCallback
|
||||
|
||||
|
||||
def test_init_handles_client_and_config_arg():
|
||||
# Client passed
|
||||
self_mock = mock.MagicMock(_initialized=False)
|
||||
with mock.patch.object(BECDispatcher, "start_cli_server"):
|
||||
BECDispatcher.__init__(self_mock, client=mock.MagicMock(name="test_client"))
|
||||
assert "test_client" in repr(self_mock.client)
|
||||
|
||||
# No client, service config object
|
||||
self_mock.reset_mock()
|
||||
self_mock._initialized = False
|
||||
with (
|
||||
mock.patch.object(BECDispatcher, "start_cli_server"),
|
||||
mock.patch("bec_widgets.utils.bec_dispatcher.BECClient") as client_cls,
|
||||
):
|
||||
config = service_config.ServiceConfig()
|
||||
BECDispatcher.__init__(self_mock, client=None, config=config)
|
||||
client_cls.assert_called_with(
|
||||
config=config, connector_cls=QtRedisConnector, name="BECWidgets"
|
||||
)
|
||||
|
||||
# No client, service config string
|
||||
self_mock.reset_mock()
|
||||
self_mock._initialized = False
|
||||
with (
|
||||
mock.patch.object(BECDispatcher, "start_cli_server"),
|
||||
mock.patch("bec_widgets.utils.bec_dispatcher.BECClient"),
|
||||
mock.patch("bec_widgets.utils.bec_dispatcher.ServiceConfig") as svc_cfg,
|
||||
mock.patch("bec_widgets.utils.bec_dispatcher.isinstance", return_value=False),
|
||||
):
|
||||
config = service_config.ServiceConfig()
|
||||
BECDispatcher.__init__(self_mock, client=None, config="test_str")
|
||||
svc_cfg.assert_called_with("test_str")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -3,8 +3,6 @@ from bec_lib import messages
|
||||
|
||||
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bec_queue_msg_full():
|
||||
|
||||
@@ -9,8 +9,6 @@ from bec_widgets.widgets.services.bec_status_box.bec_status_box import (
|
||||
BECStatusBox,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service_status_fixture():
|
||||
|
||||
@@ -5,8 +5,6 @@ from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget
|
||||
from bec_widgets import BECWidget
|
||||
from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
class _TestBusyWidget(BECWidget, QWidget):
|
||||
def __init__(
|
||||
@@ -29,7 +27,7 @@ def widget_busy(qtbot, mocked_client):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def widget_idle(qtbot):
|
||||
def widget_idle(qtbot, mocked_client):
|
||||
w = _TestBusyWidget(client=mocked_client, start_busy=False)
|
||||
qtbot.addWidget(w)
|
||||
w.resize(320, 200)
|
||||
|
||||
@@ -2,12 +2,11 @@ from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QColorDialog
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.utility.visual.color_button_native.color_button_native import (
|
||||
ColorButtonNative,
|
||||
)
|
||||
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_color_button_native(qtbot):
|
||||
cb = create_widget(qtbot, ColorButtonNative)
|
||||
|
||||
@@ -4,12 +4,11 @@ from pydantic import ValidationError
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.utils import Colors, ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.plots.waveform.curve import CurveConfig
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
|
||||
|
||||
def test_color_validation_CSS():
|
||||
|
||||
@@ -4,12 +4,10 @@ import pytest
|
||||
from qtpy.QtCore import QPointF, Qt
|
||||
from qtpy.QtGui import QTransform
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.utils import Crosshair
|
||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
|
||||
from .conftest import create_widget
|
||||
|
||||
# pylint: disable = redefined-outer-name
|
||||
|
||||
|
||||
@@ -6,14 +6,13 @@ from bec_lib.scan_history import ScanHistory
|
||||
from qtpy.QtGui import QValidator
|
||||
from qtpy.QtWidgets import QComboBox, QVBoxLayout
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_setting import CurveSetting
|
||||
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_tree import (
|
||||
CurveTree,
|
||||
ScanIndexValidator,
|
||||
)
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
from tests.unit_tests.client_mocks import dap_plugin_message, mocked_client, mocked_client_with_dap
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
|
||||
##################################################
|
||||
# CurveSetting
|
||||
@@ -21,11 +20,11 @@ from tests.unit_tests.conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def curve_setting_fixture(qtbot, mocked_client):
|
||||
def curve_setting_fixture(qtbot, mock_client_w_devices):
|
||||
"""
|
||||
Creates a CurveSetting widget targeting a mock or real Waveform widget.
|
||||
"""
|
||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||
wf = create_widget(qtbot, Waveform, client=mock_client_w_devices)
|
||||
wf.x_mode = "auto"
|
||||
curve_setting = create_widget(qtbot, CurveSetting, parent=None, target_widget=wf)
|
||||
return curve_setting, wf
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def dap_combobox(qtbot, mocked_client):
|
||||
|
||||
@@ -7,7 +7,7 @@ from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||
from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def developer_view(qtbot, mocked_client):
|
||||
|
||||
@@ -14,8 +14,6 @@ from bec_widgets.widgets.services.device_browser.device_item.device_signal_displ
|
||||
SignalDisplay,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtWidgets import QListWidgetItem
|
||||
|
||||
@@ -29,8 +27,8 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_browser(qtbot, mocked_client):
|
||||
dev_browser = DeviceBrowser(client=mocked_client)
|
||||
def device_browser(qtbot, mock_client_w_devices):
|
||||
dev_browser = DeviceBrowser(client=mock_client_w_devices)
|
||||
dev_browser.dev["samx"].read_configuration = mock.MagicMock()
|
||||
qtbot.addWidget(dev_browser)
|
||||
qtbot.waitExposed(dev_browser)
|
||||
@@ -148,8 +146,8 @@ def test_device_deletion(device_browser, qtbot):
|
||||
qtbot.waitUntil(lambda: widget.device not in device_browser.dev_list._item_dict, timeout=10000)
|
||||
|
||||
|
||||
def test_signal_display(mocked_client, qtbot):
|
||||
signal_display = SignalDisplay(client=mocked_client, device="test_device")
|
||||
def test_signal_display(mock_client_w_devices, qtbot):
|
||||
signal_display = SignalDisplay(client=mock_client_w_devices, device="test_device")
|
||||
qtbot.addWidget(signal_display)
|
||||
device_mock = mock.MagicMock()
|
||||
signal_display.dev = {"test_device": device_mock}
|
||||
@@ -158,10 +156,10 @@ def test_signal_display(mocked_client, qtbot):
|
||||
device_mock.read_configuration.assert_called()
|
||||
|
||||
|
||||
def test_signal_display_no_device(mocked_client, qtbot):
|
||||
def test_signal_display_no_device(mock_client_w_devices, qtbot):
|
||||
device_mock = mock.MagicMock()
|
||||
mocked_client.client.device_manager.devices = {"test_device_1": device_mock}
|
||||
signal_display = SignalDisplay(client=mocked_client, device="test_device_2")
|
||||
mock_client_w_devices.device_manager.devices = {"test_device_1": device_mock}
|
||||
signal_display = SignalDisplay(client=mock_client_w_devices, device="test_device_2")
|
||||
qtbot.addWidget(signal_display)
|
||||
assert (
|
||||
signal_display._content_layout.itemAt(1).widget().text()
|
||||
@@ -172,11 +170,11 @@ def test_signal_display_no_device(mocked_client, qtbot):
|
||||
device_mock.read_configuration.assert_not_called()
|
||||
|
||||
|
||||
def test_signal_display_omitted_not_added(mocked_client, qtbot):
|
||||
def test_signal_display_omitted_not_added(mock_client_w_devices, qtbot):
|
||||
device_mock = mock.MagicMock(spec=Device)
|
||||
device_mock._info = {"signals": {"signal_1": {"kind_str": "omitted"}}}
|
||||
|
||||
signal_display = SignalDisplay(client=mocked_client, device="test_device_1")
|
||||
signal_display = SignalDisplay(client=mock_client_w_devices, device="test_device_1")
|
||||
signal_display.dev = {"test_device_1": device_mock}
|
||||
signal_display._populate()
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ from bec_widgets.widgets.progress.device_initialization_progress_bar.device_init
|
||||
DeviceInitializationProgressBar,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def progress_bar(qtbot, mocked_client):
|
||||
|
||||
@@ -4,6 +4,7 @@ import pytest
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import (
|
||||
BECDeviceFilter,
|
||||
DeviceInputBase,
|
||||
@@ -11,9 +12,6 @@ from bec_widgets.widgets.control.device_input.base_classes.device_input_base imp
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
# DeviceInputBase is meant to be mixed in a QWidget
|
||||
class DeviceInputWidget(DeviceInputBase, QWidget):
|
||||
|
||||
@@ -7,21 +7,19 @@ from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit
|
||||
DeviceLineEdit,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_input_combobox(qtbot, mocked_client):
|
||||
widget = DeviceComboBox(client=mocked_client)
|
||||
def device_input_combobox(qtbot, mock_client_w_devices):
|
||||
widget = DeviceComboBox(client=mock_client_w_devices)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_input_combobox_with_kwargs(qtbot, mocked_client):
|
||||
def device_input_combobox_with_kwargs(qtbot, mock_client_w_devices):
|
||||
widget = DeviceComboBox(
|
||||
client=mocked_client,
|
||||
client=mock_client_w_devices,
|
||||
gui_id="test_gui_id",
|
||||
device_filter=[BECDeviceFilter.POSITIONER],
|
||||
default="samx",
|
||||
@@ -74,17 +72,17 @@ def test_get_device_from_input_combobox_init(device_input_combobox):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_input_line_edit(qtbot, mocked_client):
|
||||
widget = DeviceLineEdit(client=mocked_client)
|
||||
def device_input_line_edit(qtbot, mock_client_w_devices):
|
||||
widget = DeviceLineEdit(client=mock_client_w_devices)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_input_line_edit_with_kwargs(qtbot, mocked_client):
|
||||
def device_input_line_edit_with_kwargs(qtbot, mock_client_w_devices):
|
||||
widget = DeviceLineEdit(
|
||||
client=mocked_client,
|
||||
client=mock_client_w_devices,
|
||||
gui_id="test_gui_id",
|
||||
device_filter=[BECDeviceFilter.POSITIONER],
|
||||
default="samx",
|
||||
|
||||
@@ -56,8 +56,7 @@ from bec_widgets.widgets.control.device_manager.components.ophyd_validation.vali
|
||||
ValidationListItem,
|
||||
)
|
||||
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from tests.unit_tests.conftest import mocked_client
|
||||
|
||||
|
||||
class TestConstants:
|
||||
@@ -364,7 +363,7 @@ class TestDeviceTable:
|
||||
assert hasattr(device_table, "client_callback_id")
|
||||
|
||||
def test_device_table_client_device_update_callback(
|
||||
self, device_table: DeviceTable, mocked_client, qtbot
|
||||
self, device_table: DeviceTable, mock_client_w_devices, qtbot
|
||||
):
|
||||
"""
|
||||
Test that runs the client device update callback. This should update the status of devices in the table
|
||||
@@ -375,6 +374,7 @@ class TestDeviceTable:
|
||||
device from the client and run the callback. The table should update the status of the
|
||||
removed device to CAN_CONNECT and all others to CONNECTED.
|
||||
"""
|
||||
mocked_client = mock_client_w_devices
|
||||
device_configs_changed_calls = []
|
||||
requested_update_for_multiple_device_validations = []
|
||||
|
||||
|
||||
@@ -43,8 +43,6 @@ from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophy
|
||||
OphydValidation,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_config() -> dict:
|
||||
|
||||
@@ -4,6 +4,7 @@ import pytest
|
||||
from bec_lib.device import Signal
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.utils.ophyd_kind_util import Kind
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_signal_input_base import (
|
||||
@@ -15,9 +16,6 @@ from bec_widgets.widgets.control.device_input.signal_line_edit.signal_line_edit
|
||||
SignalLineEdit,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
class FakeSignal(Signal):
|
||||
"""Fake signal to test the DeviceSignalInputBase."""
|
||||
@@ -146,12 +144,12 @@ def test_signal_lineedit(device_signal_line_edit):
|
||||
|
||||
|
||||
def test_device_signal_input_base_cleanup(qtbot, mocked_client):
|
||||
with mock.patch.object(mocked_client.callbacks, "remove"):
|
||||
widget = DeviceInputWidget(client=mocked_client)
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
|
||||
widget = DeviceInputWidget(client=mocked_client)
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
|
||||
mocked_client.callbacks.remove.assert_called_once_with(widget._device_update_register)
|
||||
mocked_client.callbacks.remove.assert_called_once_with(widget._device_update_register)
|
||||
|
||||
|
||||
def test_signal_combobox_get_signal_name_with_item_data(qtbot, device_signal_combobox):
|
||||
|
||||
@@ -40,8 +40,6 @@ from bec_widgets.widgets.containers.dock_area.settings.dialogs import (
|
||||
)
|
||||
from bec_widgets.widgets.containers.dock_area.settings.workspace_manager import WorkSpaceManager
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def advanced_dock_area(qtbot, mocked_client):
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
import pytest
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QDialog
|
||||
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.utility.feedback_dialog.feedback_dialog import FeedbackDialog, StarRating
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def star_rating(qtbot):
|
||||
"""Create a StarRating widget for testing."""
|
||||
widget = StarRating()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def feedback_dialog(qtbot):
|
||||
"""Create a FeedbackDialog for testing."""
|
||||
dialog = FeedbackDialog()
|
||||
qtbot.addWidget(dialog)
|
||||
qtbot.waitExposed(dialog)
|
||||
yield dialog
|
||||
dialog.close()
|
||||
|
||||
|
||||
class TestStarRating:
|
||||
"""Tests for the StarRating widget."""
|
||||
|
||||
def test_initial_state(self, star_rating):
|
||||
"""Test that StarRating initializes with rating 0."""
|
||||
assert star_rating.rating() == 0
|
||||
assert star_rating._hovered_star == 0
|
||||
assert len(star_rating._star_buttons) == 5
|
||||
|
||||
def test_set_rating_via_method(self, star_rating):
|
||||
"""Test setting rating programmatically."""
|
||||
star_rating.set_rating(3)
|
||||
assert star_rating.rating() == 3
|
||||
|
||||
star_rating.set_rating(5)
|
||||
assert star_rating.rating() == 5
|
||||
|
||||
def test_set_rating_bounds(self, star_rating):
|
||||
"""Test that rating is bounded between 0 and 5."""
|
||||
star_rating.set_rating(0)
|
||||
assert star_rating.rating() == 0
|
||||
|
||||
star_rating.set_rating(5)
|
||||
assert star_rating.rating() == 5
|
||||
|
||||
# Out of bounds should not change rating
|
||||
initial_rating = star_rating.rating()
|
||||
star_rating.set_rating(6)
|
||||
assert star_rating.rating() == initial_rating
|
||||
|
||||
star_rating.set_rating(-1)
|
||||
assert star_rating.rating() == initial_rating
|
||||
|
||||
def test_rating_signal_emission(self, star_rating, qtbot):
|
||||
"""Test that rating_changed signal is emitted when rating changes."""
|
||||
with qtbot.waitSignal(star_rating.rating_changed, timeout=1000) as blocker:
|
||||
star_rating.set_rating(4)
|
||||
|
||||
assert blocker.args == [4]
|
||||
|
||||
def test_rating_signal_not_emitted_on_same_value(self, star_rating, qtbot):
|
||||
"""Test that signal is not emitted when setting the same rating."""
|
||||
star_rating.set_rating(3)
|
||||
|
||||
# Should not emit signal when setting same value
|
||||
with qtbot.assertNotEmitted(star_rating.rating_changed, wait=100):
|
||||
star_rating.set_rating(3)
|
||||
|
||||
def test_click_star_button(self, star_rating, qtbot):
|
||||
"""Test clicking on star buttons."""
|
||||
# Click the third star (index 2)
|
||||
with qtbot.waitSignal(star_rating.rating_changed, timeout=1000):
|
||||
qtbot.mouseClick(star_rating._star_buttons[2], Qt.LeftButton)
|
||||
|
||||
assert star_rating.rating() == 3
|
||||
|
||||
# Click the first star
|
||||
with qtbot.waitSignal(star_rating.rating_changed, timeout=1000):
|
||||
qtbot.mouseClick(star_rating._star_buttons[0], Qt.LeftButton)
|
||||
|
||||
assert star_rating.rating() == 1
|
||||
|
||||
def test_mouse_hover(self, star_rating, qtbot):
|
||||
"""Test mouse hover behavior."""
|
||||
# Set initial rating
|
||||
star_rating.set_rating(2)
|
||||
assert star_rating._hovered_star == 0
|
||||
|
||||
# Simulate mouse move over the fourth button
|
||||
btn = star_rating._star_buttons[3]
|
||||
btn_center = btn.geometry().center()
|
||||
event = qtbot.mouseMove(star_rating, pos=btn_center)
|
||||
|
||||
# Note: _hovered_star should be updated by mouseMoveEvent
|
||||
# This is a bit tricky to test directly, so we verify the method exists
|
||||
assert hasattr(star_rating, "mouseMoveEvent")
|
||||
assert hasattr(star_rating, "leaveEvent")
|
||||
|
||||
def test_leave_event(self, star_rating, qtbot):
|
||||
"""Test that leaving the widget clears hover state."""
|
||||
star_rating.set_rating(2)
|
||||
star_rating._hovered_star = 4 # Simulate hover
|
||||
|
||||
# Trigger leave event
|
||||
star_rating.leaveEvent(None)
|
||||
|
||||
assert star_rating._hovered_star == 0
|
||||
assert star_rating.rating() == 2 # Rating should remain unchanged
|
||||
|
||||
def test_update_theme_colors(self, star_rating):
|
||||
"""Test that theme colors are applied correctly."""
|
||||
assert hasattr(star_rating, "_inactive_color")
|
||||
assert hasattr(star_rating, "_active_color")
|
||||
|
||||
# Colors should be initialized
|
||||
assert star_rating._inactive_color is not None
|
||||
assert star_rating._active_color is not None
|
||||
|
||||
def test_display_update(self, star_rating):
|
||||
"""Test that display updates when rating changes."""
|
||||
star_rating.set_rating(3)
|
||||
# If this doesn't raise an exception, the display was updated successfully
|
||||
star_rating._update_display()
|
||||
|
||||
|
||||
class TestFeedbackDialog:
|
||||
"""Tests for the FeedbackDialog widget."""
|
||||
|
||||
def test_initial_state(self, feedback_dialog):
|
||||
"""Test that FeedbackDialog initializes correctly."""
|
||||
assert feedback_dialog.windowTitle() == "Feedback"
|
||||
assert feedback_dialog.isModal() is True
|
||||
assert feedback_dialog._star_rating is not None
|
||||
assert feedback_dialog._comment_field is not None
|
||||
assert feedback_dialog._email_field is not None
|
||||
assert feedback_dialog._submit_button is not None
|
||||
assert feedback_dialog._cancel_button is not None
|
||||
|
||||
def test_get_feedback_initial(self, feedback_dialog):
|
||||
"""Test getting feedback from unmodified dialog."""
|
||||
rating, comment, email = feedback_dialog.get_feedback()
|
||||
assert rating == 0
|
||||
assert comment == ""
|
||||
assert email == ""
|
||||
|
||||
def test_set_and_get_rating(self, feedback_dialog):
|
||||
"""Test setting and getting rating."""
|
||||
feedback_dialog.set_rating(4)
|
||||
rating, _, _ = feedback_dialog.get_feedback()
|
||||
assert rating == 4
|
||||
|
||||
def test_set_and_get_comment(self, feedback_dialog):
|
||||
"""Test setting and getting comment."""
|
||||
test_comment = "This is a test comment"
|
||||
feedback_dialog.set_comment(test_comment)
|
||||
_, comment, _ = feedback_dialog.get_feedback()
|
||||
assert comment == test_comment
|
||||
|
||||
def test_set_and_get_email(self, feedback_dialog):
|
||||
"""Test setting and getting email."""
|
||||
test_email = "test@example.com"
|
||||
feedback_dialog.set_email(test_email)
|
||||
_, _, email = feedback_dialog.get_feedback()
|
||||
assert email == test_email
|
||||
|
||||
def test_set_all_feedback(self, feedback_dialog):
|
||||
"""Test setting all feedback fields."""
|
||||
feedback_dialog.set_rating(5)
|
||||
feedback_dialog.set_comment("Great widget!")
|
||||
feedback_dialog.set_email("user@example.com")
|
||||
|
||||
rating, comment, email = feedback_dialog.get_feedback()
|
||||
assert rating == 5
|
||||
assert comment == "Great widget!"
|
||||
assert email == "user@example.com"
|
||||
|
||||
def test_submit_button_emits_signal(self, feedback_dialog, qtbot):
|
||||
"""Test that clicking submit emits feedback_submitted signal."""
|
||||
feedback_dialog.set_rating(3)
|
||||
feedback_dialog.set_comment("Test feedback")
|
||||
feedback_dialog.set_email("test@test.com")
|
||||
|
||||
with qtbot.waitSignal(feedback_dialog.feedback_submitted, timeout=1000) as blocker:
|
||||
qtbot.mouseClick(feedback_dialog._submit_button, Qt.LeftButton)
|
||||
|
||||
assert blocker.args == [3, "Test feedback", "test@test.com"]
|
||||
|
||||
def test_submit_button_accepts_dialog(self, feedback_dialog, qtbot):
|
||||
"""Test that clicking submit accepts the dialog."""
|
||||
feedback_dialog.set_rating(4)
|
||||
|
||||
qtbot.mouseClick(feedback_dialog._submit_button, Qt.LeftButton)
|
||||
qtbot.wait(100)
|
||||
|
||||
# Dialog should be accepted
|
||||
assert feedback_dialog.result() == QDialog.DialogCode.Accepted
|
||||
|
||||
def test_cancel_button_rejects_dialog(self, feedback_dialog, qtbot):
|
||||
"""Test that clicking cancel rejects the dialog."""
|
||||
qtbot.mouseClick(feedback_dialog._cancel_button, Qt.LeftButton)
|
||||
qtbot.wait(100)
|
||||
|
||||
# Dialog should be rejected
|
||||
assert feedback_dialog.result() == QDialog.DialogCode.Rejected
|
||||
|
||||
def test_submit_with_empty_fields(self, feedback_dialog, qtbot):
|
||||
"""Test submitting with empty fields."""
|
||||
# Don't set any values
|
||||
with qtbot.waitSignal(feedback_dialog.feedback_submitted, timeout=1000) as blocker:
|
||||
qtbot.mouseClick(feedback_dialog._submit_button, Qt.LeftButton)
|
||||
|
||||
# Should emit with empty values
|
||||
assert blocker.args == [0, "", ""]
|
||||
|
||||
def test_submit_strips_whitespace(self, feedback_dialog, qtbot):
|
||||
"""Test that whitespace is stripped from comment and email."""
|
||||
feedback_dialog.set_comment(" Test comment ")
|
||||
feedback_dialog.set_email(" test@example.com ")
|
||||
|
||||
with qtbot.waitSignal(feedback_dialog.feedback_submitted, timeout=1000) as blocker:
|
||||
qtbot.mouseClick(feedback_dialog._submit_button, Qt.LeftButton)
|
||||
|
||||
rating, comment, email = blocker.args
|
||||
assert comment == "Test comment"
|
||||
assert email == "test@example.com"
|
||||
|
||||
def test_dialog_has_correct_properties(self, feedback_dialog):
|
||||
"""Test that dialog has correct class properties."""
|
||||
assert hasattr(FeedbackDialog, "ICON_NAME")
|
||||
assert FeedbackDialog.ICON_NAME == "feedback"
|
||||
assert hasattr(FeedbackDialog, "PLUGIN")
|
||||
assert FeedbackDialog.PLUGIN is True
|
||||
|
||||
def test_comment_field_placeholder(self, feedback_dialog):
|
||||
"""Test that comment field has placeholder text."""
|
||||
assert feedback_dialog._comment_field.placeholderText() != ""
|
||||
|
||||
def test_email_field_placeholder(self, feedback_dialog):
|
||||
"""Test that email field has placeholder text."""
|
||||
assert feedback_dialog._email_field.placeholderText() != ""
|
||||
|
||||
def test_submit_button_is_default(self, feedback_dialog):
|
||||
"""Test that submit button is set as default."""
|
||||
assert feedback_dialog._submit_button.isDefault() is True
|
||||
|
||||
def test_star_rating_embedded_correctly(self, feedback_dialog, qtbot):
|
||||
"""Test that StarRating widget is properly embedded."""
|
||||
# Verify we can interact with the embedded star rating
|
||||
feedback_dialog._star_rating.set_rating(5)
|
||||
assert feedback_dialog._star_rating.rating() == 5
|
||||
|
||||
# Verify rating is reflected in feedback
|
||||
rating, _, _ = feedback_dialog.get_feedback()
|
||||
assert rating == 5
|
||||
@@ -1,14 +1,12 @@
|
||||
import pytest
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.utils.filter_io import FilterIO
|
||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||
DeviceLineEdit,
|
||||
)
|
||||
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def dap_mock(qtbot, mocked_client):
|
||||
|
||||
@@ -16,9 +16,6 @@ from bec_widgets.widgets.plots.heatmap.heatmap import (
|
||||
)
|
||||
|
||||
# pytest: disable=unused-import
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
|
||||
from .client_mocks import create_dummy_scan_item
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -9,8 +9,6 @@ from bec_widgets.utils.help_inspector.help_inspector import HelpInspector
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.widgets.control.buttons.button_abort.button_abort import AbortButton
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def help_inspector(qtbot, mocked_client):
|
||||
|
||||
@@ -4,11 +4,10 @@ import numpy as np
|
||||
import pytest
|
||||
from qtpy.QtCore import QPointF, Qt
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
||||
from bec_widgets.widgets.plots.roi.image_roi import CircularROI, RectangularROI
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Literal
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
from bec_widgets.widgets.plots.roi.image_roi import (
|
||||
CircularROI,
|
||||
@@ -12,8 +13,6 @@ from bec_widgets.widgets.plots.roi.image_roi import (
|
||||
RectangularROI,
|
||||
ROIController,
|
||||
)
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture(params=["rect", "circle", "ellipse"])
|
||||
|
||||
@@ -4,9 +4,8 @@ import pytest
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtCore import QPointF
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
|
||||
##################################################
|
||||
# Image widget base functionality tests
|
||||
|
||||
@@ -11,8 +11,6 @@ from bec_widgets.applications.launch_window import LaunchWindow
|
||||
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
base_path = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@ from unittest import mock
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def lmfit_dialog(qtbot, mocked_client):
|
||||
|
||||
@@ -18,8 +18,6 @@ from bec_widgets.widgets.utility.logpanel._util import (
|
||||
)
|
||||
from bec_widgets.widgets.utility.logpanel.logpanel import DEFAULT_LOG_COLORS, LogPanel
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
TEST_TABLE_STRING = "2025-01-15 15:57:18 | bec_server.scan_server.scan_queue | [INFO] | \n \x1b[3m primary queue / ScanQueueStatus.RUNNING \x1b[0m\n┏━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┓\n┃\x1b[1m \x1b[0m\x1b[1m queue_id \x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mscan_id\x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mis_scan\x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mtype\x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mscan_numb…\x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mIQ status\x1b[0m\x1b[1m \x1b[0m┃\n┡━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━┩\n│ bbe50c82-6… │ None │ False │ mv │ None │ PENDING │\n└─────────────┴─────────┴─────────┴──────┴────────────┴───────────┘\n\n"
|
||||
|
||||
TEST_LOG_MESSAGES = [
|
||||
|
||||
@@ -4,8 +4,6 @@ from qtpy.QtWidgets import QWidget
|
||||
from bec_widgets.applications.main_app import BECMainApp
|
||||
from bec_widgets.applications.views.view import ViewBase
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
ANIM_TEST_DURATION = 60 # ms
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from qtpy.QtCore import QEvent, QPoint, QPointF
|
||||
from qtpy.QtGui import QEnterEvent
|
||||
from qtpy.QtWidgets import QApplication, QFrame, QLabel
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.containers.main_window.addons.hover_widget import (
|
||||
HoverWidget,
|
||||
WidgetTooltip,
|
||||
@@ -13,9 +14,6 @@ from bec_widgets.widgets.containers.main_window.addons.scroll_label import Scrol
|
||||
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bec_main_window(qtbot, mocked_client):
|
||||
|
||||
@@ -8,8 +8,6 @@ from qtpy.QtWidgets import QFileDialog, QMessageBox
|
||||
from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def monaco_dock(qtbot, mocked_client) -> Generator[MonacoDock, None, None]:
|
||||
|
||||
@@ -7,7 +7,6 @@ from bec_widgets.utils.widget_io import WidgetIO
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||
from bec_widgets.widgets.editors.monaco.scan_control_dialog import ScanControlDialog
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .test_scan_control import available_scans_message
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from qtpy.QtTest import QSignalSpy
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_motor_map_initialization(qtbot, mocked_client):
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import numpy as np
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
|
||||
from .conftest import create_widget
|
||||
|
||||
##################################################
|
||||
# MultiWaveform widget base functionality tests
|
||||
|
||||
@@ -13,8 +13,6 @@ from bec_widgets.widgets.containers.main_window.addons.notification_center.notif
|
||||
SeverityKind,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def toast(qtbot):
|
||||
|
||||
@@ -4,8 +4,6 @@ from qtpy.QtPdfWidgets import QPdfView
|
||||
|
||||
from bec_widgets.widgets.utility.pdf_viewer.pdf_viewer import PdfViewerWidget
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pdf_viewer_widget(qtbot, mocked_client):
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import numpy as np
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.plot_base import PlotBase, UIMode
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
# pylint: disable=unused-import
|
||||
# pylint: disable=missing-function-docstring
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
@@ -7,7 +7,7 @@ from qtpy.QtCore import Qt, QTimer
|
||||
from qtpy.QtGui import QValidator
|
||||
from qtpy.QtWidgets import QPushButton
|
||||
|
||||
from bec_widgets.tests.utils import Positioner
|
||||
from bec_widgets.tests.utils import Positioner, create_widget
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import (
|
||||
PositionerBox,
|
||||
PositionerControlLine,
|
||||
@@ -16,9 +16,6 @@ from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit
|
||||
DeviceLineEdit,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
class PositionerWithoutPrecision(Positioner):
|
||||
"""just placeholder for testing embedded isinstance check in DeviceCombobox"""
|
||||
|
||||
@@ -2,11 +2,9 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox2D
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def positioner_box_2d(qtbot, mocked_client):
|
||||
|
||||
@@ -7,8 +7,6 @@ from qtpy.QtWidgets import QMessageBox
|
||||
|
||||
from bec_widgets.widgets.control.buttons.button_reset.button_reset import ResetButton
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_button(qtbot, mocked_client):
|
||||
|
||||
@@ -4,8 +4,6 @@ import pytest
|
||||
|
||||
from bec_widgets.widgets.control.buttons.button_resume.button_resume import ResumeButton
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def resume_button(qtbot, mocked_client):
|
||||
|
||||
@@ -10,8 +10,6 @@ from qtpy.QtGui import QColor
|
||||
from bec_widgets.utils import Colors
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ring_progress_bar(qtbot, mocked_client):
|
||||
|
||||
@@ -11,8 +11,6 @@ from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
|
||||
RingProgressContainerWidget,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ring_container(qtbot, mocked_client):
|
||||
|
||||
@@ -3,7 +3,6 @@ import pytest
|
||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_settings_cards import RingSettings
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -9,8 +9,6 @@ from bec_widgets.cli.server import GUIServer
|
||||
from bec_widgets.utils.bec_connector import BECConnector
|
||||
from bec_widgets.utils.rpc_server import RegistryNotReadyError, RPCServer, SingleshotRPCRepeat
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
class DummyWidget(BECConnector, QWidget):
|
||||
def __init__(self, parent=None, client=None, **kwargs):
|
||||
|
||||
@@ -11,8 +11,6 @@ from bec_widgets.utils.forms_from_types.items import StrFormItem
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
from bec_widgets.widgets.control.scan_control import ScanControl
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
# pylint: disable=no-member
|
||||
# pylint: disable=missing-function-docstring
|
||||
# pylint: disable=redefined-outer-name
|
||||
|
||||
@@ -15,8 +15,6 @@ from bec_widgets.widgets.services.scan_history_browser.scan_history_browser impo
|
||||
ScanHistoryBrowser,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scan_history_msg():
|
||||
|
||||
@@ -14,8 +14,6 @@ from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import (
|
||||
ScanProgressBar,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scan_progressbar(qtbot, mocked_client):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.scatter_waveform.scatter_curve import (
|
||||
ScatterCurveConfig,
|
||||
ScatterDeviceSignal,
|
||||
@@ -10,9 +11,6 @@ from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterW
|
||||
from bec_widgets.widgets.plots.scatter_waveform.settings.scatter_curve_setting import (
|
||||
ScatterCurveSettings,
|
||||
)
|
||||
from tests.unit_tests.client_mocks import create_dummy_scan_item, mocked_client
|
||||
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_waveform_initialization(qtbot, mocked_client):
|
||||
@@ -53,14 +51,16 @@ def test_scatter_waveform_update_with_scan_history(qtbot, mocked_client, monkeyp
|
||||
swf = create_widget(qtbot, ScatterWaveform, client=mocked_client)
|
||||
|
||||
dummy_scan = create_dummy_scan_item()
|
||||
mocked_client.history = MagicMock()
|
||||
# .get_by_scan_id() typically returns historical data, but we abuse it here
|
||||
# to return mock live data
|
||||
mocked_client.history.get_by_scan_id.return_value = dummy_scan
|
||||
mocked_client.history.__getitem__.return_value = dummy_scan
|
||||
|
||||
swf.plot("samx", "samy", "bpm4i", label="test_curve")
|
||||
swf.update_with_scan_history(scan_id="dummy")
|
||||
qtbot.wait(500)
|
||||
|
||||
assert swf.scan_item == dummy_scan
|
||||
qtbot.waitUntil(lambda: swf.scan_item == dummy_scan, timeout=500)
|
||||
qtbot.wait(200)
|
||||
|
||||
x_data, y_data = swf.main_curve.getData()
|
||||
np.testing.assert_array_equal(x_data, [10, 20, 30])
|
||||
|
||||
@@ -11,8 +11,6 @@ from bec_widgets.widgets.control.device_input.base_classes.device_signal_input_b
|
||||
)
|
||||
from bec_widgets.widgets.utility.signal_label.signal_label import ChoiceDialog, SignalLabel
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
SAMX_INFO_DICT = {
|
||||
"signals": {
|
||||
"readback": {
|
||||
|
||||
@@ -4,8 +4,6 @@ import pytest
|
||||
|
||||
from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButton
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stop_button(qtbot, mocked_client):
|
||||
|
||||
@@ -2,8 +2,6 @@ import pytest
|
||||
|
||||
from bec_widgets.widgets.editors.text_box.text_box import DEFAULT_TEXT, TextBox
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def text_box_widget(qtbot, mocked_client):
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
"""Test the BEC Login widget"""
|
||||
|
||||
import pytest
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QLineEdit
|
||||
|
||||
from bec_widgets.utils.bec_login import BECLogin
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def login_dialog(qtbot):
|
||||
"""Fixture to create a BECLogin instance."""
|
||||
dialog = BECLogin()
|
||||
qtbot.addWidget(dialog)
|
||||
qtbot.waitExposed(dialog) # Ensure the dialog is fully shown before running tests
|
||||
return dialog
|
||||
|
||||
|
||||
def test_utils_login_dialog_initialization(login_dialog, qtbot):
|
||||
"""Test that the BECLogin initializes correctly."""
|
||||
assert login_dialog.windowTitle() == "Login"
|
||||
assert login_dialog.username.placeholderText() == "Username"
|
||||
assert login_dialog.password.placeholderText() == "Password"
|
||||
assert login_dialog.password.echoMode() == QLineEdit.EchoMode.Password
|
||||
assert login_dialog.ok_btn.text() == "Sign in"
|
||||
|
||||
# Initially, this should be empty
|
||||
with qtbot.waitSignal(login_dialog.credentials_entered, timeout=5000) as blocker:
|
||||
qtbot.mouseClick(login_dialog.ok_btn, Qt.MouseButton.LeftButton)
|
||||
assert blocker.args == ["", ""]
|
||||
|
||||
|
||||
def test_utils_login_dialog_emit_credentials(login_dialog, qtbot):
|
||||
"""Test that the BECLogin emits credentials correctly."""
|
||||
test_username = "testuser "
|
||||
test_password = "testpass"
|
||||
|
||||
login_dialog.username.setText(test_username)
|
||||
login_dialog.password.setText(test_password)
|
||||
|
||||
with qtbot.waitSignal(login_dialog.credentials_entered, timeout=5000) as blocker:
|
||||
qtbot.mouseClick(login_dialog.ok_btn, Qt.MouseButton.LeftButton)
|
||||
|
||||
assert blocker.args == [test_username.strip(), test_password]
|
||||
assert login_dialog.password.text() == "" # Password should be cleared after emitting
|
||||
|
||||
login_dialog.password.setText(test_password)
|
||||
with qtbot.waitSignal(login_dialog.credentials_entered, timeout=5000) as blocker:
|
||||
qtbot.keyClick(login_dialog.password, Qt.Key.Key_Return)
|
||||
|
||||
assert blocker.args == [test_username.strip(), test_password]
|
||||
assert login_dialog.password.text() == "" # Password should be cleared after emitting
|
||||
@@ -3,12 +3,10 @@ from unittest import mock
|
||||
import pyqtgraph as pg
|
||||
import pytest
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
|
||||
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dap_combo_box(qtbot, mocked_client):
|
||||
|
||||
@@ -3,12 +3,10 @@ from qtpy.QtCore import QPointF
|
||||
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def plot_widget_with_arrow_item(qtbot, mocked_client):
|
||||
widget = Waveform(client=mocked_client())
|
||||
widget = Waveform(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
|
||||
@@ -17,7 +15,7 @@ def plot_widget_with_arrow_item(qtbot, mocked_client):
|
||||
|
||||
@pytest.fixture
|
||||
def plot_widget_with_tick_item(qtbot, mocked_client):
|
||||
widget = Waveform(client=mocked_client())
|
||||
widget = Waveform(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
|
||||
|
||||
@@ -12,22 +12,13 @@ from pyqtgraph.graphicsItems.DateAxisItem import DateAxisItem
|
||||
from qtpy.QtCore import QTimer
|
||||
from qtpy.QtWidgets import QApplication, QCheckBox, QDialog, QDialogButtonBox, QDoubleSpinBox
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.plots.plot_base import UIMode
|
||||
from bec_widgets.widgets.plots.waveform.curve import DeviceSignal
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
from bec_widgets.widgets.services.scan_history_browser.scan_history_browser import (
|
||||
ScanHistoryBrowser,
|
||||
)
|
||||
from tests.unit_tests.client_mocks import (
|
||||
DummyData,
|
||||
create_dummy_scan_item,
|
||||
dap_plugin_message,
|
||||
inject_scan_history,
|
||||
mocked_client,
|
||||
mocked_client_with_dap,
|
||||
)
|
||||
|
||||
from .conftest import create_widget
|
||||
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ from bec_widgets.widgets.editors.web_console.web_console import (
|
||||
_web_console_registry,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_server_startup():
|
||||
@@ -189,10 +187,10 @@ def test_bec_shell_startup_contains_gui_id(bec_shell_widget):
|
||||
assert bec_shell._is_bec_shell
|
||||
assert bec_shell._unique_id == "bec_shell"
|
||||
|
||||
assert bec_shell.startup_cmd == "bec --nogui"
|
||||
with mock.patch.object(bec_shell.bec_dispatcher, "cli_server", None):
|
||||
assert bec_shell.startup_cmd == "bec --nogui"
|
||||
|
||||
with mock.patch.object(bec_shell.bec_dispatcher, "cli_server") as mock_cli_server:
|
||||
mock_cli_server.gui_id = "test_gui_id"
|
||||
with mock.patch.object(bec_shell.bec_dispatcher.cli_server, "gui_id", "test_gui_id"):
|
||||
assert bec_shell.startup_cmd == "bec --gui-id test_gui_id"
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ from qtpy.QtCore import QUrl
|
||||
|
||||
from bec_widgets.widgets.editors.website.website import WebsiteWidget
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def website_widget(qtbot, mocked_client):
|
||||
|
||||
@@ -2,8 +2,8 @@ import pytest
|
||||
from qtpy.QtCore import QPoint, QSize, Qt
|
||||
from qtpy.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.tests.utils import create_widget
|
||||
from bec_widgets.widgets.utility.widget_finder.widget_finder import WidgetFinderComboBox
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
Reference in New Issue
Block a user