221 lines
8.1 KiB
Python
221 lines
8.1 KiB
Python
|
|
"""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)}"
|