"""Real-browser tests: Playwright drives Chromium against Django's live server. Needs the browser once per machine: `uv run playwright install chromium`. Without it these tests skip, so the rest of the suite still runs anywhere. """ import os from pathlib import Path import pytest from django.conf import settings # Playwright's sync API runs an event loop; the ORM calls in fixtures are still synchronous. os.environ.setdefault("DJANGO_ALLOW_ASYNC_UNSAFE", "true") from playwright.sync_api import Error as PlaywrightError # noqa: E402 from playwright.sync_api import sync_playwright # noqa: E402 AXE = (Path(__file__).parent.parent / "vendor" / "axe.min.js").read_text() @pytest.fixture(scope="session") def browser(): with sync_playwright() as p: try: browser = p.chromium.launch() except PlaywrightError as exc: pytest.skip(f"Chromium isn't installed (uv run playwright install chromium): {exc}") yield browser browser.close() @pytest.fixture def page(browser, live_server): """A fresh page. `page.problems` collects script errors and CSP violations. The CSP is enforced, except on `axe_page`, where axe has to be injected inline. """ context = browser.new_context(base_url=live_server.url) page = context.new_page() page.problems = [] page.on("pageerror", lambda e: page.problems.append(f"script error: {e}")) page.on( "console", lambda m: ( page.problems.append(f"console: {m.text}") if m.type == "error" and "Content Security Policy" in m.text else None ), ) yield page context.close() @pytest.fixture def axe_page(browser, live_server): context = browser.new_context(base_url=live_server.url, bypass_csp=True) page = context.new_page() yield page context.close() def share_session(client, page, live_server, user): client.force_login(user) page.context.add_cookies( [ { "name": settings.SESSION_COOKIE_NAME, "value": client.cookies[settings.SESSION_COOKIE_NAME].value, "url": live_server.url, } ] ) @pytest.fixture def sign_in(client, page, live_server): """Sign the browser in as `user` by sharing a test-client session.""" def as_user(user): share_session(client, page, live_server, user) return as_user def axe_violations(page, scheme: str) -> list[str]: """Run axe on the current page with WCAG 2.1 A and AA rules.""" page.emulate_media(color_scheme=scheme) page.add_script_tag(content=AXE) result = page.evaluate( """async () => { const r = await axe.run(document, { runOnly: {type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]}, }); return r.violations.map(v => `${v.id} (${v.impact}): ${v.help} — ${v.nodes.slice(0, 3) .map(n => n.target.join(" ")).join(", ")}`); }""" ) return result