gwoe-antragspruefer/tests/e2e/test_ui.py

211 lines
6.7 KiB
Python
Raw Normal View History

"""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"