gwoe-antragspruefer/tests/e2e/test_smoke_browser.py
Dotty Dotter f6220b52e0 test(Phase 16): 9 E2E-Smoketests gegen gwoe-dev mit DEBUG_AUTH_TOKEN
Regression-Schutz fuer die heutigen UI-Fixes:
- test_cluster_list_has_cards (#56c68d3 — drucksachen vs members)
- test_cluster_detail_visible_after_click (#d72a6f3 — display:'')
- test_cluster_force_graph_renders (#60db39d — Edge-IDs)
- test_stimmverhalten_chart_has_size (#cb6971f — controls-bar-leak)
- test_score_histogram_has_buckets
- test_antrag_detail_has_markers (Heuchelei + Konsistenz + Mehrheits-Bar)
- test_antrag_matrix_coloring (#ee93fcd — rating-Shift)
- test_merkliste_add_and_delete (#c599e5f + #0394803 — onclick-quoting + ID-Lookup)
- test_aktuelle_themen_5_tabs

Aktivierung: DEBUG_AUTH_TOKEN gesetzt + pytest -m e2e.
Lokaler Run: 9/9 passed in 52s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:50:40 +02:00

221 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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