"""Shared fixtures for operator-GUI tests. The browser fixtures lazily import Playwright (via ``importorskip``) and skip when Chromium is unavailable, so the non-browser static tests in this package still collect and run on a host without Playwright installed. """ from __future__ import annotations import sys from dataclasses import dataclass from pathlib import Path from threading import Thread from typing import Any, Iterator import pytest ROOT = Path(__file__).resolve().parents[2] if str(ROOT / "src") not in sys.path: sys.path.insert(0, str(ROOT / "src")) APP_DIR = ROOT / "web" / "operator-gui" ARTIFACT_DIR = ROOT / "artifacts" / "operator-gui-e2e" DEFAULT_VIEWPORT = {"width": 1280, "height": 900} @dataclass class OperatorServer: """A running operator server plus the paths a spec may need to assert on.""" base_url: str image_root: Path store: Any @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): # noqa: ANN001 - pytest hook signature """Stash each phase's report so fixtures can screenshot on failure.""" outcome = yield setattr(item, f"rep_{outcome.get_result().when}", outcome.get_result()) @pytest.fixture def operator_server(tmp_path: Path) -> Iterator[OperatorServer]: """Build and serve the operator app against a fresh, isolated SQLite store.""" from rights_filter.server.http_app import build_server from rights_filter.server.image_store import LocalSubmissionImageStore from rights_filter.server.sqlite_store import CopyrighterStore image_root = tmp_path / "submissions" image_root.mkdir() store = CopyrighterStore(tmp_path / "copyrighter.sqlite3") store.initialize() server = build_server( host="127.0.0.1", port=0, store=store, image_store=LocalSubmissionImageStore(image_root), static_dir=APP_DIR, ) thread = Thread(target=server.serve_forever, daemon=True) thread.start() try: yield OperatorServer( base_url=f"http://127.0.0.1:{server.server_port}", image_root=image_root, store=store, ) finally: server.shutdown() thread.join(timeout=5) @pytest.fixture def browser() -> Iterator[Any]: """Launch headless Chromium, skipping cleanly when it is unavailable.""" playwright_api = pytest.importorskip("playwright.sync_api") with playwright_api.sync_playwright() as pw: try: launched = pw.chromium.launch(headless=True) except Exception as exc: # missing browser binaries, sandbox, etc. pytest.skip(f"Playwright Chromium is not available: {exc}") try: yield launched finally: launched.close() @pytest.fixture def operator_page(browser: Any, request: pytest.FixtureRequest) -> Iterator[Any]: """A fresh page at the default viewport; screenshots to artifacts on failure.""" page = browser.new_page(viewport=DEFAULT_VIEWPORT) try: yield page finally: report = getattr(request.node, "rep_call", None) if report is not None and report.failed: ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) safe_name = request.node.name.replace("/", "_").replace("::", "_")[:120] try: page.screenshot(path=str(ARTIFACT_DIR / f"{safe_name}.png"), full_page=True) except Exception: pass # screenshot is best-effort diagnostics, never fail teardown page.close()