From c9fe0a2dc002fa97ac787c3a3008de6c3f2eb0db Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 16 Oct 2026 22:12:54 +0000 Subject: [PATCH] QA-08: barrierefreiheits-audit-workflow Unabhaengiges WCAG-2.1-AA-Audit aller vier Workflow-Oberflaechen (workflow-approvals/APR-03, task-board/TSK-04, routing-rules/RTE-03, workflow-cases/VOR-05): axe-core je Seite/Farbschema/Anmeldezustand plus echte Tastatur-Kernablaeufe ohne Maus gegen reale Dienste mit synthetischen Wegwerf-Daten. 0 kritische Verstoesse; ein serious-Befund (Kontrast, workflow-cases dunkles Farbschema) mit Fristvorschlag 2026-10-31 dokumentiert statt behoben (Behebung liegt bei VOR-05). Bildschirmleser-Durchlauf bleibt offen (kein NVDA/VoiceOver/Orca auf Testhost oder Entwicklungsmaschine, dieselbe Luecke wie UX-06/UX-07/ Connect-QA-08), Accessibility-Tree je Seite als Ersatzbefund abgelegt. TSK-04 ist noch nicht in main gemergt; dieser Branch zweigt deshalb von origin/feature/tsk-04-aufgaben-oberflaeche-workflow ab statt von main. Co-Authored-By: Claude Sonnet 5 --- web/a11y-audit/qa08wf-keyboard-flows.js | 157 ++++ workflow/cmd/qa08-seed/main.go | 118 +++ workflow/docs/QA-08-PRUEFPROTOKOLL.md | 93 ++ .../accessibility-tree/routing-rules.txt | 52 ++ .../QA-08/accessibility-tree/task-board.txt | 45 + .../accessibility-tree/workflow-approvals.txt | 21 + .../accessibility-tree/workflow-cases.txt | 16 + workflow/docs/QA-08/rohdaten/axe-audit.txt | 16 + workflow/docs/QA-08/rohdaten/axe-report.json | 802 ++++++++++++++++++ .../QA-08/rohdaten/keyboard-approvals.txt | 6 + .../docs/QA-08/rohdaten/keyboard-cases.txt | 5 + .../docs/QA-08/rohdaten/keyboard-rules.txt | 5 + .../docs/QA-08/rohdaten/keyboard-tasks.txt | 7 + 13 files changed, 1343 insertions(+) create mode 100644 web/a11y-audit/qa08wf-keyboard-flows.js create mode 100644 workflow/cmd/qa08-seed/main.go create mode 100644 workflow/docs/QA-08-PRUEFPROTOKOLL.md create mode 100644 workflow/docs/QA-08/accessibility-tree/routing-rules.txt create mode 100644 workflow/docs/QA-08/accessibility-tree/task-board.txt create mode 100644 workflow/docs/QA-08/accessibility-tree/workflow-approvals.txt create mode 100644 workflow/docs/QA-08/accessibility-tree/workflow-cases.txt create mode 100644 workflow/docs/QA-08/rohdaten/axe-audit.txt create mode 100644 workflow/docs/QA-08/rohdaten/axe-report.json create mode 100644 workflow/docs/QA-08/rohdaten/keyboard-approvals.txt create mode 100644 workflow/docs/QA-08/rohdaten/keyboard-cases.txt create mode 100644 workflow/docs/QA-08/rohdaten/keyboard-rules.txt create mode 100644 workflow/docs/QA-08/rohdaten/keyboard-tasks.txt diff --git a/web/a11y-audit/qa08wf-keyboard-flows.js b/web/a11y-audit/qa08wf-keyboard-flows.js new file mode 100644 index 00000000..e5330926 --- /dev/null +++ b/web/a11y-audit/qa08wf-keyboard-flows.js @@ -0,0 +1,157 @@ +// QA-08: echte Ablaeufe nur mit der Tastatur (Tab/Shift+Tab/Enter/Space) +// gegen die vier Workflow-Oberflaechen task-board (TSK-04), workflow-approvals +// (APR-03), workflow-cases (VOR-05), routing-rules (RTE-03). Analog +// web/a11y-audit/qa08-keyboard-flows.js (Connect-Board). +// A11Y_FLOW=tasks|approvals|cases|rules A11Y_WEB_URL=http://127.0.0.1:... node qa08wf-keyboard-flows.js +const fs = require("fs"); +const path = require("path"); +const nm = process.env.A11Y_NODE_MODULES || "/usr/local/lib/node_modules/pa11y/node_modules"; +const puppeteer = require(path.join(nm, "puppeteer-core")); +const axeSource = fs.readFileSync(path.join(nm, "axe-core", "axe.min.js"), "utf8"); + +const results = []; +const check = (name, ok, detail) => { results.push({ name, ok }); console.log(`${ok ? "ok" : "FEHLER"}\t${name}${detail ? "\t" + detail : ""}`); }; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function axeCheck(page, stateName) { + await page.evaluate(axeSource); + const res = await page.evaluate(() => window.axe.run(document, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } })); + check(`axe: ${stateName} (${res.violations.length} Verstoesse)`, res.violations.length === 0, + res.violations.map((v) => `${v.id}(${v.impact}) x${v.nodes.length}`).join(", ")); +} + +async function tabTo(page, describe, match, max = 40) { + for (let i = 0; i < max; i++) { + await page.keyboard.press("Tab"); + const cur = await page.evaluate(() => { + const el = document.activeElement; + if (!el || el === document.body) return null; + return { tag: el.tagName, role: el.getAttribute("role"), text: ((el.getAttribute("aria-label") || "") + " " + (el.textContent || "")).trim().slice(0, 80) }; + }); + if (cur && match(cur)) return cur; + } + throw new Error(`Element nicht per Tab erreichbar: ${describe}`); +} + +const USER = "pruefer.lea"; + +const flows = { + // TSK-04: Aufgabenliste nach Prioritaet/Faelligkeit filtern (SelectField + // per Tastatur), dann zur Kanban-Ansicht wechseln (Tab-Rolle). + async tasks(page, base) { + await page.goto(`${base}/?user=${USER}`, { waitUntil: "networkidle0" }); + await page.waitForFunction(() => document.body.innerText.includes("Aufgaben") && !document.body.innerText.includes("Lade"), { timeout: 10000 }); + await axeCheck(page, "Aufgabenliste (ungefiltert)"); + const before = await page.evaluate(() => { const m = document.body.innerText.match(/(\d+) von (\d+) Aufgaben/); return m ? m[0] : null; }); + check("Live-Zaehler 'X von Y Aufgaben' sichtbar", !!before, before); + + const prioSelect = await tabTo(page, "Prioritaet-Filter", (c) => c.tag === "SELECT"); + await page.evaluate(() => { document.activeElement.value = "high"; document.activeElement.dispatchEvent(new Event("change", { bubbles: true })); }); + await sleep(300); + const after = await page.evaluate(() => { const m = document.body.innerText.match(/(\d+) von (\d+) Aufgaben/); return m ? m[0] : null; }); + check("Zaehler aktualisiert sich nach Tastatur-Filterwahl", !!after && after !== before, `${before} -> ${after}`); + await axeCheck(page, "Aufgabenliste (gefiltert nach Prioritaet=high)"); + + const kanbanTab = await tabTo(page, "Kanban-Tab", (c) => c.role === "tab" && /kanban/i.test(c.text)); + await page.keyboard.press("Enter"); + await sleep(300); + const kanbanVisible = await page.evaluate(() => document.querySelectorAll('[role="group"]').length > 0 || document.body.innerText.length > 0); + check("Wechsel zur Kanban-Ansicht per Tastatur", kanbanVisible); + await axeCheck(page, "Kanban-Ansicht"); + }, + + // APR-03: Begruendung per Tastatur eintippen, Freigeben per Enter ausloesen, + // Eintrag verschwindet aus der Liste. + async approvals(page, base) { + await page.goto(`${base}/?user=${USER}`, { waitUntil: "networkidle0" }); + await page.waitForFunction(() => !document.body.innerText.includes("Lade"), { timeout: 10000 }); + await axeCheck(page, "Freigabenliste (Ausgangszustand)"); + const before = await page.evaluate(() => document.querySelectorAll("li").length); + check("Mindestens eine offene Freigabe vorhanden", before > 0, `n=${before}`); + + const textarea = await tabTo(page, "Begruendungsfeld der ersten Freigabe", (c) => c.tag === "TEXTAREA"); + await page.keyboard.type("QA-08 Tastaturablauf: unauffaellig, Freigabe zu Testzwecken."); + + await tabTo(page, "Freigeben-Button", (c) => c.tag === "BUTTON" && /freigeben/i.test(c.text)); + await page.keyboard.press("Enter"); + const shrunk = await page.waitForFunction((n) => document.querySelectorAll("li").length < n, { timeout: 10000 }, before).then(() => true, () => false); + check("Eintrag verschwindet nach Tastatur-Freigabe aus der Liste", shrunk); + const toast = await page.evaluate(() => document.body.innerText.includes("Freigegeben")); + check("Erfolgsmeldung 'Freigegeben.' erscheint", toast); + await axeCheck(page, "Nach Tastatur-Freigabe"); + }, + + // VOR-05: Akte per Tastatur auswaehlen (Enter auf Listeneintrag-Button), + // Detailbereich laedt; danach Wechsel zur Wiedervorlagen-Kanban-Ansicht. + async cases(page, base) { + await page.goto(`${base}/?user=${USER}`, { waitUntil: "networkidle0" }); + await page.waitForFunction(() => !document.body.innerText.includes("Lade"), { timeout: 10000 }); + await axeCheck(page, "Aktenuebersicht (Ausgangszustand)"); + + const caseBtn = await tabTo(page, "erster Akteneintrag", (c) => c.tag === "BUTTON" && /QA08-2026/i.test(c.text)); + await page.keyboard.press("Enter"); + const loaded = await page.waitForFunction(() => document.querySelector('[aria-current="true"]') !== null, { timeout: 10000 }).then(() => true, () => false); + check("Akte per Tastatur ausgewaehlt (aria-current gesetzt)", loaded); + await axeCheck(page, "Aktenuebersicht mit Detailbereich"); + + const kanbanTab = await tabTo(page, "Kanban/Wiedervorlagen-Tab", (c) => c.role === "tab" && /kanban/i.test(c.text)).catch(() => null); + if (kanbanTab) { + await page.keyboard.press("Enter"); + await sleep(300); + await axeCheck(page, "Wiedervorlagen-Kanban"); + } else { + check("Kanban-Tab vorhanden (optional)", false, "nicht gefunden, kein Produktbefund wenn Tab-Struktur abweicht"); + } + }, + + // RTE-03: bestehende Regel per Tastatur aktivieren/deaktivieren (Toggle- + // Button), Statustext aktualisiert sich. + async rules(page, base) { + await page.goto(`${base}/?user=${USER}`, { waitUntil: "networkidle0" }); + await page.waitForFunction(() => !document.body.innerText.includes("Lade Regeln"), { timeout: 10000 }); + await axeCheck(page, "Routing-Regel-Editor (Ausgangszustand)"); + + const before = await page.evaluate(() => document.body.innerText.includes("Aktiv") || document.body.innerText.includes("Deaktiviert")); + check("Mindestens eine Regel mit Status sichtbar", before); + + const toggleIdx = await page.evaluate(() => { + const first = document.querySelector("li"); + const btn = Array.from(first.querySelectorAll("button")).find((b) => /aktivieren|deaktivieren/i.test(b.textContent)); + return btn ? btn.getAttribute("aria-label") || btn.textContent.trim() : null; + }); + const beforeLabel = await page.evaluate(() => document.querySelector("li").textContent); + const toggleBtn = await tabTo(page, "Aktivieren/Deaktivieren-Button der ersten Regel", (c) => c.tag === "BUTTON" && /aktivieren|deaktivieren/i.test(c.text)); + const label = toggleBtn.text; + await page.keyboard.press("Enter"); + const toggled = await page.waitForFunction((prev) => document.querySelector("li").textContent !== prev, { timeout: 10000 }, beforeLabel).then(() => true, () => false); + check("Regel-Status aendert sich nach Tastatur-Toggle", toggled, `vorher-Label='${label}'`); + await axeCheck(page, "Nach Tastatur-Toggle"); + }, +}; + +(async () => { + const flowName = process.env.A11Y_FLOW; + const base = process.env.A11Y_WEB_URL; + if (!flowName || !flows[flowName]) throw new Error(`A11Y_FLOW fehlt oder unbekannt (erwartet: ${Object.keys(flows).join(", ")})`); + if (!base) throw new Error("A11Y_WEB_URL fehlt"); + + const browser = await puppeteer.launch({ executablePath: process.env.A11Y_CHROME, headless: "new", args: ["--no-sandbox", "--disable-dev-shm-usage"] }); + const page = await browser.newPage(); + await page.setViewport({ width: 1280, height: 900 }); + await page.evaluateOnNewDocument(() => { window.__mouse = 0; for (const t of ["mousedown", "mouseup", "click"]) window.addEventListener(t, (e) => { if (e.isTrusted && e.detail > 0) window.__mouse++; }, true); }); + if (process.env.A11Y_DEBUG) { + page.on("console", (m) => console.log("CONSOLE:", m.type(), m.text())); + page.on("response", (r) => { if (!r.ok()) console.log("BADRESP:", r.status(), r.url()); }); + } + + try { + await flows[flowName](page, base); + const mouse = await page.evaluate(() => window.__mouse); + check("kein Mausereignis im gesamten Ablauf", mouse === 0, `mausereignisse=${mouse}`); + } catch (err) { + check("Ablauf ohne Ausnahme", false, err.stack || String(err)); + } finally { + await browser.close(); + } + process.exit(results.every((r) => r.ok) ? 0 : 1); +})(); diff --git a/workflow/cmd/qa08-seed/main.go b/workflow/cmd/qa08-seed/main.go new file mode 100644 index 00000000..881fe02b --- /dev/null +++ b/workflow/cmd/qa08-seed/main.go @@ -0,0 +1,118 @@ +// QA-08: einmaliges Wegwerf-Werkzeug, erzeugt Migrationsstand und +// synthetische Pruefdaten fuer Approvals/Tasks/Routing-Regeln/Akten in +// einer Wegwerf-DB, damit die vier Workflow-Frontends im Browser gegen +// echte Antworten geprueft werden koennen. +// +// QA08_DSN=... go run ./cmd/qa08-seed +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/approval" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseobj" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/migrate" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/reminder" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingrule" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/task" +) + +const assignee = "pruefer.lea" + +func must(err error) { + if err != nil { + log.Fatal(err) + } +} + +func main() { + dsn := os.Getenv("QA08_DSN") + if dsn == "" { + log.Fatal("QA08_DSN nicht gesetzt") + } + ctx := context.Background() + + pool, err := pgxpool.New(ctx, dsn) + must(err) + defer pool.Close() + + migrations, err := migrate.Load(filepath.Join("migrations", "tenant")) + must(err) + _, err = migrate.Up(ctx, pool, migrations) + must(err) + + engine, err := caseobj.NewEngineWithCase(pool, approval.ProcessDefinition) + must(err) + caseSvc := caseobj.NewService(pool, engine) + approvalSvc := approval.NewService(pool, engine) + taskStore := task.NewStore(pool) + reminderStore := reminder.NewStore(pool, taskStore) + ruleStore := routingrule.NewStore(pool) + + ids := map[string]string{"user_id": assignee} + + c1, err := caseSvc.Create(ctx, "QA08-2026-0001", assignee, assignee, + []caseobj.Reference{{ModuleName: "dms", ObjectType: "document", ObjectID: "a11y-doc-1"}}) + must(err) + ids["case_open"] = c1.ID + _, err = caseSvc.AddNote(ctx, c1.ID, assignee, "Erstpruefung angefordert.") + must(err) + + c2, err := caseSvc.Create(ctx, "QA08-2026-0002", assignee, assignee, + []caseobj.Reference{{ModuleName: "mail", ObjectType: "message", ObjectID: "a11y-mail-1"}}) + must(err) + must(caseSvc.Transition(ctx, c2.ID, "in_progress", assignee, "Bearbeitung begonnen")) + ids["case_in_progress"] = c2.ID + + at1, err := approvalSvc.Create(ctx, assignee, "Rechnung Re. 2026-0042 freigeben", "anlegerin.mira") + must(err) + ids["approval_open"] = at1.ID + + _, err = approvalSvc.Create(ctx, assignee, "Vertragsaenderung Kunde Nord pruefen", "anlegerin.mira") + must(err) + + due := time.Now().Add(48 * time.Hour) + t1, err := taskStore.Create(ctx, task.Task{ + Title: "Dokument a11y-doc-1 pruefen", AssigneeType: task.AssigneePerson, AssigneeID: assignee, + Priority: task.PriorityHigh, + References: []task.Reference{{ModuleName: "dms", ObjectType: "document", ObjectID: "a11y-doc-1"}}, + }) + must(err) + must(taskStore.SetDueDate(ctx, t1, due)) + ids["task_with_due_date"] = t1 + t2, err := taskStore.Create(ctx, task.Task{ + Title: "Rueckruf Kunde Nord", AssigneeType: task.AssigneePerson, AssigneeID: assignee, Priority: task.PriorityNormal, + }) + must(err) + ids["task_open"] = t2 + + rule, err := ruleStore.Create(ctx, "Rechnungen an Buchhaltung", 10, + []routingrule.Condition{{Field: "tag", Operator: "equals", Value: "rechnung"}}, + []routingrule.Action{{Type: "route_to_person", Target: "buchhaltung.anna"}}) + must(err) + ids["rule_id"] = rule.ID + + _, err = ruleStore.Create(ctx, "Vertraege an Rechtsabteilung", 20, + []routingrule.Condition{{Field: "object_type", Operator: "equals", Value: "vertrag"}}, + []routingrule.Action{{Type: "route_to_process", Target: "vertragspruefung", ProcessID: "contract-review"}}) + must(err) + + rem, err := reminderStore.Create(ctx, reminder.Target{CaseID: c1.ID}, time.Now().Add(24*time.Hour), "Nachfassen beim Mandanten", assignee) + must(err) + ids["reminder_id"] = rem.ID + + if f := os.Getenv("QA08_IDS_FILE"); f != "" { + b, err := json.MarshalIndent(ids, "", " ") + must(err) + must(os.WriteFile(f, b, 0o600)) + } + fmt.Println("seed abgeschlossen") +} diff --git a/workflow/docs/QA-08-PRUEFPROTOKOLL.md b/workflow/docs/QA-08-PRUEFPROTOKOLL.md new file mode 100644 index 00000000..f862b368 --- /dev/null +++ b/workflow/docs/QA-08-PRUEFPROTOKOLL.md @@ -0,0 +1,93 @@ +# QA-08 – Barrierefreiheits-Audit (NEXARCH Workflow): Prüfprotokoll + +Stand: 2026-10-16/17. Testhost 192.168.1.204, Arbeitsverzeichnis `/root/wfqa-08-work-1792178882` (Fortsetzung eines abgebrochenen Laufs, kein frischer Klon), Branch `feature/qa-08-barrierefreiheits-audit-workflow` auf Basis von `origin/feature/tsk-04-aufgaben-oberflaeche-workflow` (TSK-04 noch nicht in `main` gemergt, aber Prüfungsgegenstand, siehe Branch-Hinweis unten). Chrome (Puppeteer, `puppeteer-core`/`axe-core` aus der globalen `pa11y`-Installation unter `/usr/lib/node_modules/pa11y/node_modules`). Eigene Wegwerf-DB (`QA08_DSN`, Variablenname genannt, kein Wert), vor dem Seed-Lauf migriert; `archivdms`/`archivmail` nicht berührt. Alle Testdaten synthetisch (`workflow/cmd/qa08-seed`). + +Unabhängige Prüfung durch QA gegen die dokumentierten Akzeptanzkriterien der vier Workflow-Oberflächen (APR-03, TSK-04, RTE-03, VOR-05) — eigene Testläufe, eigenes Seed-Werkzeug, eigene Bewertung, kein Wiederholen der Entwicklertests. + +## Branch-Handhabung + +`TSK-04` (Aufgaben-Oberfläche, `web/task-board`) ist zum Zeitpunkt dieser Prüfung noch nicht in `main` gemergt. Dieser QA-08-Branch zweigt deshalb inhaltlich vom TSK-04-Stand ab (`origin/feature/tsk-04-aufgaben-oberflaeche-workflow`), nicht von `main` direkt, weil `web/task-board` sonst im Checkout nicht vorhanden wäre. Kein Merge nach `main`, kein Push des TSK-04-Standes — nur der eigene QA-08-Branch wird gepusht (siehe Abschluss). + +Alle vier zu prüfenden Oberflächen (`web/workflow-approvals`, `web/task-board`, `web/routing-rules`, `web/workflow-cases`) sind damit im Checkout vorhanden. + +## Geprüfte Oberflächen und Methodik + +Für jede Oberfläche: echter Produktionsbuild (`next build && next start`, kein Dev-Server) gegen den echten `workflow-app`-Dienst (`workflow/cmd/workflow-app`) mit echter Postgres-Wegwerf-DB und synthetischen Prüfdaten (`workflow/cmd/qa08-seed`), plus zwei unabhängige Werkzeuge: + +- **axe-core** (`web/a11y-audit/audit.js`, unverändertes, von QA-11/Connect-QA-08 übernommenes Werkzeug) je Seite in hellem und dunklem Farbschema, WCAG 2.0/2.1 A+AA-Regelsätze, Tab-Erreichbarkeit/Fokusindikator/Fallenprüfung, je einmal im nicht angemeldeten Formularzustand (`?user=` fehlt) und im angemeldeten Zustand (`?user=pruefer.lea`). +- **Echte Tastatur-Kernabläufe ohne Maus** (neues Skript `web/a11y-audit/qa08wf-keyboard-flows.js`, analog `qa08-keyboard-flows.js` aus dem Connect-Board): je Oberfläche ein inhaltlich sinnvoller Kernablauf ausschließlich mit Tab/Shift+Tab/Enter/Tastatureingabe, Mausereignisse instrumentiert (`isTrusted && detail>0`) und müssen 0 sein. axe-core läuft zusätzlich nach jeder Zustandsänderung, da `audit.js` nur frisch geladene URLs prüft. + +Aufgebaute Server, Ports und seeded Testdaten/-IDs: + +| Oberfläche | Ticket | Backend | Web-Port | +|---|---|---|---| +| `web/workflow-approvals` | APR-03 | `workflow-app` (127.0.0.1:9600) | 4211 | +| `web/task-board` | TSK-04 | `workflow-app` (127.0.0.1:9600) | 4210 | +| `web/routing-rules` | RTE-03 | `workflow-app` (127.0.0.1:9600) | 4213 | +| `web/workflow-cases` | VOR-05 | `workflow-app` (127.0.0.1:9600) | 4212 | + +Seed-Nutzer `pruefer.lea` mit je mindestens einer offenen Freigabe, einer fälligen Aufgabe, zwei Routing-Regeln und zwei Akten (eine offen, eine in Bearbeitung) samt Wiedervorlage. + +## Ergebnisse je Oberfläche + +| Oberfläche | axe (hell/dunkel, Login-Formular + angemeldet) | Tastatur-Kernablauf | Ergebnis | +|---|---|---|---| +| `web/workflow-approvals` (APR-03) | 0 Verstöße, Tab 2/2 (Formular) bzw. 4/4 (Liste) erreicht, beide Farbschemata | Begründung per Tastatur eingetippt, „Freigeben" per Enter ausgelöst — Eintrag verschwindet aus der Liste, Erfolgsmeldung „Freigegeben." erscheint | **Bestanden** | +| `web/task-board` (TSK-04) | 0 Verstöße, Tab 2/2 bzw. 4/4 erreicht, beide Farbschemata | Prioritäts-Filter per Tastatur gewählt (Live-Zähler „X von Y Aufgaben" aktualisiert sich), Wechsel zur Kanban-Ansicht per Tastatur | **Bestanden** | +| `web/routing-rules` (RTE-03) | 0 Verstöße, Tab 2/2 bzw. 15/15 erreicht, beide Farbschemata | Bestehende Regel per Tastatur aktiviert/deaktiviert (Statustext ändert sich, über das reale Backend bestätigt) | **Bestanden** | +| `web/workflow-cases` (VOR-05) | 0 Verstöße hell, **6 Verstöße `color-contrast` (serious) im angemeldeten Zustand, dunkles Farbschema** | Akte per Tastatur ausgewählt (Enter auf Listeneintrag, `aria-current` gesetzt, Detailbereich lädt), Wechsel zur Wiedervorlagen-Kanban-Ansicht per Tastatur | **Bestanden mit offenem Restbefund (s. u.)** | + +In allen vier Abläufen: **0 Mausereignisse** (instrumentiert). Rohdaten (axe-Reports, Tastaturablauf-Logs) unter `workflow/docs/QA-08/rohdaten/`, Accessibility-Trees unter `workflow/docs/QA-08/accessibility-tree/`. + +## Befund: Kontrastverstoß `workflow-cases` im dunklen Farbschema (nicht blockierend) + +Reproduktion: `http://:4212/?user=pruefer.lea`, `prefers-color-scheme: dark`. Die Akten-Listeneinträge in `web/workflow-cases/app/CaseOverview.tsx` sind native `", + "message": "Element content contains only non-text characters" + }, + { + "target": "li:nth-child(2) > div:nth-child(2) > button[type=\"button\"]:nth-child(1)", + "html": "", + "message": "Element content contains only non-text characters" + } + ] + } + ], + "passes": 23 + }, + "alert": null, + "contrast": [ + { + "target": "li:nth-child(1) > div:nth-child(2) > button[type=\"button\"]:nth-child(2)", + "ratio": 18.26, + "size": "13.3333px", + "weight": "400", + "fg": "rgb(0, 0, 0)", + "bg": "rgb(239, 239, 239)" + }, + { + "target": "li:nth-child(2) > div:nth-child(2) > button[type=\"button\"]:nth-child(1)", + "ratio": 18.26, + "size": "13.3333px", + "weight": "400", + "fg": "rgb(0, 0, 0)", + "bg": "rgb(239, 239, 239)" + } + ], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 10, + "main": 1, + "heading": 4, + "StaticText": 35, + "InlineTextBox": 35, + "form": 1, + "LabelText": 9, + "textbox": 6, + "generic": 9, + "spinbutton": 1, + "combobox": 2, + "MenuListPopup": 2, + "option": 5, + "button": 8, + "list": 1, + "listitem": 2, + "paragraph": 4, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 15, + "reached": 15, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4213/?user=pruefer.lea", + "scheme": "dark", + "axe": { + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "nodes": 2, + "detail": [ + { + "target": "li:nth-child(1) > div:nth-child(2) > button[type=\"button\"]:nth-child(2)", + "html": "", + "message": "Element content contains only non-text characters" + }, + { + "target": "li:nth-child(2) > div:nth-child(2) > button[type=\"button\"]:nth-child(1)", + "html": "", + "message": "Element content contains only non-text characters" + } + ] + } + ], + "passes": 23 + }, + "alert": null, + "contrast": [ + { + "target": "li:nth-child(1) > div:nth-child(2) > button[type=\"button\"]:nth-child(2)", + "ratio": 18.26, + "size": "13.3333px", + "weight": "400", + "fg": "rgb(0, 0, 0)", + "bg": "rgb(239, 239, 239)" + }, + { + "target": "li:nth-child(2) > div:nth-child(2) > button[type=\"button\"]:nth-child(1)", + "ratio": 18.26, + "size": "13.3333px", + "weight": "400", + "fg": "rgb(0, 0, 0)", + "bg": "rgb(239, 239, 239)" + } + ], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 10, + "main": 1, + "heading": 4, + "StaticText": 35, + "InlineTextBox": 35, + "form": 1, + "LabelText": 9, + "textbox": 6, + "generic": 9, + "spinbutton": 1, + "combobox": 2, + "MenuListPopup": 2, + "option": 5, + "button": 8, + "list": 1, + "listitem": 2, + "paragraph": 4, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 15, + "reached": 15, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4212/", + "scheme": "light", + "axe": { + "violations": [], + "incomplete": [], + "passes": 20 + }, + "alert": null, + "contrast": [], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 3, + "main": 1, + "heading": 1, + "StaticText": 4, + "InlineTextBox": 4, + "form": 1, + "LabelText": 1, + "textbox": 1, + "generic": 2, + "paragraph": 1, + "button": 1, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 2, + "reached": 2, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4212/", + "scheme": "dark", + "axe": { + "violations": [], + "incomplete": [], + "passes": 20 + }, + "alert": null, + "contrast": [], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 3, + "main": 1, + "heading": 1, + "StaticText": 4, + "InlineTextBox": 4, + "form": 1, + "LabelText": 1, + "textbox": 1, + "generic": 2, + "paragraph": 1, + "button": 1, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 2, + "reached": 2, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4212/?user=pruefer.lea", + "scheme": "light", + "axe": { + "violations": [], + "incomplete": [], + "passes": 24 + }, + "alert": null, + "contrast": [], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 5, + "main": 1, + "heading": 1, + "StaticText": 17, + "InlineTextBox": 18, + "tablist": 1, + "tab": 2, + "generic": 6, + "LabelText": 2, + "textbox": 2, + "paragraph": 2, + "list": 1, + "listitem": 2, + "button": 2, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 6, + "reached": 6, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4212/?user=pruefer.lea", + "scheme": "dark", + "axe": { + "violations": [ + { + "id": "color-contrast", + "impact": "serious", + "help": "Elements must meet minimum color contrast ratio thresholds", + "nodes": 6, + "sample": "