intent/tests/browser/test_smoke.py

144 lines
5.7 KiB
Python
Raw Permalink Normal View History

"""Accessibility (axe, light and dark), CSP and keyboard checks in a real browser."""
import pytest
from django.conf import settings
from django.urls import reverse
from intent.comments.services import start_feature_thread
from ..test_a11y import _pages, populated # noqa: F401 (fixture)
from .conftest import axe_violations, share_session
pytestmark = [pytest.mark.browser, pytest.mark.django_db(transaction=True)]
def _signed_out_pages(org, project, feature):
pk = {"org_slug": org.slug, "project_slug": project.slug}
return [
reverse("home"),
reverse("account_login"),
reverse("account_signup"),
reverse("orgs:detail", kwargs={"org_slug": org.slug}),
reverse("projects:home", kwargs=pk),
reverse("features:detail", kwargs={**pk, "feature_key": feature.key}),
reverse("issues:detail", kwargs={**pk, "number": 1}),
]
@pytest.mark.parametrize("scheme", ["light", "dark"])
def test_axe_signed_in(axe_page, client, live_server, owner, org, project, populated, scheme): # noqa: F811
share_session(client, axe_page, live_server, owner)
failures = {}
for path in _pages(org, project, populated):
axe_page.goto(path)
if violations := axe_violations(axe_page, scheme):
failures[path] = violations
assert not failures, "\n".join(f"{p}:\n " + "\n ".join(v) for p, v in failures.items())
@pytest.mark.parametrize("scheme", ["light", "dark"])
def test_axe_signed_out(axe_page, org, project, populated, scheme): # noqa: F811
project.visibility, project.open_contribution = "public", True
project.save()
failures = {}
for path in _signed_out_pages(org, project, populated):
axe_page.goto(path)
if violations := axe_violations(axe_page, scheme):
failures[path] = violations
assert not failures, "\n".join(f"{p}:\n " + "\n ".join(v) for p, v in failures.items())
def test_no_csp_violations_or_script_errors(page, sign_in, owner, org, project, populated): # noqa: F811
sign_in(owner)
for path in _pages(org, project, populated):
page.goto(path)
page.wait_for_load_state("networkidle")
assert not page.problems, "\n".join(page.problems)
def _wait_for_focus(page, selector, timeout_ms=3000):
# Not wait_for_function: it evaluates a string, which the page's CSP rightly forbids.
for _ in range(timeout_ms // 50):
if page.evaluate("(s) => document.activeElement.matches(s)", selector):
return
page.wait_for_timeout(50)
raise AssertionError(f"Focus never reached {selector}")
def _tab_to(page, selector, limit=80):
"""Press Tab until `selector` has focus, like a keyboard user would."""
for _ in range(limit):
page.keyboard.press("Tab")
if page.evaluate("(s) => document.activeElement.matches(s)", selector):
return
raise AssertionError(f"Couldn't reach {selector} with Tab")
def test_keyboard_only_line_comment_and_reply(page, sign_in, owner, open_project):
from intent.features.services import create_feature
feature = create_feature(open_project, owner, title="Sign in", description="One\n\nTwo\n")
thread = start_feature_thread(feature, owner, body="First")
sign_in(owner)
page.goto(
reverse(
"features:detail",
kwargs={"org_slug": "catbriar", "project_slug": "intent", "feature_key": feature.key},
)
)
# The first Tab reaches the skip link, which moves focus into the main content.
page.keyboard.press("Tab")
assert page.evaluate("document.activeElement.className") == "skip-link"
page.keyboard.press("Enter")
assert page.evaluate("document.activeElement.id") == "main"
# Open the comment form under the second paragraph and post, all from the keyboard.
_tab_to(page, "#L3-3 summary")
page.keyboard.press("Enter")
page.keyboard.press("Tab")
assert page.evaluate("document.activeElement.id") == "body-L3-3"
page.keyboard.type("About two")
page.keyboard.press("Tab")
page.keyboard.press("Enter")
page.wait_for_selector("#L3-3 >> text=About two")
# Reply to the general thread with HTMX; focus lands on the new comment.
_tab_to(page, f"#reply-{thread.pk}")
page.keyboard.type("Keyboard reply")
page.keyboard.press("Tab")
page.keyboard.press("Enter")
page.wait_for_selector("text=Keyboard reply")
_wait_for_focus(page, ".comment")
assert "Keyboard reply" in page.evaluate("document.activeElement.textContent")
assert not page.problems, "\n".join(page.problems)
def test_theme_choice_applies_and_persists(page):
page.goto(reverse("home"))
page.select_option("#theme-select", "dark")
assert page.evaluate("document.documentElement.dataset.theme") == "dark"
page.reload()
assert page.evaluate("document.documentElement.dataset.theme") == "dark"
assert page.evaluate("getComputedStyle(document.documentElement).colorScheme") == "dark"
page.select_option("#theme-select", "system")
assert page.evaluate("document.documentElement.dataset.theme") is None
def test_admin_pages_work_under_csp(page, client, live_server, verified_admin, populated): # noqa: F811
verified_admin() # Signs `client` in with a verified device; the browser borrows it.
cookie = client.cookies[settings.SESSION_COOKIE_NAME].value
page.context.add_cookies(
[{"name": settings.SESSION_COOKIE_NAME, "value": cookie, "url": live_server.url}]
)
for name in [
"admin:index",
"admin:accounts_user_changelist",
"admin:orgs_organization_add",
"admin:issues_issue_changelist",
]:
page.goto(reverse(name))
page.wait_for_load_state("networkidle")
assert "/login/" not in page.url, name
assert not page.problems, "\n".join(page.problems)