"""Fixtures for Playwright frontend E2E tests. Architecture: - Each test gets an isolated temp SQLite DB. - A FastAPI uvicorn server runs on a random port in a background thread. - CANTEEN_SKIP_AUTH=1 skips auth middleware so Playwright doesn't need real tokens. - Playwright launches system Google Chrome (Ubuntu 26.04 can't install bundled browsers, and Chrome 148 SIGTRAPs with certain --disable-features flags; ignore_default_args workaround applied). - Viewport: iPhone 14 (390x844), Geolocation: Orlando, FL. """ # Chrome 148 on Ubuntu 26.04 (kernel 7.0) SIGTRAPs when Playwright's default # --disable-features and related flags are passed. Ignoring these defaults # allows Chrome to launch cleanly with DevTools protocol. CHROME_IGNORE_DEFAULTS = [ '--disable-field-trial-config', '--disable-background-networking', '--disable-background-timer-throttling', '--disable-breakpad', '--disable-client-side-phishing-detection', '--disable-default-apps', '--disable-dev-shm-usage', '--disable-extensions', '--disable-hang-monitor', '--disable-ipc-flooding-protection', '--disable-popup-blocking', '--disable-prompt-on-repost', '--disable-renderer-backgrounding', '--disable-sync', '--enable-automation', ] import importlib import os import socket import sys import tempfile import threading import time from pathlib import Path import pytest import uvicorn from playwright.sync_api import sync_playwright # Ensure project root is on path PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(PROJECT_ROOT)) # Global env: skip auth for all tests os.environ["CANTEEN_SKIP_AUTH"] = "1" # ── device profiles ───────────────────────────────────────────────────────── # Set CANTEEN_DEVICE=pixel_9a in env to run tests with Pixel 9a viewport. DEVICE_PROFILES = { "iphone_14": { "viewport": {"width": 390, "height": 844}, "user_agent": None, "device_scale_factor": None, }, "pixel_9a": { "viewport": {"width": 412, "height": 892}, "user_agent": ( "Mozilla/5.0 (Linux; Android 15; Pixel 9a) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/130.0.6723.58 Mobile Safari/537.36" ), "device_scale_factor": 2.625, }, } def get_device_config() -> tuple: """Return device config dict + name based on CANTEEN_DEVICE env var.""" device = os.environ.get("CANTEEN_DEVICE", "iphone_14").lower().replace("-", "_") if device in DEVICE_PROFILES: return DEVICE_PROFILES[device], device print(f"⚠ Unknown device '{device}', falling back to iphone_14") return DEVICE_PROFILES["iphone_14"], "iphone_14" # ── helpers ──────────────────────────────────────────────────────────────── def _find_free_port() -> int: """Find an available TCP port.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("", 0)) return s.getsockname()[1] # ── fixtures ──────────────────────────────────────────────────────────────── @pytest.fixture(scope="session") def browser(): """Launch system Chromium once per test session.""" pw = sync_playwright().start() browser = pw.chromium.launch( executable_path="/usr/bin/google-chrome-stable", headless=True, args=["--no-sandbox", "--disable-gpu"], ignore_default_args=CHROME_IGNORE_DEFAULTS, ) yield browser browser.close() pw.stop() @pytest.fixture def test_db_path(): """Create an isolated temp SQLite DB for each test.""" fd, path = tempfile.mkstemp(suffix=".db", prefix="canteen_frontend_test_") os.close(fd) os.environ["CANTEEN_DB_PATH"] = path yield path # Cleanup DB and WAL/SHM/journal files for suffix in ("", "-shm", "-wal", "-journal"): p = Path(path + suffix) if p.exists(): p.unlink() @pytest.fixture def live_server(test_db_path): """Start FastAPI + uvicorn on a random port in a background thread. Returns the base URL (e.g. 'http://127.0.0.1:12345'). """ port = _find_free_port() os.environ["CANTEEN_PORT"] = str(port) # Reload the server module so DB_PATH picks up the current # CANTEEN_DB_PATH (module-level constant read at import time). import server importlib.reload(server) app = server.app t = threading.Thread( target=uvicorn.run, kwargs={ "app": "server:app", "host": "127.0.0.1", "port": port, "log_level": "error", }, daemon=True, ) t.start() base_url = f"http://127.0.0.1:{port}" # Wait for server to be ready deadline = time.time() + 10 import urllib.request while time.time() < deadline: try: urllib.request.urlopen(f"{base_url}/", timeout=1) break except Exception: time.sleep(0.1) else: raise RuntimeError(f"Server did not start on {base_url} within 10s") yield base_url # Thread is daemon, will exit when test process ends @pytest.fixture def page(browser, live_server): """Create a Playwright page pointed at the live server. Mobile viewport driven by CANTEEN_DEVICE env var (default: iphone_14). Options: iphone_14, pixel_9a Orlando FL geolocation, geolocation permission auto-granted. """ device_config, device_name = get_device_config() context = browser.new_context( viewport=device_config["viewport"], user_agent=device_config["user_agent"], device_scale_factor=device_config["device_scale_factor"], geolocation={"latitude": 28.3852, "longitude": -81.5639}, permissions=["geolocation"], ) page = context.new_page() page.goto(live_server) yield page context.close()