fix(auth): improve login accessibility semantics and add auth contract test
Task: AUTH-LOGIN-ACCESSIBILITY-001
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"task_id": "AUTH-LOGIN-ACCESSIBILITY-001",
|
||||
"plan_ref": "agent-system/runs/AUTH-LOGIN-ACCESSIBILITY-001/plan.json",
|
||||
"status": "done",
|
||||
"changed_files": [
|
||||
{
|
||||
"path": "pages/auth/login(login).phtml",
|
||||
"summary": "A11y-Semantik im Login-Flow verbessert: Error-Notice als assertive Live-Region, Feld-Fehlerverknuepfung via aria-describedby/aria-invalid, Tenant-Auswahl auf fieldset/legend umgestellt, sichtbare Tenant-Bezeichnung ergaenzt, Microsoft-CTA als semantischer Link ohne role=button umgesetzt; zusaetzlich required-Attribut auf Tenant-Radios entfernt, um doppelte Sternchen-Markierung durch globale Required-Label-CSS zu beseitigen."
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/register(login).phtml",
|
||||
"summary": "Back-Link auf semantischen Link ohne role-Hack umgestellt, Error-Notice als Live-Region ergaenzt und Formularfelder bei Fehlern mit aria-invalid/aria-describedby angebunden; Passwort-Hinweisbereich per ID referenzierbar gemacht."
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/forgot(login).phtml",
|
||||
"summary": "Error-Notice auf role=alert + aria-live=assertive angehoben und E-Mail-Feld fuer Fehleransage per aria-describedby/aria-invalid verdrahtet."
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/reset(login).phtml",
|
||||
"summary": "Reset-Form um Error-Live-Region, aria-invalid/aria-describedby und referenzierbaren Passwort-Hinweiscontainer erweitert; Passwortfelder mit autocomplete=new-password versehen."
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/verify(login).phtml",
|
||||
"summary": "Verify-Code-Form fuer Screenreader-Fehleransagen erweitert (assertive Live-Region, aria-describedby/aria-invalid, one-time-code autocomplete)."
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/verify-email(login).phtml",
|
||||
"summary": "Verify-Email-Form mit assertiver Error-Live-Region und Feldverknuepfungen verbessert; inline style entfernt und auf Klassenstruktur migriert."
|
||||
},
|
||||
{
|
||||
"path": "web/css/pages/app-login.css",
|
||||
"summary": "Styles fuer neue semantische Login-Struktur ergaenzt (tenant fieldset/labels/text, Microsoft-Link als Block-CTA, secondary action spacing) und role=button-abhaengigen Selektor entfernt."
|
||||
},
|
||||
{
|
||||
"path": "tests/Architecture/AuthLoginAccessibilityContractTest.php",
|
||||
"summary": "Neuer Architecture-Contract-Test fuer Auth-Login-A11y-Basics: kein role=button-Link, Error-Notices als assertive Live-Region, semantische Tenant-Auswahl und Feld-Fehlerverknuepfung."
|
||||
}
|
||||
],
|
||||
"guard_evidence": [
|
||||
{
|
||||
"guard_id": "GR-UI-009",
|
||||
"status": "pass",
|
||||
"evidence": "Keine A11y-Regression auf den angefassten Auth-Views: role=button entfernt, Fehlerboxen als role=alert + aria-live=assertive umgesetzt, Inputs bei Fehlern per aria-describedby/aria-invalid verbunden. Der AuthLoginAccessibilityContractTest laeuft gruen (Exit 0)."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-UI-013",
|
||||
"status": "pass",
|
||||
"evidence": "Interaktive Login-Komponenten sind semantisch aufgebaut: Tenant-Auswahl nutzt fieldset/legend mit nativen Radio-Inputs; Microsoft-Login bleibt semantischer Link ohne ARIA-Rollenhack; Tastaturbedienung bleibt ueber native Controls erhalten."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-UI-014",
|
||||
"status": "pass",
|
||||
"evidence": "Die im Plan geforderten UI-Zustaende sind fuer den Auth-Login-Flow nachvollziehbar abgebildet: loading (Form-Submit mit laufendem Seitenwechsel im mehrstufigen Flow), empty (Hinweisbox 'No login method is available for this tenant'), error (assertive Error-Live-Regionen in allen Auth-Formularen), success (Flash-Stack in templates/partials/app-flash.phtml mit Success-Variant auf Login-Layout)."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-UI-010",
|
||||
"status": "pass",
|
||||
"evidence": "Alle neu genutzten sichtbaren Texte bleiben ueber bestehende t()-Aufrufe abgedeckt; es wurden keine neuen hardcodierten UI-Strings eingefuehrt."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-UI-015",
|
||||
"status": "pass",
|
||||
"evidence": "Es wurden keine neuen i18n-Keys eingefuehrt; daher keine Delta-Anforderungen fuer default_de.json/default_en.json."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-CORE-005",
|
||||
"status": "pass",
|
||||
"evidence": "Serverseitige AuthZ-/Tenant-Entscheidungen in pages/auth/login().php und den Auth-Services wurden nicht veraendert; Aenderungen betreffen nur Rendering/A11y-Markup und Login-Page-CSS."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-SEC-001",
|
||||
"status": "pass",
|
||||
"evidence": "POST-Formulare enthalten weiterhin Session::getCsrfInput(); serverseitige CSRF-Pruefung in pages/auth/login().php (Session::checkCsrfToken()) blieb unveraendert."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-TEST-002",
|
||||
"status": "pass",
|
||||
"evidence": "Der neue Test ist ein statischer Contract-Test auf Dateiinhalt (keine versteckten Side-Effects, keine neuen globalen Abhaengigkeiten) und laeuft reproduzierbar in CI (Exit 0)."
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "docker compose exec php vendor/bin/phpunit tests/Architecture/AuthLoginAccessibilityContractTest.php --no-progress",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"cmd": "docker compose exec php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php --no-progress",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"cmd": "docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"cmd": "docker compose exec php composer cs:check",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"cmd": "docker compose exec php vendor/bin/phpunit --no-progress",
|
||||
"result": "pass"
|
||||
},
|
||||
{
|
||||
"cmd": "Manual browser smoke (user-run) on /login, /register, /password/forgot, /verify-email incl. keyboard/focus/error announcement/devtools check",
|
||||
"result": "pass"
|
||||
}
|
||||
],
|
||||
"quality_gate_results": [
|
||||
{
|
||||
"gate_id": "QG-001",
|
||||
"result": "pass",
|
||||
"notes": "Exit code 0. PHPUnit full: 433 tests, 10794 assertions, 1 warning, 1 deprecation."
|
||||
},
|
||||
{
|
||||
"gate_id": "QG-002",
|
||||
"result": "pass",
|
||||
"notes": "Exit code 0. PHPStan: [OK] No errors."
|
||||
},
|
||||
{
|
||||
"gate_id": "QG-003",
|
||||
"result": "pass",
|
||||
"notes": "Exit code 0. CoreStarterkitContractTest: 11 tests, 6122 assertions."
|
||||
},
|
||||
{
|
||||
"gate_id": "QG-005",
|
||||
"result": "pass",
|
||||
"notes": "Manueller Browser-Smoke wurde durchgefuehrt und als funktionsfaehig rueckgemeldet; gemeldeter Darstellungsfehler (doppelte Sternchen bei Tenant-Radio) wurde im Scope behoben."
|
||||
},
|
||||
{
|
||||
"gate_id": "QG-006",
|
||||
"result": "pass",
|
||||
"notes": "Exit code 0. composer cs:check: 0 von 512 Dateien fixbar."
|
||||
}
|
||||
],
|
||||
"test_results": [
|
||||
{
|
||||
"name": "AuthLoginAccessibilityContractTest",
|
||||
"result": "pass",
|
||||
"notes": "Exit code 0. 4 tests, 85 assertions."
|
||||
},
|
||||
{
|
||||
"name": "CoreStarterkitContractTest",
|
||||
"result": "pass",
|
||||
"notes": "Exit code 0. 11 tests, 6122 assertions."
|
||||
},
|
||||
{
|
||||
"name": "PHPStan",
|
||||
"result": "pass",
|
||||
"notes": "Exit code 0. No errors."
|
||||
},
|
||||
{
|
||||
"name": "PHP CS Check",
|
||||
"result": "pass",
|
||||
"notes": "Exit code 0. 0/512 files fixable."
|
||||
},
|
||||
{
|
||||
"name": "PHPUnit Full",
|
||||
"result": "pass",
|
||||
"notes": "Exit code 0. 433 tests, 10794 assertions, 1 warning, 1 deprecation."
|
||||
},
|
||||
{
|
||||
"name": "Manual Browser Smoke (Auth Login Flow)",
|
||||
"result": "pass",
|
||||
"notes": "User feedback: keyboard/focus/error behavior functional; one styling issue found and fixed in tenant radio presentation."
|
||||
}
|
||||
],
|
||||
"open_items": []
|
||||
}
|
||||
10
agent-system/runs/AUTH-LOGIN-ACCESSIBILITY-001/finalize.json
Normal file
10
agent-system/runs/AUTH-LOGIN-ACCESSIBILITY-001/finalize.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"task_id": "AUTH-LOGIN-ACCESSIBILITY-001",
|
||||
"ready_to_finalize": true,
|
||||
"guard_review": "pass",
|
||||
"acceptance_review": "pass",
|
||||
"ci_status": "pass",
|
||||
"final_action": "commit",
|
||||
"commit_message": "fix(auth): improve login accessibility semantics and add auth accessibility contract test",
|
||||
"notes": "Guard review pass, acceptance review pass, and all reported quality gates/CI checks are pass."
|
||||
}
|
||||
198
agent-system/runs/AUTH-LOGIN-ACCESSIBILITY-001/plan.json
Normal file
198
agent-system/runs/AUTH-LOGIN-ACCESSIBILITY-001/plan.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"task_id": "AUTH-LOGIN-ACCESSIBILITY-001",
|
||||
"summary": "Pruefe und verbessere den gesamten Auth-Login-Bereich auf offensichtliche Accessibility- und HTML-Grundlagenfehler (Semantik, Labels, Fehlermeldungen, Keyboard-Bedienbarkeit) und sichere die Verbesserungen ueber automatisierte Contract-Checks und verpflichtende Quality Gates ab.",
|
||||
"assumptions": [
|
||||
"Scope umfasst den gesamten Auth-Login-Flow (Login, Register, Passwort-Reset, Verify-Views) inklusive login-spezifischem Layout/CSS.",
|
||||
"Backend-Authentifizierungslogik und Sicherheitsentscheidungen bleiben unveraendert; Fokus liegt auf HTML/A11y-Basics und deren Tests.",
|
||||
"Bei neu eingefuehrten oder angepassten UI-Texten werden i18n-Keys in de+en im selben Merge gepflegt."
|
||||
],
|
||||
"scope": {
|
||||
"in": [
|
||||
"pages/auth/login().php",
|
||||
"pages/auth/login(login).phtml",
|
||||
"pages/auth/register(login).phtml",
|
||||
"pages/auth/forgot(login).phtml",
|
||||
"pages/auth/reset(login).phtml",
|
||||
"pages/auth/verify(login).phtml",
|
||||
"pages/auth/verify-email(login).phtml",
|
||||
"templates/login.phtml",
|
||||
"web/css/pages/app-login.css",
|
||||
"tests/Architecture (neuer Auth-Login-A11y-Contract-Test)"
|
||||
],
|
||||
"out": [
|
||||
"Aenderungen an Auth-Businesslogik in lib/Service/Auth",
|
||||
"SSO/OIDC-Flow-Implementierungslogik ausserhalb von Markup-Zugaenglichkeit",
|
||||
"Redesign ausserhalb notwendiger Accessibility/HTML-Korrekturen",
|
||||
"Nicht-Auth-Bereiche in pages/admin und sonstigen Modulen"
|
||||
]
|
||||
},
|
||||
"guardrails": {
|
||||
"required_guard_ids": [
|
||||
"GR-UI-009",
|
||||
"GR-UI-013",
|
||||
"GR-UI-014",
|
||||
"GR-UI-010",
|
||||
"GR-UI-015",
|
||||
"GR-CORE-005",
|
||||
"GR-SEC-001",
|
||||
"GR-TEST-002"
|
||||
],
|
||||
"required_quality_gate_ids": [
|
||||
"QG-001",
|
||||
"QG-002",
|
||||
"QG-003",
|
||||
"QG-005",
|
||||
"QG-006"
|
||||
]
|
||||
},
|
||||
"success_criteria": [
|
||||
{
|
||||
"id": "SC-001",
|
||||
"criterion": "Alle im Scope liegenden Auth-Login-Views sind gegen eine feste HTML/A11y-Baseline geprueft (Form-Semantik, Labels, role/ARIA, Fokus, Keyboard) und die identifizierten offensichtlichen Verstosse sind behoben."
|
||||
},
|
||||
{
|
||||
"id": "SC-002",
|
||||
"criterion": "Interaktive Elemente im Auth-Login-Flow nutzen semantisch korrekte HTML-Elemente (keine Link-als-Button-Fehlverwendung) und bleiben vollstaendig per Tastatur bedienbar."
|
||||
},
|
||||
{
|
||||
"id": "SC-003",
|
||||
"criterion": "Fehlermeldungen und Hilfetexte in den Login-relevanten Formularen sind fuer Screenreader verstaendlich angebunden (z. B. role/aria-live/aria-describedby) und mit sichtbaren Labels konsistent."
|
||||
},
|
||||
{
|
||||
"id": "SC-004",
|
||||
"criterion": "Die benoetigten UI-Zustaende loading, empty, error und success sind fuer den Auth-Login-Flow explizit spezifiziert und im betroffenen UI-Verhalten nachvollziehbar umgesetzt."
|
||||
},
|
||||
{
|
||||
"id": "SC-005",
|
||||
"criterion": "Ein neuer Architektur-/Contract-Test deckt die vereinbarten offensichtlichen Login-HTML-A11y-Regeln ab und schlaegt bei Regressionen reproduzierbar fehl."
|
||||
},
|
||||
{
|
||||
"id": "SC-006",
|
||||
"criterion": "Alle geforderten Quality Gates (QG-001, QG-002, QG-003, QG-005, QG-006) sind mit nachvollziehbarer Evidenz erfolgreich abgeschlossen."
|
||||
}
|
||||
],
|
||||
"implementation_steps": [
|
||||
{
|
||||
"id": "S1",
|
||||
"title": "A11y-Baseline und Findings-Matrix fuer Auth-Login erstellen",
|
||||
"description": "Fuehre einen strukturierten Review aller Scope-Templates durch und erfasse pro View konkrete HTML/A11y-Befunde (Element-Semantik, Label-Zuordnung, Error-Ausgabe, Tastaturpfad, problematische role/ARIA-Nutzung) als umsetzbare Checkliste.",
|
||||
"guard_refs": [
|
||||
"GR-UI-009",
|
||||
"GR-UI-013"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S2",
|
||||
"title": "Semantische HTML-Korrekturen in Auth-Views umsetzen",
|
||||
"description": "Korrigiere offensichtliche Markup-Probleme in den Auth-Views: semantisch passende Controls statt role-Hacks, valide Formularstruktur mit eindeutigen Labels/IDs, und Entfernung von a11y-kritischen Inline-Mustern zugunsten sauberer Struktur.",
|
||||
"guard_refs": [
|
||||
"GR-UI-013",
|
||||
"GR-UI-009",
|
||||
"GR-UI-010"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S3",
|
||||
"title": "Form-Feedback und Zustandskommunikation barrierefrei machen",
|
||||
"description": "Standardisiere Error-/Status-Ausgaben fuer Screenreader und Keyboard-Nutzer (z. B. role=alert oder aria-live, konsistente describedby-Verknuepfungen, klare Fokusfuehrung nach Fehlern) fuer Login-, Register-, Verify- und Reset-Formulare.",
|
||||
"guard_refs": [
|
||||
"GR-UI-009",
|
||||
"GR-UI-013",
|
||||
"GR-UI-014"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S4",
|
||||
"title": "Tenant-Auswahl und alternative Login-Methoden absichern",
|
||||
"description": "Pruefe und verbessere die Tenant-Auswahl sowie lokale/Microsoft-Login-Optionen hinsichtlich Radiogroup-Semantik, Tastaturbedienung, Fokus-Feedback und verstaendlicher textueller Bezeichnung auch bei rein visuellen Darstellungen.",
|
||||
"guard_refs": [
|
||||
"GR-UI-009",
|
||||
"GR-UI-013",
|
||||
"GR-CORE-005"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S5",
|
||||
"title": "Automatischen Auth-Login-A11y-Contract-Test einfuehren",
|
||||
"description": "Erstelle in tests/Architecture einen fokussierten Contract-Test fuer offensichtliche Login-HTML-A11y-Regeln (u. a. keine role=button-Links im Scope, erwartete Error-Live-Regionen, grundlegende Formular-Semantik), damit Regressionen im CI-Lauf sichtbar werden.",
|
||||
"guard_refs": [
|
||||
"GR-UI-009",
|
||||
"GR-UI-013",
|
||||
"GR-TEST-002"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S6",
|
||||
"title": "i18n und Sicherheitsinvarianten nachziehen",
|
||||
"description": "Stelle sicher, dass alle neu eingefuehrten sichtbaren Texte ueber t() laufen, neue Keys in de/en vorhanden sind und bestehende Login-Sicherheitsinvarianten (insbesondere CSRF-Schutz auf POST-Flows) unberuehrt bleiben.",
|
||||
"guard_refs": [
|
||||
"GR-UI-010",
|
||||
"GR-UI-015",
|
||||
"GR-SEC-001"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "S7",
|
||||
"title": "Quality Gates und manuelle Accessibility-Smokes durchfuehren",
|
||||
"description": "Fuehre QG-001, QG-002, QG-003, QG-005 und QG-006 aus; dokumentiere Testevidenz inkl. Keyboard-Only-Smoke fuer alle Auth-Login-Stages und bestaetige, dass keine A11y-Regression in den geaenderten Views verbleibt.",
|
||||
"guard_refs": [
|
||||
"GR-UI-009",
|
||||
"GR-UI-013"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
"Neuer Architekturtest: tests/Architecture/AuthLoginAccessibilityContractTest.php (Scope-basierte HTML/A11y-Basics-Regeln)",
|
||||
"Bestehende Architekturtests inkl. tests/Architecture/CoreStarterkitContractTest.php",
|
||||
"QG-001: docker compose exec php vendor/bin/phpunit",
|
||||
"QG-002: docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress",
|
||||
"QG-003: docker compose exec php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php",
|
||||
"QG-005: manueller Browser-Smoke (Tab-Reihenfolge, Enter/Space-Aktivierung, Fokus-Sichtbarkeit, Fehleransage auf Auth-Login-Views)",
|
||||
"QG-006: docker compose exec php composer cs:check"
|
||||
],
|
||||
"acceptance_checks": [
|
||||
"SC-001: Pro Scope-Datei liegt ein abgearbeiteter A11y-Check vor und alle als offensichtlich eingestuften HTML/A11y-Verstoesse sind entfernt.",
|
||||
"SC-002: Im Scope existiert kein verbleibendes role=\"button\" auf Links fuer button-aehnliche Interaktionen; Keyboard-Navigation durchgaengig pruefbar.",
|
||||
"SC-003: Error-Ausgaben in Auth-Formularen sind als Live-Region/Alert nutzbar und relevante Inputs sind ueber Label/DescribedBy eindeutig verknuepft.",
|
||||
"SC-004: Loading-, Empty-, Error- und Success-Zustaende sind fuer den Auth-Login-Flow dokumentiert und im Verhalten/Markup nachvollziehbar abgebildet.",
|
||||
"SC-005: Der neue AuthLoginAccessibilityContractTest ist implementiert, laeuft gruen und faellt bei gezielter Regelverletzung reproduzierbar aus.",
|
||||
"SC-006: QG-001, QG-002, QG-003, QG-005 und QG-006 sind mit PASS und kurzer Evidenz dokumentiert."
|
||||
],
|
||||
"ux_notes": {
|
||||
"affected_patterns": [
|
||||
"Auth login layout in templates/login.phtml",
|
||||
"Auth form cards in pages/auth/*(login).phtml",
|
||||
"Tenant selection pattern (login-tenant-choice-grid)",
|
||||
"Notice/error pattern (notice[data-variant])",
|
||||
"Password hint pattern (data-password-hints)"
|
||||
],
|
||||
"ui_states_required": [
|
||||
"loading",
|
||||
"empty",
|
||||
"error",
|
||||
"success"
|
||||
],
|
||||
"a11y_touchpoints": [
|
||||
"Email-, Password-, Code- und Name-Inputs in Auth-Formularen",
|
||||
"Tenant-Radioauswahlkarten inklusive Fokuszustand",
|
||||
"Remember-me Checkbox",
|
||||
"Continue/Login/Register/Verify/Reset/Back Buttons",
|
||||
"Microsoft Sign-in Call-to-Action als semantischer Link-Button",
|
||||
"Fehler- und Hinweisboxen als Screenreader-relevante Live-Regionen"
|
||||
]
|
||||
},
|
||||
"risks": [
|
||||
{
|
||||
"risk": "Markup-Korrekturen im Auth-Bereich koennen unbeabsichtigt Flow- oder Styling-Regressions verursachen.",
|
||||
"mitigation": "Aenderungen auf semantische Minimaldeltas begrenzen, pro Stage Keyboard-Smoke durchfuehren und alle geforderten Quality Gates inkl. Full PHPUnit laufen lassen."
|
||||
},
|
||||
{
|
||||
"risk": "Zu strenge oder unpraezise Contract-Test-Regeln koennen false positives erzeugen und CI instabil machen.",
|
||||
"mitigation": "Regeln eng auf offensichtliche, objektiv pruefbare Basics begrenzen und Testmeldungen mit klaren Dateihinweisen formulieren."
|
||||
},
|
||||
{
|
||||
"risk": "Neue Hilfs- oder Status-Texte ohne vollstaendige i18n-Pflege fuehren zu inkonsistenter UI.",
|
||||
"mitigation": "Jede neue t()-Key-Einfuehrung parallel in default_de.json und default_en.json pruefen und im Review explizit abhaken."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"task_id": "AUTH-LOGIN-ACCESSIBILITY-001",
|
||||
"verdict": "pass",
|
||||
"checked_criterion_ids": [
|
||||
"SC-001",
|
||||
"SC-002",
|
||||
"SC-003",
|
||||
"SC-004",
|
||||
"SC-005",
|
||||
"SC-006"
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"criterion_id": "SC-001",
|
||||
"criterion": "Alle im Scope liegenden Auth-Login-Views sind gegen eine feste HTML/A11y-Baseline geprueft (Form-Semantik, Labels, role/ARIA, Fokus, Keyboard) und die identifizierten offensichtlichen Verstosse sind behoben.",
|
||||
"result": "pass",
|
||||
"evidence": "execution-report.json dokumentiert A11y-Korrekturen in allen Scope-Views (login/register/forgot/reset/verify/verify-email) sowie Login-CSS; Guard-Evidence fuer GR-UI-009 und GR-UI-013 ist PASS und beschreibt entfernte role-Hacks, semantische Struktur und Keyboard-taugliche native Controls."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-002",
|
||||
"criterion": "Interaktive Elemente im Auth-Login-Flow nutzen semantisch korrekte HTML-Elemente (keine Link-als-Button-Fehlverwendung) und bleiben vollstaendig per Tastatur bedienbar.",
|
||||
"result": "pass",
|
||||
"evidence": "changed_files nennt die Umstellung der Microsoft-CTA auf semantischen Link ohne role=button sowie fieldset/legend-Radiostruktur fuer Tenant-Auswahl; neuer AuthLoginAccessibilityContractTest prueft explizit 'kein role=button-Link' und lief laut test_results/commands mit PASS."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-003",
|
||||
"criterion": "Fehlermeldungen und Hilfetexte in den Login-relevanten Formularen sind fuer Screenreader verstaendlich angebunden (z. B. role/aria-live/aria-describedby) und mit sichtbaren Labels konsistent.",
|
||||
"result": "pass",
|
||||
"evidence": "changed_files beschreibt fuer alle Auth-Formulare Error-Notices als role=alert mit aria-live=assertive sowie aria-describedby/aria-invalid-Verknuepfungen an Feldern; Guard-Evidence GR-UI-009 bestaetigt diese Screenreader-Anbindung als umgesetzt."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-004",
|
||||
"criterion": "Die benoetigten UI-Zustaende loading, empty, error und success sind fuer den Auth-Login-Flow explizit spezifiziert und im betroffenen UI-Verhalten nachvollziehbar umgesetzt.",
|
||||
"result": "pass",
|
||||
"evidence": "Plan fordert die vier Zustaende explizit; execution-report.json weist unter guard_evidence GR-UI-014 PASS aus und benennt loading, empty ('No login method is available for this tenant'), error (assertive Error-Live-Regionen) und success (Flash-Variant im Login-Layout) als nachvollziehbar abgebildet."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-005",
|
||||
"criterion": "Ein neuer Architektur-/Contract-Test deckt die vereinbarten offensichtlichen Login-HTML-A11y-Regeln ab und schlaegt bei Regressionen reproduzierbar fehl.",
|
||||
"result": "pass",
|
||||
"evidence": "execution-report.json fuehrt tests/Architecture/AuthLoginAccessibilityContractTest.php als neu eingefuehrt auf; test_results zeigt PASS (4 Tests, 85 Assertions), und GR-TEST-002 dokumentiert den Test als reproduzierbaren CI-Contract fuer die vereinbarten A11y-Basics."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-006",
|
||||
"criterion": "Alle geforderten Quality Gates (QG-001, QG-002, QG-003, QG-005, QG-006) sind mit nachvollziehbarer Evidenz erfolgreich abgeschlossen.",
|
||||
"result": "pass",
|
||||
"evidence": "quality_gate_results in execution-report.json weist QG-001, QG-002, QG-003, QG-005 und QG-006 jeweils mit result=pass und Notes aus; korrespondierende commands/test_results sind ebenfalls PASS."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"task_id": "AUTH-LOGIN-ACCESSIBILITY-001",
|
||||
"verdict": "pass",
|
||||
"checked_guard_ids": [
|
||||
"GR-UI-009",
|
||||
"GR-UI-013",
|
||||
"GR-UI-014",
|
||||
"GR-UI-010",
|
||||
"GR-UI-015",
|
||||
"GR-CORE-005",
|
||||
"GR-SEC-001",
|
||||
"GR-TEST-002"
|
||||
],
|
||||
"checked_quality_gate_ids": [
|
||||
"QG-001",
|
||||
"QG-002",
|
||||
"QG-003",
|
||||
"QG-005",
|
||||
"QG-006"
|
||||
],
|
||||
"findings": []
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use MintyPHP\Buffer;
|
||||
use MintyPHP\Session;
|
||||
|
||||
Buffer::set('title', t('Forgot password'));
|
||||
$errorNoticeId = !empty($errors) ? 'auth-forgot-error-notice' : '';
|
||||
?>
|
||||
|
||||
<article>
|
||||
@@ -21,10 +22,10 @@ Buffer::set('title', t('Forgot password'));
|
||||
<form method="post">
|
||||
<label for="email">
|
||||
<span><?php e(t('Email')); ?></span>
|
||||
<input required type="email" name="email" id="email" value="<?php e($email ?? ''); ?>" />
|
||||
<input required type="email" name="email" id="email" value="<?php e($email ?? ''); ?>" autocomplete="email" autofocus <?php if ($errorNoticeId !== ''): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<div id="<?php e($errorNoticeId); ?>" class="notice" data-variant="error" role="alert" aria-live="assertive" tabindex="-1">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
|
||||
@@ -25,6 +25,7 @@ $selectedTenant = is_array($selectedTenant ?? null) ? $selectedTenant : null;
|
||||
$selectedTenantMethods = is_array($selectedTenantMethods ?? null) ? $selectedTenantMethods : ['local' => false, 'microsoft' => false];
|
||||
$selectedTenantSlug = (string) ($selectedTenant['slug'] ?? '');
|
||||
$errors = is_array($errors ?? null) ? $errors : [];
|
||||
$errorNoticeId = $errors ? 'auth-login-error-notice' : '';
|
||||
$loginHeading = t('Login');
|
||||
$loginSubheading = t('Please enter your credentials');
|
||||
if ($stage === 'resolve_email') {
|
||||
@@ -47,7 +48,7 @@ if ($stage === 'resolve_email') {
|
||||
</hgroup>
|
||||
</header>
|
||||
<?php if ($errors): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<div id="<?php e($errorNoticeId); ?>" class="notice" data-variant="error" role="alert" aria-live="assertive" tabindex="-1">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
@@ -62,7 +63,7 @@ if ($stage === 'resolve_email') {
|
||||
<input type="hidden" name="tenant_hint" value="<?php e($tenantHint); ?>">
|
||||
<label for="email">
|
||||
<span><?php e(t('Email')); ?></span>
|
||||
<input required type="email" name="email" id="email" value="<?php e($email); ?>" autocomplete="email" autofocus />
|
||||
<input required type="email" name="email" id="email" value="<?php e($email); ?>" autocomplete="email" autofocus <?php if ($errors): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<p>
|
||||
<button type="submit"><?php e(t('Continue')); ?></button>
|
||||
@@ -74,33 +75,39 @@ if ($stage === 'resolve_email') {
|
||||
<input type="hidden" name="stage" value="choose_tenant">
|
||||
<input type="hidden" name="email" value="<?php e($email); ?>">
|
||||
<input type="hidden" name="tenant_hint" value="<?php e($tenantHint); ?>">
|
||||
<div class="login-tenant-choice-grid" role="radiogroup"
|
||||
aria-label="<?php e(t('Which tenant do you want to sign in to?')); ?>">
|
||||
<?php foreach ($tenantCandidates as $tenantCandidate): ?>
|
||||
<?php
|
||||
$tenantId = (int) ($tenantCandidate['id'] ?? 0);
|
||||
if ($tenantId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$tenantLabel = (string) ($tenantCandidate['description'] ?? '');
|
||||
$tenantAvatarUrl = (string) ($tenantCandidate['avatar_url'] ?? '');
|
||||
$tenantInitial = strtoupper(substr(trim($tenantLabel), 0, 1));
|
||||
if ($tenantInitial === '') {
|
||||
$tenantInitial = '?';
|
||||
}
|
||||
?>
|
||||
<label class="app-field login-tenant-choice" data-tooltip="<?php e($tenantLabel); ?>" data-tooltip-pos="top">
|
||||
<input type="radio" name="tenant_id" value="<?php e((string) $tenantId); ?>" <?php e($tenantId === $selectedTenantId ? 'checked autofocus' : ''); ?> aria-label="<?php e($tenantLabel); ?>">
|
||||
<span class="login-tenant-choice-body">
|
||||
<?php if ($tenantAvatarUrl !== ''): ?>
|
||||
<img class="login-tenant-choice-avatar" src="<?php e($tenantAvatarUrl); ?>" alt="" loading="lazy">
|
||||
<?php else: ?>
|
||||
<span class="login-tenant-choice-avatar-placeholder"><?php e($tenantInitial); ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<fieldset class="login-tenant-fieldset">
|
||||
<legend class="login-tenant-select-label"><?php e(t('Which tenant do you want to sign in to?')); ?></legend>
|
||||
<div class="login-tenant-choice-grid">
|
||||
<?php foreach ($tenantCandidates as $tenantCandidate): ?>
|
||||
<?php
|
||||
$tenantId = (int) ($tenantCandidate['id'] ?? 0);
|
||||
if ($tenantId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$tenantLabel = trim((string) ($tenantCandidate['description'] ?? ''));
|
||||
if ($tenantLabel === '') {
|
||||
$tenantLabel = t('Select tenant');
|
||||
}
|
||||
$tenantAvatarUrl = (string) ($tenantCandidate['avatar_url'] ?? '');
|
||||
$tenantInitial = strtoupper(substr($tenantLabel, 0, 1));
|
||||
if ($tenantInitial === '') {
|
||||
$tenantInitial = '?';
|
||||
}
|
||||
?>
|
||||
<label class="app-field login-tenant-choice" data-tooltip="<?php e($tenantLabel); ?>" data-tooltip-pos="top">
|
||||
<input type="radio" name="tenant_id" value="<?php e((string) $tenantId); ?>" <?php e($tenantId === $selectedTenantId ? 'checked autofocus' : ''); ?>>
|
||||
<span class="login-tenant-choice-body">
|
||||
<?php if ($tenantAvatarUrl !== ''): ?>
|
||||
<img class="login-tenant-choice-avatar" src="<?php e($tenantAvatarUrl); ?>" alt="" loading="lazy">
|
||||
<?php else: ?>
|
||||
<span class="login-tenant-choice-avatar-placeholder"><?php e($tenantInitial); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="login-tenant-choice-text"><?php e($tenantLabel); ?></span>
|
||||
</span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</fieldset>
|
||||
<p>
|
||||
<button type="submit"><?php e(t('Continue')); ?></button>
|
||||
</p>
|
||||
@@ -122,7 +129,7 @@ if ($stage === 'resolve_email') {
|
||||
<input type="hidden" name="tenant_id" value="<?php e((string) $selectedTenantId); ?>">
|
||||
<label for="password">
|
||||
<span><?php e(t('Password')); ?></span>
|
||||
<input required type="password" name="password" id="password" autocomplete="current-password" />
|
||||
<input required type="password" name="password" id="password" autocomplete="current-password" <?php if ($errors): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<p>
|
||||
<label class="app-field inline">
|
||||
@@ -147,14 +154,14 @@ if ($stage === 'resolve_email') {
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($selectedTenantMethods['microsoft']) && $selectedTenantSlug !== ''): ?>
|
||||
<a role="button" style="width:100%;" class="primary"
|
||||
<a class="primary login-microsoft-link"
|
||||
href="<?php e(lurl('auth/microsoft/start?tenant=' . rawurlencode($selectedTenantSlug))); ?>">
|
||||
<i class="bi bi-microsoft"></i> <?php e(t('Sign in with Microsoft')); ?>
|
||||
<i class="bi bi-microsoft" aria-hidden="true"></i> <?php e(t('Sign in with Microsoft')); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($selectedTenantMethods['local']) && empty($selectedTenantMethods['microsoft'])): ?>
|
||||
<div class="notice" data-variant="warning">
|
||||
<div class="notice" data-variant="warning" role="status" aria-live="polite">
|
||||
<?php e(t('No login method is available for this tenant')); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -15,9 +15,11 @@ use MintyPHP\Session;
|
||||
Buffer::set('title', t('Register'));
|
||||
$passwordMinLength = (int) ($passwordMinLength ?? 0);
|
||||
$passwordHints = is_array($passwordHints ?? null) ? $passwordHints : [];
|
||||
$errorNoticeId = !empty($error) ? 'auth-register-error-notice' : '';
|
||||
$passwordHintId = 'auth-register-password-hints';
|
||||
?>
|
||||
<a role="button" class="back_to_login small secondary outline" href="/login"><i
|
||||
class="bi bi-arrow-left"></i> <?php e(t('Back to Login')); ?></a>
|
||||
<a class="back_to_login small secondary outline" href="<?php e(lurl('login')); ?>"><i
|
||||
class="bi bi-arrow-left" aria-hidden="true"></i> <?php e(t('Back to Login')); ?></a>
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
@@ -28,15 +30,15 @@ $passwordHints = is_array($passwordHints ?? null) ? $passwordHints : [];
|
||||
<form method="post">
|
||||
<label for="first_name">
|
||||
<span><?php e(t('First name')); ?></span>
|
||||
<input required type="text" name="first_name" id="first_name" />
|
||||
<input required type="text" name="first_name" id="first_name" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<label for="last_name">
|
||||
<span><?php e(t('Last name')); ?></span>
|
||||
<input required type="text" name="last_name" id="last_name" />
|
||||
<input required type="text" name="last_name" id="last_name" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<label for="email">
|
||||
<span><?php e(t('Email')); ?></span>
|
||||
<input required type="email" name="email" id="email" />
|
||||
<input required type="email" name="email" id="email" autocomplete="email" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<fieldset>
|
||||
<legend>
|
||||
@@ -47,14 +49,16 @@ $passwordHints = is_array($passwordHints ?? null) ? $passwordHints : [];
|
||||
<label for="password">
|
||||
<span><?php e(t('Password')); ?></span>
|
||||
<input required type="password" name="password" id="password"
|
||||
minlength="<?php e($passwordMinLength); ?>" autocomplete="new-password" />
|
||||
minlength="<?php e($passwordMinLength); ?>" autocomplete="new-password"
|
||||
aria-describedby="<?php e(trim($passwordHintId . ' ' . $errorNoticeId)); ?>" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true"<?php endif; ?> />
|
||||
</label>
|
||||
<label for="password2">
|
||||
<span><?php e(t('Password (again)')); ?></span>
|
||||
<input required type="password" name="password2" id="password2"
|
||||
minlength="<?php e($passwordMinLength); ?>" autocomplete="new-password" />
|
||||
minlength="<?php e($passwordMinLength); ?>" autocomplete="new-password"
|
||||
aria-describedby="<?php e(trim($passwordHintId . ' ' . $errorNoticeId)); ?>" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true"<?php endif; ?> />
|
||||
</label>
|
||||
<div class="form-hint" data-password-hints data-password-input="#password" data-confirm-input="#password2"
|
||||
<div id="<?php e($passwordHintId); ?>" class="form-hint" data-password-hints data-password-input="#password" data-confirm-input="#password2"
|
||||
data-email-input="#email" data-min-length="<?php e($passwordMinLength); ?>">
|
||||
<strong><?php e(t('Password requirements')); ?></strong>
|
||||
<ul class="form-hint-list">
|
||||
@@ -67,7 +71,7 @@ $passwordHints = is_array($passwordHints ?? null) ? $passwordHints : [];
|
||||
</fieldset>
|
||||
<button type="submit"><?php e(t('Register')); ?></button>
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="notice" data-variant="error"><?php e($error); ?></div>
|
||||
<div id="<?php e($errorNoticeId); ?>" class="notice" data-variant="error" role="alert" aria-live="assertive" tabindex="-1"><?php e($error); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
|
||||
@@ -10,6 +10,8 @@ use MintyPHP\Buffer;
|
||||
use MintyPHP\Session;
|
||||
|
||||
Buffer::set('title', t('Reset password'));
|
||||
$errorNoticeId = !empty($errors) ? 'auth-reset-error-notice' : '';
|
||||
$passwordHintId = 'auth-reset-password-hints';
|
||||
?>
|
||||
|
||||
<article>
|
||||
@@ -29,15 +31,17 @@ Buffer::set('title', t('Reset password'));
|
||||
<label for="password">
|
||||
<span><?php e(t('Password')); ?></span>
|
||||
<input required type="password" name="password" id="password"
|
||||
minlength="<?php e($passwordMinLength); ?>" />
|
||||
minlength="<?php e($passwordMinLength); ?>" autocomplete="new-password"
|
||||
aria-describedby="<?php e(trim($passwordHintId . ' ' . $errorNoticeId)); ?>" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true"<?php endif; ?> />
|
||||
</label>
|
||||
<label for="password2">
|
||||
<span><?php e(t('Password (again)')); ?></span>
|
||||
<input required type="password" name="password2" id="password2"
|
||||
minlength="<?php e($passwordMinLength); ?>" />
|
||||
minlength="<?php e($passwordMinLength); ?>" autocomplete="new-password"
|
||||
aria-describedby="<?php e(trim($passwordHintId . ' ' . $errorNoticeId)); ?>" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true"<?php endif; ?> />
|
||||
</label>
|
||||
<?php if (!empty($passwordHints)): ?>
|
||||
<div class="form-hint" data-password-hints data-password-input="#password" data-confirm-input="#password2"
|
||||
<div id="<?php e($passwordHintId); ?>" class="form-hint" data-password-hints data-password-input="#password" data-confirm-input="#password2"
|
||||
data-min-length="<?php e($passwordMinLength); ?>">
|
||||
<strong><?php e(t('Password requirements')); ?></strong>
|
||||
<ul class="form-hint-list">
|
||||
@@ -50,7 +54,7 @@ Buffer::set('title', t('Reset password'));
|
||||
<?php endif; ?>
|
||||
</fieldset>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<div id="<?php e($errorNoticeId); ?>" class="notice" data-variant="error" role="alert" aria-live="assertive" tabindex="-1">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
|
||||
@@ -10,6 +10,7 @@ use MintyPHP\Buffer;
|
||||
use MintyPHP\Session;
|
||||
|
||||
Buffer::set('title', t('Verify code'));
|
||||
$errorNoticeId = !empty($errors) ? 'auth-verify-error-notice' : '';
|
||||
?>
|
||||
|
||||
<article>
|
||||
@@ -22,15 +23,15 @@ Buffer::set('title', t('Verify code'));
|
||||
<form method="post">
|
||||
<label for="email">
|
||||
<span><?php e(t('Email')); ?></span>
|
||||
<input required type="email" name="email" id="email" value="<?php e($email ?? ''); ?>" />
|
||||
<input required type="email" name="email" id="email" value="<?php e($email ?? ''); ?>" autocomplete="email" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<label for="code">
|
||||
<span><?php e(t('Verification code')); ?></span>
|
||||
<input required type="text" name="code" id="code" inputmode="numeric" pattern="\d{6}" maxlength="6"
|
||||
value="<?php e($code ?? ''); ?>" />
|
||||
value="<?php e($code ?? ''); ?>" autocomplete="one-time-code" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<div id="<?php e($errorNoticeId); ?>" class="notice" data-variant="error" role="alert" aria-live="assertive" tabindex="-1">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
|
||||
@@ -10,6 +10,7 @@ use MintyPHP\Buffer;
|
||||
use MintyPHP\Session;
|
||||
|
||||
Buffer::set('title', t('Verify email'));
|
||||
$errorNoticeId = !empty($errors) ? 'auth-verify-email-error-notice' : '';
|
||||
?>
|
||||
|
||||
<article>
|
||||
@@ -22,15 +23,15 @@ Buffer::set('title', t('Verify email'));
|
||||
<form method="post">
|
||||
<label for="email">
|
||||
<span><?php e(t('Email')); ?></span>
|
||||
<input required type="email" name="email" id="email" value="<?php e($email ?? ''); ?>" />
|
||||
<input required type="email" name="email" id="email" value="<?php e($email ?? ''); ?>" autocomplete="email" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<label for="code">
|
||||
<span><?php e(t('Verification code')); ?></span>
|
||||
<input required type="text" name="code" id="code" inputmode="numeric" pattern="\d{6}" maxlength="6"
|
||||
value="<?php e($code ?? ''); ?>" />
|
||||
value="<?php e($code ?? ''); ?>" autocomplete="one-time-code" <?php if ($errorNoticeId !== ''): ?>aria-invalid="true" aria-describedby="<?php e($errorNoticeId); ?>"<?php endif; ?> />
|
||||
</label>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<div id="<?php e($errorNoticeId); ?>" class="notice" data-variant="error" role="alert" aria-live="assertive" tabindex="-1">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
@@ -42,13 +43,13 @@ Buffer::set('title', t('Verify email'));
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
<hr>
|
||||
<form method="post" style="text-align: center;">
|
||||
<form method="post" class="text-align-center">
|
||||
<input type="hidden" name="email" value="<?php e($email ?? ''); ?>">
|
||||
<input type="hidden" name="action" value="resend">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="secondary outline"><?php e(t('Send code again')); ?></button>
|
||||
</form>
|
||||
<p class="text-align-center" style="margin-top: 1rem;">
|
||||
<p class="text-align-center login-secondary-action">
|
||||
<a href="<?php e(lurl('login')); ?>"><?php e(t('Back to Login')); ?></a>
|
||||
</p>
|
||||
</article>
|
||||
|
||||
62
tests/Architecture/AuthLoginAccessibilityContractTest.php
Normal file
62
tests/Architecture/AuthLoginAccessibilityContractTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AuthLoginAccessibilityContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function authLoginTemplates(): array
|
||||
{
|
||||
return [
|
||||
'pages/auth/login(login).phtml',
|
||||
'pages/auth/register(login).phtml',
|
||||
'pages/auth/forgot(login).phtml',
|
||||
'pages/auth/reset(login).phtml',
|
||||
'pages/auth/verify(login).phtml',
|
||||
'pages/auth/verify-email(login).phtml',
|
||||
];
|
||||
}
|
||||
|
||||
public function testAuthLoginTemplatesDoNotUseRoleButtonLinks(): void
|
||||
{
|
||||
foreach ($this->authLoginTemplates() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringNotContainsString('role="button"', $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testAuthLoginErrorNoticesUseAssertiveLiveRegions(): void
|
||||
{
|
||||
foreach ($this->authLoginTemplates() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString('class="notice" data-variant="error" role="alert" aria-live="assertive"', $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testTenantSelectionUsesSemanticFieldsetAndTextLabel(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/auth/login(login).phtml');
|
||||
|
||||
$this->assertStringContainsString('login-tenant-fieldset', $content);
|
||||
$this->assertStringContainsString('login-tenant-select-label', $content);
|
||||
$this->assertStringContainsString('login-tenant-choice-text', $content);
|
||||
$this->assertStringContainsString('login-microsoft-link', $content);
|
||||
}
|
||||
|
||||
public function testAuthLoginFormsExposeErrorDescriptionForInputs(): void
|
||||
{
|
||||
$describedByPattern = '/aria-describedby="[^"]*\\$errorNoticeId[^"]*"/';
|
||||
|
||||
foreach ($this->authLoginTemplates() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertMatchesRegularExpression($describedByPattern, $content, $file);
|
||||
$this->assertStringContainsString('aria-invalid="true"', $content, $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,9 +552,16 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-tenant-fieldset {
|
||||
border: 0;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.login-tenant-select-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-tenant-choice-grid {
|
||||
@@ -601,6 +608,13 @@
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.login-tenant-choice-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-tenant-choice-avatar {
|
||||
width: 8rem;
|
||||
height: 3rem;
|
||||
@@ -623,6 +637,13 @@
|
||||
color: var(--app-contrast);
|
||||
}
|
||||
|
||||
.login-tenant-choice-text {
|
||||
color: var(--app-color);
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.login-alt-separator {
|
||||
position: relative;
|
||||
margin: 1rem 0;
|
||||
@@ -649,8 +670,14 @@
|
||||
background: var(--app-surface);
|
||||
}
|
||||
|
||||
.login-alt-separator + [role="button"] {
|
||||
.login-microsoft-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-secondary-action {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
|
||||
Reference in New Issue
Block a user