101 lines
3 KiB
Python
101 lines
3 KiB
Python
|
|
"""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
|