"""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_status_header_click_selects_all(panel): header = panel.table_view.frozen.horizontalHeader() header.sectionClicked.emit(0) assert len(panel.table_view.selectionModel().selectedRows()) == 3 # Other sections keep their normal sort-click behavior. panel.table_view.clearSelection() header.sectionClicked.emit(1) assert len(panel.table_view.selectionModel().selectedRows()) == 0 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]] def test_font_zoom_keeps_frozen_column_aligned_and_reautosizes(panel, qtbot): """Ctrl+plus zoom: the frozen "#" overlay header must stay exactly as tall as the main header (else its rows sit 1px higher), and the columns sized once at startup must re-autosize for the wider glyphs.""" from PySide6.QtGui import QFont from PySide6.QtWidgets import QApplication from aare.gui import styles app = QApplication.instance() assert isinstance(app, QApplication) # font() lives on QApplication, not QCoreApplication base = QFont(app.font()) # Startup look first: under a styled ancestor (MainWindow) fonts reach # the view through the QSS path; without any sheet Qt would not # propagate the app font to a child that carries WA_StyleSheet. panel.setStyleSheet(styles.build_app_stylesheet(styles.THEME_SUNRISE)) panel.resize(600, 300) panel.show() qtbot.waitExposed(panel) view = panel.table_view width_before = view.columnWidth(1) def row0_top(v): return v.viewport().mapTo(panel, v.viewport().rect().topLeft()).y() + v.rowViewportPosition( 0 ) try: # Same three steps as MainWindow._apply_theme on a zoom change. styles.set_font_scale(1.25) big = QFont(base) big.setPointSizeF(base.pointSizeF() * 1.25) app.setFont(big) panel.setStyleSheet(styles.build_app_stylesheet(styles.THEME_SUNRISE)) qtbot.waitUntil(lambda: view.columnWidth(1) > width_before) assert view.frozen.horizontalHeader().height() == view.horizontalHeader().height() assert row0_top(view.frozen) == row0_top(view) finally: styles.set_font_scale(1.0) app.setFont(base)