Files
familienarchiv/frontend/src/lib/document/DocumentMultiSelect.svelte.spec.ts
Marcel 8e9e3bba06
All checks were successful
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m2s
nightly / deploy-staging (push) Successful in 2m2s
CI / Unit & Component Tests (push) Successful in 3m58s
CI / OCR Service Tests (push) Successful in 20s
CI / Backend Unit Tests (push) Successful in 3m50s
CI / fail2ban Regex (push) Successful in 44s
CI / Unit & Component Tests (pull_request) Successful in 3m29s
CI / Semgrep Security Scan (push) Successful in 21s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m43s
CI / Compose Bucket Idempotency (push) Successful in 59s
CI / fail2ban Regex (pull_request) Successful in 45s
refactor(document): address review concerns from PR #660
- Restore JavaDoc on DocumentSearchResult.of() and .paged() factory methods
- Remove redundant null guards on @Builder.Default collections in toListItem()
- Map DocumentListItem fields explicitly in DocumentMultiSelect before cast
- Add DocumentListItem required fields to docFactory in spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:27:31 +02:00

136 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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`,
receivers: [],
tags: [],
completionPercentage: 0,
contributors: [],
matchData: {
titleOffsets: [],
senderMatched: false,
matchedReceiverIds: [],
matchedTagIds: [],
snippetOffsets: [],
summaryOffsets: []
},
status: 'UPLOADED' as const,
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 })
})
);
}
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: [docFactory('d1', 'Already attached'), 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();
});
});