All components, specs, and the generated API client now use the new DocumentListItem shape — flat access (item.title, item.sender) instead of the removed item.document.* nesting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
281 lines
9.6 KiB
TypeScript
281 lines
9.6 KiB
TypeScript
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 DocumentListItem = components['schemas']['DocumentListItem'];
|
||
|
||
function makeItem(overrides: Partial<DocumentListItem> = {}): DocumentListItem {
|
||
return {
|
||
id: '1',
|
||
title: 'Testbrief',
|
||
originalFilename: 'testbrief.pdf',
|
||
documentDate: '2024-03-15',
|
||
sender: undefined,
|
||
receivers: [],
|
||
tags: [],
|
||
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({ id: '1', documentDate: '1923-04-12' }),
|
||
makeItem({ 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({ 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({ id: '1', documentDate: '1938-01-01' }),
|
||
makeItem({ 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({ 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({
|
||
id: '1',
|
||
sender: {
|
||
id: 's1',
|
||
lastName: 'Mustermann',
|
||
displayName: 'Max Mustermann',
|
||
personType: 'PERSON',
|
||
familyMember: false
|
||
}
|
||
}),
|
||
makeItem({
|
||
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({ id: '1', sender }), makeItem({ 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({ 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({
|
||
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({
|
||
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({ 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({
|
||
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({ 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({
|
||
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');
|
||
});
|
||
});
|