import { describe, it, expect, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { tick } from 'svelte'; import * as m from '$lib/paraglide/messages.js'; import GroupingControl from './GroupingControl.svelte'; afterEach(() => cleanup()); const radios = () => Array.from(document.querySelectorAll('[role="radio"]')) as HTMLElement[]; const group = () => document.querySelector('[role="radiogroup"]') as HTMLElement; const checkedValue = () => radios() .find((r) => r.getAttribute('aria-checked') === 'true') ?.getAttribute('data-value'); describe('GroupingControl (REQ-010)', () => { it('renders three radios inside a radiogroup, each with aria-checked (a)', () => { render(GroupingControl, {}); expect(group()).not.toBeNull(); const r = radios(); expect(r).toHaveLength(3); r.forEach((radio) => expect(radio.hasAttribute('aria-checked')).toBe(true)); }); it('defaults to Datum (f)', () => { render(GroupingControl, {}); expect(radios().filter((r) => r.getAttribute('aria-checked') === 'true')).toHaveLength(1); expect(checkedValue()).toBe('date'); }); it('exposes a text label on every segment, not colour alone (d)', () => { render(GroupingControl, {}); radios().forEach((r) => expect((r.textContent ?? '').trim().length).toBeGreaterThan(0)); }); it('gives the radiogroup an accessible name (e)', () => { render(GroupingControl, {}); expect(group().getAttribute('aria-label')).toBe(m.timeline_grouping_aria_label()); }); it('each segment has a tap target of at least 44×44px (c)', () => { render(GroupingControl, {}); radios().forEach((r) => { const rect = r.getBoundingClientRect(); expect(rect.width).toBeGreaterThanOrEqual(44); expect(rect.height).toBeGreaterThanOrEqual(44); }); }); it('exposes each segment full word as an aria-label (REQ-011)', () => { render(GroupingControl, {}); const labels = radios().map((r) => r.getAttribute('aria-label')); expect(labels).toEqual([ m.timeline_grouping_segment_date(), m.timeline_grouping_segment_event(), m.timeline_grouping_segment_thema() ]); }); it('moves the selection forward with the right arrow key (b)', async () => { render(GroupingControl, { mode: 'date' }); group().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); await tick(); expect(checkedValue()).toBe('event'); }); it('wraps to the last segment with the left arrow from Datum (b)', async () => { render(GroupingControl, { mode: 'date' }); group().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })); await tick(); expect(checkedValue()).toBe('thema'); }); it('selects a segment on click', async () => { render(GroupingControl, { mode: 'date' }); const thema = radios().find((r) => r.getAttribute('data-value') === 'thema')!; thema.click(); await tick(); expect(thema.getAttribute('aria-checked')).toBe('true'); }); }); describe('GroupingControl — disabled (REQ-018)', () => { it('marks the radiogroup aria-disabled and keeps all radios in the DOM', () => { render(GroupingControl, { mode: 'event', disabled: true }); expect(group().getAttribute('aria-disabled')).toBe('true'); expect(radios()).toHaveLength(3); }); it('announces a screen-reader reason that letters are hidden', () => { render(GroupingControl, { disabled: true }); const reason = document.querySelector('[data-testid="grouping-disabled-reason"]'); expect(reason?.textContent).toContain(m.timeline_grouping_disabled_reason()); }); it('retains the active mode while disabled (no reset to Datum)', () => { render(GroupingControl, { mode: 'thema', disabled: true }); expect(checkedValue()).toBe('thema'); }); it('ignores arrow keys while disabled', () => { render(GroupingControl, { mode: 'event', disabled: true }); group().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); expect(checkedValue()).toBe('event'); }); });