Files
AareDAQ/tests/unit/gui/test_status_bar.py
duan_jandClaude Fable 5 8b6ae51ce6 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 <noreply@anthropic.com>
2026-08-20 19:54:50 +02:00

80 lines
3.0 KiB
Python

"""Status bar layout contract: passive readouts left, operable controls
right, and every operable label wears the shared hover affordance."""
from aarecommon.models.models import TokenData
from PySide6.QtCore import QEvent, QPointF, Qt
from PySide6.QtGui import QEnterEvent
from aare.gui.widgets.status_bar import StatusBar
def _bar(qtbot):
bar = StatusBar(token=TokenData(sub="u", staff=True, pgroups=["p1"], session=1))
qtbot.addWidget(bar)
bar.show()
return bar
def test_operables_share_the_hover_affordance(qtbot):
bar = _bar(qtbot)
for label in (bar.shutter_label, bar.state_label, bar.pgroup_label, bar.session_label):
assert label.cursor().shape() == Qt.CursorShape.PointingHandCursor
# Cryo is grouped with the operables but stays passive for now (TODO in
# status_bar.py); the plain readouts are passive too.
for label in (bar.cryo_label, bar.flux, bar.busy_label):
assert label.cursor().shape() != Qt.CursorShape.PointingHandCursor
# Hover underlines, leaving restores.
enter = QEnterEvent(QPointF(1, 1), QPointF(1, 1), QPointF(1, 1))
bar.state_label.enterEvent(enter)
assert bar.state_label.font().underline()
bar.state_label.leaveEvent(QEvent(QEvent.Type.Leave))
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
# temporary message — probe the grouping through that behavior.
bar.showMessage("probe")
assert not bar.flux.isVisible()
assert not bar.busy_label.isVisible()
for label in (
bar.cryo_label,
bar.shutter_label,
bar.state_label,
bar.pgroup_label,
bar.session_label,
):
assert label.isVisible()