import { describe, it, expect, vi, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page, userEvent } from 'vitest/browser'; import FileSwitcherStrip from './FileSwitcherStrip.svelte'; import type { FileEntry } from './FileSwitcherStrip.svelte'; afterEach(cleanup); function makeFiles(n: number): FileEntry[] { return Array.from({ length: n }, (_, i) => ({ id: `id-${i}`, file: new File([''], `file${i}.pdf`), title: `File ${i}`, status: 'idle' as const, previewUrl: '' })); } describe('FileSwitcherStrip', () => { it('renders N chips for N files', async () => { const files = makeFiles(4); render(FileSwitcherStrip, { files, activeId: files[0].id, onSelect: vi.fn(), onRemove: vi.fn() }); const chips = page.getByRole('listitem'); await expect.element(chips.nth(0)).toBeInTheDocument(); await expect.element(chips.nth(3)).toBeInTheDocument(); }); it('active chip has aria-current="true"', async () => { const files = makeFiles(3); const { container } = render(FileSwitcherStrip, { files, activeId: files[1].id, onSelect: vi.fn(), onRemove: vi.fn() }); const activeBtn = container.querySelector('[aria-current="true"]'); expect(activeBtn).not.toBeNull(); expect(activeBtn?.textContent).toContain('File 1'); }); it('clicking a chip fires onSelect with its id', async () => { const files = makeFiles(3); const onSelect = vi.fn(); const { container } = render(FileSwitcherStrip, { files, activeId: files[0].id, onSelect, onRemove: vi.fn() }); const chip = container.querySelector('[data-chip-id="id-2"]') as HTMLElement; expect(chip).not.toBeNull(); chip.click(); expect(onSelect).toHaveBeenCalledWith('id-2'); }); it('error chip has aria-label containing warning indicator', async () => { const files: FileEntry[] = [ { id: 'e1', file: new File([''], 'bad.pdf'), title: 'Bad file', status: 'error', previewUrl: '' } ]; const { container } = render(FileSwitcherStrip, { files, activeId: 'e1', onSelect: vi.fn(), onRemove: vi.fn() }); const errBtn = container.querySelector('[data-status="error"]'); expect(errBtn).not.toBeNull(); }); it('error chip contains a screen-reader-only error label', async () => { const files: FileEntry[] = [ { id: 'e1', file: new File([''], 'bad.pdf'), title: 'Bad file', status: 'error', previewUrl: '' } ]; const { container } = render(FileSwitcherStrip, { files, activeId: 'e1', onSelect: vi.fn(), onRemove: vi.fn() }); const errBtn = container.querySelector('[data-status="error"]'); const srOnly = errBtn?.querySelector('.sr-only'); expect(srOnly).not.toBeNull(); }); it('focus moves to the previous chip after the middle chip is removed', async () => { const files = makeFiles(3); // id-0, id-1, id-2 const onRemove = vi.fn(); const { container } = render(FileSwitcherStrip, { files, activeId: files[1].id, onSelect: vi.fn(), onRemove }); const removeBtn = container.querySelector('[data-remove-id="id-1"]') as HTMLButtonElement; expect(removeBtn).not.toBeNull(); removeBtn.click(); expect(onRemove).toHaveBeenCalledWith('id-1'); // After removal, focus should be on the chip for id-0 (the previous chip) await vi.waitFor( () => { const prevChip = container.querySelector('[data-chip-id="id-0"]') as HTMLElement | null; expect(prevChip).not.toBeNull(); expect(document.activeElement).toBe(prevChip); }, { timeout: 1000 } ); }); it('ArrowRight moves focus to next chip without leaving strip', async () => { const files = makeFiles(3); const { container } = render(FileSwitcherStrip, { files, activeId: files[0].id, onSelect: vi.fn(), onRemove: vi.fn() }); const firstBtn = container.querySelectorAll('[data-chip-id]')[0] as HTMLElement; firstBtn.focus(); await userEvent.keyboard('{ArrowRight}'); const focused = document.activeElement; expect(focused).not.toBe(firstBtn); // The new focused element should still be inside the strip const strip = container.querySelector('[data-testid="file-switcher-strip"]'); expect(strip?.contains(focused)).toBe(true); }); });