From 40e6936580ba2bdb022a26edab6b7e0af2f68de3 Mon Sep 17 00:00:00 2001 From: Dawn Date: Wed, 19 Aug 2026 12:52:34 +0200 Subject: [PATCH] fix: satisfy lint and diff-coverage gates for admin gating basedpyright requires _admin_tip_anchor to exist after __init__, so the anchor is created eagerly instead of lazily. Cover the red-tip branch and the status-bar menu gate; the menu test swaps in a QMenu subclass with a no-op exec because PySide method lookup ignores class-attr monkeypatches. Co-Authored-By: Claude Fable 5 --- src/aare/gui/main_window.py | 15 ++++++----- tests/unit/gui/test_beamline_state_panel.py | 1 + tests/unit/gui/test_status_bar.py | 29 +++++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 43f69d08..b04a58dc 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -353,6 +353,13 @@ class MainWindow(QMainWindow): # after the docks), not in the left column. Always visible. self.beamline_state_panel = BeamlineStatePanel(staff=self._decoded_token.staff, parent=self) + # Anchor for _show_admin_tip: QToolTip inherits QSS from the widget + # it is shown for, so this hidden label carries the red warning wash + # without reddening any other tooltip. Eager, not lazy — basedpyright + # requires instance attributes to exist after __init__. + self._admin_tip_anchor = QLabel(self) + self._admin_tip_anchor.hide() + # Beamline / Experiment as tabs (like the Dewar samples dock) instead # of two stacked banner groups; the pages keep their banner children. # documentMode: no pane frame, so the fixed-width panels aren't inset. @@ -1477,12 +1484,8 @@ class MainWindow(QMainWindow): def _show_admin_tip(self, message: str) -> None: # "Not admin" denials are passive red tips, not QMessageBoxes: - # nothing to click away. QToolTip inherits QSS from the widget it is - # shown for, so a hidden anchor keeps the red wash off every other - # tooltip; styled at show time so it always matches the theme. - if not hasattr(self, "_admin_tip_anchor"): - self._admin_tip_anchor = QLabel(self) - self._admin_tip_anchor.hide() + # nothing to click away. Styled at show time so it always matches + # the theme. self._admin_tip_anchor.setStyleSheet(admin_tip_qss(self._theme_mode)) QToolTip.showText(QCursor.pos(), message, self._admin_tip_anchor) diff --git a/tests/unit/gui/test_beamline_state_panel.py b/tests/unit/gui/test_beamline_state_panel.py index 944a3dfd..1b85c3b5 100644 --- a/tests/unit/gui/test_beamline_state_panel.py +++ b/tests/unit/gui/test_beamline_state_panel.py @@ -42,6 +42,7 @@ def test_non_staff_never_reach_admin_only_states(qtbot): assert "QToolTip" not in panel._buttons[BeamlineStateEnum.XtalSnapshot].styleSheet() with qtbot.assertNotEmitted(panel.beam_location): panel._emit_for_state(BeamlineStateEnum.BeamLocation) + panel._on_left_click(BeamlineStateEnum.BeamLocation) # shows the admin tip def test_no_targets_while_moving_or_unknown(qtbot): diff --git a/tests/unit/gui/test_status_bar.py b/tests/unit/gui/test_status_bar.py index 291db130..1f5bcbde 100644 --- a/tests/unit/gui/test_status_bar.py +++ b/tests/unit/gui/test_status_bar.py @@ -33,6 +33,35 @@ def test_operables_share_the_hover_affordance(qtbot): assert not bar.state_label.font().underline() +def test_state_menu_gates_beam_location_for_non_staff(qtbot, daq_status_factory, monkeypatch): + from aarecommon.models.models import BeamlineStateEnum + from PySide6.QtWidgets import QMenu + + import aare.gui.widgets.status_bar as status_bar_module + + # exec() would block on a real popup, and PySide's method lookup ignores + # a class-attribute monkeypatch — swap in a subclass instead. The menu + # stays inspectable as a child of the bar afterwards. + class _NoExecMenu(QMenu): + def exec(self): # pyright: ignore[reportIncompatibleMethodOverride] + return None + + monkeypatch.setattr(status_bar_module, "QMenu", _NoExecMenu) + + def _state_menu_entries(bar): + bar._status = daq_status_factory(state=BeamlineStateEnum.SampleAlignment) + bar.show_state_menu() + # Plain data, not QAction refs: the menu (and its actions) only + # lives until the next GC pass once show_state_menu returns. + return {a.text(): a.isEnabled() for a in bar.findChildren(QMenu)[-1].actions()} + + assert _state_menu_entries(_bar(qtbot))["Beam location"] + + non_staff = StatusBar(token=TokenData(sub="u", staff=False, pgroups=["p1"], session=1)) + qtbot.addWidget(non_staff) + assert not _state_menu_entries(non_staff)["Beam location (admin mode only)"] + + def test_passives_left_operables_right(qtbot): bar = _bar(qtbot) # QStatusBar hides only the non-permanent (left) section behind a