import numpy as np import pytest from bec_lib import messages from bec_lib.scan_data_container import ScanDataContainer from bec_lib.scan_items import ScanItem from bec_widgets.widgets.plots.heatmap.heatmap import Heatmap from bec_widgets.widgets.plots.image.image import Image from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform from bec_widgets.widgets.plots.plot_base import PlotBase, UIMode from bec_widgets.widgets.plots.plot_info_label import TextOnlyLegendSample from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform from bec_widgets.widgets.plots.waveform.waveform import Waveform 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 # pylint: disable=protected-access # pylint: disable=unused-variable def test_init_plot_base(qtbot, mocked_client): """ Test that PlotBase initializes without error and has expected default states. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) assert pb.objectName() == "PlotBase" # The default title/labels should be empty assert pb.title == "" assert pb.x_label == "" assert pb.y_label == "" # By default, no crosshair or FPS monitor assert pb.crosshair is None assert pb.fps_monitor is None # The side panel was created assert pb.side_panel is not None # The toolbar was created assert pb.toolbar is not None def test_set_title_emits_signal(qtbot, mocked_client): """ Test that setting the title updates the plot and emits a property_changed signal. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) with qtbot.waitSignal(pb.property_changed, timeout=500) as signal: pb.title = "My Plot Title" # The signal should carry ("title", "My Plot Title") assert signal.args == ["title", "My Plot Title"] assert pb.plot_item.titleLabel.text == "My Plot Title" # Get the property back from the object assert pb.title == "My Plot Title" def test_set_x_label_emits_signal(qtbot, mocked_client): """ Test setting x_label updates the plot and emits a property_changed signal. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) with qtbot.waitSignal(pb.property_changed, timeout=500) as signal: pb.x_label = "Voltage" assert signal.args == ["x_label", "Voltage"] assert pb.x_label == "Voltage" pb.x_label_units = "V" assert pb.plot_item.getAxis("bottom").labelText == "Voltage [V]" def test_set_y_label_emits_signal(qtbot, mocked_client): """ Test setting y_label updates the plot and emits a property_changed signal. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) with qtbot.waitSignal(pb.property_changed, timeout=500) as signal: pb.y_label = "Current" assert signal.args == ["y_label", "Current"] assert pb.y_label == "Current" pb.y_label_units = "A" assert pb.plot_item.getAxis("left").labelText == "Current [A]" def test_info_label_rows_are_submitted_without_forcing_visibility(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) action = pb.toolbar.components.get_action("plot_info_label").action assert action.isVisible() assert action.isEnabled() assert not action.isChecked() pb.set_info_label_rows([("Scan", "5 (live)"), ("Mode", "alignment")]) labels = [label.text for _, label in pb.info_label.items] assert labels == ["Scan: 5 (live)", "Mode: alignment"] assert pb.info_label.rows == [("Scan", "5 (live)"), ("Mode", "alignment")] assert not pb.info_label.isVisible() assert action.isVisible() assert action.isEnabled() assert not action.isChecked() def test_info_label_can_be_enabled_before_rows_are_available(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) action = pb.toolbar.components.get_action("plot_info_label").action action.trigger() assert pb.show_info_label assert action.isChecked() assert not pb.info_label.isVisible() pb.set_info_label_rows({"Scan": "5 (live)"}) assert pb.info_label.isVisible() assert action.isChecked() def test_info_label_visibility_property_and_toolbar_action(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.set_info_label_rows({"Scan": "5 (live)"}) with qtbot.waitSignal(pb.property_changed, timeout=500) as signal: pb.show_info_label = True assert signal.args == ["show_info_label", True] assert pb.info_label.isVisible() assert pb.toolbar.components.get_action("plot_info_label").action.isChecked() pb.toolbar.components.get_action("plot_info_label").action.trigger() assert not pb.show_info_label assert not pb.info_label.isVisible() def test_set_scan_info_formats_common_scan_rows(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.show_info_label = True pb.set_scan_info( scan_number=7, scan_name="grid_scan", mode="history", extra_rows=[("Detector", "eiger")] ) labels = [label.text for _, label in pb.info_label.items] assert labels == ["Scan: 7 (history)", "Scan Name: grid_scan", "Detector: eiger"] def test_update_scan_info_from_scan_item_status_message(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) scan_item = ScanItem(queue_id="queue-1", scan_number=7, scan_id="scan-7", status="open") status_message = messages.ScanStatusMessage( scan_id="scan-7", scan_number=7, scan_name="line_scan", status="open", info={} ) scan_item.status_message = status_message pb.update_scan_info_from_source(scan_item, mode="live") labels = [label.text for _, label in pb.info_label.items] assert labels == ["Scan: 7 (live)", "Scan Name: line_scan"] def test_update_scan_info_from_history_scan_item_uses_scan_number( qtbot, mocked_client, scan_history_factory ): pb = create_widget(qtbot, PlotBase, client=mocked_client) history_message = scan_history_factory( scan_id="history-id", scan_number=42, scan_name="line_scan" ) scan_item = ScanDataContainer(file_path=history_message.file_path, msg=history_message) pb.update_scan_info_from_source(scan_item, mode="history") labels = [label.text for _, label in pb.info_label.items] assert labels == ["Scan: 42 (history)", "Scan Name: line_scan"] def test_history_scan_info_does_not_fall_back_to_scan_id(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.set_scan_info(scan_id="history-id", mode="history") assert pb.info_label.rows == [] def test_update_scan_info_noops_when_source_has_no_scan_info(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) scan_item = ScanItem(queue_id="queue-1", scan_number=7, scan_id="scan-7", status="open") pb.set_scan_info(scan_id="scan-7", mode="live") pb.update_scan_info_from_source(scan_item, mode="live") assert [label.text for _, label in pb.info_label.items] == ["Scan ID: scan-7 (live)"] def test_info_label_paints_without_error(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.show_info_label = True pb.set_info_label_rows({"Scan": "5 (live)"}) samples = [sample for sample, _ in pb.info_label.items] assert samples assert all(isinstance(sample, TextOnlyLegendSample) for sample in samples) pixmap = pb.grab() assert not pixmap.isNull() def test_info_label_redraw_preserves_user_position(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.show_info_label = True pb.set_info_label_rows({"Scan": "5 (live)"}) pb.info_label.autoAnchor((42, 24), relative=False) position = pb.info_label.pos() pb.set_info_label_rows({"Scan": "6 (live)"}) assert pb.info_label.pos() == position def test_info_label_toggle_resets_position(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.set_info_label_rows({"Scan": "5 (live)"}) pb.show_info_label = True default_position = pb.info_label.pos() pb.info_label.autoAnchor((42, 24), relative=False) assert pb.info_label.pos() != default_position pb.show_info_label = False pb.show_info_label = True assert pb.info_label.pos() == default_position @pytest.mark.parametrize( "widget_cls", [PlotBase, Waveform, ScatterWaveform, MultiWaveform, MotorMap, Image, Heatmap] ) def test_info_label_is_available_on_plot_base_subclasses(qtbot, mocked_client, widget_cls): widget = create_widget(qtbot, widget_cls, client=mocked_client) if hasattr(widget, "show_config_label"): widget.show_config_label = False else: widget.show_info_label = False widget.set_info_label_rows([("Widget", widget_cls.__name__), ("Mode", "custom")]) assert [label.text for _, label in widget.info_label.items] == [ f"Widget: {widget_cls.__name__}", "Mode: custom", ] assert not widget.info_label.isVisible() if isinstance(widget, MotorMap): assert "plot_info_label" not in widget.toolbar.get_bundle("axis_popup").bundle_actions return action = widget.toolbar.components.get_action("plot_info_label").action action.trigger() assert widget.show_info_label assert widget.info_label.isVisible() assert action.isChecked() def test_set_x_min_max(qtbot, mocked_client): """ Test setting x_min, x_max changes the actual X-range of the plot and emits signals. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) # Set x_max with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_max: pb.x_max = 50 assert pb.x_max == 50.0 # Set x_min with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_min: pb.x_min = 5 assert pb.x_min == 5.0 # Confirm the actual ViewBox range in pyqtgraph assert pb.plot_item.vb.viewRange()[0] == [5.0, 50.0] def test_set_y_min_max(qtbot, mocked_client): """ Test setting y_min, y_max changes the actual Y-range of the plot """ pb = create_widget(qtbot, PlotBase, client=mocked_client) with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_max: pb.y_max = 100 assert pb.y_max == 100.0 with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_min: pb.y_min = 10 assert pb.y_min == 10.0 # Confirm the actual ViewBox range assert pb.plot_item.vb.viewRange()[1] == [10.0, 100.0] def test_auto_range_x_y(qtbot, mocked_client): """ Test enabling and disabling autoRange for x and y axes. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) # auto_range_x = True pb.auto_range_x = True assert pb.plot_item.vb.state["autoRange"][0] is True pb.auto_range_y = True assert pb.plot_item.vb.state["autoRange"][1] is True # Turn off pb.auto_range_x = False assert pb.plot_item.vb.state["autoRange"][0] is False pb.auto_range_y = False assert pb.plot_item.vb.state["autoRange"][1] is False def test_autorange_respects_visibility(qtbot, mocked_client): """ Autorange must consider only the curves whose .isVisible() flag is True. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) x = np.arange(10) small = pb.plot_item.plot(x, x, pen=(255, 0, 0)) # 0‒9 medium = pb.plot_item.plot(x, x * 10, pen=(0, 255, 0)) # 0‒90 large = pb.plot_item.plot(x, x * 100, pen=(0, 0, 255)) # 0‒900 pb.auto_range(True) qtbot.wait(200) yspan_full = pb.plot_item.vb.viewRange()[1] assert yspan_full[1] > 800, "Autorange must include the largest visible curve." # Hide the largest curve, recompute autorange, and expect the span to shrink. large.setVisible(False) pb.auto_range(True) qtbot.wait(200) yspan_reduced = pb.plot_item.vb.viewRange()[1] assert yspan_reduced[1] < 200, "Hidden curves must be excluded from autorange." def test_x_log_y_log(qtbot, mocked_client): """ Test toggling log scale on x and y axes. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) with qtbot.waitSignal(pb.property_changed, timeout=500) as sig1: pb.x_log = True assert pb.plot_item.vb.state["logMode"][0] is True with qtbot.waitSignal(pb.property_changed, timeout=500) as sig2: pb.x_log = False assert pb.plot_item.vb.state["logMode"][0] is False # Y log pb.y_log = True assert pb.plot_item.vb.state["logMode"][1] is True pb.y_log = False assert pb.plot_item.vb.state["logMode"][1] is False def test_grid(qtbot, mocked_client): """ Test x_grid and y_grid toggles. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) # By default, might be off with qtbot.waitSignal(pb.property_changed, timeout=500) as sigx: pb.x_grid = True assert sigx.args == ["x_grid", True] # Confirm in pyqtgraph assert pb.plot_item.ctrl.xGridCheck.isChecked() is True with qtbot.waitSignal(pb.property_changed, timeout=500) as sigy: pb.y_grid = True assert sigy.args == ["y_grid", True] assert pb.plot_item.ctrl.yGridCheck.isChecked() is True def test_lock_aspect_ratio(qtbot, mocked_client): """ Test locking and unlocking the aspect ratio. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) # default is unlocked assert bool(pb.plot_item.vb.getState()["aspectLocked"]) is False pb.lock_aspect_ratio = True assert bool(pb.plot_item.vb.getState()["aspectLocked"]) is True pb.lock_aspect_ratio = False assert bool(pb.plot_item.vb.getState()["aspectLocked"]) is False def test_inner_axes_toggle(qtbot, mocked_client): """ Test the 'inner_axes' property, which shows/hides bottom and left axes. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_off: pb.inner_axes = False assert sig_off.args == ["inner_axes", False] assert pb.plot_item.getAxis("bottom").isVisible() is False assert pb.plot_item.getAxis("left").isVisible() is False with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_on: pb.inner_axes = True assert sig_on.args == ["inner_axes", True] assert pb.plot_item.getAxis("bottom").isVisible() is True assert pb.plot_item.getAxis("left").isVisible() is True def test_outer_axes_toggle(qtbot, mocked_client): """ Test the 'outer_axes' property, which shows/hides top and right axes. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_on: pb.outer_axes = True assert sig_on.args == ["outer_axes", True] assert pb.plot_item.getAxis("top").isVisible() is True assert pb.plot_item.getAxis("right").isVisible() is True with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_off: pb.outer_axes = False assert sig_off.args == ["outer_axes", False] assert pb.plot_item.getAxis("top").isVisible() is False assert pb.plot_item.getAxis("right").isVisible() is False def test_crosshair_hook_unhook(qtbot, mocked_client): pb = create_widget(qtbot, PlotBase, client=mocked_client) assert pb.crosshair is None # Hook pb.hook_crosshair() assert pb.crosshair is not None # Unhook pb.unhook_crosshair() assert pb.crosshair is None # toggle pb.toggle_crosshair() assert pb.crosshair is not None pb.toggle_crosshair() assert pb.crosshair is None def test_pin_survives_crosshair_toggle_off(qtbot, mocked_client): """Toggling the crosshair off keeps the pinned marker; toggling on re-adopts it.""" pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.plot_item.plot(x=[1, 2, 3], y=[4, 5, 6], name="c") pb.hook_crosshair() pb.crosshair.set_pin(2, 5) pin_items = [ pb.crosshair.pinned_point, pb.crosshair.pinned_v_line, pb.crosshair.pinned_h_line, pb.crosshair.pinned_label, ] assert all(item is not None for item in pin_items) # Toggle the crosshair off: the live crosshair is gone but the pin remains. pb.toggle_crosshair() assert pb.crosshair is None assert pb._detached_pin is not None for item in pin_items: assert item in pb.plot_item.items # Toggle back on: the new crosshair re-adopts the very same pin items. pb.toggle_crosshair() assert pb.crosshair is not None assert pb._detached_pin is None assert pb.crosshair.pinned_point is pin_items[0] assert pb.crosshair.pinned_pos is not None # The re-adopted pin is fully managed again and can be cleared. pb.crosshair.clear_pin() assert pb.crosshair.pinned_point is None for item in pin_items: assert item not in pb.plot_item.items def test_teardown_discards_detached_pin(qtbot, mocked_client): """A pin left detached (crosshair off) is removed from the plot on teardown.""" pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.plot_item.plot(x=[1, 2, 3], y=[4, 5, 6], name="c") pb.hook_crosshair() pb.crosshair.set_pin(2, 5) pin_items = [ pb.crosshair.pinned_point, pb.crosshair.pinned_v_line, pb.crosshair.pinned_h_line, pb.crosshair.pinned_label, ] pb.toggle_crosshair() # detach the pin assert pb._detached_pin is not None # Teardown (preserve_pin=False) must remove the detached pin's items. pb.unhook_crosshair(preserve_pin=False) assert pb._detached_pin is None for item in pin_items: assert item not in pb.plot_item.items def test_mouse_mode_switch_is_exclusive(qtbot, mocked_client): """The pan/rectangle mouse modes must be mutually exclusive and always one active.""" import pyqtgraph as pg pb = create_widget(qtbot, PlotBase, client=mocked_client) switch = pb.toolbar.components.get_action("switch_mouse_mode") drag = switch.actions["drag_mode"].action rect = switch.actions["rectangle_mode"].action viewbox = pb.plot_item.getViewBox() # Switching to rectangle mode updates the viewbox and unchecks the pan action. switch.set_default_action("rectangle_mode") assert rect.isChecked() and not drag.isChecked() assert viewbox.getState()["mouseMode"] == pg.ViewBox.RectMode assert switch.main_button.isChecked() # Switching back to pan mode flips both the viewbox and the checked states. switch.set_default_action("drag_mode") assert drag.isChecked() and not rect.isChecked() assert viewbox.getState()["mouseMode"] == pg.ViewBox.PanMode # The exclusive action group forbids both modes being active at once. drag.setChecked(True) rect.setChecked(True) assert rect.isChecked() and not drag.isChecked() def test_mouse_mode_always_one_active_on_toggle_off(qtbot, mocked_client): """Toggling the active mode off directly must keep exactly one mode selected.""" import pyqtgraph as pg pb = create_widget(qtbot, PlotBase, client=mocked_client) switch = pb.toolbar.components.get_action("switch_mouse_mode") drag = switch.actions["drag_mode"].action rect = switch.actions["rectangle_mode"].action viewbox = pb.plot_item.getViewBox() switch.set_default_action("drag_mode") assert drag.isChecked() and not rect.isChecked() # Clicking the main button toggles the current action off; a mouse mode must # remain active rather than leaving both sub-actions unchecked. switch.main_button.click() assert switch.main_button.isChecked() assert drag.isChecked() and not rect.isChecked() assert viewbox.getState()["mouseMode"] == pg.ViewBox.PanMode # Switching still works after a direct toggle-off. switch.set_default_action("rectangle_mode") assert rect.isChecked() and not drag.isChecked() switch.main_button.click() assert rect.isChecked() and not drag.isChecked() assert viewbox.getState()["mouseMode"] == pg.ViewBox.RectMode def test_set_method(qtbot, mocked_client): """ Test using the set(...) convenience method to update multiple properties at once. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.set( title="Multi Set Title", x_label="Voltage", y_label="Current", x_grid=True, y_grid=True, x_log=True, outer_axes=True, ) assert pb.title == "Multi Set Title" assert pb.x_label == "Voltage" assert pb.y_label == "Current" assert pb.x_grid is True assert pb.y_grid is True assert pb.x_log is True assert pb.outer_axes is True def test_ui_mode_popup(qtbot, mocked_client): """ Test that setting ui_mode to POPUP creates a popup bundle with visible actions and hides the side panel. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.ui_mode = UIMode.POPUP # The popup bundle should be created and its actions made visible. assert "axis_popup" in pb.toolbar.bundles for action_ref in pb.toolbar.bundles["axis_popup"].bundle_actions.values(): assert action_ref().action.isVisible() is True # The side panel should be hidden. assert not pb.side_panel.isVisible() # Side panels are not properly implemented yet. Once the logic is fixed, we can re-enable this test. # See issue #742 # def test_ui_mode_side(qtbot, mocked_client): # """ # Test that setting ui_mode to SIDE shows the side panel and ensures any popup actions # are hidden. # """ # pb = create_widget(qtbot, PlotBase, client=mocked_client) # pb.ui_mode = UIMode.SIDE # # If a popup bundle exists, its actions should be hidden. # if "axis_popup" in pb.toolbar.bundles: # for action_ref in pb.toolbar.bundles["axis_popup"].bundle_actions.values(): # assert action_ref().action.isVisible() is False # def test_enable_popups_property(qtbot, mocked_client): # """ # Test the enable_popups property: when enabled, ui_mode should be POPUP, # and when disabled, ui_mode should change to NONE. # """ # pb = create_widget(qtbot, PlotBase, client=mocked_client) # pb.enable_popups = True # assert pb.ui_mode == UIMode.POPUP # # The popup bundle actions should be visible. # assert "popup_bundle" in pb.toolbar.bundles # for action_id in pb.toolbar.bundles["popup_bundle"]: # assert pb.toolbar.widgets[action_id].action.isVisible() is True # pb.enable_popups = False # assert pb.ui_mode == UIMode.NONE # def test_enable_side_panel_property(qtbot, mocked_client): # """ # Test the enable_side_panel property: when enabled, ui_mode should be SIDE, # and when disabled, ui_mode should change to NONE. # """ # pb = create_widget(qtbot, PlotBase, client=mocked_client) # pb.enable_side_panel = True # assert pb.ui_mode == UIMode.SIDE # pb.enable_side_panel = False # assert pb.ui_mode == UIMode.NONE def test_switching_between_popup_and_side_panel_closes_dialog(qtbot, mocked_client): """ Test that if a popup dialog is open (via the axis settings popup) then switching to side-panel mode closes the dialog. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.ui_mode = UIMode.POPUP # Open the axis settings popup. pb_connection = pb.toolbar.bundles["axis_popup"].get_connection("plot_base") pb_connection.show_axis_settings_popup() qtbot.wait(100) # The dialog should now exist and be visible. assert pb_connection.axis_settings_dialog is not None assert pb_connection.axis_settings_dialog.isVisible() is True # Switch to side panel mode. pb.ui_mode = UIMode.SIDE qtbot.wait(100) # The axis settings dialog should be closed (and reference cleared). qtbot.waitUntil(lambda: pb_connection.axis_settings_dialog is None, timeout=5000) def test_enable_fps_monitor_property(qtbot, mocked_client): """ Test the enable_fps_monitor property: when enabled, the FPS monitor should be hooked (resulting in a non-None fps_monitor and visible fps_label), and when disabled, the FPS monitor should be unhooked and the label hidden. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.enable_fps_monitor = True assert pb.fps_monitor is not None pb.enable_fps_monitor = False assert pb.fps_monitor is None def test_minimal_crosshair_precision_default(qtbot, mocked_client): """ By default PlotBase should expose a floor of 3 decimals, with no crosshair yet. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) assert pb.minimal_crosshair_precision == 3 assert pb.crosshair is None def test_minimal_crosshair_precision_before_hook(qtbot, mocked_client): """ If the floor is changed before hook_crosshair(), the new crosshair must pick it up. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.minimal_crosshair_precision = 5 pb.hook_crosshair() assert pb.crosshair is not None assert pb.crosshair.min_precision == 5 def test_minimal_crosshair_precision_after_hook(qtbot, mocked_client): """ Changing the floor after the crosshair exists should update it immediately and emit the property_changed signal. """ pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.hook_crosshair() assert pb.crosshair is not None with qtbot.waitSignal(pb.property_changed, timeout=500) as sig: pb.minimal_crosshair_precision = 1 assert sig.args == ["minimal_crosshair_precision", 1] assert pb.crosshair.min_precision == 1 def test_limits_accept_fractional_values(qtbot, mocked_client): """Regression: tuple limits were converted through QPoint, silently truncating fractional values to integers.""" pb = create_widget(qtbot, PlotBase, client=mocked_client) pb.x_limits = (0.5, 9.25) pb.y_limits = (-1.75, 3.5) assert (pb.x_limits.x(), pb.x_limits.y()) == (0.5, 9.25) assert (pb.y_limits.x(), pb.y_limits.y()) == (-1.75, 3.5)