QA-08 (Connect): Barrierefreiheits-Audit #321
No files matched your search
@@ -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-<timestamp>` (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:<pw>@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=<Pfad zu einem synthetischen paperless-ngx-Exportarchiv>` (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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
Executable
+59
@@ -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 &
|
||||
for _ in $(seq 40); do curl -s -o /dev/null -H "X-User-Id: u1" -H "X-Tenant-Slug: acme" "http://127.0.0.1:$API_PORT/connectors/catalog" && break; sleep 0.5; done
|
||||
curl -s -o /dev/null -H "X-User-Id: u1" -H "X-Tenant-Slug: acme" "http://127.0.0.1:$API_PORT/connectors/catalog" || { echo "setup-a11y-fixture startet nicht"; cat "$OUT/setup-app.log"; exit 2; }
|
||||
|
||||
export NEXT_PUBLIC_CONNECT_API_URL="http://127.0.0.1:$API_PORT"
|
||||
export NEXT_PUBLIC_USER_ID="u1"
|
||||
export NEXT_PUBLIC_TENANT_SLUG="acme"
|
||||
d=connect-setup
|
||||
(cd "$repo/web/$d" && { [ -d node_modules ] || npm install --no-audit --no-fund --loglevel=error; } >/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 </dev/null &
|
||||
)
|
||||
for _ in $(seq 60); do curl -s -o /dev/null "http://127.0.0.1:$WEB_PORT/setup" && break; sleep 0.5; done
|
||||
curl -s -o /dev/null "http://127.0.0.1:$WEB_PORT/setup" || { echo "$d startet nicht"; cat "$OUT/serve-$d.log"; exit 2; }
|
||||
|
||||
rc=0
|
||||
echo "http://127.0.0.1:$WEB_PORT/setup" > "$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"
|
||||
Executable
+80
@@ -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 &
|
||||
for _ in $(seq 40); do curl -s -o /dev/null -H "X-User-Id: u1" -H "X-Tenant-Slug: acme" "http://127.0.0.1:$API_PORT/connectors/status" && break; sleep 0.5; done
|
||||
curl -s -o /dev/null -H "X-User-Id: u1" -H "X-Tenant-Slug: acme" "http://127.0.0.1:$API_PORT/connectors/status" || { echo "status-a11y-fixture startet nicht"; cat "$OUT/connect-app.log"; exit 2; }
|
||||
|
||||
python3 <<PYEOF
|
||||
import subprocess, sys, os
|
||||
dsn = os.environ["A11Y_DSN"]
|
||||
sql = '''
|
||||
INSERT INTO sync_jobs (id, tenant_slug, connector_name, connector_instance_id, interval_seconds, next_run_at)
|
||||
VALUES
|
||||
('11111111-1111-1111-1111-111111111111','acme','erp-demo','acme-erp-1',3600, now() + interval '1 hour'),
|
||||
('22222222-2222-2222-2222-222222222222','acme','crm-demo','acme-crm-1',3600, now() + interval '1 hour')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
INSERT INTO sync_job_runs (id, sync_job_id, trigger, status, started_at, finished_at, last_error)
|
||||
VALUES
|
||||
('33333333-3333-3333-3333-333333333333','11111111-1111-1111-1111-111111111111','scheduled','succeeded', now() - interval '10 minutes', now() - interval '9 minutes', NULL),
|
||||
('44444444-4444-4444-4444-444444444444','22222222-2222-2222-2222-222222222222','manual','failed', now() - interval '5 minutes', now() - interval '4 minutes', 'verbindung zu drittsystem abgelehnt')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
'''
|
||||
r = subprocess.run(["psql", dsn, "-v", "ON_ERROR_STOP=1", "-c", sql], capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
print(r.stdout, r.stderr, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PYEOF
|
||||
[ $? -eq 0 ] || { echo "seed fehlgeschlagen"; exit 2; }
|
||||
|
||||
export NEXT_PUBLIC_CONNECT_API_URL="http://127.0.0.1:$API_PORT"
|
||||
export NEXT_PUBLIC_USER_ID="u1"
|
||||
export NEXT_PUBLIC_TENANT_SLUG="acme"
|
||||
d=connect-status
|
||||
(cd "$repo/web/$d" && { [ -d node_modules ] || npm install --no-audit --no-fund --loglevel=error; } >/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 </dev/null &
|
||||
)
|
||||
for _ in $(seq 60); do curl -s -o /dev/null "http://127.0.0.1:$WEB_PORT/status" && break; sleep 0.5; done
|
||||
curl -s -o /dev/null "http://127.0.0.1:$WEB_PORT/status" || { echo "$d startet nicht"; cat "$OUT/serve-$d.log"; exit 2; }
|
||||
|
||||
rc=0
|
||||
echo "http://127.0.0.1:$WEB_PORT/status" > "$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"
|
||||
Executable
+58
@@ -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 &
|
||||
for _ in $(seq 40); do curl -s -o /dev/null -H "X-User-Id: u1" -H "X-Tenant-Slug: a11y" "http://127.0.0.1:$API_PORT/datev/exports" && break; sleep 0.5; done
|
||||
curl -s -o /dev/null -H "X-User-Id: u1" -H "X-Tenant-Slug: a11y" "http://127.0.0.1:$API_PORT/datev/exports" || { echo "datev-a11y-fixture startet nicht"; cat "$OUT/datev-app.log"; exit 2; }
|
||||
|
||||
export NEXT_PUBLIC_CONNECT_API_URL="http://127.0.0.1:$API_PORT"
|
||||
export NEXT_PUBLIC_USER_ID="u1"
|
||||
export NEXT_PUBLIC_TENANT_SLUG="a11y"
|
||||
d=datev-export
|
||||
(cd "$repo/web/$d" && { [ -d node_modules ] || npm install --no-audit --no-fund --loglevel=error; } >/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 </dev/null &
|
||||
)
|
||||
for _ in $(seq 60); do curl -s -o /dev/null "http://127.0.0.1:$WEB_PORT/exports" && break; sleep 0.5; done
|
||||
curl -s -o /dev/null "http://127.0.0.1:$WEB_PORT/exports" || { echo "$d startet nicht"; cat "$OUT/serve-$d.log"; exit 2; }
|
||||
|
||||
rc=0
|
||||
echo "http://127.0.0.1:$WEB_PORT/exports" > "$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"
|
||||
@@ -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);
|
||||
})();
|
||||
Executable
+60
@@ -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 &
|
||||
for _ in $(seq 40); do curl -s -o /dev/null "http://127.0.0.1:$API_PORT/healthz" && break; sleep 0.5; done
|
||||
curl -s -o /dev/null "http://127.0.0.1:$API_PORT/healthz" || { echo "connect-app startet nicht"; cat "$OUT/connect-app.log"; exit 2; }
|
||||
|
||||
export DMS2_IMPORT_API_URL="http://127.0.0.1:$API_PORT"
|
||||
d=dms2-import
|
||||
(cd "$repo/web/$d" && { [ -d node_modules ] || npm install --no-audit --no-fund --loglevel=error; } >/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 </dev/null &
|
||||
)
|
||||
for _ in $(seq 60); do curl -s -o /dev/null "http://127.0.0.1:$WEB_PORT/" && break; sleep 0.5; done
|
||||
curl -s -o /dev/null "http://127.0.0.1:$WEB_PORT/" || { echo "$d startet nicht"; cat "$OUT/serve-$d.log"; exit 2; }
|
||||
|
||||
rc=0
|
||||
printf '%s\n%s\n' "http://127.0.0.1:$WEB_PORT/" "http://127.0.0.1:$WEB_PORT/?user=u1&tenant=a11y" > "$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"
|
||||
@@ -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);
|
||||
})();
|
||||
Reference in new issue
Block a user