import numpy as np 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 # 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_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_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