feat(b17): Stufe 4 banner-tour + Stufe 5 annotierte Screenshots + V2-default
Stufe 4 — Cookie-Banner-Tour vor dem Accept-Klick:
- audit_walk_banner_tour.tour_cookie_banner(): öffnet Settings
(16 Phrase-Varianten), scrollt vertikal, aktiviert jedes
[role=tab], expandet jedes [aria-expanded=false] / details /
summary + 14 CMP-spezifische Selektoren. Max 35 Klicks,
Best-Effort.
- audit_walk_recorder ruft tour_cookie_banner() VOR
_try_accept_banner auf — Reviewer sieht den vollen Consent-
Katalog im Video (Vendor-Liste, Kategorien, Zwecke).
- Recorder unter 500 LOC (412+155 split).
Stufe 5 — Annotierte Screenshots pro Finding:
- finding_annotator.annotate_url(): WebKit headless, JS-Inject
eines rot-banner-Labels oben + roter Outline um das Element
(Selector oder Text-Match).
- finding_annotator.annotate_findings(): dispatched 3 Cases —
B1 Tap-Target (Anchor markiert mit "Tap-Target X×Y px"),
B16 URL-Slug-Drift (404-Seite mit "/<slug> 404"),
B13 Widerruf (Footer markiert "Widerruf-Link fehlt").
- routes_audit_walk.POST /annotate-findings (consent-tester).
- _b17_wiring ruft annotate-findings nach record_audit_walk und
speichert annotations in walk.annotations.
- audit_walk_zip_builder packt PNGs nach findings/<name>.png ins
ZIP — Reviewer hat Beweis-Bilder im Postfach.
Plausibility Circuit-Breaker:
- Nach 6 consecutive empty batches (PLAUSIBILITY_EMPTY_BUDGET=6)
bricht die ganze Phase ab statt 200 Calls zu warten. Fix für
qwen3-down + große DSE-Sites (BMW: ohne Breaker 21min, mit
Breaker ~3min).
audit_walk_zip_builder fängt walk.annotations ab und legt sie unter
findings/<fname>.png im ZIP-Anhang ab.
V2-Default:
- docker-compose.yml backend-compliance.environment.MAIL_RENDER_V2:
default 'true'. Ohne diesen Override liefert die Engine
weiterhin das alte Legacy-Mail-Layout, in dem die B-Wiring-
Blöcke nicht sichtbar sind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
"""Cookie-Banner-Tour vor dem Accept-Klick.
|
||||
|
||||
Öffnet im Banner Settings/Mehr-Optionen, scrollt vertikal, aktiviert
|
||||
jeden Tab, expandet jedes Akkordeon — sodass das Walk-Video den
|
||||
vollen Consent-Katalog (Vendoren, Zwecke, Speicherdauern) zeigt.
|
||||
|
||||
Wird vom audit_walk_recorder vor `_try_accept_banner` aufgerufen.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Tour-Trigger: Öffne im Banner zuerst die Detail-Ansicht /
|
||||
# Einstellungen — DORT findet sich die Vendor-Liste und die Kategorie-
|
||||
# Klappmenüs.
|
||||
_TOUR_OPEN_PHRASES = (
|
||||
"einstellungen", "individuelle einstellungen",
|
||||
"cookie-einstellungen", "anpassen", "details", "details anzeigen",
|
||||
"mehr optionen", "auswahl anpassen", "verwalten",
|
||||
"konfigurieren", "manage", "customize", "manage cookies",
|
||||
"manage preferences", "settings", "show purposes",
|
||||
"show vendors", "show details", "purposes", "vendors",
|
||||
)
|
||||
|
||||
# Banner-Akkordeon/Tab-Pattern: was wir IM Banner aufklappen wollen.
|
||||
_BANNER_EXPAND_SELECTORS = (
|
||||
"[role=tab]",
|
||||
"[role=tablist] [role=tab]",
|
||||
"[role=tabpanel] button",
|
||||
"[aria-expanded='false']",
|
||||
"button[data-toggle='collapse']",
|
||||
"summary",
|
||||
"details:not([open]) > summary",
|
||||
"[class*=accordion] button",
|
||||
"[class*=expand] button",
|
||||
".purposes-list button",
|
||||
".vendor-list button",
|
||||
"[data-testid*='expand']",
|
||||
"[data-testid*='vendor']",
|
||||
"[data-testid*='purpose']",
|
||||
)
|
||||
|
||||
|
||||
def _ts() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
async def tour_cookie_banner(page, max_clicks: int = 35,
|
||||
dwell_ms: int = 700) -> dict:
|
||||
"""BEFORE accepting the banner: open Settings, scroll through it,
|
||||
expand every category / vendor / accordion / tab inside.
|
||||
|
||||
Strategie:
|
||||
1. Suche Buttons mit Tour-Phrases ("Einstellungen", "Vendor" etc.)
|
||||
— klicken erste Detail-Ansicht.
|
||||
2. Scrolle Banner vertikal (sodass mehr Inhalte rendern).
|
||||
3. Aktiviere jedes [role=tab] nacheinander.
|
||||
4. Expand jedes [aria-expanded=false] / details / summary im
|
||||
Banner-Container.
|
||||
|
||||
Alles im Video aufgezeichnet — Reviewer sieht den vollen Vendor-
|
||||
und Zweck-Katalog. Best-Effort; jeder Klick mit try/except.
|
||||
"""
|
||||
started = _ts()
|
||||
clicks = 0
|
||||
notes: list[str] = []
|
||||
|
||||
# Phase 1: open "Settings" / "Mehr Optionen"
|
||||
settings_opened = False
|
||||
for phrase in _TOUR_OPEN_PHRASES:
|
||||
try:
|
||||
btn = page.get_by_role("button", name=phrase, exact=False).first
|
||||
if await btn.count() > 0:
|
||||
await btn.scroll_into_view_if_needed(timeout=1500)
|
||||
await btn.click(timeout=2000)
|
||||
await page.wait_for_timeout(1500)
|
||||
notes.append(f"opened: {phrase}")
|
||||
settings_opened = True
|
||||
clicks += 1
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not settings_opened:
|
||||
try:
|
||||
link = page.get_by_role("link", name=phrase,
|
||||
exact=False).first
|
||||
if await link.count() > 0:
|
||||
await link.scroll_into_view_if_needed(timeout=1500)
|
||||
await link.click(timeout=2000)
|
||||
await page.wait_for_timeout(1500)
|
||||
notes.append(f"opened (link): {phrase}")
|
||||
settings_opened = True
|
||||
clicks += 1
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Phase 2: scroll the banner / dialog to expose more content
|
||||
try:
|
||||
await page.mouse.wheel(0, 600)
|
||||
await page.wait_for_timeout(500)
|
||||
await page.mouse.wheel(0, 600)
|
||||
await page.wait_for_timeout(500)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Phase 3: activate every [role=tab] in any role=tablist
|
||||
try:
|
||||
tabs = await page.query_selector_all("[role=tab]")
|
||||
for tab in tabs:
|
||||
if clicks >= max_clicks:
|
||||
break
|
||||
try:
|
||||
await tab.scroll_into_view_if_needed(timeout=1000)
|
||||
await tab.click(timeout=1200)
|
||||
await page.wait_for_timeout(dwell_ms)
|
||||
clicks += 1
|
||||
except Exception:
|
||||
continue
|
||||
if tabs:
|
||||
notes.append(f"tabs activated: {len(tabs)}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Phase 4: expand every aria-expanded=false / details / summary
|
||||
for sel in _BANNER_EXPAND_SELECTORS:
|
||||
if clicks >= max_clicks:
|
||||
break
|
||||
try:
|
||||
els = await page.query_selector_all(sel)
|
||||
except Exception:
|
||||
continue
|
||||
for el in els:
|
||||
if clicks >= max_clicks:
|
||||
break
|
||||
try:
|
||||
await el.scroll_into_view_if_needed(timeout=800)
|
||||
await el.click(timeout=1000)
|
||||
await page.wait_for_timeout(dwell_ms)
|
||||
clicks += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return {
|
||||
"timestamp": started,
|
||||
"action": "tour_cookie_banner",
|
||||
"clicks": clicks,
|
||||
"settings_opened": settings_opened,
|
||||
"notes": notes,
|
||||
}
|
||||
Reference in New Issue
Block a user