test: cover the sample-status logic, wheel guard, pop-outs, and log mirror
CI / lint (push) Skipped
CI / test (3.11) (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / test (3.11) (pull_request) Successful in 58s
CI / test (3.13) (pull_request) Successful in 55s
CI / test (3.12) (pull_request) Successful in 1m1s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m4s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m3s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m18s
CI / test-with-coverage (pull_request) Successful in 1m29s
CI / coverage-analysis (pull_request) Successful in 3s
CI / lint (pull_request) Failing after 2m32s
CI / lint (push) Skipped
CI / test (3.11) (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / test (3.11) (pull_request) Successful in 58s
CI / test (3.13) (pull_request) Successful in 55s
CI / test (3.12) (pull_request) Successful in 1m1s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m4s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m3s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m18s
CI / test-with-coverage (pull_request) Successful in 1m29s
CI / coverage-analysis (pull_request) Successful in 3s
CI / lint (pull_request) Failing after 2m32s
New tests for what the port added: status tint priority and context-dependent chip filters on the dewar/queue model, the right-button wheel guard (motor protection), PopoutWindow edge maths, manual-resize fallback, close-hides behavior and DockTitleBar, the chip drag-drop relabeling, and the console-log pop-out mirror. The widgets' __main__ self-checks are superseded by these and removed. Brings PR diff coverage from 70% to 81% (gate: 80%). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f324ecc19
commit
b4d680494b
7 files changed
+532
-52
No files matched your search
@@ -201,21 +201,3 @@ class PopoutWindow(QWidget):
|
||||
self._press_global = None
|
||||
self._press_geom = None
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# ponytail: smallest check that fails if the edge maths breaks
|
||||
import os
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
from PySide6.QtWidgets import QApplication, QLabel
|
||||
|
||||
app = QApplication([])
|
||||
w = PopoutWindow("t", QLabel("x"))
|
||||
w.resize(400, 300)
|
||||
assert w._edges_at(QPoint(5, 150)) == Qt.Edge.LeftEdge
|
||||
assert w._edges_at(QPoint(398, 298)) == (Qt.Edge.RightEdge | Qt.Edge.BottomEdge)
|
||||
assert w._edges_at(QPoint(200, 150)) == Qt.Edge(0)
|
||||
assert w._cursor_for(Qt.Edge.LeftEdge | Qt.Edge.TopEdge) == Qt.CursorShape.SizeFDiagCursor
|
||||
assert w._cursor_for(Qt.Edge.RightEdge | Qt.Edge.TopEdge) == Qt.CursorShape.SizeBDiagCursor
|
||||
print("gude")
|
||||
@@ -46,37 +46,3 @@ class WheelValueGuard(QObject):
|
||||
QApplication.sendEvent(area.viewport(), relayed)
|
||||
return True
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# ponytail: smallest check that fails if the guard logic breaks
|
||||
import os
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
from PySide6.QtCore import QPoint, QPointF
|
||||
|
||||
app = QApplication([])
|
||||
guard = WheelValueGuard()
|
||||
app.installEventFilter(guard)
|
||||
slider = QSlider(Qt.Orientation.Horizontal)
|
||||
slider.setRange(0, 100)
|
||||
slider.setValue(50)
|
||||
slider.show()
|
||||
|
||||
def wheel(buttons):
|
||||
return QWheelEvent(
|
||||
QPointF(5, 5),
|
||||
QPointF(5, 5),
|
||||
QPoint(0, 0),
|
||||
QPoint(0, 120),
|
||||
buttons,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
Qt.ScrollPhase.NoScrollPhase,
|
||||
False,
|
||||
)
|
||||
|
||||
QApplication.sendEvent(slider, wheel(Qt.MouseButton.NoButton))
|
||||
assert slider.value() == 50, "bare wheel must not adjust the slider"
|
||||
QApplication.sendEvent(slider, wheel(Qt.MouseButton.RightButton))
|
||||
assert slider.value() != 50, "right-button + wheel must adjust the slider"
|
||||
print("gude")
|
||||
@@ -0,0 +1,29 @@
|
||||
"""The console-log pop-out is a second view on the same emitter: history is
|
||||
copied on open, live lines reach both views, and clear() empties both."""
|
||||
|
||||
from aare.gui.panels.log_panel import LogDock
|
||||
|
||||
|
||||
def test_log_popout_mirrors_and_clears(qtbot):
|
||||
dock = LogDock()
|
||||
qtbot.addWidget(dock)
|
||||
dock.emitter.message.emit("first line")
|
||||
assert "first line" in dock.view.toPlainText()
|
||||
|
||||
dock._open_popout()
|
||||
assert dock._popout is not None
|
||||
assert dock._popout.isVisible()
|
||||
# History copied on open, live lines reach both views.
|
||||
assert "first line" in dock._popout_view.toPlainText()
|
||||
dock.emitter.message.emit("second line")
|
||||
assert "second line" in dock.view.toPlainText()
|
||||
assert "second line" in dock._popout_view.toPlainText()
|
||||
|
||||
# Reopening reuses the window instead of stacking mirrors.
|
||||
popout = dock._popout
|
||||
dock._open_popout()
|
||||
assert dock._popout is popout
|
||||
|
||||
dock.clear()
|
||||
assert dock.view.toPlainText() == ""
|
||||
assert dock._popout_view.toPlainText() == ""
|
||||
@@ -121,3 +121,127 @@ def test_sample_queue_model_flags(sample_list):
|
||||
model = SampleQueueSpreadsheet(samples=sample_list[:2])
|
||||
flags = model.flags(model.index(0, 0))
|
||||
assert flags & Qt.ItemFlag.ItemIsDropEnabled
|
||||
|
||||
|
||||
# --- Status logic of the combined dewar/queue view ---------------------------
|
||||
# The dewar table doubles as the queue view: the frozen "#" column carries a
|
||||
# status fill (mounted > queued > flagged > measured) and the chip row filters
|
||||
# by status. This is the logic a local contact trusts at a glance, so it gets
|
||||
# its own tests.
|
||||
|
||||
|
||||
def _status(model, row):
|
||||
brush = model.data(model.index(row, 0), Qt.ItemDataRole.BackgroundRole)
|
||||
return None if brush is None else brush.color().name().lower()
|
||||
|
||||
|
||||
def _row_of(model, db_id):
|
||||
return next(r for r in range(model.rowCount()) if model.get_id(r).db_id == db_id)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def status_model(sample_list):
|
||||
from aarecommon.models.models import DewarAddress, SampleShortInfo
|
||||
|
||||
# A measured sample: rotation_count > 1 (exactly 1 must NOT count).
|
||||
sample_list.append(
|
||||
SampleShortInfo(
|
||||
db_id=4,
|
||||
puck_name="P3",
|
||||
dewar_name="D3",
|
||||
sample_name="S4",
|
||||
run_number=4,
|
||||
user="U1",
|
||||
pin=4,
|
||||
rotation_count=2,
|
||||
location=DewarAddress(segment="B", pos=1),
|
||||
)
|
||||
)
|
||||
model = UserSampleSpreadsheet(samples=sample_list)
|
||||
model.set_show_all_pgroups(True)
|
||||
return model
|
||||
|
||||
|
||||
def test_status_color_priority(status_model):
|
||||
from aare.gui.styles import (
|
||||
SAMPLE_ROW_QUEUED_BG,
|
||||
SAMPLE_STATUS_FLAGGED_BG,
|
||||
SAMPLE_STATUS_MEASURED_BG,
|
||||
SAMPLE_STATUS_QUEUED_BG,
|
||||
)
|
||||
|
||||
model = status_model
|
||||
assert _status(model, _row_of(model, 1)) is None
|
||||
|
||||
model.set_queued_ids({1})
|
||||
model.set_flagged(1, True)
|
||||
# Queued beats flagged in the All view.
|
||||
assert _status(model, _row_of(model, 1)) == SAMPLE_STATUS_QUEUED_BG.lower()
|
||||
model.set_queued_ids(set())
|
||||
assert _status(model, _row_of(model, 1)) == SAMPLE_STATUS_FLAGGED_BG.lower()
|
||||
|
||||
# Measured is automatic: rotation_count 2 counts, the fixture's 1-3 don't.
|
||||
assert _status(model, _row_of(model, 4)) == SAMPLE_STATUS_MEASURED_BG.lower()
|
||||
assert _status(model, _row_of(model, 2)) is None
|
||||
|
||||
# Mounted always wins.
|
||||
model.updateCurrentSample(current_puck="P1", current_sample=1)
|
||||
assert _status(model, _row_of(model, 1)) == SAMPLE_ROW_QUEUED_BG.lower()
|
||||
|
||||
|
||||
def test_status_filter_selects_rows(status_model):
|
||||
model = status_model
|
||||
model.set_queued_ids({1, 2})
|
||||
model.set_flagged(3, True)
|
||||
|
||||
model.set_status_filter("queued")
|
||||
assert {model.get_id(r).db_id for r in range(model.rowCount())} == {1, 2}
|
||||
model.set_status_filter("flagged")
|
||||
assert {model.get_id(r).db_id for r in range(model.rowCount())} == {3}
|
||||
model.set_status_filter("measured")
|
||||
assert {model.get_id(r).db_id for r in range(model.rowCount())} == {4}
|
||||
model.set_status_filter(None)
|
||||
assert model.rowCount() == 4
|
||||
|
||||
|
||||
def test_status_tints_are_context_dependent(status_model):
|
||||
from aare.gui.styles import SAMPLE_STATUS_FLAGGED_BG, SAMPLE_STATUS_QUEUED_BG
|
||||
|
||||
model = status_model
|
||||
model.set_queued_ids({1, 2})
|
||||
model.set_flagged(1, True)
|
||||
|
||||
# Queued view: own tint suppressed, only the also-flagged mark shows.
|
||||
model.set_status_filter("queued")
|
||||
assert _status(model, _row_of(model, 1)) == SAMPLE_STATUS_FLAGGED_BG.lower()
|
||||
assert _status(model, _row_of(model, 2)) is None
|
||||
|
||||
# Flagged view: a re-queued sample wears the queued mark.
|
||||
model.set_status_filter("flagged")
|
||||
assert _status(model, _row_of(model, 1)) == SAMPLE_STATUS_QUEUED_BG.lower()
|
||||
|
||||
|
||||
def test_status_sets_refilter_while_chip_active(status_model):
|
||||
model = status_model
|
||||
model.set_status_filter("queued")
|
||||
assert model.rowCount() == 0
|
||||
model.set_queued_ids({2})
|
||||
assert {model.get_id(r).db_id for r in range(model.rowCount())} == {2}
|
||||
|
||||
|
||||
def test_status_column_is_display_only(status_model):
|
||||
model = status_model
|
||||
assert model.data(model.index(0, 0), Qt.ItemDataRole.DisplayRole) == 1
|
||||
before = [model.get_id(r).db_id for r in range(model.rowCount())]
|
||||
model.sort(0, Qt.SortOrder.DescendingOrder) # no-op on the "#" column
|
||||
assert [model.get_id(r).db_id for r in range(model.rowCount())] == before
|
||||
|
||||
|
||||
def test_mime_data_round_trips_for_chip_drops(status_model):
|
||||
from aarecommon.models.models import SampleShortInfoList
|
||||
|
||||
model = status_model
|
||||
payload = model.mimeData([model.index(0, 1), model.index(1, 1)])
|
||||
samples = SampleShortInfoList.model_validate_json(payload.text())
|
||||
assert len(samples.s) == 2
|
||||
assert samples.s[0].db_id == model.get_id(0).db_id
|
||||
@@ -0,0 +1,171 @@
|
||||
"""PopoutWindow replaces dock floating: an additional top-level window whose
|
||||
edge band resizes, whose close only hides, and whose first show lands near
|
||||
the cursor. DockTitleBar puts the pop-out button next to the close box."""
|
||||
|
||||
from PySide6.QtCore import QEvent, QPoint, QPointF, Qt
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
from PySide6.QtWidgets import QDockWidget, QLabel
|
||||
|
||||
from aare.gui.widgets.popout_window import DockTitleBar, PopoutWindow
|
||||
|
||||
|
||||
def _window(qtbot):
|
||||
w = PopoutWindow("test", QLabel("content"))
|
||||
qtbot.addWidget(w)
|
||||
w.resize(400, 300)
|
||||
return w
|
||||
|
||||
|
||||
def test_edges_at(qtbot):
|
||||
w = _window(qtbot)
|
||||
band = PopoutWindow.RESIZE_MARGIN + PopoutWindow.OUTER_GRIP
|
||||
assert w._edges_at(QPoint(band - 1, 150)) == Qt.Edge.LeftEdge
|
||||
assert w._edges_at(QPoint(200, band - 1)) == Qt.Edge.TopEdge
|
||||
assert w._edges_at(QPoint(399, 299)) == (Qt.Edge.RightEdge | Qt.Edge.BottomEdge)
|
||||
assert w._edges_at(QPoint(200, 150)) == Qt.Edge(0)
|
||||
|
||||
|
||||
def test_cursor_for(qtbot):
|
||||
w = _window(qtbot)
|
||||
assert w._cursor_for(Qt.Edge.LeftEdge) == Qt.CursorShape.SizeHorCursor
|
||||
assert w._cursor_for(Qt.Edge.BottomEdge) == Qt.CursorShape.SizeVerCursor
|
||||
assert w._cursor_for(Qt.Edge.LeftEdge | Qt.Edge.TopEdge) == Qt.CursorShape.SizeFDiagCursor
|
||||
assert w._cursor_for(Qt.Edge.RightEdge | Qt.Edge.TopEdge) == Qt.CursorShape.SizeBDiagCursor
|
||||
assert w._cursor_for(Qt.Edge(0)) is None
|
||||
|
||||
|
||||
def test_close_hides_and_keeps_geometry(qtbot):
|
||||
w = _window(qtbot)
|
||||
w.show()
|
||||
qtbot.waitExposed(w)
|
||||
w.move(120, 130)
|
||||
geometry = w.geometry()
|
||||
w.close()
|
||||
assert not w.isVisible()
|
||||
# Reopen: the first-show placement must not run again (geometry kept).
|
||||
w.show()
|
||||
assert w._placed
|
||||
assert w.geometry() == geometry
|
||||
|
||||
|
||||
def test_manual_resize_fallback(qtbot):
|
||||
w = _window(qtbot)
|
||||
w.show()
|
||||
qtbot.waitExposed(w)
|
||||
w.resize(400, 300)
|
||||
start = w.geometry()
|
||||
|
||||
grab = QPointF(399.0, 299.0) # bottom-right band
|
||||
press = QMouseEvent(
|
||||
QEvent.Type.MouseButtonPress,
|
||||
grab,
|
||||
w.mapToGlobal(grab.toPoint()).toPointF(),
|
||||
Qt.MouseButton.LeftButton,
|
||||
Qt.MouseButton.LeftButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
)
|
||||
w.mousePressEvent(press)
|
||||
if not w._manual_edges:
|
||||
# The platform accepted startSystemResize; the manual path is not
|
||||
# reachable here, and the press must not have moved the window.
|
||||
assert w.geometry() == start
|
||||
return
|
||||
|
||||
move_to = grab + QPointF(40.0, 25.0)
|
||||
move = QMouseEvent(
|
||||
QEvent.Type.MouseMove,
|
||||
move_to,
|
||||
w.mapToGlobal(move_to.toPoint()).toPointF(),
|
||||
Qt.MouseButton.NoButton,
|
||||
Qt.MouseButton.LeftButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
)
|
||||
w.mouseMoveEvent(move)
|
||||
assert w.width() == start.width() + 40
|
||||
assert w.height() == start.height() + 25
|
||||
|
||||
release = QMouseEvent(
|
||||
QEvent.Type.MouseButtonRelease,
|
||||
move_to,
|
||||
w.mapToGlobal(move_to.toPoint()).toPointF(),
|
||||
Qt.MouseButton.LeftButton,
|
||||
Qt.MouseButton.NoButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
)
|
||||
w.mouseReleaseEvent(release)
|
||||
assert not w._manual_edges
|
||||
|
||||
|
||||
def test_hover_sets_resize_cursor(qtbot):
|
||||
w = _window(qtbot)
|
||||
w.show()
|
||||
qtbot.waitExposed(w)
|
||||
|
||||
def hover(pos):
|
||||
w.mouseMoveEvent(
|
||||
QMouseEvent(
|
||||
QEvent.Type.MouseMove,
|
||||
pos,
|
||||
w.mapToGlobal(pos.toPoint()).toPointF(),
|
||||
Qt.MouseButton.NoButton,
|
||||
Qt.MouseButton.NoButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
)
|
||||
)
|
||||
|
||||
hover(QPointF(2.0, 150.0))
|
||||
assert w.cursor().shape() == Qt.CursorShape.SizeHorCursor
|
||||
hover(QPointF(200.0, 150.0))
|
||||
assert w.cursor().shape() == Qt.CursorShape.ArrowCursor
|
||||
|
||||
|
||||
def test_dock_title_bar_buttons(qtbot):
|
||||
dock = QDockWidget("Console Log")
|
||||
qtbot.addWidget(dock)
|
||||
opened = []
|
||||
bar = DockTitleBar(dock, on_popout=lambda: opened.append(True))
|
||||
dock.setTitleBarWidget(bar)
|
||||
dock.show()
|
||||
qtbot.waitExposed(dock)
|
||||
|
||||
qtbot.mouseClick(bar.popout_button, Qt.MouseButton.LeftButton)
|
||||
assert opened == [True]
|
||||
assert dock.isVisible()
|
||||
|
||||
|
||||
def test_manual_resize_from_top_left(qtbot):
|
||||
w = _window(qtbot)
|
||||
w.show()
|
||||
qtbot.waitExposed(w)
|
||||
w.resize(400, 300)
|
||||
# Drive the manual path directly; whether startSystemResize is available
|
||||
# is platform luck and the top/left arithmetic deserves coverage either way.
|
||||
w._manual_edges = Qt.Edge.LeftEdge | Qt.Edge.TopEdge
|
||||
w._press_global = w.mapToGlobal(QPoint(2, 2))
|
||||
w._press_geom = w.geometry()
|
||||
move_to = QPointF(2.0 + 30.0, 2.0 + 20.0)
|
||||
w.mouseMoveEvent(
|
||||
QMouseEvent(
|
||||
QEvent.Type.MouseMove,
|
||||
move_to,
|
||||
w.mapToGlobal(move_to.toPoint()).toPointF(),
|
||||
Qt.MouseButton.NoButton,
|
||||
Qt.MouseButton.LeftButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
)
|
||||
)
|
||||
assert w.width() == 400 - 30
|
||||
assert w.height() == 300 - 20
|
||||
|
||||
|
||||
def test_paint_border_knob(qtbot, monkeypatch):
|
||||
from PySide6.QtGui import QPixmap
|
||||
|
||||
from aare.gui.widgets import popout_window as mod
|
||||
|
||||
w = _window(qtbot)
|
||||
# Default FRAME_L1_WIDTH "0px": render must take the paint-nothing path.
|
||||
w.render(QPixmap(w.size()))
|
||||
# With a visible level-1 border the painter path runs.
|
||||
monkeypatch.setattr(mod, "FRAME_L1_WIDTH", "2px")
|
||||
w.render(QPixmap(w.size()))
|
||||
@@ -0,0 +1,134 @@
|
||||
"""The combined sample dock: chip row filters the table by status, chips
|
||||
double as drop targets for queue/flag relabeling, and a pop-out panel shares
|
||||
the docked panel's model so both stay in sync without wiring."""
|
||||
|
||||
import pytest
|
||||
from aarecommon.models.models import DewarAddress, SampleShortInfo, SampleShortInfoList
|
||||
|
||||
from aare.gui.panels.tell_sample_panel import TellSamplePanel
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def samples():
|
||||
return SampleShortInfoList(
|
||||
s=[
|
||||
SampleShortInfo(
|
||||
db_id=i,
|
||||
puck_name=f"P{i}",
|
||||
dewar_name="D1",
|
||||
sample_name=f"S{i}",
|
||||
run_number=i,
|
||||
user="U1",
|
||||
pin=i,
|
||||
location=DewarAddress(segment="A", pos=i),
|
||||
)
|
||||
for i in (1, 2, 3)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def panel(qtbot, samples):
|
||||
panel = TellSamplePanel(samples=samples)
|
||||
qtbot.addWidget(panel)
|
||||
panel.table_model.set_show_all_pgroups(True)
|
||||
return panel
|
||||
|
||||
|
||||
def _chip(panel, key):
|
||||
return next(c for c in panel.status_chips.buttons() if c.property("status_key") == key)
|
||||
|
||||
|
||||
def test_chip_click_drives_the_status_filter(panel):
|
||||
panel.table_model.set_queued_ids({2})
|
||||
_chip(panel, "queued").click()
|
||||
assert panel.table_model.status_filter == "queued"
|
||||
assert panel.table_model.rowCount() == 1
|
||||
_chip(panel, None).click()
|
||||
assert panel.table_model.status_filter is None
|
||||
assert panel.table_model.rowCount() == 3
|
||||
|
||||
|
||||
def test_set_status_chip_syncs_without_filtering(panel):
|
||||
panel.set_status_chip("flagged")
|
||||
assert _chip(panel, "flagged").isChecked()
|
||||
# Sync only checks the chip; it must not fire the filter.
|
||||
assert panel.table_model.status_filter is None
|
||||
|
||||
|
||||
def test_queued_chip_drop_relays_to_the_queue(panel, qtbot, samples):
|
||||
chip = _chip(panel, "queued")
|
||||
with qtbot.waitSignal(panel.add_to_queue) as blocker:
|
||||
chip.samples_dropped.emit(samples)
|
||||
assert [s.db_id for s in blocker.args[0].s] == [1, 2, 3]
|
||||
|
||||
|
||||
def test_flagged_chip_drop_flags_in_the_model(panel, samples):
|
||||
panel._flag_dropped_samples(SampleShortInfoList(s=samples.s[:2]))
|
||||
assert panel.table_model.flagged_ids == {1, 2}
|
||||
|
||||
|
||||
def test_popout_panel_shares_the_model(panel, qtbot):
|
||||
popout = TellSamplePanel(model=panel.table_model)
|
||||
qtbot.addWidget(popout)
|
||||
assert popout.table_model is panel.table_model
|
||||
panel.table_model.set_flagged(3, True)
|
||||
assert 3 in popout.table_model.flagged_ids
|
||||
|
||||
|
||||
def test_new_sample_list_updates_rows(panel, samples):
|
||||
extra = samples.s + [
|
||||
SampleShortInfo(
|
||||
db_id=9,
|
||||
puck_name="P9",
|
||||
dewar_name="D2",
|
||||
sample_name="S9",
|
||||
run_number=9,
|
||||
user="U2",
|
||||
pin=9,
|
||||
location=DewarAddress(segment="B", pos=1),
|
||||
)
|
||||
]
|
||||
panel.new_sample_list(SampleShortInfoList(s=extra))
|
||||
assert panel.table_model.rowCount() == 4
|
||||
|
||||
|
||||
def test_queue_drop_chip_accepts_sample_payloads(panel, qtbot, samples):
|
||||
from PySide6.QtCore import QMimeData, QPointF, Qt
|
||||
from PySide6.QtGui import QDropEvent
|
||||
|
||||
from aare.gui.panels.tell_sample_panel import QueueDropChip
|
||||
|
||||
chip = next(c for c in panel.status_chips.buttons() if isinstance(c, QueueDropChip))
|
||||
|
||||
# The event only borrows the QMimeData (C++ pointer), so the mime must
|
||||
# outlive the dropEvent call — hence created in the test's scope.
|
||||
def drop(mime):
|
||||
return QDropEvent(
|
||||
QPointF(1, 1),
|
||||
Qt.DropAction.CopyAction,
|
||||
mime,
|
||||
Qt.MouseButton.NoButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
)
|
||||
|
||||
good = QMimeData()
|
||||
good.setText(samples.model_dump_json())
|
||||
with qtbot.waitSignal(chip.samples_dropped):
|
||||
chip.dropEvent(drop(good))
|
||||
|
||||
# A non-sample payload is ignored, not crashed on.
|
||||
bad = QMimeData()
|
||||
bad.setText("not json")
|
||||
with qtbot.assertNotEmitted(chip.samples_dropped):
|
||||
chip.dropEvent(drop(bad))
|
||||
|
||||
|
||||
def test_selected_samples_follow_the_click(panel):
|
||||
view = panel.table_view
|
||||
view.selectRow(0)
|
||||
row_ids = [panel.table_model.get_id(r).db_id for r in range(3)]
|
||||
# Click inside the selection: the selection is acted on.
|
||||
assert [s.db_id for s in panel._selected_samples(0)] == [row_ids[0]]
|
||||
# Click outside the selection: only the clicked row is acted on.
|
||||
assert [s.db_id for s in panel._selected_samples(2)] == [row_ids[2]]
|
||||
@@ -0,0 +1,74 @@
|
||||
"""The wheel guard is motor protection: a bare wheel over a value widget must
|
||||
never change the value (it scrolls the page instead); adjusting requires the
|
||||
deliberate right-button + wheel gesture."""
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QPoint, QPointF, Qt
|
||||
from PySide6.QtGui import QWheelEvent
|
||||
from PySide6.QtWidgets import QApplication, QScrollArea, QSlider, QSpinBox, QVBoxLayout, QWidget
|
||||
|
||||
from aare.gui.widgets.wheel_value_guard import WheelValueGuard
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def guard(qapp):
|
||||
guard = WheelValueGuard()
|
||||
qapp.installEventFilter(guard)
|
||||
yield guard
|
||||
qapp.removeEventFilter(guard)
|
||||
|
||||
|
||||
def _wheel(buttons):
|
||||
return QWheelEvent(
|
||||
QPointF(5, 5),
|
||||
QPointF(5, 5),
|
||||
QPoint(0, 0),
|
||||
QPoint(0, 120),
|
||||
buttons,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
Qt.ScrollPhase.NoScrollPhase,
|
||||
False,
|
||||
)
|
||||
|
||||
|
||||
def test_bare_wheel_does_not_adjust(guard, qtbot):
|
||||
slider = QSlider(Qt.Orientation.Horizontal)
|
||||
qtbot.addWidget(slider)
|
||||
slider.setRange(0, 100)
|
||||
slider.setValue(50)
|
||||
QApplication.sendEvent(slider, _wheel(Qt.MouseButton.NoButton))
|
||||
assert slider.value() == 50
|
||||
|
||||
|
||||
def test_right_button_wheel_adjusts(guard, qtbot):
|
||||
slider = QSlider(Qt.Orientation.Horizontal)
|
||||
qtbot.addWidget(slider)
|
||||
slider.setRange(0, 100)
|
||||
slider.setValue(50)
|
||||
QApplication.sendEvent(slider, _wheel(Qt.MouseButton.RightButton))
|
||||
assert slider.value() != 50
|
||||
|
||||
|
||||
def test_bare_wheel_scrolls_the_enclosing_area(guard, qtbot):
|
||||
area = QScrollArea()
|
||||
qtbot.addWidget(area)
|
||||
content = QWidget()
|
||||
layout = QVBoxLayout(content)
|
||||
spin = QSpinBox()
|
||||
spin.setRange(0, 100)
|
||||
spin.setValue(50)
|
||||
layout.addWidget(spin)
|
||||
# Tall filler so the area has something to scroll.
|
||||
filler = QWidget()
|
||||
filler.setFixedHeight(2000)
|
||||
layout.addWidget(filler)
|
||||
area.setWidget(content)
|
||||
area.resize(200, 200)
|
||||
area.show()
|
||||
bar = area.verticalScrollBar()
|
||||
bar.setValue(bar.maximum() // 2)
|
||||
before = bar.value()
|
||||
|
||||
QApplication.sendEvent(spin, _wheel(Qt.MouseButton.NoButton))
|
||||
assert spin.value() == 50
|
||||
assert bar.value() != before
|
||||
Reference in new issue
Block a user