import { beforeAll, describe, expect, it, vi } from 'vitest'; import { ladeSitzung, saveSession } from '@/services/storage'; /** * Befund 45 zur Fassung 5.9.0 (WCAG 2.4.3). * * "Rückgängig" und "Wiederherstellen" riefen nach der Ruecknahme * `shell.navigate(currentViewId())`. `navigate` laeuft mit der Vorgabe * `fokusSetzen = true` und endet mit `focus()` auf der Ueberschrift der Ansicht * und `scrollTop = 0`. Wer die Kopfzeilenschaltflaeche mit der Tastatur * betaetigt, steht danach auf der Ueberschrift; ein zweiter Schritt zurueck * verlangt den Weg ueber die gesamte Navigation. Wer Strg+Z in der * Zwischenzeitenmatrix drueckt - deren Zellen Schaltflaechen sind, nicht * Eingabefelder -, verliert Fokus und Scrollstand mitten in der Arbeit. * * Genau diese Folgen zaehlt der Kommentar an `navigate` selbst als Grund dafuer * auf, dass ein Neuaufbau den Fokus bewahren muss, und die Shell haelt dafuer * `baueSichtbareAnsichtNeu()` bereit: derselbe vollstaendige Ab- und * Wiederaufbau, nur ohne Fokussprung. Zu 2.4.3 ist ausdruecklich zugesagt: * "Der Fokus überlebt den Neuaufbau einer Ansicht." * * Fuer "Neu" und "Öffnen" bleibt `navigate` richtig - dort wechselt nach einem * modalen Fenster das ganze Projekt. * * Geprueft wird am echten Einstiegspunkt: `src/main.ts` wird eingelesen und * startet die ganze Anwendung. Nur der Sitzungsspeicher ist ersetzt. */ vi.mock('@/services/storage', async (importOriginal) => { const echt = await importOriginal(); return { ...echt, ladeSitzung: vi.fn(), saveSession: vi.fn(), addRecoveryPoint: vi.fn(), }; }); async function warte(ms = 0): Promise { await new Promise((fertig) => setTimeout(fertig, ms)); } function schaltflaeche(text: string): HTMLButtonElement { const treffer = [...document.querySelectorAll('button')].find( (b) => b.textContent?.trim() === text || b.getAttribute('aria-label') === text, ); if (!treffer) throw new Error(`Schaltflaeche "${text}" nicht gefunden.`); return treffer; } function feld(beschriftung: string): HTMLInputElement { const label = [...document.querySelectorAll('label')].find( (l) => l.textContent?.trim() === beschriftung, ); const id = label?.getAttribute('for'); const eingabe = id === null || id === undefined ? null : document.getElementById(id); if (!(eingabe instanceof HTMLInputElement)) { throw new Error(`Eingabefeld "${beschriftung}" nicht gefunden.`); } return eingabe; } function trageEin(beschriftung: string, wert: string): void { const eingabe = feld(beschriftung); eingabe.value = wert; eingabe.dispatchEvent(new Event('change')); } function druecke(taste: string, ziel: EventTarget): void { ziel.dispatchEvent( new KeyboardEvent('keydown', { key: taste, ctrlKey: true, bubbles: true, cancelable: true }), ); } beforeAll(async () => { vi.mocked(ladeSitzung).mockResolvedValue({ art: 'leer' }); vi.mocked(saveSession).mockResolvedValue({ ok: true, message: '' }); document.body.replaceChildren(); const wurzel = document.createElement('div'); wurzel.id = 'anwendung'; document.body.append(wurzel); await import('@/main'); await warte(20); // Drei Aenderungen an drei Feldern: Danach laesst sich mehrfach // zuruecknehmen und wieder herstellen, die beiden Schaltflaechen bleiben also // bedienbar. Drei verschiedene Felder, weil aufeinanderfolgende Aenderungen // desselben Feldes zu einem Schritt zusammengefasst werden. trageEin('Projektbezeichnung', 'Bahnhofstraße / Ringstraße'); await warte(0); trageEin('Projektnummer', 'VB-2026/0815'); await warte(0); trageEin('Auftraggeber', 'Straßenverkehrsbehörde'); await warte(0); }); describe('Fokus nach "Rückgängig" und "Wiederherstellen"', () => { it('bleibt auf der Schaltfläche "Rückgängig", statt auf die Überschrift zu springen', async () => { const knopf = schaltflaeche('Rückgängig'); knopf.focus(); expect(document.activeElement).toBe(knopf); knopf.click(); await warte(10); expect( document.activeElement, 'Der Fokus sprang auf die Ueberschrift - ein zweiter Schritt zurueck ist nur ueber die ganze Navigation erreichbar', ).toBe(knopf); expect(knopf.disabled, 'Ein zweiter Schritt zurueck muss noch moeglich sein').toBe(false); }); it('bleibt auf der Schaltfläche "Wiederherstellen"', async () => { const knopf = schaltflaeche('Wiederherstellen'); knopf.focus(); expect(document.activeElement).toBe(knopf); knopf.click(); await warte(10); expect(document.activeElement).toBe(knopf); }); it('bleibt bei Strg+Z dort, wo der Anwender gerade steht', async () => { // Der Tastenempfaenger haengt am Fenster und laeuft fuer alles durch, was // kein Eingabefeld ist - die Zellen der Zwischenzeitenmatrix zum Beispiel. const knopf = schaltflaeche('Rückgängig'); knopf.focus(); druecke('z', knopf); await warte(10); expect(document.activeElement).toBe(knopf); }); });