Files
familienarchiv/frontend/src/lib/document/DocumentMultiSelect.svelte.spec.ts
Marcel b56b9dfa74 feat(frontend): render honest precision dates in detail, list and search
Wires formatDocumentDate/DocumentDate into the read sites: the document
detail top bar + metadata drawer (the drawer shows the visible "Originaltext:"
raw line for UNKNOWN/SEASON/APPROX), the search/list rows (DocumentRow,
mobile + desktop), and the document multi-select dropdown label. A MONTH or
SEASON document now reads "Juni 1916"/"Sommer 1916" everywhere instead of a
fabricated day.

Adds metaDatePrecision to the DocumentRow/DocumentMultiSelect test fixtures
(required on DocumentListItem since #671) and updates the multi-select label
assertion to the honest long date.

Refs #666

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:56:49 +02:00

138 lines
4.4 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,
metaDatePrecision: 'DAY' as const,
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();
// DAY precision renders the honest long date (formatDocumentDate), not 01.05.1882.
await expect.element(page.getByText(/1\. Mai 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();
});
});