"""E2E UI tests with Playwright/Chromium (#120). Run: pytest tests/e2e/ -m e2e --headed (with browser) pytest tests/e2e/ -m e2e (headless) Requires: pip install playwright && playwright install chromium Target: https://gwoe.toppyr.de (live) """ import pytest BASE_URL = "https://gwoe.toppyr.de" pytestmark = pytest.mark.e2e @pytest.fixture(scope="module") def browser(): from playwright.sync_api import sync_playwright pw = sync_playwright().start() br = pw.chromium.launch(headless=True) yield br br.close() pw.stop() @pytest.fixture def page(browser): p = browser.new_page() yield p p.close() # ─── Page Load ──────────────────────────────────────────────────────── def test_main_page_loads(page): page.goto(BASE_URL) page.wait_for_timeout(3000) assert page.title() == "GWÖ-Antragsprüfer" def test_assessments_render(page): page.goto(BASE_URL) page.wait_for_timeout(3000) items = page.locator(".list-item").count() assert items >= 1, "Keine Assessments in der Liste" def test_no_js_errors(page): errors = [] page.on("pageerror", lambda err: errors.append(err.message)) page.goto(BASE_URL) page.wait_for_timeout(3000) assert not errors, f"JS-Fehler: {errors}" # ─── Navigation ────────────────────────────────────────────────────── def test_detail_loads_on_click(page): page.goto(BASE_URL) page.wait_for_timeout(3000) first = page.locator(".list-item").first first.click() page.wait_for_timeout(2000) detail = page.locator("#detail-panel").inner_text() assert len(detail) > 50, "Detail-Panel leer nach Klick" def test_keyboard_navigation(page): page.goto(BASE_URL) page.wait_for_timeout(3000) # j selects first item page.keyboard.press("j") active = page.locator(".list-item.active").count() assert active == 1, "j-Taste markiert kein Item" # k goes back (stays on first) page.keyboard.press("k") assert page.locator(".list-item.active").count() == 1 def test_search(page): page.goto(BASE_URL) page.wait_for_timeout(3000) search = page.locator("#search-input") search.fill("Bildung") page.wait_for_timeout(1000) # Should still show items (or search results) items = page.locator(".list-item").count() assert items >= 0 # search may return 0, but shouldn't error # ─── Subpages ──────────────────────────────────────────────────────── def test_impressum_page(page): page.goto(f"{BASE_URL}/impressum") page.wait_for_timeout(1000) assert "Angaben" in page.content() def test_datenschutz_page(page): page.goto(f"{BASE_URL}/datenschutz") page.wait_for_timeout(1000) assert "Datenschutz" in page.content() def test_methodik_page(page): page.goto(f"{BASE_URL}/methodik") page.wait_for_timeout(1000) assert "Methodik" in page.content() def test_auswertungen_page(page): errors = [] page.on("pageerror", lambda err: errors.append(err.message)) page.goto(f"{BASE_URL}/auswertungen") page.wait_for_timeout(2000) assert not errors, f"JS-Fehler auf Auswertungen: {errors}" def test_quellen_page(page): page.goto(f"{BASE_URL}/quellen") page.wait_for_timeout(1000) assert "Wahlprogramm" in page.content() or "Quellen" in page.content() # ─── API ───────────────────────────────────────────────────────────── def test_api_assessments(page): resp = page.request.get(f"{BASE_URL}/api/assessments") assert resp.ok data = resp.json() assert isinstance(data, list) assert len(data) > 0 # Lightweight: should NOT contain gwoeMatrix (memory optimization) assert "gwoeMatrix" not in data[0], "List API should not contain detail fields" def test_api_assessment_detail(page): list_resp = page.request.get(f"{BASE_URL}/api/assessments") first = list_resp.json()[0] ds = first["drucksache"] resp = page.request.get(f"{BASE_URL}/api/assessment?drucksache={ds}") assert resp.ok detail = resp.json() assert "gwoeMatrix" in detail, "Detail API should contain gwoeMatrix" assert "gwoeBegründung" in detail def test_api_export_json(page): resp = page.request.get(f"{BASE_URL}/api/auswertungen/export.json") assert resp.ok data = resp.json() assert "meta" in data assert data["meta"]["license"] == "CC BY 4.0" assert len(data["assessments"]) > 0 def test_api_votes(page): resp = page.request.get(f"{BASE_URL}/api/votes?drucksache=18/8125") assert resp.ok data = resp.json() assert "counts" in data assert "my_votes" in data def test_api_health(page): resp = page.request.get(f"{BASE_URL}/health") assert resp.ok assert resp.json()["status"] == "ok" # ─── Dark Mode ─────────────────────────────────────────────────────── def test_dark_mode_toggle(page): page.goto(BASE_URL) page.wait_for_timeout(2000) # Initially light theme = page.evaluate("document.documentElement.getAttribute('data-theme')") assert theme != "dark" # Toggle via JS (simulates button) page.evaluate("document.documentElement.setAttribute('data-theme', 'dark')") theme = page.evaluate("document.documentElement.getAttribute('data-theme')") assert theme == "dark" # ─── Accessibility ─────────────────────────────────────────────────── def test_search_has_aria_label(page): page.goto(BASE_URL) page.wait_for_timeout(2000) label = page.locator("#search-input").get_attribute("aria-label") assert label, "Suchfeld hat kein aria-label" def test_hamburger_has_aria_label(page): page.goto(BASE_URL) page.wait_for_timeout(2000) btn = page.locator("button[aria-label='Menü öffnen']") assert btn.count() == 1, "Hamburger-Button hat kein aria-label" def test_focus_visible_indicator(page): page.goto(BASE_URL) page.wait_for_timeout(2000) # Check that :focus-visible style exists has_focus = page.evaluate("""() => { const rules = [...document.styleSheets[0].cssRules]; return rules.some(r => r.selectorText && r.selectorText.includes('focus-visible')); }""") assert has_focus, "Kein :focus-visible CSS-Regel gefunden"