Files
VacatureRadar/tests/e2e/test_vr114_browser_and_accessibility.py
T
2026-07-22 05:12:07 +02:00

265 lines
11 KiB
Python

from __future__ import annotations
from datetime import timedelta
from decimal import Decimal
from pathlib import Path
import pytest
from bs4 import BeautifulSoup
from django.urls import reverse
from django.utils import timezone
from apps.jobs.models import Application, Feedback, ScoreRun
try:
from playwright.sync_api import sync_playwright
except Exception: # pragma: no cover - optional dependency
sync_playwright = None
def _seed_dashboard_score(profile, job) -> None:
ScoreRun.objects.create(
job=job,
profile=profile,
profile_version=profile.version,
score=Decimal("88.2"),
confidence=Decimal("0.76"),
recommendation=ScoreRun.Recommendation.STRONG,
components={"content": 42.0, "skills": 23.0, "location": 23.2},
positives=["Relevante locatie", "Ervaring met ops"],
concerns=[],
hard_exclusions=[],
evidence={"tests": ["vr-114"]},
model_version="vr-114",
prompt_version="1.0.0",
)
def _assert_basic_html_accessibility(payload: bytes | str, *, page_name: str) -> None:
html = payload.decode("utf-8", errors="replace") if isinstance(payload, bytes) else payload
soup = BeautifulSoup(html, "html.parser")
headings = soup.find_all("h1")
assert headings, f"{page_name}: ontbreekt h1"
assert len(headings) == 1, f"{page_name}: verwacht exact één h1, gevonden {len(headings)}"
assert soup.html is not None
assert soup.html.get("lang"), f"{page_name}: <html> mist lang-attribuut"
for field in soup.find_all(["input", "textarea", "select"]):
if field.get("type") == "hidden" or field.get("name") == "csrfmiddlewaretoken":
continue
field_id = field.get("id")
has_label = bool(field.find_parent("label"))
has_label = has_label or bool(field.get("aria-label") or field.get("aria-labelledby"))
has_label = has_label or (
field_id is not None and bool(soup.find("label", attrs={"for": field_id}))
)
assert has_label, (
f"{page_name}: formulierveld zonder label of aria-voorziening: "
f"{field.get('name') or field.get('type')}"
)
for button in soup.find_all("button"):
label = button.get_text(" ", strip=True) or button.get("aria-label")
assert label, f"{page_name}: knop zonder tekst of aria-label"
for element in soup.select("script[src], link[rel='stylesheet'][href]"):
asset_url = element.get("src") or element.get("href") or ""
assert not asset_url.startswith(("http://", "https://")), (
f"{page_name}: externe productie-asset gevonden: {asset_url}"
)
def _run_html_accessibility_probe(client, user, profile, job) -> None:
_seed_dashboard_score(profile=profile, job=job)
response = client.get(reverse("login"))
assert response.status_code == 200
_assert_basic_html_accessibility(response.content, page_name="login")
client.force_login(user)
response = client.get(reverse("dashboard:today"))
assert response.status_code == 200
_assert_basic_html_accessibility(response.content, page_name="dashboard")
assert "Zoek door vacatures" in response.content.decode("utf-8")
response = client.get(reverse("jobs:list"), {"q": job.original_title})
assert response.status_code == 200
_assert_basic_html_accessibility(response.content, page_name="jobs-list")
jobs_body = response.content.decode("utf-8")
assert "1 vacatures gevonden" in jobs_body
assert "88%" in jobs_body
response = client.get(reverse("jobs:detail", kwargs={"pk": job.pk}))
assert response.status_code == 200
_assert_basic_html_accessibility(response.content, page_name="jobs-detail")
response = client.get(reverse("profiles:list"))
assert response.status_code == 200
_assert_basic_html_accessibility(response.content, page_name="profiles-list")
response = client.get(reverse("profiles:edit", kwargs={"pk": profile.pk}))
assert response.status_code == 200
_assert_basic_html_accessibility(response.content, page_name="profiles-edit")
response = client.get(reverse("jobs:applications"))
assert response.status_code == 200
_assert_basic_html_accessibility(response.content, page_name="applications")
assert "Sollicitatiepipeline" in response.content.decode("utf-8")
response = client.get(reverse("sources:list"))
assert response.status_code == 200
_assert_basic_html_accessibility(response.content, page_name="sources")
response = client.get(reverse("dashboard:system"))
assert response.status_code == 200
_assert_basic_html_accessibility(response.content, page_name="system")
def _assert_playwright_accessibility(page, *, expected_h1: str) -> None:
page.wait_for_load_state("networkidle")
assert page.get_by_role("heading", level=1).count() == 1
assert expected_h1 in page.get_by_role("heading", level=1).all_text_contents()
page.keyboard.press("Tab")
focused_tag = page.evaluate("document.activeElement.tagName.toLowerCase()")
assert focused_tag != "body"
outline_style = page.evaluate("getComputedStyle(document.activeElement).outlineStyle")
assert outline_style != "none"
def _take_failure_screenshot(page, tmp_path: Path, viewport_name: str) -> None:
directory = tmp_path / "artifacts"
directory.mkdir(parents=True, exist_ok=True)
page.screenshot(path=str(directory / f"vr114_e2e_failure_{viewport_name}.png"), full_page=True)
@pytest.mark.integration
@pytest.mark.django_db
def test_vr114_html_a11y_probe(client, user, profile, job):
_run_html_accessibility_probe(client, user, profile, job)
@pytest.mark.integration
@pytest.mark.django_db
@pytest.mark.parametrize(
"viewport",
[
{"name": "desktop", "width": 1366, "height": 900},
{"name": "mobile-360", "width": 360, "height": 780},
],
)
def test_vr114_browser_flow(viewport, live_server, client, tmp_path, user, profile, job):
_seed_dashboard_score(profile=profile, job=job)
if sync_playwright is None:
_run_html_accessibility_probe(client, user, profile, job)
pytest.skip("Playwright niet geïnstalleerd. Vult alleen de HTML/a11y-probe in.")
base = live_server.url
dashboard_url = f"{base}{reverse('dashboard:today')}"
login_url = f"{base}{reverse('login')}"
jobs_url = f"{base}{reverse('jobs:list')}"
detail_url = f"{base}{reverse('jobs:detail', kwargs={'pk': job.pk})}"
profile_list_url = f"{base}{reverse('profiles:list')}"
applications_url = f"{base}{reverse('jobs:applications')}"
sources_url = f"{base}{reverse('sources:list')}"
with sync_playwright() as playwright:
try:
browser = playwright.chromium.launch(headless=True)
except Exception as exc: # pragma: no cover - env-dependent
_run_html_accessibility_probe(client, user, profile, job)
pytest.skip(f"Playwright-browser niet beschikbaar: {exc}")
context = browser.new_context(
viewport={"width": viewport["width"], "height": viewport["height"]},
reduced_motion="reduce",
)
page = context.new_page()
try:
# Login.
page.goto(login_url)
_assert_playwright_accessibility(page, expected_h1="Welkom terug")
page.get_by_label("Gebruikersnaam").fill(user.username)
page.get_by_label("Wachtwoord").fill("correct-horse-battery-staple")
page.get_by_role("button", name="Aanmelden").click()
page.wait_for_url(dashboard_url)
_assert_playwright_accessibility(page, expected_h1="Goedemorgen")
# Dashboard -> filter -> detail.
page.get_by_role("link", name="Vacatures").click()
page.get_by_role("textbox", name="Zoeken").fill(job.original_title)
page.get_by_role("button", name="Filters toepassen").click()
assert page.url.startswith(jobs_url)
page.get_by_role("link", name=job.original_title).first.click()
page.wait_for_url(detail_url)
_assert_playwright_accessibility(page, expected_h1=job.original_title)
# Vier feedbackacties.
page.get_by_role("button", name="Interessant").click()
page.wait_for_selector(".message.success")
page.get_by_role("button", name="Bewaren").click()
page.wait_for_selector(".message.success")
page.get_by_label("Reden (optioneel)").select_option("Te ver")
page.get_by_role("button", name="Verbergen").click()
page.wait_for_selector(".message.success")
page.get_by_role("button", name="Gesolliciteerd").click()
page.wait_for_selector(".message.success")
# Profiel aanpassen.
page.get_by_role("link", name="Zoekprofiel").click()
page.wait_for_url(profile_list_url)
page.get_by_role("link", name="Profiel bewerken").click()
page.get_by_label("Infrastructure engineer").check()
page.get_by_label("System engineer").check()
page.get_by_role("button", name="Instellingen opslaan").click()
page.wait_for_url(profile_list_url)
# Sollicitatie dossier openen en bijwerken.
application = Application.objects.filter(user=user, job=job).first()
assert application is not None
page.get_by_role("link", name="Sollicitaties").click()
page.wait_for_url(applications_url)
page.goto(f"{base}{reverse('jobs:application-edit', kwargs={'pk': application.pk})}")
page.locator("select[name='status']").select_option("interview")
page.locator("input[name='contact_name']").fill("Sofie Janssens")
page.locator("input[name='contact_email']").fill("sofia@example.invalid")
page.locator("input[name='follow_up_date']").fill(
(timezone.localdate() + timedelta(days=7)).isoformat()
)
page.locator("textarea[name='notes']").fill("Eerste follow-up ingepland.")
page.get_by_role("button", name="Dossier opslaan").click()
page.wait_for_url(applications_url)
# Handmatige import via plaktekst.
page.get_by_role("link", name="Bronnen").click()
page.wait_for_url(sources_url)
page.locator(".import-disclosure > summary").click()
page.locator("textarea[name='pasted_text']").fill(
"Vacaturetitel: Infrastructure Engineer\nWerkgever: Example IT\nLocatie: Hasselt"
)
page.get_by_role("button", name="Import uitvoeren").click()
page.wait_for_selector("text=Importresultaat")
assert page.get_by_text("Modus: paste").is_visible()
assert page.get_by_text("Herkenbare items").is_visible()
# Keyboardvolgorde en focuseigenschappen controleren.
page.keyboard.press("Tab")
page.keyboard.press("Tab")
page.keyboard.press("Tab")
_assert_playwright_accessibility(page, expected_h1="Bronnen")
assert Feedback.objects.filter(user=user, job=job).count() >= 4
except Exception:
_take_failure_screenshot(page, tmp_path, viewport["name"])
raise
finally:
context.close()
browser.close()