diff --git a/src/aare/gui/panels/illumination_panel.py b/src/aare/gui/panels/illumination_panel.py index 1c8d2dd7..d96bcbc6 100644 --- a/src/aare/gui/panels/illumination_panel.py +++ b/src/aare/gui/panels/illumination_panel.py @@ -84,50 +84,3 @@ class IlluminationPanel(QWidget): slider.blockSignals(True) slider.setValue(round(val)) slider.blockSignals(False) - - -if __name__ == "__main__": - # ponytail: smallest check that fails if the send/readback wiring breaks - import os - from types import SimpleNamespace - - os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") - from PySide6.QtCore import QPoint, QPointF - from PySide6.QtGui import QWheelEvent - from PySide6.QtWidgets import QApplication - - app = QApplication([]) - panel = IlluminationPanel() - sent = [] - panel.front_light.connect(sent.append) - - # wheel notch must emit (was silently discarded before) - panel.front_light_slider.setFocus() - QApplication.sendEvent( - panel.front_light_slider, - QWheelEvent( - QPointF(5, 5), - QPointF(5, 5), - QPoint(0, 0), - QPoint(0, 120), - Qt.MouseButton.RightButton, - Qt.KeyboardModifier.NoModifier, - Qt.ScrollPhase.NoScrollPhase, - False, - ), - ) - assert sent, "wheel adjustment must emit front_light" - - # readback right after a user change must not clobber the slider - held = panel.front_light_slider.value() - status = SimpleNamespace(bl=SimpleNamespace(front_light=90.0, back_light=10.0)) - panel.update_daq_status(status) - assert panel.front_light_slider.value() == held, "grace window must hold user value" - - # after the grace window, readback applies without emitting a PUT - panel._last_user_change = time.monotonic() - READBACK_GRACE_S - 1 - n_sent = len(sent) - panel.update_daq_status(status) - assert panel.front_light_slider.value() == 90, "readback must apply after grace" - assert len(sent) == n_sent, "readback must not echo a PUT" - print("gude") diff --git a/tests/unit/gui/test_busy_overlay.py b/tests/unit/gui/test_busy_overlay.py new file mode 100644 index 00000000..2b83d71c --- /dev/null +++ b/tests/unit/gui/test_busy_overlay.py @@ -0,0 +1,36 @@ +"""The Axis views' busy flag is a passive status: plain shadowed text, not +the badge pill (which reads as a button). These checks fail if the text +renderer stops painting or the video view stops routing through it.""" + +from PySide6.QtCore import Qt +from PySide6.QtGui import QPainter, QPixmap + +from aare.gui.widgets.busy_overlay import build_busy_overlay_style, draw_busy_status_text +from aare.gui.widgets.video_image import VideoGraphicsView + + +def _busy_style(): + style = build_busy_overlay_style(is_busy=True, tell_state=None) + assert style is not None and style.text == "BEAMLINE BUSY" + return style + + +def test_status_text_paints_something(qapp): + pixmap = QPixmap(400, 300) + pixmap.fill(Qt.GlobalColor.black) + blank = pixmap.toImage() + painter = QPainter(pixmap) + draw_busy_status_text(painter, 400, 300, _busy_style()) + painter.end() + assert pixmap.toImage() != blank, "busy text must actually paint" + + +def test_video_view_paints_busy_text(qtbot): + view = VideoGraphicsView() + qtbot.addWidget(view) + view.resize(400, 300) + idle = view.grab().toImage() + view.set_busy_overlay_style(_busy_style()) + assert view.grab().toImage() != idle, "busy style must change the rendered view" + view.set_busy_overlay_style(None) + assert view.grab().toImage() == idle, "clearing the style must restore the view" diff --git a/tests/unit/gui/test_illumination_panel.py b/tests/unit/gui/test_illumination_panel.py new file mode 100644 index 00000000..897509cb --- /dev/null +++ b/tests/unit/gui/test_illumination_panel.py @@ -0,0 +1,80 @@ +"""Send/readback wiring for the light sliders (was a __main__ self-check in +the panel; promoted here so CI counts it). Every deliberate input path must +emit, and the status-poll readback must neither clobber a fresh user change +nor echo a PUT back to the server.""" + +import time +from types import SimpleNamespace + +import pytest +from PySide6.QtCore import QPoint, QPointF, Qt +from PySide6.QtGui import QWheelEvent +from PySide6.QtWidgets import QApplication + +from aare.gui.panels.illumination_panel import READBACK_GRACE_S, IlluminationPanel + + +@pytest.fixture +def panel(qtbot): + panel = IlluminationPanel() + qtbot.addWidget(panel) + return panel + + +def _wheel_notch(slider): + # One wheel notch; sliderReleased never fires for this path, which is + # exactly the case the valueChanged wiring exists for. + QApplication.sendEvent( + slider, + QWheelEvent( + QPointF(5, 5), + QPointF(5, 5), + QPoint(0, 0), + QPoint(0, 120), + Qt.MouseButton.NoButton, + Qt.KeyboardModifier.NoModifier, + Qt.ScrollPhase.NoScrollPhase, + False, + ), + ) + + +def _status(front=90.0, back=10.0): + return SimpleNamespace(bl=SimpleNamespace(front_light=front, back_light=back)) + + +def test_wheel_notch_emits_both_lights(panel): + front, back = [], [] + panel.front_light.connect(front.append) + panel.back_light.connect(back.append) + _wheel_notch(panel.front_light_slider) + _wheel_notch(panel.back_light_slider) + assert front, "wheel adjustment must emit front_light" + assert back, "wheel adjustment must emit back_light" + + +def test_readback_grace_holds_user_value(panel): + _wheel_notch(panel.front_light_slider) + held = panel.front_light_slider.value() + panel.update_daq_status(_status()) + assert panel.front_light_slider.value() == held, "grace window must hold user value" + + +def test_readback_applies_after_grace_without_echo(panel): + sent = [] + panel.front_light.connect(sent.append) + panel._last_user_change = time.monotonic() - READBACK_GRACE_S - 1 + panel.update_daq_status(_status()) + assert panel.front_light_slider.value() == 90, "readback must apply after grace" + assert panel.back_light_slider.value() == 10 + assert not sent, "readback must not echo a PUT" + + +def test_readback_skipped_while_sliding(panel): + panel.on_slider_pressed() + panel._last_user_change = 0.0 + panel.update_daq_status(_status(front=55.0)) + assert panel.front_light_slider.value() != 55 + panel.on_slider_released() + panel.update_daq_status(_status(front=55.0)) + assert panel.front_light_slider.value() == 55 diff --git a/tests/unit/gui/test_motor_move_group.py b/tests/unit/gui/test_motor_move_group.py index daf08375..20143fd8 100644 --- a/tests/unit/gui/test_motor_move_group.py +++ b/tests/unit/gui/test_motor_move_group.py @@ -3,6 +3,7 @@ targets are staged (orange), sent only by the Move button (green), and the box returns to neutral when the motor actually arrives.""" import pytest +from PySide6.QtCore import Qt from aare.gui.widgets.motor_move_group import MotorMoveGroup from aare.gui.widgets.number_line_edit import NumberLineEdit @@ -65,3 +66,21 @@ def test_over_max_is_invalid_and_never_sent(chi, qtbot): group.button.click() assert not sent # out-of-range target is never applied assert box.property("movestate") == "pending" + + +def test_incomplete_entry_stays_pending(chi, qtbot): + _group, box = chi + _type(qtbot, box, "-") # not a number (yet): no tips, no crash + assert box.property("movestate") == "pending" + # Enter on the incomplete text must be swallowed just as quietly + qtbot.keyClick(box, Qt.Key.Key_Return) + assert box.property("movestate") == "pending" + + +def test_below_min_enter_hints_but_never_sends(chi, qtbot): + group, box = chi + sent = [] + group.applied.connect(sent.append) + _type(qtbot, box, "-5") # below the -0.2 bottom + qtbot.keyClick(box, Qt.Key.Key_Return) # triggers the min-value tooltip path + assert not sent # Enter must never start a move