refactor: move document domain core to lib/document/
Moves ~25 components, utils (search, filename, groupDocuments, documentStatusLabel, validateFile), bulkSelection store, and TranscriptionSection sub-component. Fixes broken relative imports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
145
frontend/src/lib/document/FileSwitcherStrip.svelte.spec.ts
Normal file
145
frontend/src/lib/document/FileSwitcherStrip.svelte.spec.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user