gwoe-antragspruefer/tests/e2e/test_smoke_browser.py

221 lines
8.1 KiB
Python
Raw Normal View History

"""E2E-Smoketests gegen gwoe-dev.toppyr.de (Phase 16).
Nutzt den DEBUG_AUTH_TOKEN-Bypass (siehe app/auth.py:_check_debug_token,
ENV `DEBUG_AUTH_TOKEN`), damit Auth-required Pages ohne Keycloak-Login
geladen werden können.
Aktivierung lokal:
DEBUG_AUTH_TOKEN=<secret> pytest tests/e2e/test_smoke_browser.py -m e2e
Wird in CI ohne DEBUG_AUTH_TOKEN automatisch übersprungen.
Test-Coverage als Regression-Schutz für die heutigen UI-Fixes:
- Cluster-Liste rendert (vormals leere Cards: members vs drucksachen)
- Cluster-Detail wird sichtbar (vormals display:'')
- Force-Graph rendert (vormals Edge-IDs vs drucksache-IDs)
- Stimmverhalten-Tab hat Charts mit echter Größe
(vormals: ungeschlossener controls-bar div verschluckte alle Folge-Panels)
- Antrag-Detail-Marker rendern (Heuchelei + Konsistenz + Mehrheits-Bar)
- Merkliste: Add + Click-Delete entfernt Row aus DOM
"""
from __future__ import annotations
import os
import urllib.parse
import pytest
TOKEN = os.environ.get("DEBUG_AUTH_TOKEN", "").strip()
BASE = os.environ.get("E2E_BASE_URL", "https://gwoe-dev.toppyr.de").rstrip("/")
pytestmark = [
pytest.mark.e2e,
pytest.mark.skipif(not TOKEN, reason="DEBUG_AUTH_TOKEN nicht gesetzt"),
]
@pytest.fixture(scope="module")
def browser_ctx():
try:
from playwright.sync_api import sync_playwright
except ImportError:
pytest.skip("playwright nicht installiert")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
ctx = browser.new_context(
extra_http_headers={"X-Debug-Token": TOKEN},
)
yield ctx
browser.close()
@pytest.fixture
def page(browser_ctx):
p = browser_ctx.new_page()
p.on("pageerror", lambda exc: pytest.fail(f"page-error: {exc}"))
yield p
p.close()
# ─── Cluster ────────────────────────────────────────────────────────────────
def test_cluster_list_has_cards(page):
"""`/v2/cluster` zeigt mindestens 1 Cluster-Card."""
page.goto(f"{BASE}/v2/cluster", wait_until="networkidle", timeout=15000)
page.wait_for_timeout(2000)
cards = page.query_selector_all(".cluster-card")
assert len(cards) >= 1, "keine Cluster-Cards im DOM"
def test_cluster_detail_visible_after_click(page):
"""Klick auf Card → `#cluster-detail` wird sichtbar (display:block)."""
page.goto(f"{BASE}/v2/cluster", wait_until="networkidle", timeout=15000)
page.wait_for_timeout(2000)
card = page.query_selector(".cluster-card")
if not card:
pytest.skip("keine Cluster zum Klicken")
card.click()
page.wait_for_timeout(1500)
detail = page.query_selector("#cluster-detail")
assert detail and detail.is_visible(), "cluster-detail nicht sichtbar"
def test_cluster_force_graph_renders(page):
"""Force-Graph hat mindestens 1 SVG-Circle nach Card-Klick."""
page.goto(f"{BASE}/v2/cluster", wait_until="networkidle", timeout=15000)
page.wait_for_timeout(2000)
card = page.query_selector(".cluster-card")
if not card:
pytest.skip()
card.click()
page.wait_for_timeout(2500) # d3 force-sim braucht etwas
circles = page.query_selector_all("#cluster-graph svg circle")
assert len(circles) >= 2, f"Force-Graph hat nur {len(circles)} Circles"
# ─── Stimmverhalten / Auswertungen ──────────────────────────────────────────
def test_stimmverhalten_chart_has_size(page):
"""`/stimmverhalten` rendert Stimm-Index-Chart mit echter Größe.
Regression: ungeschlossener `<div class='controls-bar'>` verschluckte
alle nachfolgenden Panels, Charts hatten 0×0 Bounding-Box.
"""
page.goto(f"{BASE}/stimmverhalten", wait_until="networkidle", timeout=15000)
page.wait_for_timeout(3000)
info = page.evaluate(
"() => {"
" const el = document.getElementById('sv-index-chart');"
" if (!el) return null;"
" const r = el.getBoundingClientRect();"
" return {w: r.width, h: r.height};"
"}"
)
assert info is not None, "sv-index-chart nicht im DOM"
assert info["w"] > 100 and info["h"] > 100, (
f"Chart hat zu kleine Größe: {info}"
)
def test_score_histogram_has_buckets(page):
"""`/auswertungen` Tab Score-Verteilung rendert Histogram-Canvas."""
page.goto(f"{BASE}/auswertungen", wait_until="networkidle", timeout=15000)
page.wait_for_timeout(2000)
btn = page.query_selector(".auswert-tab[onclick*=\"'histogram'\"]")
if not btn:
pytest.skip("histogram-tab nicht gefunden")
btn.click()
page.wait_for_timeout(2500)
info = page.evaluate(
"() => { const el = document.getElementById('hist-chart');"
" return el ? el.getBoundingClientRect() : null; }"
)
assert info and info["width"] > 100 and info["height"] > 50
# ─── Antrag-Detail ──────────────────────────────────────────────────────────
def test_antrag_detail_has_markers(page):
"""Heuchelei-⚠ und Konsistenz-Block auf einem bekannten Vote-Antrag."""
page.goto(
f"{BASE}/antrag/18/18089?bundesland=NRW",
wait_until="networkidle", timeout=15000,
)
page.wait_for_timeout(2000)
body = page.content()
assert "stimmte mit Nein" in body or "stimmte mit Ja" in body, (
"weder Heuchelei- noch Opportunismus-Marker rendert"
)
assert "Mehrheit deckt sich" in body or "Mehrheit kontra" in body, (
"Konsistenz-Block fehlt"
)
assert "Fraktionen Ja" in body, "Mehrheits-Bar fehlt"
def test_antrag_matrix_coloring(page):
"""Matrix-Zelle mit rating>=4 hat m-pp-Klasse (kräftiges Grün), nicht m-p."""
page.goto(
f"{BASE}/antrag/18/18246?bundesland=NRW",
wait_until="networkidle", timeout=15000,
)
page.wait_for_timeout(1500)
pp_count = page.evaluate(
"() => document.querySelectorAll('.v2-matrix-mini .m-pp').length"
)
assert pp_count >= 1, "keine m-pp-Klasse — rating-Shift-Bug zurück?"
# ─── Merkliste ──────────────────────────────────────────────────────────────
def _get_csrf_or_token_header():
return {"X-Debug-Token": TOKEN}
def test_merkliste_add_and_delete(browser_ctx, page):
"""End-to-End: Eintrag via API anlegen, dann via UI-Klick entfernen."""
import requests
headers = {"X-Debug-Token": TOKEN, "Content-Type": "application/json"}
# Cleanup: bestehenden Eintrag löschen (idempotent)
requests.delete(
f"{BASE}/api/me/merkliste/{urllib.parse.quote('18/18089', safe='')}",
headers=headers, timeout=10,
)
# Add
r = requests.post(
f"{BASE}/api/me/merkliste",
json={"antrag_id": "18/18089"},
headers=headers, timeout=10,
)
assert r.status_code == 200, f"add fehlgeschlagen: {r.status_code} {r.text}"
page.goto(f"{BASE}/v2/merkliste", wait_until="networkidle", timeout=15000)
page.wait_for_timeout(2500)
pre = page.evaluate(
"() => document.querySelectorAll('.merkliste-entry').length"
)
assert pre >= 1, "Eintrag nicht in Merkliste"
btn = page.query_selector('button[onclick*="merkliste_remove"]')
assert btn, "kein Lösch-Button"
btn.click()
page.wait_for_timeout(2000)
post = page.evaluate(
"() => document.querySelectorAll('.merkliste-entry').length"
)
assert post == pre - 1, f"Row nicht entfernt: pre={pre} post={post}"
# ─── Aktuelle Themen ────────────────────────────────────────────────────────
def test_aktuelle_themen_5_tabs(page):
"""`/aktuelle-themen` rendert 5 Tabs."""
page.goto(f"{BASE}/aktuelle-themen", wait_until="networkidle", timeout=15000)
page.wait_for_timeout(2000)
tabs = page.query_selector_all(".at-tab")
assert len(tabs) >= 5, f"erwartet ≥5 Tabs, gefunden {len(tabs)}"