lsa-planer

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

/ tests ui fokusNachFensterkette.test.ts

4,1 KB Rohdatei
tests/ui/fokusNachFensterkette.test.ts — 109 Zeilen
1 import { afterEach, describe, expect, it } from 'vitest';
2 import { button, el } from '@/ui/dom';
3 import { Panel, confirmDialog } from '@/ui/feedback';
4 import { hilfeKnopf, schliesseHilfe } from '@/ui/help/hilfe';
5
6 /**
7 * Befund 17 zur Fassung 5.9.0 (WCAG 2.4.3 Fokusreihenfolge).
8 *
9 * WAS DER ANWENDER SONST ERLEBT
10 *
11 * Er drueckt in einem Tabellenkopf auf "?", liest die Erklaerung und geht von
12 * dort mit dem Knopf "Glossar" weiter (src/ui/help/hilfe.ts: `panel.close();`
13 * gefolgt von `zeigeGlossar();`). Schliesst er das Glossar, liegt der Fokus
14 * NIRGENDS: Der Erbauer des Glossars hat sich als ausloesendes Element den
15 * Seitenkoerper gemerkt - das alte Overlay samt fokussiertem Knopf war in
16 * diesem Augenblick schon entfernt -, und `gibFokusZurueck` nahm den
17 * Seitenkoerper als gueltiges Rueckgabeziel an. `focus()` auf einem `body`
18 * ohne `tabindex` ist ein Nichttun, das `return` dahinter uebersprang beide
19 * Rueckfallstufen (Fenster darunter, `#inhalt`). Der naechste Tabulatordruck
20 * beginnt danach wieder ganz oben bei der Sprungmarke.
21 *
22 * Zugesagt ist das Gegenteil, zu Kriterium 2.4.3: "sonst in das Fenster
23 * darunter, sonst in den Inhaltsbereich".
24 *
25 * Dieselbe Stelle traegt die Rueckfrage (`confirmDialog`), deshalb der zweite
26 * Fall.
27 */
28
29 afterEach(() => {
30 schliesseHilfe();
31 document.body.replaceChildren();
32 });
33
34 /** Wartet einen Mikroschritt ab - die Fokusrueckgabe laeuft in einem solchen. */
35 async function mikroschritt(): Promise<void> {
36 await new Promise<void>((fertig) => {
37 queueMicrotask(fertig);
38 });
39 }
40
41 /** Der Inhaltsbereich, wie ihn `src/ui/shell.ts` aufbaut: fokussierbar, aber nicht im Tabulatorlauf. */
42 function inhaltsbereich(): HTMLElement {
43 const inhalt = el('div', { id: 'inhalt' });
44 inhalt.tabIndex = -1;
45 document.body.append(inhalt);
46 return inhalt;
47 }
48
49 /** Schaltflaeche in der Fusszeile des obersten Fensters. */
50 function fussknopf(beschriftung: string): HTMLButtonElement {
51 const knopf = [
52 ...document.querySelectorAll<HTMLButtonElement>('.dialog-hintergrund .dialog-aktionen button'),
53 ].find((b) => (b.textContent ?? '').trim() === beschriftung);
54 if (knopf === undefined) throw new Error(`Keine Schaltflaeche "${beschriftung}"`);
55 return knopf;
56 }
57
58 describe('Fokus nach einem Fenster, das aus einem gerade geschlossenen heraus geoeffnet wurde', () => {
59 it('faellt in den Inhaltsbereich statt auf den Seitenkoerper (Hilfe -> Glossar -> schliessen)', async () => {
60 const inhalt = inhaltsbereich();
61 const frage = hilfeKnopf('konfliktbeziehung');
62 document.body.append(frage);
63 frage.focus();
64
65 frage.click();
66 await mikroschritt();
67 await mikroschritt();
68
69 // Die Kette aus hilfe.ts: schliessen und im selben Takt das naechste
70 // Fenster oeffnen. Der Erbauer des Glossars liest hier `document.body`.
71 fussknopf('Glossar').click();
72 await mikroschritt();
73 await mikroschritt();
74 expect(document.activeElement, 'das Glossar hat den Fokus nicht bekommen').not.toBe(
75 document.body,
76 );
77
78 fussknopf('Schließen').click();
79 await mikroschritt();
80 await mikroschritt();
81
82 expect(document.activeElement, 'der Fokus liegt nach dem Glossar nirgends').toBe(inhalt);
83 });
84
85 it('gibt den Fokus auch nach einer Rueckfrage aus einem geschlossenen Fenster heraus zurueck', async () => {
86 const inhalt = inhaltsbereich();
87 const ausloeser = button({ label: 'Zwischenzeit bearbeiten', onClick: () => {} });
88 document.body.append(ausloeser);
89 ausloeser.focus();
90
91 const panel = new Panel({ title: 'Zwischenzeit' });
92 panel.setActions(button({ label: 'Weiter', onClick: () => {} }));
93 await mikroschritt();
94
95 // Erst schliessen, dann - noch im selben Takt - die Rueckfrage stellen.
96 // `previousFocus` der Rueckfrage ist damit der Seitenkoerper.
97 panel.close();
98 const antwort = confirmDialog({ title: 'Verwerfen?', message: 'Die Angaben gehen verloren.' });
99 ausloeser.remove();
100 await mikroschritt();
101
102 fussknopf('Abbrechen').click();
103 expect(await antwort).toBe('abgebrochen');
104 await mikroschritt();
105 await mikroschritt();
106
107 expect(document.activeElement, 'der Fokus liegt nach der Rueckfrage nirgends').toBe(inhalt);
108 });
109 });