Chip label text increased from 11px to 12px (text-xs) and number badge from 9px to 11px for the 60+ senior audience on laptops/tablets. After removing a chip via the × button, focus moves to the previous chip (falling back to the next chip when the first chip is removed) so keyboard users are not stranded on <body>. Uses Svelte tick() to wait for DOM update. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
146 lines
4.1 KiB
TypeScript
146 lines
4.1 KiB
TypeScript
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);
|
|
});
|
|
});
|