Files
familienarchiv/frontend/src/lib/components/document/FileSwitcherStrip.svelte.spec.ts
Marcel 0ab85d888b fix(bulk-upload): chip readability and focus management in FileSwitcherStrip
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>
2026-04-25 11:14:31 +02:00

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);
});
});