"""Offscreen UI smoke tests: construction, layout adaptation, theme tokens.""" from __future__ import annotations import pytest from tests.conftest import visible_descendants RESOLUTIONS = ((1280, 720), (1600, 900), (1920, 1080)) def _show(window, size): window.resize(*size) window.show() from PyQt5.QtWidgets import QApplication QApplication.processEvents() return window @pytest.mark.parametrize(("theme", "sensor_type"), [ ("dark", "box"), ("light", "box"), ("dark", "finger"), ("light", "finger"), ]) def test_window_builds_in_every_theme(make_window, theme, sensor_type): window = _show(make_window(theme=theme, sensor_type=sensor_type), (1400, 880)) assert window.windowTitle() assert window.content_splitter.count() == 4 @pytest.mark.parametrize("size", RESOLUTIONS) def test_panels_fit_within_window(make_window, size): window = _show( make_window(sensor_type="finger", show_cal_panel=True), size, ) sizes = window.content_splitter.sizes() panels = [ window.mesh3d_panel, window.arrow_panel, window.force_plots, window.cal_panel, ] for panel, width in zip(panels, sizes): assert panel.isVisible() assert width >= panel.minimumWidth(), ( f"{panel.objectName() or type(panel).__name__} too narrow: {width}" ) assert window.force_plots.width() >= window.force_plots.minimumWidth() assert window.arrow_panel.width() >= window.arrow_panel.minimumWidth() @pytest.mark.parametrize("size", RESOLUTIONS) def test_children_stay_inside_their_parent(make_window, size): window = _show(make_window(sensor_type="finger", show_cal_panel=True), size) oversize = [] for child in visible_descendants(window): parent = child.parentWidget() if parent is None or not parent.isVisible(): continue geometry = child.geometry() if geometry.width() > parent.width() + 1 or geometry.height() > parent.height() + 1: oversize.append( f"{type(child).__name__} {geometry.width()}x{geometry.height()}" f" > parent {parent.width()}x{parent.height()}" ) assert not oversize, oversize def test_theme_tokens_have_identical_keys(): from orisys_app_shared.theme import build_qss, load_colors dark = set(load_colors("dark")) light = set(load_colors("light")) assert dark == light, {"dark_only": sorted(dark - light), "light_only": sorted(light - dark)} assert len(build_qss("dark")) > 1000 assert len(build_qss("light")) > 1000 def test_arrow_canvas_differs_from_accent(): """The pure-color arrow canvas must not collide with the accent pen color.""" from orisys_app_shared.theme import load_colors for theme in ("dark", "light"): colors = load_colors(theme) assert colors["arrow_canvas"] != colors["accent"], theme def test_disabled_buttons_use_muted_text_not_hover_background(): from orisys_app_shared.theme import build_qss, load_colors for theme in ("dark", "light"): colors = load_colors(theme) qss = build_qss(theme) assert f"color: {colors['text_muted']}" in qss, theme # The old rule painted disabled buttons with the brighter hover color. assert f"background-color: {colors['button_hover']};\n border-color: {colors['panel_border']}" not in qss def test_splitter_sizes_round_trip_through_prefs(make_window): window = _show(make_window(), (1440, 880)) window.content_splitter.setSizes([300, 420, 520, 0]) window._persist_prefs() stored = window.prefs["splitter_sizes"] assert len(stored) == 4 assert stored[2] > stored[0] def test_status_chip_tracks_phase(make_window): window = _show(make_window(), (1400, 880)) assert window._session_phase == "idle" window._set_phase("running") assert window.status_chip.text() assert "status_running" not in window.status_chip.text() window._set_phase("nonexistent-phase") assert window._session_phase == "idle" def test_run_tooltip_explains_disabled_reason(make_window): window = _show(make_window(source_mode="video", video_path=""), (1400, 880)) assert not window.toolbar.btn_run.isEnabled() assert window.toolbar.btn_run.toolTip() window.toolbar.set_video_path("C:/tmp/demo.mp4") window._update_run_enabled() assert window.toolbar.btn_run.isEnabled() def test_toolbar_run_button_is_labelled(make_window): window = _show(make_window(), (1400, 880)) label = window.toolbar.btn_run.text() assert label.strip() assert label != "▶" window.toolbar.set_run_checked(True) assert window.toolbar.btn_run.text() != label def test_theme_switch_updates_panels(make_window): window = _show(make_window(), (1400, 880)) window._set_theme("light") assert window._theme == "light" window._set_theme("dark") assert window._theme == "dark" def test_cal_panel_records_and_lists_samples(make_window): window = _show(make_window(), (1400, 880)) cal = window.cal_panel cal.set_running(True) assert cal.btn_record.isEnabled() assert cal.btn_record.text() from orisys_app_shared.force.calibration_io import CalSample cal._store.upsert("fn", CalSample(weight_n=1.0, raw_mean=9.7, n=120, std=0.02)) cal._refresh_table() assert cal.table.rowCount() == 1 assert not cal.empty_hint.isVisible() def test_force_plots_render_demo_data(make_window): window = _show(make_window(), (1440, 880)) for i in range(60): window.force_plots.push(i / 30.0, float(i), -1.0, 1.0, unit="N") from PyQt5.QtWidgets import QApplication QApplication.processEvents() assert window.force_plots.history.sample_count > 0 assert all(label.text() for label in window.force_plots.value_labels)