intent/tests/browser/conftest.py

101 lines
3 KiB
Python
Raw Permalink Normal View History

"""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