"""Shared fixtures for the studio app UI tests. The tests render real Qt widgets with the offscreen platform plugin, so they need no display and no camera hardware. A single ``QApplication`` is shared by the whole session (Qt forbids creating more than one). """ from __future__ import annotations import itertools import os import shutil import sys from pathlib import Path import pytest os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") if sys.platform == "win32" and "QT_QPA_FONTDIR" not in os.environ: _fonts = Path(os.environ.get("WINDIR", r"C:\Windows")) / "Fonts" if _fonts.is_dir(): os.environ["QT_QPA_FONTDIR"] = str(_fonts) # ``tools/ui_check.py`` owns the palette rules; importing it keeps one source of # truth between the pytest suite and the dependency-free checker. _TOOLS_DIR = Path(__file__).resolve().parents[1] / "tools" if _TOOLS_DIR.is_dir() and str(_TOOLS_DIR) not in sys.path: sys.path.insert(0, str(_TOOLS_DIR)) @pytest.fixture(scope="session") def qt_app(): from PyQt5.QtWidgets import QApplication app = QApplication.instance() or QApplication([]) yield app @pytest.fixture def make_window(qt_app, tmp_path, monkeypatch): """Build a ``StudioMainWindow`` whose preference writes stay in tmp_path.""" from orisys_app_shared import prefs as prefs_pkg from orisys_app_shared import paths as paths_mod from orisys_studio.app_window import StudioMainWindow prefs_file = tmp_path / "preference.json" monkeypatch.setattr(paths_mod, "preference_path", lambda: prefs_file) monkeypatch.setattr(prefs_pkg.store, "preference_path", lambda: prefs_file) default_prefs = { "language": "zh_CN", "theme": "dark", "sensor_type": "box", "source_mode": "camera", } created = [] def _make(**overrides): data = dict(default_prefs) data.update(overrides) window = StudioMainWindow(prefs=data) created.append(window) return window yield _make for window in created: try: window.close() except Exception: pass def visible_descendants(widget): """Yield every visible descendant widget of ``widget``.""" for child in widget.findChildren(object): if not hasattr(child, "isVisible") or not hasattr(child, "geometry"): continue if child.isVisible(): yield child def show_window(window, size): """Show a window at ``size`` and flush pending Qt events.""" from PyQt5.QtWidgets import QApplication window.resize(*size) window.show() QApplication.processEvents() return window def running_window(make_window): """Return a running window backed by a controllable test worker.""" import ui_check window = show_window( make_window(sensor_type="box", source_mode="camera"), (1440, 880), ) stuck = ui_check._StuckWorker() window._ensure_workers = lambda: None window._box_worker = stuck window._finger_worker = stuck window._start_run() window._run_timer.stop() window._set_phase("running") return window, stuck def finish_run(window, stuck): """Stop a test run and release its controllable worker.""" window._stop_run(reason="user_stop") stuck.release() _RECORD_SCRATCH = ( Path(__file__).resolve().parents[1] / "outputs" / "_test_record" ) _RECORD_3D_SCRATCH = ( Path(__file__).resolve().parents[1] / "outputs" / "_test_record_3d" ) @pytest.fixture def record_root(): """A clean workspace-local directory for recording tests.""" shutil.rmtree(_RECORD_SCRATCH, ignore_errors=True) _RECORD_SCRATCH.mkdir(parents=True, exist_ok=True) yield _RECORD_SCRATCH shutil.rmtree(_RECORD_SCRATCH, ignore_errors=True) @pytest.fixture def next_record_root(): """Create isolated recording directories for comparisons within one test.""" shutil.rmtree(_RECORD_3D_SCRATCH, ignore_errors=True) _RECORD_3D_SCRATCH.mkdir(parents=True, exist_ok=True) counter = itertools.count() def _next() -> Path: path = _RECORD_3D_SCRATCH / f"run{next(counter)}" path.mkdir(parents=True, exist_ok=True) return path yield _next shutil.rmtree(_RECORD_3D_SCRATCH, ignore_errors=True) @pytest.fixture def replay_recording(record_root): """One short finished recording (video, forces, frames, and metadata).""" import ui_check from orisys_app_shared.record import ExperimentRecorder, RecordConfig recorder = ExperimentRecorder( RecordConfig(video=True, fps=30.0, segment_seconds=30.0) ) recording = recorder.start( root=record_root, session_id=1, metadata={ "device": { "sensor_type": "box", "config_ref": str(record_root / "sensor.json"), "config_sha256": "recorded", }, "versions": {"sdk": "0.5.1"}, "depth": {"stored": False, "replay": "recompute"}, }, ) for index in range(60): recorder.on_packet(ui_check._fake_packet(index)) recorder.stop() yield Path(recording)