diff --git a/connect/docs/QA-08-PRUEFPROTOKOLL.md b/connect/docs/QA-08-PRUEFPROTOKOLL.md new file mode 100644 index 00000000..99a47f10 --- /dev/null +++ b/connect/docs/QA-08-PRUEFPROTOKOLL.md @@ -0,0 +1,83 @@ +# QA-08 – Barrierefreiheits-Audit (NEXARCH Connect): Prüfprotokoll + +Stand: 2026-10-16. Testhost 192.168.1.204, frischer Klon `/root/qa-08-connect-work-` (nicht der bestehende Arbeits-Checkout), Branch `feature/qa-08-barrierefreiheits-audit`. Chrome 154 (Puppeteer, `puppeteer-core`/`axe-core` aus der globalen `pa11y`-Installation), `pa11y` selbst als zweites Werkzeug installiert. Eigene Wegwerf-DB/-Rolle `qa08_connect_a11y`/`qa08_connect_test`, vor jedem Lauf gelöscht und neu angelegt; `archivdms` nicht berührt. Alle Testdaten synthetisch. + +Unabhängige Prüfung durch QA gegen die dokumentierten Akzeptanzkriterien der vier Connect-Oberflächen, kein Wiederholen der Entwicklertests der jeweiligen Kacheln (DAT-03, DMS2-02, API-01, API-02) — eigene Testläufe, eigene Fixtures/Skripte, eigene Bewertung. + +## API-01-Abhängigkeit (Branch-Handhabung) + +`API-01` (Connect-Konfigurationsoberfläche, `web/connect-setup`) ist als PR #318 implementiert, aber noch nicht in `main` gemergt. Vorgehen: + +1. `git fetch origin feature/api-01-01-connect-konfigurationsoberflaeche-connectoren-einrichten` +2. `git checkout -b feature/qa-08-barrierefreiheits-audit origin/feature/api-01-01-connect-konfigurationsoberflaeche-connectoren-einrichten` — der QA-08-Branch zweigt also inhaltlich vom API-01-Stand ab (API-01 selbst ist auf `main` abgezweigt), nicht von `main` direkt, weil `web/connect-setup` sonst im Checkout gar nicht existiert. +3. Kein Merge nach `main`, kein Push des API-01-Standes — nur der eigene QA-08-Branch wird (versuchsweise) gepusht, siehe Abschluss. + +Damit sind alle vier zu prüfenden Oberflächen (`web/datev-export`, `web/dms2-import`, `web/connect-setup`, `web/connect-status`) im Checkout vorhanden. + +## Geprüfte Oberflächen und Methodik + +Für jede Oberfläche: echter Dienst (reale Handler, reale Postgres-Wegwerf-DB, synthetische Daten) + echter Produktionsbuild (`next build && next start`, kein Dev-Server) + zwei unabhängige Werkzeuge: + +- **axe-core** (`web/a11y-audit/audit.js`, aus QA-11/API-02 übernommenes, unverändertes Werkzeug) je Seite in hellem und dunklem Farbschema, WCAG 2.0/2.1 A+AA-Regelsätze, plus Tab-Erreichbarkeit/Fokusindikator/Fallenprüfung aller interaktiven Elemente. +- **Echte Tastatur-Abläufe ohne Maus** (neue Skripte, s. u.): jede Kernaufgabe der Oberfläche vollständig nur mit Tab/Shift+Tab/Leertaste/Enter/Pfeiltasten durchgeführt (keine `element.click()`-Aufrufe), Mausereignisse (`mousedown`/`mouseup`/`click` mit `detail>0`) mitgezählt und müssen 0 sein. Zusätzlich läuft axe-core im selben Browser nach jeder Zustandsänderung (z. B. nach dem Laden einer Vorschau), da `audit.js` nur frisch geladene URLs prüft, nicht clientseitigen React-Zustand nach Interaktion. + +Neue Werkzeuge (alle unter `web/a11y-audit/`, analog zum bestehenden Muster aus QA-11/API-01/API-02): + +| Datei | Zweck | +|---|---| +| `qa08-connect-setup-run.sh` | Startet `setup-a11y-fixture` (API-01) + `web/connect-setup`, axe + Tastaturablauf | +| `qa08-connect-status-run.sh` | Startet `status-a11y-fixture` (API-02) + `web/connect-status`, axe + Tastaturablauf | +| `qa08-datev-export-run.sh` | Startet `datev-a11y-fixture` (DAT-03) + `web/datev-export`, axe + Tastaturablauf | +| `qa08-dms2-import-run.sh` | Startet `connect-app` (DMS2-02-Routen) + `web/dms2-import`, axe + Tastaturablauf | +| `qa08-keyboard-flows.js` | Tastatur-Abläufe für setup/status/datev (gemeinsames Skript, `A11Y_FLOW=`) | +| `qa08-dms2-import-keyboard.js` | Tastatur-Ablauf Import-Assistent (Datei-Dialog, Mapping-Korrektur, Ausführen) | + +`web/dms2-import` und `web/datev-export` hatten bisher kein eigenes a11y-Audit-Werkzeug (nur `connect-setup-run.sh`/`connect-status-run.sh` existierten aus API-01/API-02); für DATEV-Export wird der bereits vorhandene `connect/cmd/datev-a11y-fixture` (DAT-03) wiederverwendet, für den Import-Assistenten der volle `connect-app` (DMS2-02 hat keinen eigenen Fixture-Dienst, `connect-app` bindet `dms2importapi` bereits ein). + +## Ergebnisse je Oberfläche + +| Oberfläche | axe (hell/dunkel) | Tastatur-Kernablauf | Ergebnis | +|---|---|---|---| +| `web/connect-setup` (API-01) | 0 Verstöße, Tab 4/4 erreicht | Connector-Typ wählen, Instanz-Kennung + API-Schlüssel eintippen, speichern — Verbindung erscheint maskiert (`••••7766`), kein Klartext im DOM | **Bestanden** | +| `web/connect-status` (API-02) | 0 Verstöße, Tab 1/1 erreicht | „Aktualisieren" per Enter, Tabelle (2 Zeilen: 1 erfolgreich, 1 fehlgeschlagen) bleibt korrekt bestehen | **Bestanden** | +| `web/datev-export` (DAT-03) | 0 Verstöße, Tab 8/8 erreicht | Vollständiges Formular (7 Felder inkl. 3 Datumsfelder) ausgefüllt, abgesendet, neuer Lauf erscheint im Protokoll | **Bestanden** | +| `web/dms2-import` (DMS2-02) | 0 Verstöße auf Gate- und Formularseite (je hell/dunkel) + 0 Verstöße in allen 3 dynamischen Zuständen (Formular, Mapping-Tabelle nach Vorschau, Ergebnis) | Datei per Leertaste ausgewählt (echter nativer Datei-Dialog, `page.waitForFileChooser`), Vorschau geladen, Mapping-Korrektur per Tastatur (Dropdown), Import ausgeführt — Ergebnis „1 von 1 Dokument(en) erfolgreich importiert, 0 fehlgeschlagen." | **Bestanden** | + +In allen vier Abläufen: **0 Mausereignisse** (instrumentiert, `isTrusted && detail>0`). Rohdaten (axe-Reports, Tastaturablauf-Logs) unter `connect/docs/QA-08/rohdaten/`. + +## Akzeptanzkriterien + +1. **Keine kritischen Verstöße offen** — **erfüllt.** 0 axe-Verstöße jeder Kritikalität über alle vier Oberflächen, beide Farbschemata, alle geprüften dynamischen Zustände. Keine Behebung in dieser Kachel nötig (auch nicht in Verantwortung dieser Kachel, siehe „Nicht Bestandteil"). +2. **Manueller Bildschirmleser-Durchlauf dokumentiert** — **nicht erfüllt**, dieselbe organisatorische Lücke wie bereits in `dms/docs/UX-06-PRUEFPROTOKOLL.md` und `mail/docs/UX-07-PRUEFPROTOKOLL.md` dokumentiert: weder auf dem Testhost 204 noch auf der Entwicklungsmaschine ist ein Bildschirmleser (NVDA/VoiceOver/Orca) installiert. Als Ersatzbefund liegt der vollständige Accessibility-Tree (Rolle/Name/Zustand je Element, exakt das, was ein Bildschirmleser ansagen würde) jeder Oberfläche vor: `connect/docs/QA-08/accessibility-tree/`. Das ersetzt den manuellen Durchlauf nicht. +3. **Restbefunde sind mit Frist eingeplant** — **erfüllt für Connect-spezifische Befunde** (es gibt keine: 0 axe-Verstöße). Die unter AK2 dokumentierte Bildschirmleser-Lücke ist kein Connect-spezifischer Code-Befund, sondern dieselbe projektweite Werkzeug-/Organisationslücke, die bereits unter `UX-06` (DMS, Fristvorschlag 2026-10-31) nachverfolgt wird; eine zusätzliche, inhaltsgleiche Kachel auf `connect-kanban` würde dieselbe Lücke nur duplizieren. + +## Prüfungen vor der Abnahme + +1. **Automatisierte Prüfung über alle Hauptseiten** — bestanden, s. Tabelle oben (10 axe-Läufe: 2×4 Seitenaufrufe + 3 dynamische Zustände bei `dms2-import`, 0 Verstöße). +2. **Vollständiger Ablauf ohne Maus durchgeführt** — bestanden für alle vier Oberflächen, je ein vollständiger, inhaltlich sinnvoller Kernablauf (nicht nur Tab-Erreichbarkeit), 0 Mausereignisse, Ergebnis jeweils über das reale DOM/Backend bestätigt. +3. **Auditbericht abgelegt** — dieses Protokoll plus `connect/docs/QA-08/` (Accessibility-Trees, axe-Reports, Tastaturablauf-Logs). + +## Hinweise zur Durchführung (Werkzeug-Stolpersteine, kein Produktbefund) + +- `audit.js`s Standardpfad für `puppeteer-core`/`axe-core` (`/usr/local/lib/node_modules/pa11y/node_modules`) existiert auf Host 204 nicht; die tatsächliche globale `pa11y`-Installation liegt unter `/usr/lib/node_modules/pa11y/node_modules`. Alle Läufe wurden mit `A11Y_NODE_MODULES=/usr/lib/node_modules/pa11y/node_modules` aufgerufen. `audit.js` selbst wurde nicht geändert (gemeinsames Werkzeug mehrerer Kacheln, Änderung außerhalb des Ticketumfangs). +- Erster Testlauf gegen `connect-setup` schlug mit „verbindungen konnten nicht gelesen werden" fehl: selbstverschuldet (zwei aufeinanderfolgende manuelle Testläufe gegen dieselbe, nicht zurückgesetzte DB mit unterschiedlichem, zufällig erzeugtem `NEXARCH_CONNECT_APP_KEK` — der zweite Lauf konnte die mit dem ersten KEK verschlüsselte Zugangsdaten-Zeile nicht mehr entschlüsseln). Kein Produktbefund; seitdem wird die Wegwerf-DB vor jedem Lauf gelöscht und neu angelegt. +- Erster Tastaturablauf gegen `dms2-import` schlug mit `fieldmapping: invalid mapping: external field "content" targeted more than once` fehl, weil die Vorschau bereits `full_text → content` vorschlägt und der Testablauf zusätzlich `title → content` setzte. Echtes, korrektes Validierungsverhalten (bereits in `connect/docs/DMS2-02-PRUEFPROTOKOLL.md` als Testfehler dokumentiert), kein Produktbefund — Testablauf entsprechend angepasst (`sender`-Zuordnung wird vor der `title`-Umbelegung freigegeben). +- `connect/internal/dms2import`-Freitext zeigt eine kleine Inkonsistenz in der UI-Beschriftung (`"Dokument(e)"` in der Vorschau-Überschrift, `"Dokument(en)"` im Ergebnistext) — rein kosmetisch, keine Barrierefreiheits-Auswirkung, daher kein Befund im Sinne dieser Kachel; nicht behoben (Behebung liegt laut Ticketumfang bei der jeweiligen Frontend-Kachel, nicht bei QA-08). + +## Nicht Bestandteil dieser Kachel + +- Core `SHL-01` WCAG-Basis-Komponenten selbst — nur deren Nutzung wurde geprüft. +- Behebung gefundener Verstöße in den jeweiligen Frontend-Kacheln (entfällt hier: keine Verstöße gefunden). + +## Reproduktion + +``` +A11Y_NODE_MODULES=/usr/lib/node_modules/pa11y/node_modules \ +A11Y_CHROME=/usr/bin/google-chrome \ +A11Y_DSN='postgres://qa08_connect_test:@127.0.0.1:5432/qa08_connect_a11y' \ +OUT=/tmp/qa08-setup-out bash web/a11y-audit/qa08-connect-setup-run.sh +``` + +Analog `qa08-connect-status-run.sh`, `qa08-datev-export-run.sh`; `qa08-dms2-import-run.sh` zusätzlich mit `A11Y_FIXTURE_ZIP=` (Struktur: `manifest.json` + `documents/originals/`, siehe `connect/internal/dms2import/parse_test.go:buildFixtureZip`). Vor jedem Lauf `qa08_connect_a11y` droppen und neu anlegen (eigene Rolle `qa08_connect_test`). + +Kein Merge, kein Deploy. diff --git a/connect/docs/QA-08/accessibility-tree/connect-setup.txt b/connect/docs/QA-08/accessibility-tree/connect-setup.txt new file mode 100644 index 00000000..f1c01fd9 --- /dev/null +++ b/connect/docs/QA-08/accessibility-tree/connect-setup.txt @@ -0,0 +1,24 @@ +RootWebArea: "NEXARCH Connect einrichten" + main + heading: "Connectoren einrichten" + region: "Neue Verbindung" + heading: "Neue Verbindung" + form + StaticText: "Connector-Typ" + combobox: "Connector-Typ" = ERP-System [expanded=false] + option: "ERP-System" [selected] + option: "CRM-System" + option: "Microsoft 365" + option: "Backup-Ziel" + option: "DATEV" + StaticText: "Instanz-Kennung" + textbox: "Instanz-Kennung" [fokus] + StaticText: "Frei wählbarer Name dieser Verbindung, z. B. 'mandant-a-prod'." + StaticText: "API-Schlüssel" + textbox: "API-Schlüssel" + button: "Verbindung speichern" + region: "Eingerichtete Verbindungen" + heading: "Eingerichtete Verbindungen" + StaticText: "Noch keine Connector-Verbindungen eingerichtet." + status + alert diff --git a/connect/docs/QA-08/accessibility-tree/connect-status.txt b/connect/docs/QA-08/accessibility-tree/connect-status.txt new file mode 100644 index 00000000..ce805c5d --- /dev/null +++ b/connect/docs/QA-08/accessibility-tree/connect-status.txt @@ -0,0 +1,28 @@ +RootWebArea: "NEXARCH Verbindungsstatus" + main + heading: "Verbindungsstatus" + button: "Aktualisieren" [fokus] + StaticText: "Übersicht aller eingerichteten Connector-Verbindungen mit Status, letztem Sync-Lauf und Fehlerdiagnose" + StaticText: "Connector" + StaticText: "Instanz" + StaticText: "Status" + StaticText: "Letzter Lauf" + StaticText: "Fehlerdiagnose" + StaticText: "Nächster Lauf" + StaticText: "erp-demo" + StaticText: "acme-erp-1" + StaticText: "Erfolgreich" + StaticText: "16.10.2026, 14:29:48" + StaticText: " (geplant)" + StaticText: "—" + StaticText: "16.10.2026, 15:39:48" + StaticText: "crm-demo" + StaticText: "acme-crm-1" + StaticText: "Fehlgeschlagen" + StaticText: "16.10.2026, 14:34:48" + StaticText: " (manuell)" + status + StaticText: "verbindung zu drittsystem abgelehnt" + StaticText: "16.10.2026, 15:39:48" + status + alert diff --git a/connect/docs/QA-08/accessibility-tree/datev-export.txt b/connect/docs/QA-08/accessibility-tree/datev-export.txt new file mode 100644 index 00000000..ac7e99b1 --- /dev/null +++ b/connect/docs/QA-08/accessibility-tree/datev-export.txt @@ -0,0 +1,60 @@ +RootWebArea: "NEXARCH DATEV-Export" + main + heading: "DATEV-Export" + form + StaticText: "Beraternummer" + textbox: "Beraternummer" + StaticText: "Mandantennummer" + textbox: "Mandantennummer" + StaticText: "Wirtschaftsjahresbeginn" + Date: "Wirtschaftsjahresbeginn" + spinbutton: "Month Month" + StaticText: "/" + spinbutton: "Day Day" + StaticText: "/" + spinbutton: "Year Year" + button: "Show date picker Show date picker" + StaticText: "Sachkontenlänge" + spinbutton: "Sachkontenlänge" = 4 + StaticText: "Bezeichnung" + textbox: "Bezeichnung" + StaticText: "Zeitraum von" + Date: "Zeitraum von" + spinbutton: "Month Month" + StaticText: "/" + spinbutton: "Day Day" + StaticText: "/" + spinbutton: "Year Year" + button: "Show date picker Show date picker" + StaticText: "Zeitraum bis" + Date: "Zeitraum bis" + spinbutton: "Month Month" + StaticText: "/" + spinbutton: "Day Day" + StaticText: "/" + spinbutton: "Year Year" + button: "Show date picker Show date picker" [fokus] + button: "Export anstoßen" + heading: "Exportprotokoll" + StaticText: "Protokoll aller DATEV-Exportläufe mit Übertragungsstatus" + StaticText: "Lauf" + StaticText: "Zeitraum" + StaticText: "Belege" + StaticText: "Übertragungsstatus" + StaticText: "Fehlerdiagnose" + StaticText: "Erstellt" + StaticText: "b2d8a220" + StaticText: "2026-03-01 – 2026-03-31" + StaticText: "1" + StaticText: "Wiederholung ausstehend" + status + StaticText: "datevtransfer: zielsystem antwortete 503: simulierter uebertragungsfehler" + StaticText: "16.10.2026, 14:40:17" + StaticText: "9e23398d" + StaticText: "2026-03-01 – 2026-03-31" + StaticText: "1" + StaticText: "Erfolgreich" + StaticText: "—" + StaticText: "16.10.2026, 14:40:17" + status + alert diff --git a/connect/docs/QA-08/accessibility-tree/dms2-import-gate.txt b/connect/docs/QA-08/accessibility-tree/dms2-import-gate.txt new file mode 100644 index 00000000..bac08073 --- /dev/null +++ b/connect/docs/QA-08/accessibility-tree/dms2-import-gate.txt @@ -0,0 +1,10 @@ +RootWebArea: "NEXARCH Import-Assistent" + main + heading: "Import-Assistent fuer Drittsystem-Exporte" + region: "Export hochladen" + heading: "Export hochladen" + StaticText: "Drittsystem-Export (.zip)" + button: "Drittsystem-Export (.zip)" = No file chosen [fokus] + button: "Vorschau laden" + status + alert diff --git a/connect/docs/QA-08/rohdaten/connect-setup-axe-report.json b/connect/docs/QA-08/rohdaten/connect-setup-axe-report.json new file mode 100644 index 00000000..5c0ef28f --- /dev/null +++ b/connect/docs/QA-08/rohdaten/connect-setup-axe-report.json @@ -0,0 +1,96 @@ +[ + { + "url": "http://127.0.0.1:4194/setup", + "scheme": "light", + "axe": { + "violations": [], + "incomplete": [], + "passes": 22 + }, + "alert": null, + "contrast": [], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 3, + "main": 1, + "heading": 3, + "StaticText": 9, + "InlineTextBox": 9, + "region": 2, + "form": 1, + "generic": 5, + "LabelText": 3, + "combobox": 1, + "MenuListPopup": 1, + "option": 5, + "textbox": 2, + "paragraph": 2, + "button": 1, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 4, + "reached": 4, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4194/setup", + "scheme": "dark", + "axe": { + "violations": [], + "incomplete": [], + "passes": 22 + }, + "alert": null, + "contrast": [], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 3, + "main": 1, + "heading": 3, + "StaticText": 9, + "InlineTextBox": 9, + "region": 2, + "form": 1, + "generic": 5, + "LabelText": 3, + "combobox": 1, + "MenuListPopup": 1, + "option": 5, + "textbox": 2, + "paragraph": 2, + "button": 1, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 4, + "reached": 4, + "missing": 0, + "noIndicator": [], + "trapped": null + } + } +] \ No newline at end of file diff --git a/connect/docs/QA-08/rohdaten/connect-setup-tastaturablauf.txt b/connect/docs/QA-08/rohdaten/connect-setup-tastaturablauf.txt new file mode 100644 index 00000000..577fb45b --- /dev/null +++ b/connect/docs/QA-08/rohdaten/connect-setup-tastaturablauf.txt @@ -0,0 +1,6 @@ +ok axe: Formular leer (0 Verstoesse) +ok Verbindung erscheint nach Tastatur-Speichern in der Liste +ok Geheimnis erscheint nicht im Klartext im DOM +ok Zugangsdaten sind maskiert dargestellt ••••7766 +ok axe: Nach Speichern (Liste sichtbar) (0 Verstoesse) +ok kein Mausereignis im gesamten Ablauf mausereignisse=0 diff --git a/connect/docs/QA-08/rohdaten/connect-status-axe-report.json b/connect/docs/QA-08/rohdaten/connect-status-axe-report.json new file mode 100644 index 00000000..f7c6980d --- /dev/null +++ b/connect/docs/QA-08/rohdaten/connect-status-axe-report.json @@ -0,0 +1,92 @@ +[ + { + "url": "http://127.0.0.1:4195/status", + "scheme": "light", + "axe": { + "violations": [], + "incomplete": [], + "passes": 19 + }, + "alert": null, + "contrast": [], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 3, + "main": 1, + "heading": 1, + "StaticText": 23, + "InlineTextBox": 28, + "button": 1, + "table": 1, + "caption": 1, + "rowgroup": 1, + "row": 3, + "columnheader": 6, + "cell": 12, + "status": 2, + "generic": 1, + "alert": 1 + }, + "tableCount": 1, + "keyboard": { + "interactive": 1, + "reached": 1, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4195/status", + "scheme": "dark", + "axe": { + "violations": [], + "incomplete": [], + "passes": 19 + }, + "alert": null, + "contrast": [], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 3, + "main": 1, + "heading": 1, + "StaticText": 23, + "InlineTextBox": 28, + "button": 1, + "table": 1, + "caption": 1, + "rowgroup": 1, + "row": 3, + "columnheader": 6, + "cell": 12, + "status": 2, + "generic": 1, + "alert": 1 + }, + "tableCount": 1, + "keyboard": { + "interactive": 1, + "reached": 1, + "missing": 0, + "noIndicator": [], + "trapped": null + } + } +] \ No newline at end of file diff --git a/connect/docs/QA-08/rohdaten/connect-status-tastaturablauf.txt b/connect/docs/QA-08/rohdaten/connect-status-tastaturablauf.txt new file mode 100644 index 00000000..58255f71 --- /dev/null +++ b/connect/docs/QA-08/rohdaten/connect-status-tastaturablauf.txt @@ -0,0 +1,4 @@ +ok axe: Statusuebersicht (0 Verstoesse) +ok Statustabelle zeigt reale Zeilen vor dem Aktualisieren +ok Statustabelle nach Tastatur-Aktualisieren weiterhin vorhanden +ok kein Mausereignis im gesamten Ablauf mausereignisse=0 diff --git a/connect/docs/QA-08/rohdaten/datev-export-axe-report.json b/connect/docs/QA-08/rohdaten/datev-export-axe-report.json new file mode 100644 index 00000000..7a7ce658 --- /dev/null +++ b/connect/docs/QA-08/rohdaten/datev-export-axe-report.json @@ -0,0 +1,102 @@ +[ + { + "url": "http://127.0.0.1:4192/exports", + "scheme": "light", + "axe": { + "violations": [], + "incomplete": [], + "passes": 22 + }, + "alert": null, + "contrast": [], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 7, + "main": 1, + "heading": 2, + "StaticText": 45, + "InlineTextBox": 51, + "form": 1, + "generic": 18, + "LabelText": 7, + "textbox": 3, + "Date": 3, + "spinbutton": 10, + "button": 4, + "table": 1, + "caption": 1, + "rowgroup": 1, + "row": 3, + "columnheader": 6, + "cell": 12, + "status": 2, + "alert": 1 + }, + "tableCount": 1, + "keyboard": { + "interactive": 8, + "reached": 8, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4192/exports", + "scheme": "dark", + "axe": { + "violations": [], + "incomplete": [], + "passes": 22 + }, + "alert": null, + "contrast": [], + "layout": { + "horizontalScroll": false, + "clipped": [], + "missingKeys": 0, + "lang": "de", + "tables": [], + "overflowing": [] + }, + "roles": { + "RootWebArea": 1, + "none": 7, + "main": 1, + "heading": 2, + "StaticText": 45, + "InlineTextBox": 51, + "form": 1, + "generic": 18, + "LabelText": 7, + "textbox": 3, + "Date": 3, + "spinbutton": 10, + "button": 4, + "table": 1, + "caption": 1, + "rowgroup": 1, + "row": 3, + "columnheader": 6, + "cell": 12, + "status": 2, + "alert": 1 + }, + "tableCount": 1, + "keyboard": { + "interactive": 8, + "reached": 8, + "missing": 0, + "noIndicator": [], + "trapped": null + } + } +] \ No newline at end of file diff --git a/connect/docs/QA-08/rohdaten/datev-export-tastaturablauf.txt b/connect/docs/QA-08/rohdaten/datev-export-tastaturablauf.txt new file mode 100644 index 00000000..74aa78eb --- /dev/null +++ b/connect/docs/QA-08/rohdaten/datev-export-tastaturablauf.txt @@ -0,0 +1,11 @@ +ok axe: Formular + Protokoll (Ausgangszustand) (0 Verstoesse) +ok Feld 'Beraternummer' per Tab erreichbar +ok Feld 'Mandantennummer' per Tab erreichbar +ok Feld 'Wirtschaftsjahresbeginn' per Tab erreichbar +ok Feld 'Sachkontenlänge' per Tab erreichbar +ok Feld 'Bezeichnung' per Tab erreichbar +ok Feld 'Zeitraum von' per Tab erreichbar +ok Feld 'Zeitraum bis' per Tab erreichbar +ok Neuer Lauf erscheint nach Tastatur-Absenden im Protokoll +ok axe: Nach Tastatur-Export (Protokoll aktualisiert) (0 Verstoesse) +ok kein Mausereignis im gesamten Ablauf mausereignisse=0 diff --git a/connect/docs/QA-08/rohdaten/dms2-import-axe-report.json b/connect/docs/QA-08/rohdaten/dms2-import-axe-report.json new file mode 100644 index 00000000..f3598739 --- /dev/null +++ b/connect/docs/QA-08/rohdaten/dms2-import-axe-report.json @@ -0,0 +1,170 @@ +[ + { + "url": "http://127.0.0.1:4193/", + "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": 5, + "InlineTextBox": 5, + "form": 1, + "LabelText": 2, + "textbox": 2, + "generic": 4, + "paragraph": 1, + "button": 1, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 3, + "reached": 3, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4193/", + "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": 5, + "InlineTextBox": 5, + "form": 1, + "LabelText": 2, + "textbox": 2, + "generic": 4, + "paragraph": 1, + "button": 1, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 3, + "reached": 3, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4193/?user=u1&tenant=a11y", + "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": 2, + "StaticText": 4, + "InlineTextBox": 4, + "region": 1, + "LabelText": 1, + "button": 2, + "generic": 2, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 1, + "reached": 1, + "missing": 0, + "noIndicator": [], + "trapped": null + } + }, + { + "url": "http://127.0.0.1:4193/?user=u1&tenant=a11y", + "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": 2, + "StaticText": 4, + "InlineTextBox": 4, + "region": 1, + "LabelText": 1, + "button": 2, + "generic": 2, + "status": 1, + "alert": 1 + }, + "tableCount": 0, + "keyboard": { + "interactive": 1, + "reached": 1, + "missing": 0, + "noIndicator": [], + "trapped": null + } + } +] \ No newline at end of file diff --git a/connect/docs/QA-08/rohdaten/dms2-import-tastaturablauf.txt b/connect/docs/QA-08/rohdaten/dms2-import-tastaturablauf.txt new file mode 100644 index 00000000..48de2189 --- /dev/null +++ b/connect/docs/QA-08/rohdaten/dms2-import-tastaturablauf.txt @@ -0,0 +1,12 @@ +ok axe: Formular/Gate-Zustand (0 Verstoesse) +ok Datei-Eingabe ist per Tab erreichbar +ok Leertaste oeffnet den Dateidialog +ok Vorschau (Mapping-Tabelle) erscheint nach Enter auf 'Vorschau laden' +ok axe: Mapping-Tabelle nach Vorschau (0 Verstoesse) +ok Mapping-Auswahl 'Titel' per Tab erreichbar +ok Mapping-Wert per Tastatur-fokussiertem Select geaendert title -> correspondent +ok Mapping-Auswahl 'Absender' per Tab erreichbar +ok Import-Ergebnis erscheint nach Enter auf 'Import ausfuehren' +ok Ergebnis zeigt reale Zaehlung (1 von 1 erfolgreich, 0 fehlgeschlagen) 1 von 1 Dokument(en) erfolgreich importiert, 0 fehlgeschlagen. +ok axe: Ergebnis-Zustand (0 Verstoesse) +ok kein Mausereignis im gesamten Ablauf mausereignisse=0 diff --git a/web/a11y-audit/qa08-connect-setup-run.sh b/web/a11y-audit/qa08-connect-setup-run.sh new file mode 100755 index 00000000..b96ff6aa --- /dev/null +++ b/web/a11y-audit/qa08-connect-setup-run.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# QA-08: unabhaengiger Audit-Lauf gegen connect-setup (API-01) — eigener +# Start/Stop statt Wiederverwendung von connect-setup-run.sh, damit axe.js +# UND der echte Tastatur-Ablauf (qa08-keyboard-flows.js, A11Y_FLOW=setup) +# gegen dieselbe laufende Instanz laufen, bevor sie beendet wird. +# Env: A11Y_DSN, A11Y_CHROME +# Ergebnis: $OUT/report.json, $OUT/keyboard.txt ; Exit 1 bei Befund +set -uo pipefail +repo="$(cd "$(dirname "$0")/../.." && pwd)" +OUT="${OUT:-/tmp/qa08-setup2-out}"; mkdir -p "$OUT" +: "${A11Y_DSN:?A11Y_DSN fehlt}" +if [ -z "${A11Y_CHROME:-}" ]; then + A11Y_CHROME="$(ls -d "$HOME"/.cache/puppeteer/chrome/*/chrome-linux64/chrome /root/.cache/puppeteer/chrome/*/chrome-linux64/chrome 2>/dev/null | tail -1)" + [ -n "$A11Y_CHROME" ] || A11Y_CHROME="$(command -v google-chrome || command -v chromium || true)" +fi +: "${A11Y_CHROME:?A11Y_CHROME fehlt und kein Chrome gefunden}" +export A11Y_CHROME +: "${A11Y_KEK:=$(head -c 32 /dev/urandom | base64)}" + +API_PORT=9594 +WEB_PORT=4194 +PORTS="$API_PORT $WEB_PORT" +listener_pid() { ss -ltnpH "sport = :$1" 2>/dev/null | grep -o 'pid=[0-9]*' | head -1 | cut -d= -f2; } +stop_servers() { for port in $PORTS; do pid="$(listener_pid "$port")"; [ -n "$pid" ] && kill "$pid" 2>/dev/null; done; sleep 1; } +trap stop_servers EXIT +for port in $PORTS; do + if curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$port/"; then echo "Port $port ist belegt, Abbruch (kein fremder Prozess wird beendet)"; exit 2; fi +done + +(cd "$repo/connect" && go build -o "$OUT/setup-a11y-fixture" ./cmd/setup-a11y-fixture) || exit 2 +(cd "$repo/connect" && NEXARCH_CONNECT_TENANT_DSN="$A11Y_DSN" go run ./cmd/migrate) || { echo "migration fehlgeschlagen"; exit 2; } +NEXARCH_CONNECT_APP_TENANT_DSN="$A11Y_DSN" NEXARCH_CONNECT_APP_LISTEN_ADDR="127.0.0.1:$API_PORT" NEXARCH_CONNECT_APP_KEK="$A11Y_KEK" \ + setsid nohup "$OUT/setup-a11y-fixture" >"$OUT/setup-app.log" 2>&1 /dev/null 2>&1 + rm -rf node_modules/@nexarch/shl; mkdir -p node_modules/@nexarch/shl + (cd ../shl && tar cf - --exclude=node_modules --exclude=__tests__ .) | tar xf - -C node_modules/@nexarch/shl + rm -rf .next; npx next build >"$OUT/build-$d.log" 2>&1 || { echo "build $d fehlgeschlagen"; tail -20 "$OUT/build-$d.log"; exit 2; } + PORT=$WEB_PORT setsid nohup npx next start -p "$WEB_PORT" >"$OUT/serve-$d.log" 2>&1 "$OUT/pages.txt" +A11Y_PAGES="$OUT/pages.txt" A11Y_OUT="$OUT/report.json" node "$repo/web/a11y-audit/audit.js" | tee "$OUT/audit.txt" +[ "${PIPESTATUS[0]}" -eq 0 ] || rc=1 + +echo "--- echter Tastaturablauf (Connector-Typ waehlen, Felder ausfuellen, speichern) ---" +A11Y_FLOW=setup A11Y_WEB_URL="http://127.0.0.1:$WEB_PORT" node "$repo/web/a11y-audit/qa08-keyboard-flows.js" | tee "$OUT/keyboard.txt" +[ "${PIPESTATUS[0]}" -eq 0 ] || rc=1 + +exit "$rc" diff --git a/web/a11y-audit/qa08-connect-status-run.sh b/web/a11y-audit/qa08-connect-status-run.sh new file mode 100755 index 00000000..9896b224 --- /dev/null +++ b/web/a11y-audit/qa08-connect-status-run.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# QA-08: unabhaengiger Audit-Lauf gegen connect-status (API-02) — eigener +# Start/Stop statt Wiederverwendung von connect-status-run.sh, damit axe.js +# UND der echte Tastatur-Ablauf (qa08-keyboard-flows.js, A11Y_FLOW=status) +# gegen dieselbe laufende Instanz laufen, bevor sie beendet wird. +# Env: A11Y_DSN, A11Y_CHROME +# Ergebnis: $OUT/report.json, $OUT/keyboard.txt ; Exit 1 bei Befund +set -uo pipefail +repo="$(cd "$(dirname "$0")/../.." && pwd)" +OUT="${OUT:-/tmp/qa08-status2-out}"; mkdir -p "$OUT" +: "${A11Y_DSN:?A11Y_DSN fehlt}" +if [ -z "${A11Y_CHROME:-}" ]; then + A11Y_CHROME="$(ls -d "$HOME"/.cache/puppeteer/chrome/*/chrome-linux64/chrome /root/.cache/puppeteer/chrome/*/chrome-linux64/chrome 2>/dev/null | tail -1)" + [ -n "$A11Y_CHROME" ] || A11Y_CHROME="$(command -v google-chrome || command -v chromium || true)" +fi +: "${A11Y_CHROME:?A11Y_CHROME fehlt und kein Chrome gefunden}" +export A11Y_CHROME + +API_PORT=9595 +WEB_PORT=4195 +PORTS="$API_PORT $WEB_PORT" +listener_pid() { ss -ltnpH "sport = :$1" 2>/dev/null | grep -o 'pid=[0-9]*' | head -1 | cut -d= -f2; } +stop_servers() { for port in $PORTS; do pid="$(listener_pid "$port")"; [ -n "$pid" ] && kill "$pid" 2>/dev/null; done; sleep 1; } +trap stop_servers EXIT +for port in $PORTS; do + if curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$port/"; then echo "Port $port ist belegt, Abbruch (kein fremder Prozess wird beendet)"; exit 2; fi +done + +(cd "$repo/connect" && go build -o "$OUT/status-a11y-fixture" ./cmd/status-a11y-fixture) || exit 2 +(cd "$repo/connect" && NEXARCH_CONNECT_TENANT_DSN="$A11Y_DSN" go run ./cmd/migrate) || { echo "migration fehlgeschlagen"; exit 2; } +NEXARCH_CONNECT_APP_TENANT_DSN="$A11Y_DSN" NEXARCH_CONNECT_APP_LISTEN_ADDR="127.0.0.1:$API_PORT" \ + setsid nohup "$OUT/status-a11y-fixture" >"$OUT/connect-app.log" 2>&1 /dev/null 2>&1 + rm -rf node_modules/@nexarch/shl; mkdir -p node_modules/@nexarch/shl + (cd ../shl && tar cf - --exclude=node_modules --exclude=__tests__ .) | tar xf - -C node_modules/@nexarch/shl + rm -rf .next; npx next build >"$OUT/build-$d.log" 2>&1 || { echo "build $d fehlgeschlagen"; tail -20 "$OUT/build-$d.log"; exit 2; } + PORT=$WEB_PORT setsid nohup npx next start -p "$WEB_PORT" >"$OUT/serve-$d.log" 2>&1 "$OUT/pages.txt" +A11Y_PAGES="$OUT/pages.txt" A11Y_OUT="$OUT/report.json" node "$repo/web/a11y-audit/audit.js" | tee "$OUT/audit.txt" +[ "${PIPESTATUS[0]}" -eq 0 ] || rc=1 + +echo "--- echter Tastaturablauf (Aktualisieren) ---" +A11Y_FLOW=status A11Y_WEB_URL="http://127.0.0.1:$WEB_PORT" node "$repo/web/a11y-audit/qa08-keyboard-flows.js" | tee "$OUT/keyboard.txt" +[ "${PIPESTATUS[0]}" -eq 0 ] || rc=1 + +exit "$rc" diff --git a/web/a11y-audit/qa08-datev-export-run.sh b/web/a11y-audit/qa08-datev-export-run.sh new file mode 100755 index 00000000..4b205243 --- /dev/null +++ b/web/a11y-audit/qa08-datev-export-run.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# QA-08: startet den echten datev-a11y-fixture-Dienst (DAT-03, reale +# Handler, zwei echte Testlaeufe vorgeseeded: ein Erfolg, ein Fehlschlag), +# baut und startet web/datev-export dagegen, fuehrt axe/Layout-Audit aus +# (Muster: connect-status-run.sh). +# Env: A11Y_DSN (PostgreSQL), A11Y_CHROME (Chrome-Binary) +# Ergebnis: $OUT/report.json ; Exit 1 bei Befund +set -uo pipefail +repo="$(cd "$(dirname "$0")/../.." && pwd)" +OUT="${OUT:-/tmp/qa08-datev-out}"; mkdir -p "$OUT" +: "${A11Y_DSN:?A11Y_DSN fehlt}" +if [ -z "${A11Y_CHROME:-}" ]; then + A11Y_CHROME="$(ls -d "$HOME"/.cache/puppeteer/chrome/*/chrome-linux64/chrome /root/.cache/puppeteer/chrome/*/chrome-linux64/chrome 2>/dev/null | tail -1)" + [ -n "$A11Y_CHROME" ] || A11Y_CHROME="$(command -v google-chrome || command -v chromium || true)" +fi +: "${A11Y_CHROME:?A11Y_CHROME fehlt und kein Chrome gefunden}" +export A11Y_CHROME + +API_PORT=9592 +WEB_PORT=4192 +PORTS="$API_PORT $WEB_PORT" +listener_pid() { ss -ltnpH "sport = :$1" 2>/dev/null | grep -o 'pid=[0-9]*' | head -1 | cut -d= -f2; } +stop_servers() { for port in $PORTS; do pid="$(listener_pid "$port")"; [ -n "$pid" ] && kill "$pid" 2>/dev/null; done; sleep 1; } +trap stop_servers EXIT +for port in $PORTS; do + if curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$port/"; then echo "Port $port ist belegt, Abbruch (kein fremder Prozess wird beendet)"; exit 2; fi +done + +(cd "$repo/connect" && go build -o "$OUT/datev-a11y-fixture" ./cmd/datev-a11y-fixture) || exit 2 +(cd "$repo/connect" && NEXARCH_CONNECT_TENANT_DSN="$A11Y_DSN" go run ./cmd/migrate) || { echo "migration fehlgeschlagen"; exit 2; } +NEXARCH_CONNECT_APP_TENANT_DSN="$A11Y_DSN" NEXARCH_CONNECT_APP_LISTEN_ADDR="127.0.0.1:$API_PORT" \ + setsid nohup "$OUT/datev-a11y-fixture" >"$OUT/datev-app.log" 2>&1 /dev/null 2>&1 + rm -rf node_modules/@nexarch/shl; mkdir -p node_modules/@nexarch/shl + (cd ../shl && tar cf - --exclude=node_modules --exclude=__tests__ .) | tar xf - -C node_modules/@nexarch/shl + rm -rf .next; npx next build >"$OUT/build-$d.log" 2>&1 || { echo "build $d fehlgeschlagen"; tail -20 "$OUT/build-$d.log"; exit 2; } + PORT=$WEB_PORT setsid nohup npx next start -p "$WEB_PORT" >"$OUT/serve-$d.log" 2>&1 "$OUT/pages.txt" +A11Y_PAGES="$OUT/pages.txt" A11Y_OUT="$OUT/report.json" node "$repo/web/a11y-audit/audit.js" | tee "$OUT/audit.txt" +[ "${PIPESTATUS[0]}" -eq 0 ] || rc=1 + +echo "--- echter Tastaturablauf (Exportformular ausfuellen und absenden) ---" +A11Y_FLOW=datev A11Y_WEB_URL="http://127.0.0.1:$WEB_PORT" node "$repo/web/a11y-audit/qa08-keyboard-flows.js" | tee "$OUT/keyboard.txt" +[ "${PIPESTATUS[0]}" -eq 0 ] || rc=1 + +exit "$rc" diff --git a/web/a11y-audit/qa08-dms2-import-keyboard.js b/web/a11y-audit/qa08-dms2-import-keyboard.js new file mode 100644 index 00000000..7d828048 --- /dev/null +++ b/web/a11y-audit/qa08-dms2-import-keyboard.js @@ -0,0 +1,117 @@ +// QA-08: vollstaendiger Import-Assistent-Ablauf nur mit der Tastatur +// (Datei-Dialog per Leertaste, Mapping-Auswahl per Tastatur, Ausfuehren +// per Enter), gegen den echten connect-app-Dienst (DMS2-02). Mausereignisse +// werden mitgezaehlt, Ergebnis ueber das sichtbare DOM geprueft. Zusaetzlich +// laeuft axe-core an jedem der drei dynamischen UI-Zustaende (Gate/Formular, +// Mapping-Tabelle nach Vorschau, Ergebnis nach Ausfuehrung) im selben Browser, +// da audit.js (QA-11) nur frisch geladene URLs prueft, nicht client-seitigen +// React-Zustand nach Interaktion. +// A11Y_CHROME=... A11Y_WEB_URL=http://127.0.0.1:4193 A11Y_FIXTURE_ZIP=/tmp/x.zip node qa08-dms2-import-keyboard.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 : ""}`); }; + +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(", ")); + return res.violations; +} + +async function tabTo(page, describe, match, max = 30) { + 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, type: el.getAttribute("type"), id: el.id, text: (el.textContent || "").trim().slice(0, 60) }; + }); + if (cur && match(cur)) return cur; + } + throw new Error(`Element nicht per Tab erreichbar: ${describe}`); +} +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +(async () => { + const base = process.env.A11Y_WEB_URL; + const zip = process.env.A11Y_FIXTURE_ZIP; + if (!base) throw new Error("A11Y_WEB_URL fehlt"); + if (!zip) throw new Error("A11Y_FIXTURE_ZIP 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 page.goto(`${base}/?user=u1&tenant=a11y`, { waitUntil: "networkidle0" }); + await axeCheck(page, "Formular/Gate-Zustand"); + + // --- Datei per Tastatur auswaehlen (Tab, Leertaste oeffnet Dateidialog) --- + const reached = await tabTo(page, "Datei-Eingabe", (c) => c.tag === "INPUT" && c.type === "file").then(() => true, () => false); + check("Datei-Eingabe ist per Tab erreichbar", reached); + const chooserP = page.waitForFileChooser({ timeout: 5000 }).catch(() => null); + await page.keyboard.press("Space"); + const chooser = await chooserP; + check("Leertaste oeffnet den Dateidialog", !!chooser); + if (chooser) await chooser.accept([zip]); + + // --- Vorschau laden --- + await tabTo(page, "Vorschau laden", (c) => c.tag === "BUTTON" && /vorschau laden/i.test(c.text)); + await page.keyboard.press("Enter"); + const previewShown = await page.waitForFunction(() => document.body.innerText.includes("Dokument(e) gefunden"), { timeout: 8000 }).then(() => true, () => false); + check("Vorschau (Mapping-Tabelle) erscheint nach Enter auf 'Vorschau laden'", previewShown); + if (previewShown) await axeCheck(page, "Mapping-Tabelle nach Vorschau"); + + // --- Mapping-Feld per Tastatur aendern (erstes select: Titel) --- + const mappingSelect = await tabTo(page, "Mapping-Auswahl 'Titel'", (c) => c.tag === "SELECT" && c.id === "mapping-title"); + check("Mapping-Auswahl 'Titel' per Tab erreichbar", !!mappingSelect); + const before = await page.evaluate(() => document.getElementById("mapping-title").value); + await page.select("#mapping-title", "correspondent"); + const after = await page.evaluate(() => document.getElementById("mapping-title").value); + check("Mapping-Wert per Tastatur-fokussiertem Select geaendert", before !== after && after === "correspondent", `${before} -> ${after}`); + + // "Absender" gab bisher "correspondent" vor; wird freigegeben, sonst + // wiese fieldmapping.applyMapping die Zuordnung wegen Doppelbelegung + // korrekt zurueck (echtes, bereits in DMS2-02 nachgewiesenes Verhalten, + // kein Testfehler — siehe DMS2-02-Pruefprotokoll). + const senderSelect = await tabTo(page, "Mapping-Auswahl 'Absender'", (c) => c.tag === "SELECT" && c.id === "mapping-sender"); + check("Mapping-Auswahl 'Absender' per Tab erreichbar", !!senderSelect); + await page.select("#mapping-sender", ""); + + // --- Import ausfuehren --- + await tabTo(page, "Import ausfuehren", (c) => c.tag === "BUTTON" && /import ausf/i.test(c.text)); + await page.keyboard.press("Enter"); + await sleep(1500); + const resultShown = await page.waitForFunction(() => document.body.innerText.includes("erfolgreich importiert"), { timeout: 8000 }).then(() => true, () => false); + check("Import-Ergebnis erscheint nach Enter auf 'Import ausfuehren'", resultShown); + const resultText = await page.evaluate(() => { + const txt = document.body.innerText; + const m = txt.match(/\d+ von \d+ Dokument\(en\) erfolgreich importiert, \d+ fehlgeschlagen\./); + if (m) return m[0]; + const idx = txt.indexOf("erfolgreich importiert"); + return idx >= 0 ? JSON.stringify(txt.slice(Math.max(0, idx - 60), idx + 60)) : "(nicht gefunden) " + txt.slice(-400); + }); + check("Ergebnis zeigt reale Zaehlung (1 von 1 erfolgreich, 0 fehlgeschlagen)", resultText === "1 von 1 Dokument(en) erfolgreich importiert, 0 fehlgeschlagen.", resultText); + if (!resultShown && process.env.A11Y_DEBUG) console.log("BODY:", (await page.evaluate(() => document.body.innerText)).slice(0, 800)); + if (resultShown) await axeCheck(page, "Ergebnis-Zustand"); + + 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/web/a11y-audit/qa08-dms2-import-run.sh b/web/a11y-audit/qa08-dms2-import-run.sh new file mode 100755 index 00000000..dad8fea0 --- /dev/null +++ b/web/a11y-audit/qa08-dms2-import-run.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# QA-08: startet den echten connect-app-Dienst (DMS2-02-Routen, reale +# Handler) gegen eine Wegwerf-DB, baut und startet web/dms2-import dagegen, +# fuehrt axe/Layout-Audit auf dem Identitaets-Gate und auf dem Assistenten +# aus (Muster: connect-status-run.sh) sowie den echten Tastatur-Ablauf +# (qa08-dms2-import-keyboard.js: Datei waehlen, Vorschau, Mapping aendern, +# Import ausfuehren, alles ohne Maus). +# Env: A11Y_DSN (PostgreSQL), A11Y_CHROME (Chrome-Binary), A11Y_FIXTURE_ZIP +# Ergebnis: $OUT/report.json, $OUT/keyboard.txt ; Exit 1 bei Befund +set -uo pipefail +repo="$(cd "$(dirname "$0")/../.." && pwd)" +OUT="${OUT:-/tmp/qa08-dms2-out}"; mkdir -p "$OUT" +: "${A11Y_DSN:?A11Y_DSN fehlt}" +: "${A11Y_FIXTURE_ZIP:?A11Y_FIXTURE_ZIP fehlt}" +if [ -z "${A11Y_CHROME:-}" ]; then + A11Y_CHROME="$(ls -d "$HOME"/.cache/puppeteer/chrome/*/chrome-linux64/chrome /root/.cache/puppeteer/chrome/*/chrome-linux64/chrome 2>/dev/null | tail -1)" + [ -n "$A11Y_CHROME" ] || A11Y_CHROME="$(command -v google-chrome || command -v chromium || true)" +fi +: "${A11Y_CHROME:?A11Y_CHROME fehlt und kein Chrome gefunden}" +export A11Y_CHROME + +API_PORT=9593 +WEB_PORT=4193 +PORTS="$API_PORT $WEB_PORT" +listener_pid() { ss -ltnpH "sport = :$1" 2>/dev/null | grep -o 'pid=[0-9]*' | head -1 | cut -d= -f2; } +stop_servers() { for port in $PORTS; do pid="$(listener_pid "$port")"; [ -n "$pid" ] && kill "$pid" 2>/dev/null; done; sleep 1; } +trap stop_servers EXIT +for port in $PORTS; do + if curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$port/"; then echo "Port $port ist belegt, Abbruch (kein fremder Prozess wird beendet)"; exit 2; fi +done + +KEK="$(head -c 32 /dev/urandom | base64)" +(cd "$repo/connect" && go build -o "$OUT/connect-app" ./cmd/connect-app) || exit 2 +(cd "$repo/connect" && NEXARCH_CONNECT_TENANT_DSN="$A11Y_DSN" go run ./cmd/migrate) || { echo "migration fehlgeschlagen"; exit 2; } +NEXARCH_CONNECT_APP_TENANT_DSN="$A11Y_DSN" NEXARCH_CONNECT_APP_LISTEN_ADDR="127.0.0.1:$API_PORT" NEXARCH_CONNECT_APP_KEK="$KEK" \ + setsid nohup "$OUT/connect-app" >"$OUT/connect-app.log" 2>&1 /dev/null 2>&1 + rm -rf node_modules/@nexarch/shl; mkdir -p node_modules/@nexarch/shl + (cd ../shl && tar cf - --exclude=node_modules --exclude=__tests__ .) | tar xf - -C node_modules/@nexarch/shl + rm -rf .next; npx next build >"$OUT/build-$d.log" 2>&1 || { echo "build $d fehlgeschlagen"; tail -20 "$OUT/build-$d.log"; exit 2; } + PORT=$WEB_PORT DMS2_IMPORT_API_URL="http://127.0.0.1:$API_PORT" setsid nohup npx next start -p "$WEB_PORT" >"$OUT/serve-$d.log" 2>&1 "$OUT/pages.txt" +A11Y_PAGES="$OUT/pages.txt" A11Y_OUT="$OUT/report.json" node "$repo/web/a11y-audit/audit.js" | tee "$OUT/audit.txt" +[ "${PIPESTATUS[0]}" -eq 0 ] || rc=1 + +echo "--- echter Tastaturablauf + axe je dynamischem Zustand (Datei waehlen, Vorschau, Mapping, Import ausfuehren) ---" +A11Y_WEB_URL="http://127.0.0.1:$WEB_PORT" A11Y_FIXTURE_ZIP="$A11Y_FIXTURE_ZIP" node "$repo/web/a11y-audit/qa08-dms2-import-keyboard.js" | tee "$OUT/keyboard.txt" +[ "${PIPESTATUS[0]}" -eq 0 ] || rc=1 + +exit "$rc" diff --git a/web/a11y-audit/qa08-keyboard-flows.js b/web/a11y-audit/qa08-keyboard-flows.js new file mode 100644 index 00000000..9678fb82 --- /dev/null +++ b/web/a11y-audit/qa08-keyboard-flows.js @@ -0,0 +1,158 @@ +// QA-08: echte Ablaeufe nur mit der Tastatur (Tab/Shift+Tab/Enter/Space, +// Werte per page.keyboard.type bzw. page.select auf einem per Tab +// fokussierten Element) gegen connect-setup, connect-status und +// datev-export, im selben Browser zusaetzlich axe-core je Zustand (siehe +// qa08-dms2-import-keyboard.js fuer die Begruendung). +// A11Y_FLOW=setup|status|datev A11Y_WEB_URL=http://127.0.0.1:... node qa08-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 = 30) { + 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, type: el.getAttribute("type"), id: el.id, 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}`); +} + +async function labelFor(page, labelText) { + return page.evaluate((t) => { + const label = Array.from(document.querySelectorAll("label")).find((l) => l.textContent.includes(t)); + return label ? label.getAttribute("for") : null; + }, labelText); +} + +const flows = { + // API-01: Connector-Typ waehlen, Instanz-Kennung + API-Schluessel per + // Tastatur eingeben, speichern, maskierte Anzeige in der Liste pruefen. + async setup(page, base) { + await page.goto(`${base}/setup`, { waitUntil: "networkidle0" }); + await axeCheck(page, "Formular leer"); + + const typeId = await labelFor(page, "Connector-Typ"); + if (!typeId) throw new Error("Connector-Typ-Feld nicht gefunden"); + await tabTo(page, "Connector-Typ-Auswahl", (c) => c.id === typeId); + await page.select("#" + typeId, "erp"); + + const instanceId = await labelFor(page, "Instanz-Kennung"); + await tabTo(page, "Instanz-Kennung-Feld", (c) => c.id === instanceId); + await page.keyboard.type("qa08-tastatur-erp-1"); + + const apiKeyId = await labelFor(page, "API-Schlüssel"); + if (!apiKeyId) throw new Error("API-Schluessel-Feld nicht gefunden"); + await tabTo(page, "API-Schluessel-Feld", (c) => c.id === apiKeyId); + const secret = "qa08-tastatur-geheimnis-99887766"; + await page.keyboard.type(secret); + + await tabTo(page, "Verbindung speichern", (c) => c.tag === "BUTTON" && /verbindung speichern/i.test(c.text)); + await page.keyboard.press("Enter"); + const listed = await page.waitForFunction(() => document.body.innerText.includes("qa08-tastatur-erp-1"), { timeout: 10000 }).then(() => true, () => false); + check("Verbindung erscheint nach Tastatur-Speichern in der Liste", listed); + + const bodyHTML = await page.content(); + check("Geheimnis erscheint nicht im Klartext im DOM", !bodyHTML.includes(secret)); + + const maskedText = await page.evaluate(() => { + const cell = document.querySelector('[data-masked="true"]'); + return cell ? cell.textContent : null; + }); + check("Zugangsdaten sind maskiert dargestellt", !!maskedText && maskedText.startsWith("••••") && maskedText !== secret, maskedText); + await axeCheck(page, "Nach Speichern (Liste sichtbar)"); + }, + + // API-02: einzige interaktive Steuerung (Aktualisieren) per Tastatur + // ausloesen, Tabelle bleibt mit den geseedeten Laeufen bestehen. + async status(page, base) { + await page.goto(`${base}/status`, { waitUntil: "networkidle0" }); + await axeCheck(page, "Statusuebersicht"); + const before = await page.evaluate(() => document.body.innerText.includes("acme-erp-1") || document.querySelectorAll("table tbody tr").length > 0); + check("Statustabelle zeigt reale Zeilen vor dem Aktualisieren", before); + + await tabTo(page, "Aktualisieren", (c) => c.tag === "BUTTON" && /aktualisieren/i.test(c.text)); + await page.keyboard.press("Enter"); + await sleep(1000); + const after = await page.evaluate(() => document.querySelectorAll("table tbody tr").length > 0); + check("Statustabelle nach Tastatur-Aktualisieren weiterhin vorhanden", after); + }, + + // DAT-03: vollstaendiges Exportformular per Tastatur ausfuellen und + // absenden, neuer Lauf erscheint im Protokoll. + async datev(page, base) { + await page.goto(`${base}/exports`, { waitUntil: "networkidle0" }); + await axeCheck(page, "Formular + Protokoll (Ausgangszustand)"); + const before = await page.evaluate(() => document.querySelectorAll("table tbody tr").length); + + // Datumsfelder (type="date", en-US-Segmentreihenfolge des Testhosts): + // Ziffern ohne Trenner, MMTTJJJJ, der Browser springt automatisch zum + // naechsten Segment (siehe Sondierung vor diesem Lauf). + const fields = [ + ["Beraternummer", "9999"], + ["Mandantennummer", "77"], + ["Wirtschaftsjahresbeginn", "01012026"], + ["Sachkontenlänge", "4"], + ["Bezeichnung", "qa08-tastatur-export"], + ["Zeitraum von", "03012026"], + ["Zeitraum bis", "03312026"], + ]; + for (const [label, value] of fields) { + const id = await labelFor(page, label); + if (!id) throw new Error(`Feld nicht gefunden: ${label}`); + const el = await tabTo(page, label, (c) => c.id === id).catch(() => null); + check(`Feld '${label}' per Tab erreichbar`, !!el); + if (!el) continue; + await page.keyboard.type(String(value)); + } + + await tabTo(page, "Export anstoßen (Submit)", (c) => c.tag === "BUTTON" && /anstoßen/i.test(c.text)); + await page.keyboard.press("Enter"); + const grew = await page.waitForFunction((n) => document.querySelectorAll("table tbody tr").length > n, { timeout: 10000 }, before).then(() => true, () => false); + check("Neuer Lauf erscheint nach Tastatur-Absenden im Protokoll", grew); + await axeCheck(page, "Nach Tastatur-Export (Protokoll aktualisiert)"); + }, +}; + +(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); +})();