1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-05 00:12:49 +01:00
Files
bec_widgets/tests/unit_tests/test_image_view_next_gen.py

866 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import numpy as np
import pyqtgraph as pg
import pytest
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import QPointF
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
##################################################
def _set_signal_config(
client,
device_name: str,
signal_name: str,
signal_class: str,
ndim: int,
obj_name: str | None = None,
):
device = client.device_manager.devices[device_name]
device._info["signals"][signal_name] = {
"obj_name": obj_name or signal_name,
"signal_class": signal_class,
"component_name": signal_name,
"describe": {"signal_info": {"ndim": ndim}},
}
def test_initialization_defaults(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
assert bec_image_view.color_map == "plasma"
assert bec_image_view.autorange is True
assert bec_image_view.autorange_mode == "mean"
assert bec_image_view.config.lock_aspect_ratio is True
assert bec_image_view.main_image is not None
assert bec_image_view._color_bar is None
def test_setting_color_map(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.color_map = "viridis"
assert bec_image_view.color_map == "viridis"
assert bec_image_view.config.color_map == "viridis"
def test_invalid_color_map_handling(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
previous_colormap = bec_image_view.color_map
bec_image_view.color_map = "invalid_colormap_name"
assert bec_image_view.color_map == previous_colormap
assert bec_image_view.main_image.color_map == previous_colormap
def test_toggle_autorange(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.autorange = False
assert bec_image_view.autorange is False
bec_image_view.toggle_autorange(True, "max")
assert bec_image_view.autorange is True
assert bec_image_view.autorange_mode == "max"
assert bec_image_view.main_image.autorange is True
assert bec_image_view.main_image.autorange_mode == "max"
assert bec_image_view.main_image.config.autorange is True
assert bec_image_view.main_image.config.autorange_mode == "max"
def test_lock_aspect_ratio(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.lock_aspect_ratio = True
assert bec_image_view.lock_aspect_ratio is True
assert bool(bec_image_view.plot_item.getViewBox().state["aspectLocked"]) is True
assert bec_image_view.config.lock_aspect_ratio is True
def test_set_vrange(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.v_range = (10, 100)
assert bec_image_view.v_range == QPointF(10, 100)
assert bec_image_view.main_image.levels == (10, 100)
assert bec_image_view.main_image.config.v_range == (10, 100)
def test_enable_simple_colorbar(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.enable_simple_colorbar = True
assert bec_image_view.enable_simple_colorbar is True
assert bec_image_view.config.color_bar == "simple"
assert isinstance(bec_image_view._color_bar, pg.ColorBarItem)
# Enabling color bar should not cancel autorange
assert bec_image_view.autorange is True
assert bec_image_view.autorange_mode == "mean"
assert bec_image_view.main_image.autorange is True
assert bec_image_view.main_image.autorange_mode == "mean"
def test_enable_full_colorbar(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.enable_full_colorbar = True
assert bec_image_view.enable_full_colorbar is True
assert bec_image_view.config.color_bar == "full"
assert isinstance(bec_image_view._color_bar, pg.HistogramLUTItem)
# Enabling color bar should not cancel autorange
assert bec_image_view.autorange is True
assert bec_image_view.autorange_mode == "mean"
assert bec_image_view.main_image.autorange is True
assert bec_image_view.main_image.autorange_mode == "mean"
@pytest.mark.parametrize("colorbar_type", ["simple", "full"])
def test_enable_colorbar_with_vrange(qtbot, mocked_client, colorbar_type):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.enable_colorbar(True, colorbar_type, (0, 100))
if colorbar_type == "simple":
assert isinstance(bec_image_view._color_bar, pg.ColorBarItem)
assert bec_image_view.enable_simple_colorbar is True
else:
assert isinstance(bec_image_view._color_bar, pg.HistogramLUTItem)
assert bec_image_view.enable_full_colorbar is True
assert bec_image_view.config.color_bar == colorbar_type
assert bec_image_view.v_range == QPointF(0, 100)
assert bec_image_view.main_image.levels == (0, 100)
assert bec_image_view._color_bar is not None
##############################################
# Device/signal update mechanism
def test_image_setup_preview_signal_1d(qtbot, mocked_client):
"""
Ensure that calling .image() with a 1D PreviewSignal connects using the 1D path
and updates correctly.
"""
import numpy as np
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(
mocked_client,
"waveform1d",
"img",
signal_class="PreviewSignal",
ndim=1,
obj_name="waveform1d_img",
)
view.image(device_name="waveform1d", device_entry="img")
# Subscriptions should indicate 1D preview connection
sub = view.subscriptions["main"]
assert sub.source == "device_monitor_1d"
assert sub.monitor_type == "1d"
assert view.device_name == "waveform1d"
assert view.device_entry == "img"
# Simulate a waveform update from the dispatcher
waveform = np.arange(25, dtype=float)
view.on_image_update_1d({"data": waveform}, {"scan_id": "scan_test"})
assert view.main_image.raw_data.shape == (1, 25)
np.testing.assert_array_equal(view.main_image.raw_data[0], waveform)
def test_image_setup_preview_signal_2d(qtbot, mocked_client):
"""
Ensure that calling .image() with a 2D PreviewSignal connects using the 2D path
and updates correctly.
"""
import numpy as np
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(
mocked_client,
"eiger",
"img2d",
signal_class="PreviewSignal",
ndim=2,
obj_name="eiger_img2d",
)
view.image(device_name="eiger", device_entry="img2d")
# Subscriptions should indicate 2D preview connection
sub = view.subscriptions["main"]
assert sub.source == "device_monitor_2d"
assert sub.monitor_type == "2d"
assert view.device_name == "eiger"
assert view.device_entry == "img2d"
# Simulate a 2D image update
test_data = np.arange(16, dtype=float).reshape(4, 4)
view.on_image_update_2d({"data": test_data}, {})
np.testing.assert_array_equal(view.main_image.image, test_data)
def test_preview_signals_skip_0d_entries(qtbot, mocked_client, monkeypatch):
"""
Preview/async combobox should omit 0D signals.
"""
view = create_widget(qtbot, Image, client=mocked_client)
def fake_get(signal_class_filter):
signal_classes = (
signal_class_filter
if isinstance(signal_class_filter, (list, tuple, set))
else [signal_class_filter]
)
if "PreviewSignal" in signal_classes:
return [
(
"eiger",
"sig0d",
{
"obj_name": "sig0d",
"signal_class": "PreviewSignal",
"describe": {"signal_info": {"ndim": 0}},
},
),
(
"eiger",
"sig2d",
{
"obj_name": "sig2d",
"signal_class": "PreviewSignal",
"describe": {"signal_info": {"ndim": 2}},
},
),
]
return []
monkeypatch.setattr(view.client.device_manager, "get_bec_signals", fake_get)
device_selection = view.toolbar.components.get_action("device_selection").widget
device_selection.signal_combo_box.set_device("eiger")
device_selection.signal_combo_box.update_signals_from_signal_classes()
texts = [
device_selection.signal_combo_box.itemText(i)
for i in range(device_selection.signal_combo_box.count())
]
assert "sig0d" not in texts
assert "sig2d" in texts
def test_image_async_signal_uses_obj_name(qtbot, mocked_client, monkeypatch):
"""
Verify async signals use obj_name for endpoints/payloads and reconnect with scan_id.
"""
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(
mocked_client, "eiger", "img", signal_class="AsyncSignal", ndim=1, obj_name="async_obj"
)
view.image(device_name="eiger", device_entry="img")
assert view.subscriptions["main"].async_signal_name == "async_obj"
assert view.async_update is True
# Prepare scan ids and capture dispatcher calls
view.old_scan_id = "old_scan"
view.scan_id = "new_scan"
connected = []
disconnected = []
monkeypatch.setattr(
view.bec_dispatcher,
"connect_slot",
lambda slot, endpoint, from_start=False, cb_info=None: connected.append(
(slot, endpoint, from_start, cb_info)
),
)
monkeypatch.setattr(
view.bec_dispatcher,
"disconnect_slot",
lambda slot, endpoint: disconnected.append((slot, endpoint)),
)
view._setup_async_image(view.scan_id)
expected_new = MessageEndpoints.device_async_signal("new_scan", "eiger", "async_obj")
expected_old = MessageEndpoints.device_async_signal("old_scan", "eiger", "async_obj")
assert any(ep == expected_new for _, ep, _, _ in connected)
assert any(ep == expected_old for _, ep in disconnected)
# Payload extraction should use obj_name
payload = np.array([1, 2, 3])
msg = {"signals": {"async_obj": {"value": payload}}}
assert np.array_equal(view._get_payload_data(msg), payload)
def test_disconnect_clears_async_state(qtbot, mocked_client, monkeypatch):
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(
mocked_client, "eiger", "img", signal_class="AsyncSignal", ndim=2, obj_name="async_obj"
)
view.image(device_name="eiger", device_entry="img")
view.scan_id = "scan_x"
view.old_scan_id = "scan_y"
view.subscriptions["main"].async_signal_name = "async_obj"
# Avoid touching real dispatcher
monkeypatch.setattr(view.bec_dispatcher, "disconnect_slot", lambda *args, **kwargs: None)
view.disconnect_monitor(device_name="eiger", device_entry="img")
assert view.subscriptions["main"].async_signal_name is None
assert view.async_update is False
##############################################
# Connection guardrails
def test_image_setup_rejects_unsupported_signal_class(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(mocked_client, "eiger", "img", signal_class="Signal", ndim=2)
view.image(device_name="eiger", device_entry="img")
assert view.subscriptions["main"].source is None
assert view.subscriptions["main"].monitor_type is None
assert view.async_update is False
def test_image_disconnects_with_missing_entry(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(mocked_client, "eiger", "img", signal_class="PreviewSignal", ndim=2)
view.image(device_name="eiger", device_entry="img")
assert view.device_name == "eiger"
assert view.device_entry == "img"
view.image(device_name="eiger", device_entry=None)
assert view.device_name == ""
assert view.device_entry == ""
def test_handle_scan_change_clears_buffers_and_resets_crosshair(qtbot, mocked_client, monkeypatch):
view = create_widget(qtbot, Image, client=mocked_client)
view.scan_id = "scan_1"
view.main_image.buffer = [np.array([1.0, 2.0])]
view.main_image.max_len = 2
clear_called = []
monkeypatch.setattr(view.main_image, "clear", lambda: clear_called.append(True))
reset_called = []
if view.crosshair is not None:
monkeypatch.setattr(view.crosshair, "reset", lambda: reset_called.append(True))
view._handle_scan_change("scan_2")
assert view.old_scan_id == "scan_1"
assert view.scan_id == "scan_2"
assert clear_called == [True]
assert view.main_image.buffer == []
assert view.main_image.max_len == 0
if view.crosshair is not None:
assert reset_called == [True]
def test_handle_scan_change_reconnects_async(qtbot, mocked_client, monkeypatch):
view = create_widget(qtbot, Image, client=mocked_client)
view.scan_id = "scan_1"
view.async_update = True
called = []
monkeypatch.setattr(view, "_setup_async_image", lambda scan_id: called.append(scan_id))
view._handle_scan_change("scan_2")
assert called == ["scan_2"]
def test_handle_scan_change_same_scan_noop(qtbot, mocked_client, monkeypatch):
view = create_widget(qtbot, Image, client=mocked_client)
view.scan_id = "scan_1"
view.main_image.buffer = [np.array([1.0])]
view.main_image.max_len = 1
clear_called = []
monkeypatch.setattr(view.main_image, "clear", lambda: clear_called.append(True))
view._handle_scan_change("scan_1")
assert view.scan_id == "scan_1"
assert clear_called == []
assert view.main_image.buffer == [np.array([1.0])]
assert view.main_image.max_len == 1
def test_image_data_update_2d(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
test_data = np.random.rand(20, 30)
message = {"data": test_data}
metadata = {}
bec_image_view.on_image_update_2d(message, metadata)
np.testing.assert_array_equal(bec_image_view.main_image.image, test_data)
def test_image_data_update_1d(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
waveform1 = np.random.rand(50)
waveform2 = np.random.rand(60) # Different length, tests padding logic
metadata = {"scan_id": "scan_test"}
bec_image_view.on_image_update_1d({"data": waveform1}, metadata)
assert bec_image_view.main_image.raw_data.shape == (1, 50)
bec_image_view.on_image_update_1d({"data": waveform2}, metadata)
assert bec_image_view.main_image.raw_data.shape == (2, 60)
##############################################
# Toolbar and Actions Tests
def test_toolbar_actions_presence(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
assert bec_image_view.toolbar.components.exists("image_autorange")
assert bec_image_view.toolbar.components.exists("lock_aspect_ratio")
assert bec_image_view.toolbar.components.exists("image_processing_fft")
assert bec_image_view.toolbar.components.exists("device_selection")
def test_auto_emit_syncs_image_toolbar_actions(qtbot, mocked_client):
from unittest.mock import Mock
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
fft_action = bec_image_view.toolbar.components.get_action("image_processing_fft").action
log_action = bec_image_view.toolbar.components.get_action("image_processing_log").action
transpose_action = bec_image_view.toolbar.components.get_action(
"image_processing_transpose"
).action
mock_handler = Mock()
bec_image_view.property_changed.connect(mock_handler)
bec_image_view.fft = True
bec_image_view.log = True
bec_image_view.transpose = True
assert fft_action.isChecked()
assert log_action.isChecked()
assert transpose_action.isChecked()
mock_handler.assert_any_call("fft", True)
mock_handler.assert_any_call("log", True)
mock_handler.assert_any_call("transpose", True)
def test_image_processing_fft_toggle(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.fft = True
assert bec_image_view.fft is True
bec_image_view.fft = False
assert bec_image_view.fft is False
def test_image_processing_log_toggle(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.log = True
assert bec_image_view.log is True
bec_image_view.log = False
assert bec_image_view.log is False
def test_image_rotation_and_transpose(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.num_rotation_90 = 2
assert bec_image_view.num_rotation_90 == 2
bec_image_view.transpose = True
assert bec_image_view.transpose is True
@pytest.mark.parametrize("colorbar_type", ["none", "simple", "full"])
def test_setting_vrange_with_colorbar(qtbot, mocked_client, colorbar_type):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
if colorbar_type == "simple":
bec_image_view.enable_simple_colorbar = True
elif colorbar_type == "full":
bec_image_view.enable_full_colorbar = True
bec_image_view.v_range = (0, 100)
assert bec_image_view.v_range == QPointF(0, 100)
assert bec_image_view.main_image.levels == (0, 100)
assert bec_image_view.main_image.config.v_range == (0, 100)
assert bec_image_view.v_min == 0
assert bec_image_view.v_max == 100
if colorbar_type == "simple":
assert isinstance(bec_image_view._color_bar, pg.ColorBarItem)
assert bec_image_view._color_bar.levels() == (0, 100)
elif colorbar_type == "full":
assert isinstance(bec_image_view._color_bar, pg.HistogramLUTItem)
assert bec_image_view._color_bar.getLevels() == (0, 100)
###################################
# Toolbar Actions
###################################
def test_setup_image_from_toolbar(qtbot, mocked_client, monkeypatch):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(mocked_client, "eiger", "img", signal_class="PreviewSignal", ndim=2)
monkeypatch.setattr(
mocked_client.device_manager,
"get_bec_signals",
lambda signal_class_filter: (
[
(
"eiger",
"img",
{
"obj_name": "img",
"signal_class": "PreviewSignal",
"describe": {"signal_info": {"ndim": 2}},
},
)
]
if "PreviewSignal" in (signal_class_filter or [])
else []
),
)
device_selection = bec_image_view.toolbar.components.get_action("device_selection").widget
device_selection.device_combo_box.update_devices_from_filters()
device_selection.device_combo_box.setCurrentText("eiger")
device_selection.signal_combo_box.setCurrentText("img")
bec_image_view.on_device_selection_changed(None)
qtbot.wait(200)
assert bec_image_view.device_name == "eiger"
assert bec_image_view.device_entry == "img"
assert bec_image_view.subscriptions["main"].source == "device_monitor_2d"
assert bec_image_view.subscriptions["main"].monitor_type == "2d"
assert bec_image_view.main_image.raw_data is None
assert bec_image_view.main_image.image is None
def test_image_actions_interactions(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.autorange = False # Change the initial state to False
bec_image_view.toolbar.components.get_action("image_autorange_mean").action.trigger()
assert bec_image_view.autorange is True
assert bec_image_view.main_image.autorange is True
assert bec_image_view.autorange_mode == "mean"
bec_image_view.toolbar.components.get_action("image_autorange_max").action.trigger()
assert bec_image_view.autorange is True
assert bec_image_view.main_image.autorange is True
assert bec_image_view.autorange_mode == "max"
bec_image_view.toolbar.components.get_action("lock_aspect_ratio").action.trigger()
assert bec_image_view.lock_aspect_ratio is False
assert bool(bec_image_view.plot_item.getViewBox().state["aspectLocked"]) is False
def test_image_toggle_action_fft(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.toolbar.components.get_action("image_processing_fft").action.trigger()
assert bec_image_view.fft is True
assert bec_image_view.main_image.fft is True
assert bec_image_view.main_image.config.processing.fft is True
def test_image_toggle_action_log(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.toolbar.components.get_action("image_processing_log").action.trigger()
assert bec_image_view.log is True
assert bec_image_view.main_image.log is True
assert bec_image_view.main_image.config.processing.log is True
def test_image_toggle_action_transpose(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.toolbar.components.get_action("image_processing_transpose").action.trigger()
assert bec_image_view.transpose is True
assert bec_image_view.main_image.transpose is True
assert bec_image_view.main_image.config.processing.transpose is True
def test_image_toggle_action_rotate_right(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.toolbar.components.get_action("image_processing_rotate_right").action.trigger()
assert bec_image_view.num_rotation_90 == 3
assert bec_image_view.main_image.num_rotation_90 == 3
assert bec_image_view.main_image.config.processing.num_rotation_90 == 3
def test_image_toggle_action_rotate_left(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
bec_image_view.toolbar.components.get_action("image_processing_rotate_left").action.trigger()
assert bec_image_view.num_rotation_90 == 1
assert bec_image_view.main_image.num_rotation_90 == 1
assert bec_image_view.main_image.config.processing.num_rotation_90 == 1
def test_image_toggle_action_reset(qtbot, mocked_client):
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
# Setup some processing
bec_image_view.fft = True
bec_image_view.log = True
bec_image_view.transpose = True
bec_image_view.num_rotation_90 = 2
bec_image_view.toolbar.components.get_action("image_processing_reset").action.trigger()
assert bec_image_view.num_rotation_90 == 0
assert bec_image_view.main_image.num_rotation_90 == 0
assert bec_image_view.main_image.config.processing.num_rotation_90 == 0
assert bec_image_view.fft is False
assert bec_image_view.main_image.fft is False
assert bec_image_view.log is False
assert bec_image_view.main_image.log is False
assert bec_image_view.transpose is False
assert bec_image_view.main_image.transpose is False
def test_roi_add_remove_and_properties(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
# Add ROIs
rect = view.add_roi(kind="rect", name="rect_roi", line_width=7)
circ = view.add_roi(kind="circle", name="circ_roi", line_width=5)
assert rect in view.roi_controller.rois
assert circ in view.roi_controller.rois
assert rect.label == "rect_roi"
assert circ.label == "circ_roi"
assert rect.line_width == 7
assert circ.line_width == 5
# Change properties
rect.label = "rect_roi2"
circ.line_color = "#ff0000"
assert rect.label == "rect_roi2"
assert circ.line_color == "#ff0000"
# Remove by name
view.remove_roi("rect_roi2")
assert rect not in view.roi_controller.rois
# Remove by index
view.remove_roi(0)
assert not view.roi_controller.rois
def test_roi_controller_palette_signal(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
controller = view.roi_controller
changed = []
controller.paletteChanged.connect(lambda cmap: changed.append(cmap))
view.add_roi(kind="rect")
controller.colormap = "plasma"
assert changed and changed[0] == "plasma"
def test_roi_controller_clear_and_get_methods(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
r1 = view.add_roi(kind="rect", name="r1")
r2 = view.add_roi(kind="circle", name="c1")
controller = view.roi_controller
assert controller.get_roi_by_name("r1") == r1
assert controller.get_roi(1) == r2
controller.clear()
assert not controller.rois
def test_roi_get_data_from_image_with_no_image(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
roi = view.add_roi(kind="rect")
# Remove all images from scene
for item in list(view.plot_item.items):
if hasattr(item, "image"):
view.plot_item.removeItem(item)
with pytest.raises(RuntimeError):
roi.get_data_from_image()
##################################################
# Settings and popups
##################################################
def test_show_roi_manager_popup(qtbot, mocked_client):
"""
Verify that the ROI-manager dialog opens and closes correctly,
and that the matching toolbar icon stays in sync.
"""
view = create_widget(qtbot, Image, client=mocked_client, popups=True)
# ROI-manager toggle is exposed via the toolbar.
assert view.toolbar.components.exists("roi_mgr")
roi_action = view.toolbar.components.get_action("roi_mgr").action
assert roi_action.isChecked() is False, "Should start unchecked"
# Open the popup.
view.show_roi_manager_popup()
assert view.roi_manager_dialog is not None
assert view.roi_manager_dialog.isVisible()
assert roi_action.isChecked() is True, "Icon should toggle on"
# Close again.
view.roi_manager_dialog.close()
assert view.roi_manager_dialog is None
assert roi_action.isChecked() is False, "Icon should toggle off"
###################################
# ROI Plots & Crosshair Switch
###################################
def test_crosshair_roi_panels_visibility(qtbot, mocked_client):
"""
Verify that enabling the ROI-crosshair shows ROI panels and disabling hides them.
"""
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
switch = bec_image_view.toolbar.components.get_action("image_switch_crosshair")
# Initially panels should be hidden
assert bec_image_view.side_panel_x.panel_height == 0
assert bec_image_view.side_panel_y.panel_width == 0
# Enable ROI crosshair
switch.actions["crosshair_roi"].action.trigger()
# Panels must be visible
qtbot.waitUntil(
lambda: all(
[
bec_image_view.side_panel_x.panel_height > 0,
bec_image_view.side_panel_y.panel_width > 0,
]
),
timeout=500,
)
# Disable ROI crosshair
switch.actions["crosshair_roi"].action.trigger()
# Panels hidden again
qtbot.waitUntil(
lambda: all(
[
bec_image_view.side_panel_x.panel_height == 0,
bec_image_view.side_panel_y.panel_width == 0,
]
),
timeout=500,
)
def test_roi_plot_data_from_image(qtbot, mocked_client):
"""
Check that ROI plots receive correct slice data from the 2D image.
"""
import numpy as np
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
# Provide deterministic 2D data
test_data = np.arange(25).reshape(5, 5)
bec_image_view.on_image_update_2d({"data": test_data}, {})
# Activate ROI crosshair
switch = bec_image_view.toolbar.components.get_action("image_switch_crosshair")
switch.actions["crosshair_roi"].action.trigger()
qtbot.wait(50)
# Simulate crosshair at row 2, col 3
bec_image_view.update_image_slices((0, 2, 3))
# Extract plotted data
x_items = bec_image_view.x_roi.plot_item.listDataItems()
y_items = bec_image_view.y_roi.plot_item.listDataItems()
assert len(x_items) == 1
assert len(y_items) == 1
# Vertical slice (column)
_, v_slice = x_items[0].getData()
np.testing.assert_array_equal(v_slice, test_data[:, 3])
# Horizontal slice (row)
h_slice, _ = y_items[0].getData()
np.testing.assert_array_equal(h_slice, test_data[2])
##############################################
# Device selection toolbar sync
##############################################
def test_device_selection_syncs_from_properties(qtbot, mocked_client, monkeypatch):
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(mocked_client, "eiger", "img2d", signal_class="PreviewSignal", ndim=2)
monkeypatch.setattr(
view.client.device_manager,
"get_bec_signals",
lambda signal_class_filter: (
[
(
"eiger",
"img2d",
{
"obj_name": "img2d",
"signal_class": "PreviewSignal",
"describe": {"signal_info": {"ndim": 2}},
},
)
]
if "PreviewSignal" in (signal_class_filter or [])
else []
),
)
view.device_name = "eiger"
view.device_entry = "img2d"
qtbot.wait(200) # Allow signal processing
device_selection = view.toolbar.components.get_action("device_selection").widget
qtbot.waitUntil(
lambda: device_selection.device_combo_box.currentText() == "eiger"
and device_selection.signal_combo_box.currentText() == "img2d",
timeout=1000,
)
def test_device_entry_syncs_from_toolbar(qtbot, mocked_client):
view = create_widget(qtbot, Image, client=mocked_client)
_set_signal_config(mocked_client, "eiger", "img_a", signal_class="PreviewSignal", ndim=2)
_set_signal_config(mocked_client, "eiger", "img_b", signal_class="PreviewSignal", ndim=2)
view.device_name = "eiger"
view.device_entry = "img_a"
device_selection = view.toolbar.components.get_action("device_selection").widget
device_selection.signal_combo_box.blockSignals(True)
device_selection.signal_combo_box.setCurrentText("img_b")
device_selection.signal_combo_box.blockSignals(False)
view._sync_device_entry_from_toolbar()
assert view.device_entry == "img_b"