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>
80 lines
3.0 KiB
Python
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()
|