lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests ui ruecknahmeFokus.test.ts

4,9 KB Rohdatei
tests/ui/ruecknahmeFokus.test.ts — 137 Zeilen
1 import { beforeAll, describe, expect, it, vi } from 'vitest';
2 import { ladeSitzung, saveSession } from '@/services/storage';
3
4 /**
5 * Befund 45 zur Fassung 5.9.0 (WCAG 2.4.3).
6 *
7 * "Rückgängig" und "Wiederherstellen" riefen nach der Ruecknahme
8 * `shell.navigate(currentViewId())`. `navigate` laeuft mit der Vorgabe
9 * `fokusSetzen = true` und endet mit `focus()` auf der Ueberschrift der Ansicht
10 * und `scrollTop = 0`. Wer die Kopfzeilenschaltflaeche mit der Tastatur
11 * betaetigt, steht danach auf der Ueberschrift; ein zweiter Schritt zurueck
12 * verlangt den Weg ueber die gesamte Navigation. Wer Strg+Z in der
13 * Zwischenzeitenmatrix drueckt - deren Zellen Schaltflaechen sind, nicht
14 * Eingabefelder -, verliert Fokus und Scrollstand mitten in der Arbeit.
15 *
16 * Genau diese Folgen zaehlt der Kommentar an `navigate` selbst als Grund dafuer
17 * auf, dass ein Neuaufbau den Fokus bewahren muss, und die Shell haelt dafuer
18 * `baueSichtbareAnsichtNeu()` bereit: derselbe vollstaendige Ab- und
19 * Wiederaufbau, nur ohne Fokussprung. Zu 2.4.3 ist ausdruecklich zugesagt:
20 * "Der Fokus überlebt den Neuaufbau einer Ansicht."
21 *
22 * Fuer "Neu" und "Öffnen" bleibt `navigate` richtig - dort wechselt nach einem
23 * modalen Fenster das ganze Projekt.
24 *
25 * Geprueft wird am echten Einstiegspunkt: `src/main.ts` wird eingelesen und
26 * startet die ganze Anwendung. Nur der Sitzungsspeicher ist ersetzt.
27 */
28
29 vi.mock('@/services/storage', async (importOriginal) => {
30 const echt = await importOriginal<typeof import('@/services/storage')>();
31 return {
32 ...echt,
33 ladeSitzung: vi.fn(),
34 saveSession: vi.fn(),
35 addRecoveryPoint: vi.fn(),
36 };
37 });
38
39 async function warte(ms = 0): Promise<void> {
40 await new Promise((fertig) => setTimeout(fertig, ms));
41 }
42
43 function schaltflaeche(text: string): HTMLButtonElement {
44 const treffer = [...document.querySelectorAll('button')].find(
45 (b) => b.textContent?.trim() === text || b.getAttribute('aria-label') === text,
46 );
47 if (!treffer) throw new Error(`Schaltflaeche "${text}" nicht gefunden.`);
48 return treffer;
49 }
50
51 function feld(beschriftung: string): HTMLInputElement {
52 const label = [...document.querySelectorAll('label')].find(
53 (l) => l.textContent?.trim() === beschriftung,
54 );
55 const id = label?.getAttribute('for');
56 const eingabe = id === null || id === undefined ? null : document.getElementById(id);
57 if (!(eingabe instanceof HTMLInputElement)) {
58 throw new Error(`Eingabefeld "${beschriftung}" nicht gefunden.`);
59 }
60 return eingabe;
61 }
62
63 function trageEin(beschriftung: string, wert: string): void {
64 const eingabe = feld(beschriftung);
65 eingabe.value = wert;
66 eingabe.dispatchEvent(new Event('change'));
67 }
68
69 function druecke(taste: string, ziel: EventTarget): void {
70 ziel.dispatchEvent(
71 new KeyboardEvent('keydown', { key: taste, ctrlKey: true, bubbles: true, cancelable: true }),
72 );
73 }
74
75 beforeAll(async () => {
76 vi.mocked(ladeSitzung).mockResolvedValue({ art: 'leer' });
77 vi.mocked(saveSession).mockResolvedValue({ ok: true, message: '' });
78
79 document.body.replaceChildren();
80 const wurzel = document.createElement('div');
81 wurzel.id = 'anwendung';
82 document.body.append(wurzel);
83
84 await import('@/main');
85 await warte(20);
86
87 // Drei Aenderungen an drei Feldern: Danach laesst sich mehrfach
88 // zuruecknehmen und wieder herstellen, die beiden Schaltflaechen bleiben also
89 // bedienbar. Drei verschiedene Felder, weil aufeinanderfolgende Aenderungen
90 // desselben Feldes zu einem Schritt zusammengefasst werden.
91 trageEin('Projektbezeichnung', 'Bahnhofstraße / Ringstraße');
92 await warte(0);
93 trageEin('Projektnummer', 'VB-2026/0815');
94 await warte(0);
95 trageEin('Auftraggeber', 'Straßenverkehrsbehörde');
96 await warte(0);
97 });
98
99 describe('Fokus nach "Rückgängig" und "Wiederherstellen"', () => {
100 it('bleibt auf der Schaltfläche "Rückgängig", statt auf die Überschrift zu springen', async () => {
101 const knopf = schaltflaeche('Rückgängig');
102 knopf.focus();
103 expect(document.activeElement).toBe(knopf);
104
105 knopf.click();
106 await warte(10);
107
108 expect(
109 document.activeElement,
110 'Der Fokus sprang auf die Ueberschrift - ein zweiter Schritt zurueck ist nur ueber die ganze Navigation erreichbar',
111 ).toBe(knopf);
112 expect(knopf.disabled, 'Ein zweiter Schritt zurueck muss noch moeglich sein').toBe(false);
113 });
114
115 it('bleibt auf der Schaltfläche "Wiederherstellen"', async () => {
116 const knopf = schaltflaeche('Wiederherstellen');
117 knopf.focus();
118 expect(document.activeElement).toBe(knopf);
119
120 knopf.click();
121 await warte(10);
122
123 expect(document.activeElement).toBe(knopf);
124 });
125
126 it('bleibt bei Strg+Z dort, wo der Anwender gerade steht', async () => {
127 // Der Tastenempfaenger haengt am Fenster und laeuft fuer alles durch, was
128 // kein Eingabefeld ist - die Zellen der Zwischenzeitenmatrix zum Beispiel.
129 const knopf = schaltflaeche('Rückgängig');
130 knopf.focus();
131
132 druecke('z', knopf);
133 await warte(10);
134
135 expect(document.activeElement).toBe(knopf);
136 });
137 });