test: cover the camera overlays, theme migration, layout heal, and splash
Diff coverage vs main was 77%, under the 80% CI gate; the new camera help/error/badge code and the theme+layout main_window paths were the uncovered bulk. Locally: 82%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import pytest
|
||||
from aarecommon.math.coordinate import Coordinate, SmargonCoordinate
|
||||
from aarecommon.math.diffraction_geometry import DiffractionGeometry
|
||||
from aarecommon.math.sample_geometry import SampleGeometryModel
|
||||
from aarecommon.models.models import (
|
||||
BeamlineStateEnum,
|
||||
BeamlineStatus,
|
||||
CrystalSize,
|
||||
DAQStatusModel,
|
||||
SampleCameraSettings,
|
||||
SessionsStateEnum,
|
||||
SessionStatus,
|
||||
)
|
||||
from PySide6.QtCore import QEvent, QPoint, QPointF, Qt
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
|
||||
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
|
||||
from aare.gui.styles import THEME_SUNRISE, THEME_SUNSET
|
||||
from aare.gui.widgets.camera_image import SampleCameraImageLabel
|
||||
|
||||
|
||||
def _geom() -> SampleGeometryModel:
|
||||
return SampleGeometryModel(
|
||||
beam_location_pxl=Coordinate(x=1000, y=1000),
|
||||
pixel_in_mm=0.001,
|
||||
aerotech=Coordinate(),
|
||||
aerotech_meas=Coordinate(),
|
||||
smargon=SmargonCoordinate(sh_mm=Coordinate(), phi_deg=0, chi_deg=0),
|
||||
omega_deg=0,
|
||||
beam_size_mm=Coordinate(x=0.01, y=0.01),
|
||||
)
|
||||
|
||||
|
||||
def _status(*, busy: bool, session: SessionsStateEnum) -> DAQStatusModel:
|
||||
return DAQStatusModel(
|
||||
geom=_geom(),
|
||||
diffraction=DiffractionGeometry(
|
||||
energy_keV=12.4,
|
||||
dtz_mm=100.0,
|
||||
detector_size_pxl=(1553, 1630),
|
||||
pixel_size_mm=0.150,
|
||||
beam_center_pxl=(750.0, 750.0),
|
||||
detector_description="PILATUS 4",
|
||||
detector_serial_number="1",
|
||||
poni_rot1_rad=0.0,
|
||||
poni_rot2_rad=0.0,
|
||||
),
|
||||
bl=BeamlineStatus(
|
||||
name="SIMULATED",
|
||||
ring_current_mA=400.0,
|
||||
front_light=50.0,
|
||||
back_light=50.0,
|
||||
cryojet_K=100.0,
|
||||
shutter_open=False,
|
||||
exp_shutter_open=False,
|
||||
flux_ph_s=1e12,
|
||||
sample_camera=SampleCameraSettings(gain=1.0, exposure=0.02),
|
||||
transmission=1.0,
|
||||
zoom=1.0,
|
||||
commissioning_mode=False,
|
||||
dtz_min=120.0,
|
||||
dtz_max=1600.0,
|
||||
),
|
||||
state=BeamlineStateEnum.SampleAlignment,
|
||||
busy=busy,
|
||||
session=SessionStatus(session=session, current_pgroup="p123", staff=True),
|
||||
crystal_size=CrystalSize(x=0, y=0, z=0),
|
||||
)
|
||||
|
||||
|
||||
def _mouse_move(widget, pos: QPoint) -> None:
|
||||
# qtbot.mouseMove drives the real cursor, which the offscreen platform
|
||||
# ignores — deliver the move event directly instead.
|
||||
event = QMouseEvent(
|
||||
QEvent.Type.MouseMove,
|
||||
QPointF(pos),
|
||||
QPointF(widget.mapToGlobal(pos)),
|
||||
Qt.MouseButton.NoButton,
|
||||
Qt.MouseButton.NoButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
)
|
||||
widget.mouseMoveEvent(event)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def camera(qtbot):
|
||||
geom = _geom()
|
||||
label = SampleCameraImageLabel(geom=geom, raster=RasterGridManager(geom), default_image=None)
|
||||
qtbot.addWidget(label)
|
||||
label.resize(800, 600)
|
||||
return label
|
||||
|
||||
|
||||
def test_help_badge_click_toggles_cheatsheet(camera, qtbot):
|
||||
camera.grab() # paint records the collapsed "?" badge hit rect
|
||||
badge = camera._help_hit_rect
|
||||
assert badge is not None
|
||||
assert not camera._help_expanded
|
||||
|
||||
qtbot.mouseClick(camera.viewport(), Qt.MouseButton.LeftButton, pos=badge.center().toPoint())
|
||||
assert camera._help_expanded
|
||||
|
||||
camera.grab() # expanded overlay: hit rect grows to the whole cheatsheet box
|
||||
box = camera._help_hit_rect
|
||||
assert box is not None
|
||||
assert box.height() > badge.height()
|
||||
|
||||
qtbot.mouseClick(camera.viewport(), Qt.MouseButton.LeftButton, pos=box.center().toPoint())
|
||||
assert not camera._help_expanded
|
||||
|
||||
|
||||
def test_camera_error_message_rewords_and_draws(camera):
|
||||
camera.set_camera_available(False)
|
||||
camera.set_camera_error_message("Sample camera feed unavailable: cable unplugged")
|
||||
assert camera._camera_error_message == (
|
||||
"Sample camera feed unavailable because cable unplugged"
|
||||
)
|
||||
camera.grab() # exercises the bottom-center unavailable overlay text path
|
||||
|
||||
camera.set_camera_available(True)
|
||||
assert camera._camera_error_message is None
|
||||
|
||||
|
||||
def test_busy_warning_is_not_a_click_target(camera):
|
||||
camera.update_daq_status(_status(busy=True, session=SessionsStateEnum.OwnedByYou))
|
||||
style = camera._busy_overlay_style
|
||||
assert style is not None
|
||||
assert style.text == "BEAMLINE BUSY"
|
||||
camera.grab()
|
||||
assert camera._session_badge_rect is None
|
||||
|
||||
|
||||
def test_vacant_badge_hover_click_and_theme(camera, qtbot):
|
||||
camera.update_daq_status(_status(busy=False, session=SessionsStateEnum.Vacant))
|
||||
style = camera._busy_overlay_style
|
||||
assert style is not None
|
||||
assert style.text == "In viewing mode"
|
||||
assert style.subtext # the grab-baton hint line
|
||||
|
||||
camera.grab() # paint records the badge rect
|
||||
badge = camera._session_badge_rect
|
||||
assert badge is not None
|
||||
|
||||
_mouse_move(camera, badge.center())
|
||||
assert camera._session_badge_hovered
|
||||
camera.grab() # hover fill, light-theme darken branch
|
||||
|
||||
camera.set_theme(THEME_SUNSET)
|
||||
assert camera._dark_theme
|
||||
camera.grab() # hover fill, sunset brighten branch
|
||||
camera.set_theme(THEME_SUNRISE)
|
||||
assert not camera._dark_theme
|
||||
|
||||
_mouse_move(camera, QPoint(1, 1))
|
||||
assert not camera._session_badge_hovered
|
||||
|
||||
_mouse_move(camera, badge.center())
|
||||
camera.leaveEvent(QEvent(QEvent.Type.Leave))
|
||||
assert not camera._session_badge_hovered
|
||||
|
||||
with qtbot.waitSignal(camera.session_badge_clicked, timeout=1000):
|
||||
qtbot.mouseClick(camera.viewport(), Qt.MouseButton.LeftButton, pos=badge.center())
|
||||
@@ -1,8 +1,11 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QSettings
|
||||
from PySide6.QtWidgets import QDockWidget
|
||||
|
||||
from aare.gui.main_window import MainWindow
|
||||
from aare.gui.styles import THEME_BLUEBIRD, THEME_SUNRISE, THEME_SUNSET
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -362,3 +365,122 @@ def test_cleanup_returns_from_compact_automation_view(qtbot, mock_ui_state):
|
||||
win.cleanup()
|
||||
|
||||
assert win.content_stack.currentWidget() is win._standard_main_page
|
||||
|
||||
|
||||
def _make_window(qtbot):
|
||||
win = MainWindow(
|
||||
base_url=None,
|
||||
token="header.payload.signature",
|
||||
default_image=None,
|
||||
zmq_addr=None,
|
||||
pred_zmq_addr=None,
|
||||
beamline_cam_addr=None,
|
||||
gonio_cam_addr=None,
|
||||
gonio_cam_id=None,
|
||||
)
|
||||
qtbot.addWidget(win)
|
||||
return win
|
||||
|
||||
|
||||
def test_theme_settings_migrate_and_slots_switch(qtbot, mock_ui_state):
|
||||
with (
|
||||
patch("requests.get"),
|
||||
patch("aare.gui.main_window.DAQWorker"),
|
||||
patch("aare.gui.main_window.PredictionSubscriber"),
|
||||
patch("aare.gui.main_window.VideoThread"),
|
||||
patch("aare.gui.main_window.JFJochDBusClient"),
|
||||
patch("aare.gui.main_window.jwt.decode") as mock_jwt,
|
||||
):
|
||||
mock_jwt.return_value = {
|
||||
"sub": "testuser",
|
||||
"staff": True,
|
||||
"pgroups": ["p123"],
|
||||
"session": 15,
|
||||
}
|
||||
win = _make_window(qtbot)
|
||||
|
||||
settings = QSettings("PSI", "AareGUI")
|
||||
saved = settings.value("appearance/theme")
|
||||
try:
|
||||
# Pre-rename tokens saved by older builds must map to the new ones.
|
||||
settings.setValue("appearance/theme", "portrait")
|
||||
win._restore_theme_settings()
|
||||
assert win._theme_mode == THEME_SUNSET
|
||||
|
||||
settings.setValue("appearance/theme", "original")
|
||||
win._restore_theme_settings()
|
||||
assert win._theme_mode == THEME_SUNRISE
|
||||
|
||||
settings.setValue("appearance/theme", THEME_BLUEBIRD)
|
||||
win._restore_theme_settings()
|
||||
assert win._theme_mode == THEME_BLUEBIRD
|
||||
finally:
|
||||
if saved is None:
|
||||
settings.remove("appearance/theme")
|
||||
else:
|
||||
settings.setValue("appearance/theme", saved)
|
||||
|
||||
win.use_bluebird_theme()
|
||||
assert win._theme_mode == THEME_BLUEBIRD
|
||||
win.use_portrait_theme() # exercises the sunset palette flip
|
||||
assert win._theme_mode == THEME_SUNSET
|
||||
win.use_legacy_theme()
|
||||
assert win._theme_mode == THEME_SUNRISE
|
||||
|
||||
|
||||
def test_restore_window_state_heals_all_hidden_docks(qtbot, mock_ui_state):
|
||||
with (
|
||||
patch("requests.get"),
|
||||
patch("aare.gui.main_window.DAQWorker"),
|
||||
patch("aare.gui.main_window.PredictionSubscriber"),
|
||||
patch("aare.gui.main_window.VideoThread"),
|
||||
patch("aare.gui.main_window.JFJochDBusClient"),
|
||||
patch("aare.gui.main_window.jwt.decode") as mock_jwt,
|
||||
):
|
||||
mock_jwt.return_value = {
|
||||
"sub": "testuser",
|
||||
"staff": True,
|
||||
"pgroups": ["p123"],
|
||||
"session": 15,
|
||||
}
|
||||
win = _make_window(qtbot)
|
||||
|
||||
for dock in win.findChildren(QDockWidget):
|
||||
dock.hide()
|
||||
assert all(d.isHidden() for d in win.findChildren(QDockWidget))
|
||||
|
||||
# state_manager is mocked, so restore_window is a no-op and the
|
||||
# all-hidden layout survives to the heal check.
|
||||
win._restore_window_state()
|
||||
|
||||
assert not win.tell_samples_dock.isHidden()
|
||||
|
||||
|
||||
def test_close_restores_pre_watch_layout(qtbot, mock_ui_state):
|
||||
with (
|
||||
patch("requests.get"),
|
||||
patch("aare.gui.main_window.DAQWorker"),
|
||||
patch("aare.gui.main_window.PredictionSubscriber"),
|
||||
patch("aare.gui.main_window.VideoThread"),
|
||||
patch("aare.gui.main_window.JFJochDBusClient"),
|
||||
patch("aare.gui.main_window.jwt.decode") as mock_jwt,
|
||||
):
|
||||
mock_jwt.return_value = {
|
||||
"sub": "testuser",
|
||||
"staff": True,
|
||||
"pgroups": ["p123"],
|
||||
"session": 15,
|
||||
}
|
||||
win = _make_window(qtbot)
|
||||
|
||||
pre_watch = win.saveState()
|
||||
for dock in win.findChildren(QDockWidget):
|
||||
dock.hide()
|
||||
win._session_operations_enabled = False
|
||||
win._pre_watch_dock_state = pre_watch
|
||||
|
||||
win.close()
|
||||
|
||||
# closeEvent put the pre-watch layout back before saving state, so
|
||||
# the all-hidden fold was not persisted.
|
||||
assert not win.tell_samples_dock.isHidden()
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from PySide6.QtGui import QPixmap
|
||||
|
||||
from aare.gui.widgets.splash_screen import LoadingSplashScreen
|
||||
|
||||
|
||||
def test_splash_progress_and_message(qtbot):
|
||||
splash = LoadingSplashScreen(QPixmap(200, 100))
|
||||
qtbot.addWidget(splash)
|
||||
|
||||
splash.set_progress(42, "Loading panels")
|
||||
assert splash.progress.value() == 42
|
||||
|
||||
splash.set_progress(43) # message-less update takes the no-showMessage branch
|
||||
assert splash.progress.value() == 43
|
||||
Reference in New Issue
Block a user