import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axe from 'axe-core'; import { jsdomOptionen } from '../src/shared/wcag'; import { useState } from 'react'; import { describe, expect, it } from 'vitest'; import { ThemaWahl } from '../src/renderer/src/components/ThemaWahl'; import { type ThemeAuswahl } from '../src/shared/theme'; /** Minimale Hülle, die die Auswahl wie in der echten App zustandsbehaftet hält. */ function Pruefstand({ start = 'system' }: { start?: ThemeAuswahl }): React.JSX.Element { const [auswahl, setzeAuswahl] = useState(start); return ( <>

Farbschema

); } describe('ThemaWahl – ARIA-Struktur', () => { it('rendert eine benannte Radiogruppe mit allen vier Optionen', () => { render(); const gruppe = screen.getByRole('radiogroup', { name: 'Farbschema' }); expect(gruppe).toBeInTheDocument(); const optionen = screen.getAllByRole('radio'); expect(optionen.map((o) => o.textContent.replace(/[●○]/gu, '').trim())).toEqual([ 'Systemvorgabe', 'Hell', 'Dunkel', 'Hoher Kontrast', ]); }); it('markiert genau eine Option als ausgewählt', () => { render(); expect(screen.getByRole('radio', { name: /Dunkel/u })).toBeChecked(); expect( screen.getAllByRole('radio').filter((o) => o.getAttribute('aria-checked') === 'true'), ).toHaveLength(1); }); it('nutzt Roving Tabindex: nur die aktive Option ist per Tab erreichbar', () => { render(); const optionen = screen.getAllByRole('radio'); const tabIndizes = optionen.map((o) => o.getAttribute('tabindex')); expect(tabIndizes).toEqual(['-1', '0', '-1', '-1']); }); }); describe('ThemaWahl – Tastaturbedienung nach ARIA APG', () => { it('wählt mit Tab und Pfeiltasten ohne Maus', async () => { const nutzer = userEvent.setup(); render(); await nutzer.tab(); expect(screen.getByRole('radio', { name: /Systemvorgabe/u }).matches(':focus')).toBe(true); await nutzer.keyboard('{ArrowRight}'); expect(screen.getByRole('radio', { name: /Hell/u })).toBeChecked(); await nutzer.keyboard('{ArrowRight}'); expect(screen.getByRole('radio', { name: /Dunkel/u })).toBeChecked(); await nutzer.keyboard('{ArrowLeft}'); expect(screen.getByRole('radio', { name: /Hell/u })).toBeChecked(); }); it('springt mit Pos1 und Ende an Anfang und Ende', async () => { const nutzer = userEvent.setup(); render(); await nutzer.tab(); await nutzer.keyboard('{End}'); expect(screen.getByRole('radio', { name: /Hoher Kontrast/u })).toBeChecked(); await nutzer.keyboard('{Home}'); expect(screen.getByRole('radio', { name: /Systemvorgabe/u })).toBeChecked(); }); it('läuft an den Rändern rundherum weiter', async () => { const nutzer = userEvent.setup(); render(); await nutzer.tab(); await nutzer.keyboard('{ArrowLeft}'); expect(screen.getByRole('radio', { name: /Hoher Kontrast/u })).toBeChecked(); await nutzer.keyboard('{ArrowRight}'); expect(screen.getByRole('radio', { name: /Systemvorgabe/u })).toBeChecked(); }); it('wählt die fokussierte Option mit der Leertaste', async () => { const nutzer = userEvent.setup(); render(); await nutzer.tab(); await nutzer.keyboard(' '); expect(screen.getByRole('radio', { name: /Hell/u })).toBeChecked(); }); }); describe('ThemaWahl – Barrierefreiheits-Rauchtest (axe-core)', () => { it('meldet keine Verstöße gegen WCAG 2.1 A/AA', async () => { const { container } = render(); const ergebnis = await axe.run(container, jsdomOptionen()); const zusammenfassung = ergebnis.violations.map((v) => `${v.id}: ${v.help}`); expect(zusammenfassung).toEqual([]); }); });