test(geschichten): cover DocumentMultiSelect search, chip add/remove
Browser-based component spec mirroring PersonTypeahead.svelte.spec.ts: renders empty input, surfaces pre-selected chips with formatted date, emits hidden documentIds inputs for each chip, debounces the search against /api/documents/search, adds a chip on click, hides already- selected docs from new dropdown results, and removes a chip on × click. Closes Felix's review B2 on PR #382. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
126
frontend/src/lib/components/DocumentMultiSelect.svelte.spec.ts
Normal file
126
frontend/src/lib/components/DocumentMultiSelect.svelte.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { cleanup, render } from 'vitest-browser-svelte';
|
||||||
|
import { page, userEvent } from 'vitest/browser';
|
||||||
|
import DocumentMultiSelect from './DocumentMultiSelect.svelte';
|
||||||
|
|
||||||
|
const waitForDebounce = () => new Promise((r) => setTimeout(r, 350));
|
||||||
|
|
||||||
|
const docFactory = (id: string, title: string, date = '1880-01-01') => ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
documentDate: date,
|
||||||
|
originalFilename: `${title}.pdf`,
|
||||||
|
status: 'UPLOADED',
|
||||||
|
metadataComplete: false,
|
||||||
|
scriptType: 'UNKNOWN' as const,
|
||||||
|
createdAt: '2024-01-01T00:00:00',
|
||||||
|
updatedAt: '2024-01-01T00:00:00'
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockSearchResponse(items: ReturnType<typeof docFactory>[]) {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: vi.fn().mockResolvedValue({ items: items.map((document) => ({ document })) })
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DocumentMultiSelect — rendering', () => {
|
||||||
|
it('renders an empty chip-input by default', async () => {
|
||||||
|
render(DocumentMultiSelect);
|
||||||
|
await expect.element(page.getByPlaceholder('Dokument suchen…')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders pre-selected documents as chips with their date', async () => {
|
||||||
|
render(DocumentMultiSelect, {
|
||||||
|
selectedDocuments: [docFactory('d1', 'Brief vom 1. Mai', '1882-05-01')]
|
||||||
|
});
|
||||||
|
await expect.element(page.getByText(/Brief vom 1\. Mai/)).toBeInTheDocument();
|
||||||
|
await expect.element(page.getByText(/01\.05\.1882/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits a hidden documentIds input for each pre-selected document', async () => {
|
||||||
|
render(DocumentMultiSelect, {
|
||||||
|
selectedDocuments: [docFactory('d1', 'A'), docFactory('d2', 'B')]
|
||||||
|
});
|
||||||
|
const inputs = document.querySelectorAll<HTMLInputElement>(
|
||||||
|
'input[type="hidden"][name="documentIds"]'
|
||||||
|
);
|
||||||
|
expect(inputs).toHaveLength(2);
|
||||||
|
expect([inputs[0].value, inputs[1].value].sort()).toEqual(['d1', 'd2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DocumentMultiSelect — search and select', () => {
|
||||||
|
it('queries /api/documents/search after debounce and shows results', async () => {
|
||||||
|
mockSearchResponse([docFactory('d1', 'Brief von Eugenie')]);
|
||||||
|
render(DocumentMultiSelect);
|
||||||
|
|
||||||
|
await userEvent.fill(page.getByPlaceholder('Dokument suchen…'), 'Eug');
|
||||||
|
await waitForDebounce();
|
||||||
|
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||||
|
expect.stringMatching(/^\/api\/documents\/search\?q=Eug/)
|
||||||
|
);
|
||||||
|
await expect.element(page.getByText(/Brief von Eugenie/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a chip when a search result is clicked', async () => {
|
||||||
|
mockSearchResponse([docFactory('d1', 'Brief von Eugenie')]);
|
||||||
|
render(DocumentMultiSelect);
|
||||||
|
|
||||||
|
await userEvent.fill(page.getByPlaceholder('Dokument suchen…'), 'Eug');
|
||||||
|
await waitForDebounce();
|
||||||
|
await userEvent.click(page.getByText(/Brief von Eugenie/));
|
||||||
|
|
||||||
|
// After selection the search field clears and the chip is rendered
|
||||||
|
const hidden = document.querySelector<HTMLInputElement>(
|
||||||
|
'input[type="hidden"][name="documentIds"]'
|
||||||
|
);
|
||||||
|
expect(hidden?.value).toBe('d1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides already-selected documents from new search results', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: vi.fn().mockResolvedValue({
|
||||||
|
items: [
|
||||||
|
{ document: docFactory('d1', 'Already attached') },
|
||||||
|
{ document: docFactory('d2', 'Not attached') }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
render(DocumentMultiSelect, {
|
||||||
|
selectedDocuments: [docFactory('d1', 'Already attached')]
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.fill(page.getByPlaceholder('Dokument suchen…'), 'attached');
|
||||||
|
await waitForDebounce();
|
||||||
|
|
||||||
|
// "Not attached" appears in the dropdown; "Already attached" only as the chip.
|
||||||
|
const matches = await page.getByText(/Already attached/).all();
|
||||||
|
expect(matches.length).toBe(1); // chip only, not in dropdown
|
||||||
|
await expect.element(page.getByText(/Not attached/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DocumentMultiSelect — remove', () => {
|
||||||
|
it('removes a chip when its × button is clicked', async () => {
|
||||||
|
render(DocumentMultiSelect, {
|
||||||
|
selectedDocuments: [docFactory('d1', 'Brief A')]
|
||||||
|
});
|
||||||
|
await userEvent.click(page.getByLabelText('Entfernen'));
|
||||||
|
expect(
|
||||||
|
document.querySelector<HTMLInputElement>('input[type="hidden"][name="documentIds"]')
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user