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[]) { 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( '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( '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('input[type="hidden"][name="documentIds"]') ).toBeNull(); }); });