import { describe, expect, it, vi, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import DocumentList from './DocumentList.svelte'; vi.mock('$app/navigation', () => ({ goto: vi.fn() })); afterEach(() => cleanup()); const baseProps = { documents: [], canWrite: false, error: null, total: 0, q: '', matchData: {} as Record< string, import('$lib/generated/api').components['schemas']['SearchMatchData'] > }; type DocOverrides = { id?: string; title?: string; documentDate?: string | null; sender?: { id?: string; firstName?: string | null; lastName: string; displayName: string } | null; receivers?: { id?: string; firstName?: string | null; lastName: string; displayName: string }[]; tags?: { id: string; name: string }[]; }; const makeDoc = (overrides: DocOverrides = {}) => ({ id: '1', title: 'Testbrief', originalFilename: 'testbrief.pdf', status: 'UPLOADED' as const, documentDate: '2024-03-15', location: null, sender: null, receivers: [] as { id?: string; firstName?: string | null; lastName: string; displayName: string; }[], tags: [], ...overrides }); describe('DocumentList – result count', () => { it('shows result count when total > 0', async () => { render(DocumentList, { ...baseProps, documents: [makeDoc()], total: 1, q: 'test' }); await expect.element(page.getByText('1 Dokumente')).toBeInTheDocument(); }); it('does not show result count when total is 0 and there is no error', async () => { render(DocumentList, { ...baseProps, total: 0, q: '' }); const count = page.getByText(/\d+ Dokumente/); await expect.element(count).not.toBeInTheDocument(); }); }); describe('DocumentList – empty state with search term', () => { it('shows generic empty heading when q is empty', async () => { render(DocumentList, { ...baseProps }); await expect.element(page.getByText(/Keine Dokumente/)).toBeInTheDocument(); }); it('shows search term in empty state when q is set', async () => { render(DocumentList, { ...baseProps, q: 'Urlaub' }); await expect.element(page.getByText(/"Urlaub"/)).toBeInTheDocument(); }); }); // ─── Group headers ──────────────────────────────────────────────────────────── describe('DocumentList – group headers', () => { it('renders group-divider elements when DATE sort spans multiple years', async () => { const documents = [ makeDoc({ id: '1', documentDate: '1923-04-12' }), makeDoc({ id: '2', documentDate: '1965-08-03' }) ]; render(DocumentList, { ...baseProps, documents, total: 2, sort: 'DATE' }); await expect.element(page.getByTestId('group-divider').first()).toBeInTheDocument(); }); it('does not render group-divider when DATE sort has only one distinct year', async () => { const documents = [ makeDoc({ id: '1', documentDate: '1938-01-01' }), makeDoc({ id: '2', documentDate: '1938-06-15' }) ]; render(DocumentList, { ...baseProps, documents, total: 2, sort: 'DATE' }); await expect.element(page.getByTestId('group-divider')).not.toBeInTheDocument(); }); it('does not render group-divider for TITLE sort', async () => { const documents = [ makeDoc({ id: '1', documentDate: '1923-04-12' }), makeDoc({ id: '2', documentDate: '1965-08-03' }) ]; render(DocumentList, { ...baseProps, documents, total: 2, sort: 'TITLE' }); await expect.element(page.getByTestId('group-divider')).not.toBeInTheDocument(); }); it('shows Undatiert fallback label when sort is undefined and doc has no date', async () => { const documents = [ makeDoc({ id: '1', documentDate: '1938-01-01' }), makeDoc({ id: '2', documentDate: null }) ]; render(DocumentList, { ...baseProps, documents, total: 2 }); // sort omitted — defaults to DATE grouping await expect.element(page.getByText(/UNDATIERT/i)).toBeInTheDocument(); }); it('a doc with two receivers appears in both receiver groups', async () => { const documents = [ makeDoc({ id: '1', receivers: [ { firstName: null, lastName: 'Müller', displayName: 'Anna Müller' }, { firstName: null, lastName: 'Bauer', displayName: 'Karl Bauer' } ] }) ]; render(DocumentList, { ...baseProps, documents, total: 1, sort: 'RECEIVER' }); const links = page.getByRole('link', { name: /Testbrief/ }); await expect.element(links.first()).toBeInTheDocument(); await expect.element(links.nth(1)).toBeInTheDocument(); }); }); // ─── Match data: snippet and title highlighting ─────────────────────────────── describe('DocumentList – match snippets and highlights', () => { it('shows transcription snippet when matchData has one for the document', async () => { const doc = makeDoc({ id: 'doc1' }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: { doc1: { transcriptionSnippet: 'Er schrieb einen langen Brief', titleOffsets: [], senderMatched: false, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] } } }); await expect.element(page.getByText('Er schrieb einen langen Brief')).toBeInTheDocument(); }); it('does not show snippet section when matchData has no entry for the document', async () => { const doc = makeDoc({ id: 'doc1' }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: {} }); await expect.element(page.getByTestId('search-snippet')).not.toBeInTheDocument(); }); it('renders a element when titleOffsets are present', async () => { const doc = makeDoc({ id: 'doc1', title: 'Brief an Anna' }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: { doc1: { transcriptionSnippet: undefined, titleOffsets: [{ start: 0, length: 5 }], // "Brief" senderMatched: false, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] } } }); // The word "Brief" should be inside a element const mark = page.getByRole('mark'); await expect.element(mark).toBeInTheDocument(); await expect.element(mark).toHaveTextContent('Brief'); }); it('renders title as plain text when titleOffsets is empty', async () => { const doc = makeDoc({ id: 'doc1', title: 'Brief an Anna' }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: { doc1: { transcriptionSnippet: undefined, titleOffsets: [], senderMatched: false, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] } } }); await expect.element(page.getByRole('mark')).not.toBeInTheDocument(); await expect.element(page.getByText('Brief an Anna')).toBeInTheDocument(); }); it('renders inside snippet when snippetOffsets are present', async () => { const doc = makeDoc({ id: 'doc1' }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: { doc1: { transcriptionSnippet: 'Er schrieb einen Brief', titleOffsets: [], senderMatched: false, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [{ start: 17, length: 5 }], // "Brief" summaryOffsets: [] } } }); const snippet = page.getByTestId('search-snippet'); await expect.element(snippet).toBeInTheDocument(); const mark = snippet.getByRole('mark'); await expect.element(mark).toBeInTheDocument(); await expect.element(mark).toHaveTextContent('Brief'); }); it('renders snippet as plain text when snippetOffsets is empty', async () => { const doc = makeDoc({ id: 'doc1' }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: { doc1: { transcriptionSnippet: 'Er schrieb einen Brief', titleOffsets: [], senderMatched: false, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] } } }); const snippet = page.getByTestId('search-snippet'); await expect.element(snippet).toBeInTheDocument(); // No mark elements inside the snippet when offsets is empty await expect.element(snippet.getByRole('mark')).not.toBeInTheDocument(); }); it('visually marks sender when senderMatched is true', async () => { const doc = makeDoc({ id: 'doc1', sender: { id: 'sender-1', firstName: 'Walter', lastName: 'Raddatz', displayName: 'Walter Raddatz' } }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: { doc1: { transcriptionSnippet: undefined, titleOffsets: [], senderMatched: true, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] } } }); const senderMark = page.getByTestId('sender-match'); await expect.element(senderMark).toBeInTheDocument(); await expect.element(senderMark).toHaveTextContent('Walter Raddatz'); }); it('does not mark sender when senderMatched is false', async () => { const doc = makeDoc({ id: 'doc1', sender: { id: 'sender-1', firstName: 'Walter', lastName: 'Raddatz', displayName: 'Walter Raddatz' } }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: { doc1: { transcriptionSnippet: undefined, titleOffsets: [], senderMatched: false, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] } } }); await expect.element(page.getByTestId('sender-match')).not.toBeInTheDocument(); }); it('visually marks matched receiver when their id is in matchedReceiverIds', async () => { const doc = makeDoc({ id: 'doc1', receivers: [ { id: 'p-1', firstName: 'Anna', lastName: 'Schmidt', displayName: 'Anna Schmidt' }, { id: 'p-2', firstName: 'Karl', lastName: 'Bauer', displayName: 'Karl Bauer' } ] }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: { doc1: { transcriptionSnippet: undefined, titleOffsets: [], senderMatched: false, matchedReceiverIds: ['p-1'], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] } } }); // Only Anna Schmidt should be marked const receiverMark = page.getByTestId('receiver-match'); await expect.element(receiverMark).toBeInTheDocument(); await expect.element(receiverMark).toHaveTextContent('Anna Schmidt'); }); it('renders a color dot on tag chips that have a color', async () => { const doc = makeDoc({ id: 'doc1', tags: [{ id: 'tag-1', name: 'Familie', color: 'sage' }] }); render(DocumentList, { ...baseProps, documents: [doc], total: 1 }); const dot = page.getByTestId('tag-color-dot'); await expect.element(dot).toBeInTheDocument(); await expect.element(dot).toHaveAttribute('data-color', 'sage'); }); it('does not render a color dot on tag chips without a color', async () => { const doc = makeDoc({ id: 'doc1', tags: [{ id: 'tag-1', name: 'Familie' }] }); render(DocumentList, { ...baseProps, documents: [doc], total: 1 }); await expect.element(page.getByTestId('tag-color-dot')).not.toBeInTheDocument(); }); it('visually marks matched tag when its id is in matchedTagIds', async () => { const doc = makeDoc({ id: 'doc1', tags: [ { id: 'tag-1', name: 'Familiengeschichte' }, { id: 'tag-2', name: 'Reise' } ] }); render(DocumentList, { ...baseProps, documents: [doc], total: 1, matchData: { doc1: { transcriptionSnippet: undefined, titleOffsets: [], senderMatched: false, matchedReceiverIds: [], matchedTagIds: ['tag-1'], snippetOffsets: [], summaryOffsets: [] } } }); const tagMark = page.getByTestId('tag-match'); await expect.element(tagMark).toBeInTheDocument(); await expect.element(tagMark).toHaveTextContent('Familiengeschichte'); }); });