waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app tests ThemaWahl.test.tsx

6,5 KB Rohdatei
app/tests/ThemaWahl.test.tsx — 184 Zeilen
1 import { render, screen } from '@testing-library/react';
2 import userEvent from '@testing-library/user-event';
3 import axe from 'axe-core';
4 import { jsdomOptionen } from '../src/shared/wcag';
5 import { useState } from 'react';
6 import { describe, expect, it } from 'vitest';
7
8 import { ThemaWahl } from '../src/renderer/src/components/ThemaWahl';
9 import { type ThemeAuswahl } from '../src/shared/theme';
10
11 /** Minimale Hülle, die die Auswahl wie in der echten App zustandsbehaftet hält. */
12 function Pruefstand({ start = 'system' }: { start?: ThemeAuswahl }): React.JSX.Element {
13 const [auswahl, setzeAuswahl] = useState<ThemeAuswahl>(start);
14 return (
15 <>
16 <h3 id="farbschema-titel">Farbschema</h3>
17 <ThemaWahl auswahl={auswahl} onAuswahl={setzeAuswahl} beschriftetVon="farbschema-titel" />
18 </>
19 );
20 }
21
22 /**
23 * Wie oben, aber mit einem zweiten Bedienelement daneben.
24 *
25 * Auf dem Startbildschirm steht die Farbschemawahl zwischen elf weiteren
26 * Einstellungen; jede von ihnen baut die Karte neu auf. Genau dieser
27 * Nachbar fehlte im Prüfstand – und deshalb ist nie aufgefallen, dass ein
28 * stehengebliebener Fokuswunsch beim nächsten Neuaufbau zuschlägt.
29 */
30 function PruefstandMitNachbar(): React.JSX.Element {
31 const [auswahl, setzeAuswahl] = useState<ThemeAuswahl>('system');
32 const [an, setzeAn] = useState(false);
33 return (
34 <>
35 <h3 id="farbschema-titel">Farbschema</h3>
36 <ThemaWahl auswahl={auswahl} onAuswahl={setzeAuswahl} beschriftetVon="farbschema-titel" />
37 <label>
38 Nachbar
39 <input
40 type="checkbox"
41 checked={an}
42 onChange={() => {
43 setzeAn((bisher) => !bisher);
44 }}
45 />
46 </label>
47 </>
48 );
49 }
50
51 describe('ThemaWahl – ARIA-Struktur', () => {
52 it('rendert eine benannte Radiogruppe mit allen vier Optionen', () => {
53 render(<Pruefstand />);
54
55 const gruppe = screen.getByRole('radiogroup', { name: 'Farbschema' });
56 expect(gruppe).toBeInTheDocument();
57
58 const optionen = screen.getAllByRole('radio');
59 expect(optionen.map((o) => o.textContent.replace(/[●○]/gu, '').trim())).toEqual([
60 'Systemvorgabe',
61 'Hell',
62 'Dunkel',
63 'Hoher Kontrast',
64 ]);
65 });
66
67 it('markiert genau eine Option als ausgewählt', () => {
68 render(<Pruefstand start="dunkel" />);
69
70 expect(screen.getByRole('radio', { name: /Dunkel/u })).toBeChecked();
71 expect(
72 screen.getAllByRole('radio').filter((o) => o.getAttribute('aria-checked') === 'true'),
73 ).toHaveLength(1);
74 });
75
76 it('nutzt Roving Tabindex: nur die aktive Option ist per Tab erreichbar', () => {
77 render(<Pruefstand start="hell" />);
78
79 const optionen = screen.getAllByRole('radio');
80 const tabIndizes = optionen.map((o) => o.getAttribute('tabindex'));
81 expect(tabIndizes).toEqual(['-1', '0', '-1', '-1']);
82 });
83 });
84
85 describe('ThemaWahl – Tastaturbedienung nach ARIA APG', () => {
86 it('wählt mit Tab und Pfeiltasten ohne Maus', async () => {
87 const nutzer = userEvent.setup();
88 render(<Pruefstand start="system" />);
89
90 await nutzer.tab();
91 expect(screen.getByRole('radio', { name: /Systemvorgabe/u }).matches(':focus')).toBe(true);
92
93 await nutzer.keyboard('{ArrowRight}');
94 expect(screen.getByRole('radio', { name: /Hell/u })).toBeChecked();
95
96 await nutzer.keyboard('{ArrowRight}');
97 expect(screen.getByRole('radio', { name: /Dunkel/u })).toBeChecked();
98
99 await nutzer.keyboard('{ArrowLeft}');
100 expect(screen.getByRole('radio', { name: /Hell/u })).toBeChecked();
101 });
102
103 it('springt mit Pos1 und Ende an Anfang und Ende', async () => {
104 const nutzer = userEvent.setup();
105 render(<Pruefstand start="hell" />);
106
107 await nutzer.tab();
108 await nutzer.keyboard('{End}');
109 expect(screen.getByRole('radio', { name: /Hoher Kontrast/u })).toBeChecked();
110
111 await nutzer.keyboard('{Home}');
112 expect(screen.getByRole('radio', { name: /Systemvorgabe/u })).toBeChecked();
113 });
114
115 it('läuft an den Rändern rundherum weiter', async () => {
116 const nutzer = userEvent.setup();
117 render(<Pruefstand start="system" />);
118
119 await nutzer.tab();
120 await nutzer.keyboard('{ArrowLeft}');
121 expect(screen.getByRole('radio', { name: /Hoher Kontrast/u })).toBeChecked();
122
123 await nutzer.keyboard('{ArrowRight}');
124 expect(screen.getByRole('radio', { name: /Systemvorgabe/u })).toBeChecked();
125 });
126
127 it('wählt die fokussierte Option mit der Leertaste', async () => {
128 const nutzer = userEvent.setup();
129 render(<Pruefstand start="hell" />);
130
131 await nutzer.tab();
132 await nutzer.keyboard(' ');
133 expect(screen.getByRole('radio', { name: /Hell/u })).toBeChecked();
134 });
135 });
136
137 describe('ThemaWahl – Barrierefreiheits-Rauchtest (axe-core)', () => {
138 it('meldet keine Verstöße gegen WCAG 2.1 A/AA', async () => {
139 const { container } = render(<Pruefstand />);
140
141 const ergebnis = await axe.run(container, jsdomOptionen());
142
143 const zusammenfassung = ergebnis.violations.map((v) => `${v.id}: ${v.help}`);
144 expect(zusammenfassung).toEqual([]);
145 });
146 });
147
148 describe('ThemaWahl – der Fokus bleibt, wo er hingehört', () => {
149 it('reißt den Fokus nicht aus einem anderen Bedienelement', async () => {
150 /*
151 Befund der Prüfrunde zu 0.27.2. Der Kommentar über der Referenz sagt zu:
152 „Fokus nur nach Tastaturnavigation setzen, nicht bei jedem Render.“ Der
153 Effekt darunter lief aber ohne Abhängigkeitsliste, und der Merkwert
154 wurde ausschließlich in diesem Effekt geleert. Wählt man die bereits
155 aktive Option, bekommt React zweimal denselben Zustand, bricht ohne
156 Render ab — und der Merkwert bleibt stehen. Der nächste beliebige
157 Neuaufbau holte den Fokus dann aus dem gerade bedienten Element zurück
158 auf die Farbschema-Option.
159 */
160 const nutzer = userEvent.setup();
161 render(<PruefstandMitNachbar />);
162
163 const system = screen.getByRole('radio', { name: /Systemvorgabe/u });
164 system.focus();
165 await nutzer.keyboard(' ');
166
167 const nachbar = screen.getByRole('checkbox', { name: 'Nachbar' });
168 await nutzer.click(nachbar);
169
170 expect(nachbar).toHaveFocus();
171 });
172
173 it('setzt den Fokus bei der Pfeilnavigation weiterhin auf die neue Option', async () => {
174 /* Die Gegenprobe: Die Behebung darf die Tastaturnavigation nicht
175 stilllegen — das Muster verlangt den Fokuswechsel (APG, Radiogruppe). */
176 const nutzer = userEvent.setup();
177 render(<Pruefstand />);
178
179 screen.getByRole('radio', { name: /Systemvorgabe/u }).focus();
180 await nutzer.keyboard('{ArrowRight}');
181
182 expect(screen.getByRole('radio', { name: /Hell/u })).toHaveFocus();
183 });
184 });