a11y-CI-Job (axe) für vier Kernseiten (#60) - #71
Merged
Conversation
Prüft das gerenderte DOM von /, /umfragen, /anliegen und /anmelden mit pa11y-ci + axe-Runner (WCAG2AA). Ergänzt das statische jsx-a11y-Lint-Gate um genau die Verstoßklasse, die dieses prinzipbedingt nicht sehen kann: Kontraste, Landmarks, Formular-Labels, Heading-Reihenfolge. Zentral abgesichert: die Tenant-Auflösung ist host-basiert, localhost ist eine HAUPT-Domain und liefert ohne PILOT_TENANT_SLUG die neutrale Landing-Page — ein a11y-Lauf dagegen wäre grün und wertlos. scripts/a11y-preflight.ts erzwingt vor jedem pa11y-Lauf: HTTP-Readiness-Poll (kein sleep), Status 200 für alle vier Seiten und eine harte Assertion, dass /umfragen den Wortlaut einer geseedeten Umfrage aus db/seeds/polls.json enthält. axe-'incomplete'-Befunde werden auf warning gedeckelt (levelCapWhenNeedsReview): .pz-hero hat einen Verlaufshintergrund, bei dem axe den Kontrast grundsätzlich nicht berechnen kann. Ein hartes Gate darauf wäre nur durch seitenweites Abschalten von color-contrast zu beruhigen — genau der Regel, die auf der Startseite am meisten wert ist. Belegte Verstöße blocken weiterhin hart. Eingefrorene Altlast, regel- UND seitengenau: link-in-text-block auf /anliegen. Keine UI-Änderung in diesem PR. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BervJUSWK8ymMbcuBuu9d9
…eit #60) Das Gate meldete 0 Befunde, waehrend 6 belegte axe-Verstoesse bestanden. Zwei Ursachen, beide in pa11y selbst: 1. `levelCapWhenNeedsReview: "warning"` deckelt die GESAMTE axe-incomplete- Klasse, nie regelbezogen (pa11y/lib/runners/axe.js, `choosePa11yLevel` prueft nur das Flag `issueNeedsReview`). axe schreibt `duplicate-id-aria` wegen `reviewOnFail: true` nach `incomplete` — der Deckel machte aus 2 belegten critical-Verstoessen unsichtbare Warnungen. 2. pa11y bildet die Impact-Klasse `moderate` pauschal auf `warning` ab und zaehlt Warnungen per Default nicht. `heading-order` (/umfragen) und `form-field-multiple-labels` (/) konnten deshalb nie blocken. Fix: zwei pa11y-ci-Laeufe. LAUF 1 (.pa11yci.js) — Deckel an, Kontrastregel auf allen Seiten aktiv; blockt belegte critical/serious- Verstoesse inkl. echter color-contrast-Fehler. LAUF 2 (.pa11yci.streng.js) — kein Deckel, includeWarnings + includeNotices; blockt incomplete jeder Impact-Klasse sowie moderate/minor-Verstoesse. `color-contrast` ist dort nur auf `/` aus (Verlaufs-Hero, technisch unentscheidbar) — keine Luecke, LAUF 1 deckt es. Die dadurch sichtbar gewordenen 3 Bestandsbefunde (duplicate-id-aria 2x und form-field-multiple-labels 2x auf /, heading-order 1x auf /umfragen) sind regel- UND seitengenau eingefroren, jeweils mit Regel-ID, Seite, Element, Grund und Abbau-Hinweis. Sie verschwinden im UI-Folge-PR ersatzlos; dieser PR aendert bewusst keine Komponente. Weiter: - .pa11yci.json → .pa11yci.js: Basis-URL (A11Y_BASE_URL) und Seitenliste lagen doppelt vor (Config + Preflight). Jetzt eine Quelle, die beide Configs und scripts/a11y-preflight.ts teilen — sonst prueft der Preflight einen anderen Server als pa11y, ohne dass es auffaellt. - Preflight prueft zusaetzlich, dass die Stylesheets wirklich ausgeliefert werden. Ohne CSS verschwinden visuelle Befunde (link-in-text-block) spurlos und der Lauf waere wieder gruen und wertlos. - CI startet `npm run start:standalone` statt `npm run start`: next.config.ts setzt `output: "standalone"`, Next 16 unterstuetzt `next start` damit nicht, und Produktion faehrt .next/standalone/server.js (Dockerfile, Stage runner). - concurrency 2 → 1: zusammen mit dem Inkognito-Kontext je Ziel gab es sporadische `Protocol error (Target.closeTarget)`-Abbrueche. Bei vier URLs bringt Parallelitaet nichts, ein flackerndes Pflicht-Gate schon. - ci.yml benennt die Reichweite ehrlich: vier anonyme Sichten, keine Aussage ueber die Beteiligungsfluesse (/umfrage/[id], /mitmachen, /konto, /verifizieren, Admin) und nichts Interaktives. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BervJUSWK8ymMbcuBuu9d9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Schließt #60.
Was
Ein eigenständiger CI-Job
a11yprüft das gerenderte DOM von vier anonymerreichbaren Seiten mit
pa11y-ci+ axe-Runner (WCAG2AA)://umfragen/anliegen/anmeldenDas ergänzt das bestehende harte statische
jsx-a11y-Gate (npm run lint),es ersetzt es nicht: Kontraste, Landmarks, Formular-Labels und
Heading-Reihenfolge entstehen erst beim Rendern und sind für statisches Lint
unsichtbar.
Nachgearbeitet nach Gate-B (wichtigster Teil)
Die erste Fassung dieses PRs meldete 0 Befunde, während 6 belegte
axe-Verstöße bestanden. Zwei Ursachen, beide in pa11y selbst:
levelCapWhenNeedsReview: "warning"wirkt pauschal.choosePa11yLevel(
pa11y/lib/runners/axe.js) prüft nur das FlagissueNeedsReview, nie dieRegel-ID — der Deckel trifft die gesamte axe-
incomplete-Klasse.axe schreibt
duplicate-id-ariawegenreviewOnFail: trueebenfalls nachincomplete, obwohl der Verstoß belegt ist. Ergebnis: 2 critical-Verstößewurden zu Warnungen, die niemand zählt.
moderateblockte grundsätzlich nie. pa11y bildetmoderateaufwarningundminoraufnoticeab(
axeImpactToPa11yLevel) und zählt beides per Default nicht(
pa11y/lib/option.jsschiebtwarning/noticesonst in die Ignore-Liste).heading-orderundform-field-multiple-labelskonnten so nie rot werden.Fix: zwei pa11y-ci-Läufe
Ein einzelner Lauf kann die unentscheidbaren Kontrastfälle des Verlaufs-Heros
nicht dämpfen, ohne gleichzeitig belegte Verstöße stumm zu schalten. Deshalb:
app/.pa11yci.js)app/.pa11yci.streng.js)levelCapWhenNeedsReview"warning"error)includeWarnings/includeNoticescolor-contrast/ausviolationsmit Impact critical/serious, insbesondere echte Kontrastverstößeincompletejeder Impact-Klasse +moderate/minor-Verstößenpm run a11y= Preflight&&Lauf 1&&Lauf 2; jeder Lauf bricht die Kettebei Exit ≠ 0.
Keine Lücke durch die Kontrast-Ausnahme in Lauf 2: Lauf 1 lässt
color-contrastüberall aktiv, und ein belegter Kontrast-Verstoß hat Impactserious→ Stufeerror→ blockt dort weiterhin. Nachgestellt mit einemDokument, das einen echten Verstoß enthält (
#ccccccauf Weiß, 1.6:1), gegenexakt die Defaults von Lauf 1:
Der Fallstrick, der hier abgesichert ist
Die Tenant-Auflösung ist host-basiert (
app/src/middleware.ts).localhoststeht in
MAIN_HOSTNAMES— ein Server ohnePILOT_TENANT_SLUGliefert dort dieneutrale Landing-Page, nicht die Tenant-App. Ein a11y-Job dagegen wäre grün
und wertlos.
app/scripts/a11y-preflight.tsläuft vor den pa11y-Läufen und bricht hartab, wenn eine der vier Bedingungen fehlt:
sleepals Synchronisation./umfragenmuss den Wortlaut mindestens einer geseedetenUmfrage aus
db/seeds/polls.jsonenthalten.<link rel="stylesheet">, und jedes davon liefert mit 200 und nicht-leeremRumpf aus. Punkt 2 und 3 beweisen nur Inhalt; viele axe-Regeln sind rein
visuell. Mit blockiertem Stylesheet verschwindet z. B. die bekannte
link-in-text-block-Violation spurlos — der Lauf wäre wieder grün undwertlos.
Gegenprobe zu Punkt 4 (statische Assets weggenommen, Server läuft weiter):
Fail-Modus: hart, Bestand regel- und seitengenau eingefroren
Aktuelle Bestandsbefunde auf den vier Seiten (vollständige Inventur über beide
Läufe, ohne jede Ausnahme):
duplicate-id-aria/form-field-multiple-labels/heading-order/umfragenlink-in-text-block/anliegencolor-contrast/Die ersten vier Zeilen sind echte UI-Fehler. Sie sind hier regel- und
seitengenau eingefroren — jede Ausnahme steht mit Regel-ID, Seite, Element,
Grund und Abbau-Hinweis in der Config und verschwindet im UI-Folge-PR
ersatzlos. Dieser PR ändert bewusst keine Komponente.
color-contrastauf/ist keine Altlast, sondern technisch bedingt:.pz-herohat einen Verlaufals Hintergrund, dort kann axe den Kontrast prinzipiell nicht berechnen.
Beleg, dass das Gate beißt — dieselbe strenge Config, nur ohne die vier
eingefrorenen Altlasten (Kontrast-Ausnahme bleibt):
mit den Elementen im Klartext:
#plzund#plz-funktion(je 2×duplicate-id-ariabzw.form-field-multiple-labels),#main-content > main > div > div:nth-child(1) > h3(heading-order),#main-content > main > p > a(link-in-text-block).Mit den Ausnahmen:
✔ 4/4 URLs passedin beiden Läufen,EXIT=0.Reichweite dieses Gates — ehrlich
(
/umfrage/[id],/mitmachen,/konto,/verifizieren, Admin) sind nichtabgedeckt. Der Job trägt also eine Aussage über diese vier Seiten, keine über
die Anwendung als Ganzes. Die frühere Formulierung „belastbare
BITV-Argumentation gegenüber Kommunen" in
ci.ymlist entsprechend entschärft.Fokusführung, nichts, was erst nach einer Interaktion entsteht.
sinnvolle Alternativtexte bleiben Handarbeit.
/istcolor-contrastin Lauf 2 abgeschaltet; belegte Kontrastverstößefängt dort Lauf 1 (oben belegt), unentscheidbare Fälle bleiben unentschieden.
Weitere Nacharbeiten
.pa11yci.json→.pa11yci.js. Basis-URL (A11Y_BASE_URL) undSeitenliste lagen doppelt vor (Config + Preflight); driftet eines, prüft der
Preflight einen anderen Server oder andere Seiten als pa11y, ohne dass es
auffällt. Jetzt eine Quelle, die beide Configs und der Preflight teilen
(pa11y-ci lädt
.cjs/.jsüberloadConfigModule)..pa11yci.streng.jswirft zusätzlich, wenn eine Ausnahme auf eine Seite zeigt, die gar nicht mehr
geprüft wird.
npm run start:standalonestattnpm run start.next.config.tssetztoutput: "standalone", Next 16 unterstütztnext startdamit ausdrücklich nicht, und Produktion fährt.next/standalone/server.js(Dockerfile, Stagerunner). Das neue Skriptkopiert
.next/staticundpublicdaneben — genau wie das Dockerfile.concurrency2 → 1. Zusammen mit dem Inkognito-Browser-Kontext je Zielgab es sporadische
Protocol error (Target.closeTarget)-Abbrüche. DieRichtung wäre sicher (rot statt grün), aber ein flackerndes Pflicht-Gate wird
weggeklickt; bei vier URLs bringt Parallelität ohnehin nichts.
"ignore": []ist kein Opt-out aus globalen Ignores — pa11y-ci mergedURL-Optionen über lodash
defaultsDeep, und das merged Arrays indexweise.Heute folgenlos (
defaultssetzt keinignore), in beiden Configs alsKommentar festgehalten.
Lokal verifiziert
Postgres-Container,
db:migrate+db:seed,npm run build, Server persetsidmitPILOT_TENANT_SLUG=taunussteinübernpm run start:standalone,npm run a11y, Cleanup über Prozessgruppe — die CI-Schritte 1:1 nachgestellt:npm run lint0,npm run typecheck0,npx vitest run:Test Files 95 passed (95) · Tests 1168 passed (1168).Nicht Teil dieses PRs
Keine UI-Änderung. Die vier echten Verstöße sind eingefroren und dokumentiert,
nicht gefixt — der Folge-PR macht die doppelten IDs auf
/eindeutig (nimmtform-field-multiple-labelsmit), korrigiert die Überschriftenebene auf/umfragenund den nur farblich markierten Link auf/anliegen, und entferntdabei alle vier Ausnahmen ersatzlos.
ciundupgrade-pathsind unverändert.🤖 Generated with Claude Code
https://claude.ai/code/session_01BervJUSWK8ymMbcuBuu9d9