Publish curated VacatureRadar source snapshot
This commit is contained in:
commit
e29774cb7f
338 files changed
+32699
No files matched your search
@@ -0,0 +1,298 @@
|
||||
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 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 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")
|
||||
profile_body = response.content.decode("utf-8")
|
||||
assert 'class="radar-illustration"' in profile_body
|
||||
assert 'aria-hidden="true"' in profile_body
|
||||
assert "data-radar-illustration" in profile_body
|
||||
premium_css = (Path(__file__).resolve().parents[2] / "static/css/premium.css").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
app_js = (Path(__file__).resolve().parents[2] / "static/js/app.js").read_text(encoding="utf-8")
|
||||
assert "prefers-reduced-motion: reduce" in premium_css
|
||||
assert 'matchMedia("(prefers-reduced-motion: reduce)")' in app_js
|
||||
|
||||
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
|
||||
headings = page.get_by_role("heading", level=1).all_text_contents()
|
||||
assert any(expected_h1 in heading for heading in headings)
|
||||
|
||||
page.keyboard.press("Tab")
|
||||
focused_tag = page.evaluate("document.activeElement.tagName.toLowerCase()")
|
||||
assert focused_tag != "body"
|
||||
focus_indicator = page.evaluate(
|
||||
"""() => {
|
||||
const style = getComputedStyle(document.activeElement);
|
||||
return {outline: style.outlineStyle, shadow: style.boxShadow};
|
||||
}"""
|
||||
)
|
||||
assert focus_indicator["outline"] != "none" or focus_indicator["shadow"] != "none"
|
||||
|
||||
viewport_width = page.evaluate("window.innerWidth")
|
||||
document_width = page.evaluate("document.documentElement.scrollWidth")
|
||||
assert document_width <= viewport_width + 1
|
||||
|
||||
|
||||
def _take_failure_screenshot(page, tmp_path: Path, viewport_name: str) -> None:
|
||||
del tmp_path
|
||||
directory = Path(__file__).resolve().parents[2] / "artifacts" / "browser-tests"
|
||||
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": "mobile-390", "width": 390, "height": 844},
|
||||
{"name": "tablet-768", "width": 768, "height": 1024},
|
||||
{"name": "laptop-1280", "width": 1280, "height": 800},
|
||||
{"name": "desktop-1920", "width": 1920, "height": 1080},
|
||||
{"name": "wide-2560", "width": 2560, "height": 1440},
|
||||
{"name": "ultrawide-3440", "width": 3440, "height": 1200},
|
||||
],
|
||||
)
|
||||
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:
|
||||
pytest.fail("Playwright is verplicht voor de E2E- en accessibilitygate.")
|
||||
_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
|
||||
pytest.fail(f"Playwright Chromium is verplicht maar niet beschikbaar: {exc}")
|
||||
# De afzonderlijke HTML/a11y-probe is altijd uitgevoerd. Buiten de
|
||||
# Playwright-context opnieuw databasewerk doen kan Django als async zien.
|
||||
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()
|
||||
console_errors: list[str] = []
|
||||
page_errors: list[str] = []
|
||||
page.on(
|
||||
"console",
|
||||
lambda message: console_errors.append(message.text)
|
||||
if message.type == "error"
|
||||
else None,
|
||||
)
|
||||
page.on("pageerror", lambda error: page_errors.append(str(error)))
|
||||
|
||||
try:
|
||||
# Login (e-mailadres + wachtwoord, Stitch-ontwerp).
|
||||
page.goto(login_url)
|
||||
_assert_playwright_accessibility(page, expected_h1="Inloggen")
|
||||
page.get_by_label("E-mailadres").fill(user.email)
|
||||
page.get_by_role("textbox", name="Wachtwoord", exact=True).fill(
|
||||
"correct-horse-battery-staple"
|
||||
)
|
||||
page.get_by_role("button", name="Inloggen").click()
|
||||
page.wait_for_url(dashboard_url)
|
||||
_assert_playwright_accessibility(page, expected_h1="Goedemorgen")
|
||||
|
||||
# Dashboard -> filter -> detail.
|
||||
page.goto(jobs_url)
|
||||
page.get_by_label("Zoeken", exact=True).fill(job.original_title)
|
||||
page.get_by_role("button", name="Filter", exact=True).click()
|
||||
assert page.url.startswith(jobs_url)
|
||||
page.get_by_role("link", name="Volledige analyse").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(".toast.success")
|
||||
page.get_by_role("button", name="Bewaren").click()
|
||||
page.wait_for_selector(".toast.success")
|
||||
page.get_by_role("button", name="Verbergen").click()
|
||||
page.wait_for_selector(".toast.success")
|
||||
page.get_by_role("button", name="Dossier starten").click()
|
||||
page.wait_for_selector(".toast.success")
|
||||
|
||||
# Profiel aanpassen.
|
||||
page.goto(profile_list_url)
|
||||
page.get_by_role("link", name="Radar configureren").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.
|
||||
page.goto(applications_url)
|
||||
page.get_by_role("link", name="Open dossier").click()
|
||||
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.goto(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="Scan-endpoints")
|
||||
assert console_errors == []
|
||||
assert page_errors == []
|
||||
|
||||
except Exception:
|
||||
_take_failure_screenshot(page, tmp_path, viewport["name"])
|
||||
raise
|
||||
finally:
|
||||
context.close()
|
||||
browser.close()
|
||||
Reference in new issue
Block a user