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'; import type { components } from '$lib/generated/api'; vi.mock('$app/navigation', () => ({ goto: vi.fn() })); afterEach(() => cleanup()); type DocumentSearchItem = components['schemas']['DocumentSearchItem']; function makeItem(overrides: Partial = {}): DocumentSearchItem { return { document: { id: '1', title: 'Testbrief', originalFilename: 'testbrief.pdf', status: 'UPLOADED', documentDate: '2024-03-15', sender: undefined, receivers: [], tags: [], createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', metadataComplete: false, scriptType: 'UNKNOWN' }, matchData: { titleOffsets: [], senderMatched: false, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] }, completionPercentage: 0, contributors: [], ...overrides }; } const baseProps = { items: [], canWrite: false, error: null, total: 0, q: '' }; // ─── Result count ───────────────────────────────────────────────────────────── describe('DocumentList – result count', () => { it('shows result count when total > 0', async () => { render(DocumentList, { ...baseProps, items: [makeItem()], total: 1, q: 'test' }); await expect.element(page.getByText('1 Dokumente')).toBeInTheDocument(); }); it('does not show result count when total is 0', async () => { render(DocumentList, { ...baseProps, total: 0, q: '' }); await expect.element(page.getByText(/\d+ Dokumente/)).not.toBeInTheDocument(); }); }); // ─── Empty state ────────────────────────────────────────────────────────────── describe('DocumentList – empty state', () => { 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(); }); }); // ─── Year grouping ──────────────────────────────────────────────────────────── describe('DocumentList – year grouping', () => { it('groups documents by year into separate cards', async () => { const items = [ makeItem({ document: { ...makeItem().document, id: '1', documentDate: '1923-04-12' } }), makeItem({ document: { ...makeItem().document, id: '2', documentDate: '1965-08-03' } }) ]; render(DocumentList, { ...baseProps, items, total: 2 }); const groupCards = page.getByTestId('group-card'); await expect.element(groupCards.first()).toBeInTheDocument(); await expect.element(groupCards.nth(1)).toBeInTheDocument(); }); it('uses undated label for items with no documentDate', async () => { const items = [ makeItem({ document: { ...makeItem().document, id: '1', documentDate: undefined } }) ]; render(DocumentList, { ...baseProps, items, total: 1 }); await expect.element(page.getByText('Undatiert')).toBeInTheDocument(); }); it('single year renders one group-card', async () => { const items = [ makeItem({ document: { ...makeItem().document, id: '1', documentDate: '1938-01-01' } }), makeItem({ document: { ...makeItem().document, id: '2', documentDate: '1938-06-15' } }) ]; render(DocumentList, { ...baseProps, items, total: 2 }); const groupCards = page.getByTestId('group-card'); await expect.element(groupCards.first()).toBeInTheDocument(); await expect.element(groupCards.nth(1)).not.toBeInTheDocument(); }); }); // ─── Sort fallback ──────────────────────────────────────────────────────────── describe('DocumentList – sort fallback', () => { it('falls back to year grouping when sort is not SENDER or RECEIVER', async () => { const items = [ makeItem({ document: { ...makeItem().document, id: '1', documentDate: '2024-03-15' } }) ]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'TITLE' }); await expect .element(page.getByTestId('group-header').filter({ hasText: '2024' })) .toBeInTheDocument(); }); }); // ─── Sender grouping ───────────────────────────────────────────────────────── describe('DocumentList – sender grouping', () => { it('groups by sender displayName when sort is SENDER', async () => { const items = [ makeItem({ document: { ...makeItem().document, id: '1', sender: { id: 's1', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON', familyMember: false } } }), makeItem({ document: { ...makeItem().document, id: '2', sender: { id: 's2', lastName: 'Musterfrau', displayName: 'Anna Musterfrau', personType: 'PERSON', familyMember: false } } }) ]; render(DocumentList, { ...baseProps, items, total: 2, sort: 'SENDER' }); await expect .element(page.getByTestId('group-header').filter({ hasText: 'Max Mustermann' })) .toBeInTheDocument(); await expect .element(page.getByTestId('group-header').filter({ hasText: 'Anna Musterfrau' })) .toBeInTheDocument(); }); it('groups documents with the same sender into one card', async () => { const sender = { id: 's1', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON' as const, familyMember: false }; const items = [ makeItem({ document: { ...makeItem().document, id: '1', sender } }), makeItem({ document: { ...makeItem().document, id: '2', sender } }) ]; render(DocumentList, { ...baseProps, items, total: 2, sort: 'SENDER' }); const cards = page.getByTestId('group-card'); await expect.element(cards.first()).toBeInTheDocument(); await expect.element(cards.nth(1)).not.toBeInTheDocument(); }); it('places items with no sender under fallback label', async () => { const items = [makeItem({ document: { ...makeItem().document, id: '1', sender: undefined } })]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'SENDER' }); await expect.element(page.getByText('Unbekannter Absender')).toBeInTheDocument(); }); }); // ─── Receiver grouping ──────────────────────────────────────────────────────── describe('DocumentList – receiver grouping', () => { it('groups by receiver displayName when sort is RECEIVER', async () => { const items = [ makeItem({ document: { ...makeItem().document, id: '1', receivers: [ { id: 'r1', lastName: 'Brandt', displayName: 'Felix Brandt', personType: 'PERSON', familyMember: false } ] } }) ]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' }); await expect .element(page.getByTestId('group-header').filter({ hasText: 'Felix Brandt' })) .toBeInTheDocument(); }); it('duplicates a document into each receiver group', async () => { const items = [ makeItem({ document: { ...makeItem().document, id: '1', title: 'Rundbriefchen', receivers: [ { id: 'r1', lastName: 'Brandt', displayName: 'Felix Brandt', personType: 'PERSON', familyMember: false }, { id: 'r2', lastName: 'Meier', displayName: 'Hans Meier', personType: 'PERSON', familyMember: false } ] } }) ]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' }); await expect .element(page.getByTestId('group-header').filter({ hasText: 'Felix Brandt' })) .toBeInTheDocument(); await expect .element(page.getByTestId('group-header').filter({ hasText: 'Hans Meier' })) .toBeInTheDocument(); const cards = page.getByTestId('group-card'); await expect.element(cards.nth(1)).toBeInTheDocument(); }); it('places items with no receivers under fallback label', async () => { const items = [makeItem({ document: { ...makeItem().document, id: '1', receivers: [] } })]; render(DocumentList, { ...baseProps, items, total: 1, sort: 'RECEIVER' }); await expect.element(page.getByText('Unbekannter Empfänger')).toBeInTheDocument(); }); }); // ─── DocumentRow rendering (delegated) ─────────────────────────────────────── describe('DocumentList – DocumentRow delegation', () => { it('shows transcription snippet when matchData has one', async () => { const items = [ makeItem({ document: { ...makeItem().document, id: 'doc1' }, matchData: { transcriptionSnippet: 'Er schrieb einen langen Brief', titleOffsets: [], senderMatched: false, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] } }) ]; render(DocumentList, { ...baseProps, items, total: 1 }); await expect.element(page.getByText('Er schrieb einen langen Brief')).toBeInTheDocument(); }); it('does not render snippet when matchData has no transcription snippet', async () => { const items = [makeItem({ document: { ...makeItem().document, id: 'doc1' } })]; render(DocumentList, { ...baseProps, items, total: 1 }); await expect.element(page.getByTestId('search-snippet')).not.toBeInTheDocument(); }); it('renders mark for title highlight when titleOffsets present', async () => { const items = [ makeItem({ document: { ...makeItem().document, id: 'doc1', title: 'Brief an Anna' }, matchData: { titleOffsets: [{ start: 0, length: 5 }], // "Brief" senderMatched: false, matchedReceiverIds: [], matchedTagIds: [], snippetOffsets: [], summaryOffsets: [] } }) ]; render(DocumentList, { ...baseProps, items, total: 1 }); const mark = page.getByRole('mark'); await expect.element(mark).toBeInTheDocument(); await expect.element(mark).toHaveTextContent('Brief'); }); });