/** * Die Ruhepause vor einer Live-Region. * * **Der Anlass.** Die Fragensuche schickte ihre Trefferansage seit jeher erst * nach 400 Millisekunden Ruhe an die Live-Region – mit der Begründung, die der * Prüfplan zu WCAG 4.1.3 gibt: Eine höfliche Region, die bei jedem * Tastenanschlag eine neue Zahl spricht, erzeugt beim Tippen eine Kette * einander unterbrechender Ansagen zwischen den Zeichen-Echos. * * Die Glossaransicht rendert dieselbe Art Auskunft („N von 87 Einträgen“) – * und tat das bis 0.22.0 **unmittelbar**. Dieselbe Sache, zwei * Verhaltensweisen; die eine davon war die, vor der die eigene Begründung * warnt. * * Geprüft wird hier die Mechanik und daneben, dass beide Ansichten wirklich * aus derselben Quelle schöpfen – sonst stünde die gemeinsame Regel zwar da, * bliebe aber wirkungslos. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { act, renderHook } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { RUHE_MS, useVerzoegerteAnsage } from '../src/renderer/src/hooks/useVerzoegerteAnsage'; /* `process.cwd()` und nicht `import.meta.url`: Diese Datei braucht jsdom für den Hook, und dort ist `import.meta.url` eine http-Adresse – derselbe Stolperstein, den `tests/sicherheit.test.ts` in seinem Kopf benennt. Vitest läuft aus `app/`. */ function quelle(...teile: string[]): string { return readFileSync(join(process.cwd(), 'src', 'renderer', 'src', ...teile), 'utf8'); } beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); describe('Ansage mit Ruhepause', () => { it('zeigt den ersten Text sofort', () => { /* Beim Aufbau der Ansicht wartet niemand: Die Region muss von der ersten Darstellung an im Baum stehen und etwas sagen. */ const { result } = renderHook(() => useVerzoegerteAnsage('87 Einträge')); expect(result.current).toBe('87 Einträge'); }); it('schweigt, solange getippt wird', () => { const { result, rerender } = renderHook(({ text }) => useVerzoegerteAnsage(text), { initialProps: { text: '87 Einträge' }, }); for (const zwischenstand of ['12 von 87', '5 von 87', '2 von 87']) { rerender({ text: zwischenstand }); act(() => { vi.advanceTimersByTime(RUHE_MS - 100); }); expect(result.current).toBe('87 Einträge'); } }); it('zieht nach der Ruhepause nach', () => { const { result, rerender } = renderHook(({ text }) => useVerzoegerteAnsage(text), { initialProps: { text: '87 Einträge' }, }); rerender({ text: '2 von 87 Einträgen' }); act(() => { vi.advanceTimersByTime(RUHE_MS + 10); }); expect(result.current).toBe('2 von 87 Einträgen'); }); it('überspringt die Zwischenstände, statt sie nachzuholen', () => { /* Die Zwischenstände dürfen nicht nachträglich hintereinander gesprochen werden – das wäre dieselbe Kette, nur später. */ const { result, rerender } = renderHook(({ text }) => useVerzoegerteAnsage(text), { initialProps: { text: 'a' }, }); rerender({ text: 'b' }); rerender({ text: 'c' }); rerender({ text: 'd' }); act(() => { vi.advanceTimersByTime(RUHE_MS + 10); }); expect(result.current).toBe('d'); }); }); describe('Beide Filterfelder schöpfen aus derselben Quelle', () => { it('die Fragensuche', () => { expect(quelle('hooks', 'useFragensuche.ts')).toContain('useVerzoegerteAnsage'); }); it('die Glossaransicht', () => { /* Der eigentliche Befund: Hier stand die Zahl unmittelbar am `role="status"`-Element. */ expect(quelle('components', 'lernen', 'Glossaransicht.tsx')).toContain('useVerzoegerteAnsage'); }); it('und keine von beiden führt eine eigene Ruhezahl', () => { /* Zwei Zahlen an zwei Stellen laufen auseinander, und eine davon wäre dann die falsche. */ for (const datei of [ quelle('hooks', 'useFragensuche.ts'), quelle('components', 'lernen', 'Glossaransicht.tsx'), ]) { expect(datei).not.toMatch(/RUHE_MS\s*=\s*\d/u); } }); });