import React, { useState } from 'react'; import { describe, it, expect, vi } from 'vitest'; import { render, fireEvent, act } from '@testing-library/react'; import { PixelToggleGroup, PixelToggle } from '../../forms/PixelToggleGroup'; // In single-select mode, toggles expose role=radio + aria-checked so SR // users hear "one of N" semantics. In multi-select mode, they stay // role=button + aria-pressed. The check below uses aria-checked for single. function pressedAttr(el: HTMLElement, type: 'single' | 'multiple'): string | null { return type === 'single' ? el.getAttribute('aria-checked') : el.getAttribute('aria-pressed'); } describe('PixelToggleGroup', () => { it('type=single only allows one pressed at a time', () => { const onChange = vi.fn(); function Wrapper() { const [val, setVal] = useState('a'); return ( { onChange(next); setVal(next as string); }} > A B C ); } const { getByText } = render(); const a = getByText('A').closest('button')!; const b = getByText('B').closest('button')!; const c = getByText('C').closest('button')!; expect(pressedAttr(a, 'single')).toBe('true'); expect(pressedAttr(b, 'single')).toBe('false'); expect(pressedAttr(c, 'single')).toBe('false'); expect(a.getAttribute('role')).toBe('radio'); act(() => { fireEvent.click(b); }); expect(onChange).toHaveBeenLastCalledWith('b'); expect(pressedAttr(a, 'single')).toBe('false'); expect(pressedAttr(b, 'single')).toBe('true'); // Clicking pressed item in single-mode unsets it (empty string) act(() => { fireEvent.click(b); }); expect(onChange).toHaveBeenLastCalledWith(''); expect(pressedAttr(b, 'single')).toBe('false'); }); it('type=multiple allows multiple', () => { const onChange = vi.fn(); function Wrapper() { const [val, setVal] = useState(['a']); return ( { onChange(next); setVal(next as string[]); }} > A B C ); } const { getByText } = render(); const a = getByText('A').closest('button')!; const b = getByText('B').closest('button')!; const c = getByText('C').closest('button')!; expect(a.getAttribute('aria-pressed')).toBe('true'); expect(b.getAttribute('aria-pressed')).toBe('false'); act(() => { fireEvent.click(b); }); expect(onChange).toHaveBeenLastCalledWith(['a', 'b']); expect(a.getAttribute('aria-pressed')).toBe('true'); expect(b.getAttribute('aria-pressed')).toBe('true'); act(() => { fireEvent.click(c); }); expect(onChange).toHaveBeenLastCalledWith(['a', 'b', 'c']); // Toggle off act(() => { fireEvent.click(a); }); expect(onChange).toHaveBeenLastCalledWith(['b', 'c']); expect(a.getAttribute('aria-pressed')).toBe('false'); }); // Standalone (group-less) PixelToggle behavior lives in ./PixelToggle.test.tsx. it('Arrow keys move focus with rovingFocus=true', () => { const { getByText } = render( A B C , ); const a = getByText('A').closest('button') as HTMLButtonElement; const b = getByText('B').closest('button') as HTMLButtonElement; const c = getByText('C').closest('button') as HTMLButtonElement; // Initial roving tabindex: first item = 0, others = -1 expect(a.tabIndex).toBe(0); expect(b.tabIndex).toBe(-1); expect(c.tabIndex).toBe(-1); act(() => { a.focus(); }); expect(document.activeElement).toBe(a); act(() => { fireEvent.keyDown(a, { key: 'ArrowRight' }); }); expect(document.activeElement).toBe(b); act(() => { fireEvent.keyDown(b, { key: 'ArrowRight' }); }); expect(document.activeElement).toBe(c); // Loop wraps act(() => { fireEvent.keyDown(c, { key: 'ArrowRight' }); }); expect(document.activeElement).toBe(a); // ArrowLeft loops backwards act(() => { fireEvent.keyDown(a, { key: 'ArrowLeft' }); }); expect(document.activeElement).toBe(c); // Home / End act(() => { fireEvent.keyDown(c, { key: 'Home' }); }); expect(document.activeElement).toBe(a); act(() => { fireEvent.keyDown(a, { key: 'End' }); }); expect(document.activeElement).toBe(c); }); it('forwards className', () => { const { container } = render( A , ); const group = container.querySelector('.my-custom-group'); expect(group).toBeTruthy(); const toggle = container.querySelector('.my-custom-toggle'); expect(toggle).toBeTruthy(); }); });