import { describe, it, expect, vi, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import FileSwitcherStrip from './FileSwitcherStrip.svelte'; afterEach(cleanup); const makeEntry = (id: string, title: string, overrides: Record = {}) => ({ id, title, status: 'idle' as 'idle' | 'error', previewUrl: '', ...overrides }); describe('FileSwitcherStrip', () => { it('renders the prev and next buttons', async () => { render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A.pdf')], activeId: 'f1', onSelect: () => {}, onRemove: () => {} } }); await expect.element(page.getByRole('button', { name: /vorherige datei/i })).toBeVisible(); await expect.element(page.getByRole('button', { name: /nächste datei/i })).toBeVisible(); }); it('renders one chip per file', async () => { render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A.pdf'), makeEntry('f2', 'B.pdf'), makeEntry('f3', 'C.pdf')], activeId: 'f1', onSelect: () => {}, onRemove: () => {} } }); const chips = document.querySelectorAll('[data-chip-id]'); expect(chips.length).toBe(3); }); it('marks the active chip with aria-current=true', async () => { render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A'), makeEntry('f2', 'B')], activeId: 'f2', onSelect: () => {}, onRemove: () => {} } }); const f2 = document.querySelector('[data-chip-id="f2"]') as HTMLElement; const f1 = document.querySelector('[data-chip-id="f1"]') as HTMLElement; expect(f2.getAttribute('aria-current')).toBe('true'); expect(f1.getAttribute('aria-current')).toBeNull(); }); it('shows the error indicator on chips with status="error"', async () => { render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A.pdf', { status: 'error' })], activeId: 'f1', onSelect: () => {}, onRemove: () => {} } }); const chip = document.querySelector('[data-chip-id="f1"]') as HTMLElement; expect(chip.getAttribute('data-status')).toBe('error'); }); it('calls onSelect with the chip id when clicked', async () => { const onSelect = vi.fn(); render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A'), makeEntry('f2', 'B')], activeId: 'f1', onSelect, onRemove: () => {} } }); const f2 = document.querySelector('[data-chip-id="f2"]') as HTMLElement; f2.click(); expect(onSelect).toHaveBeenCalledWith('f2'); }); it('calls onRemove when the remove button is clicked', async () => { const onRemove = vi.fn(); render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A'), makeEntry('f2', 'B')], activeId: 'f1', onSelect: () => {}, onRemove } }); const remove = document.querySelector('[data-remove-id="f1"]') as HTMLElement; remove.click(); expect(onRemove).toHaveBeenCalledWith('f1'); }); it('renders the active title in the sr-only announcer', async () => { render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'Ein Brief.pdf'), makeEntry('f2', 'B')], activeId: 'f1', onSelect: () => {}, onRemove: () => {} } }); const announcer = document.querySelector('[aria-live="polite"]'); expect(announcer?.textContent).toContain('Ein Brief.pdf'); }); it('prev button on a single-file strip is a no-op (active chip stays)', async () => { const onSelect = vi.fn(); render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A.pdf')], activeId: 'f1', onSelect, onRemove: () => {} } }); await page.getByRole('button', { name: /vorherige datei/i }).click(); // The active chip is still f1 and onSelect was not invoked with a different id. expect(document.querySelector('[data-chip-id="f1"]')?.getAttribute('aria-current')).toBe( 'true' ); expect(onSelect).not.toHaveBeenCalled(); }); it('next button on a single-file strip is a no-op (active chip stays)', async () => { const onSelect = vi.fn(); render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A.pdf')], activeId: 'f1', onSelect, onRemove: () => {} } }); await page.getByRole('button', { name: /nächste datei/i }).click(); expect(document.querySelector('[data-chip-id="f1"]')?.getAttribute('aria-current')).toBe( 'true' ); expect(onSelect).not.toHaveBeenCalled(); }); it('navigates with ArrowRight key on focused chip', async () => { render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A'), makeEntry('f2', 'B'), makeEntry('f3', 'C')], activeId: 'f1', onSelect: () => {}, onRemove: () => {} } }); const f1 = document.querySelector('[data-chip-id="f1"]') as HTMLElement; f1.focus(); f1.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); await vi.waitFor(() => { expect(document.activeElement?.getAttribute('data-chip-id')).toBe('f2'); }); }); it('navigates with ArrowLeft key on focused chip (wraps around)', async () => { render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A'), makeEntry('f2', 'B')], activeId: 'f1', onSelect: () => {}, onRemove: () => {} } }); const f1 = document.querySelector('[data-chip-id="f1"]') as HTMLElement; f1.focus(); f1.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true })); await vi.waitFor(() => { // ArrowLeft from index 0 wraps to last (f2). expect(document.activeElement?.getAttribute('data-chip-id')).toBe('f2'); }); }); it('ArrowDown is treated as ArrowRight (vertical key alias)', async () => { render(FileSwitcherStrip, { props: { files: [makeEntry('f1', 'A'), makeEntry('f2', 'B')], activeId: 'f1', onSelect: () => {}, onRemove: () => {} } }); const f1 = document.querySelector('[data-chip-id="f1"]') as HTMLElement; f1.focus(); f1.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); await vi.waitFor(() => { expect(document.activeElement?.getAttribute('data-chip-id')).toBe('f2'); }); }); });