import os import subprocess import time from typing import Generator import pytest from playwright.sync_api import Browser, Page, Playwright, sync_playwright import httpx # Use a different port for the test server to avoid conflicts TEST_PORT = 8001 BASE_URL = f"http://localhost:{TEST_PORT}" @pytest.fixture(scope="session", autouse=True) def live_server() -> Generator[str, None, None]: """Launch a live test server in a separate process.""" process = subprocess.Popen( [ "uvicorn", "main:app", "--host", "127.0.0.1", f"--port={TEST_PORT}", ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=os.environ.copy(), ) deadline = time.perf_counter() + 30 last_error: Exception | None = None while time.perf_counter() < deadline: if process.poll() is not None: raise RuntimeError("uvicorn server exited before becoming ready") try: response = httpx.get(BASE_URL, timeout=1.0) if response.status_code < 500: break except Exception as exc: # noqa: BLE001 last_error = exc time.sleep(0.5) else: process.terminate() process.wait(timeout=5) raise TimeoutError( "Timed out waiting for uvicorn test server to start" ) from last_error try: yield BASE_URL finally: if process.poll() is None: process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() process.wait(timeout=5) @pytest.fixture(scope="session") def playwright_instance() -> Generator[Playwright, None, None]: """Provide a Playwright instance for the test session.""" with sync_playwright() as p: yield p @pytest.fixture(scope="session") def browser( playwright_instance: Playwright, ) -> Generator[Browser, None, None]: """Provide a browser instance for the test session.""" browser = playwright_instance.chromium.launch() yield browser browser.close() @pytest.fixture() def page(browser: Browser, live_server: str) -> Generator[Page, None, None]: """Provide a new page for each test.""" page = browser.new_page(base_url=live_server) page.goto("/") page.wait_for_load_state("networkidle") yield page page.close()