@@ -0,0 +1,251 @@
|
||||
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"
|
||||
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
response = client.get(reverse("sources:list"))
|
||||
assert response.status_code == 200
|
||||
_assert_basic_html_accessibility(response.content, page_name="sources")
|
||||
|
||||
|
||||
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="Vandaag")
|
||||
|
||||
# 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="Filter").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="Profiel").click()
|
||||
page.wait_for_url(profile_list_url)
|
||||
page.get_by_role("link", name="Bewerken").click()
|
||||
page.get_by_label("Gewenste functietitels").fill(
|
||||
"Infrastructure Engineer\nSysteembeheerder"
|
||||
)
|
||||
page.get_by_role("button", name="Profiel 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="Opslaan").click()
|
||||
page.wait_for_url(applications_url)
|
||||
|
||||
# Handmatige import via plaktekst.
|
||||
page.get_by_role("link", name="Brongezondheid").click()
|
||||
page.wait_for_url(sources_url)
|
||||
page.locator("textarea[name='pasted_text']").fill(
|
||||
"Vacaturetitel: Infrastructure Engineer\n"
|
||||
"Werkgever: Example IT\n"
|
||||
"Locatie: 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="Brongezondheid")
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user