Files
familienarchiv/frontend/src/routes/DocumentList.svelte.spec.ts
2026-04-16 16:14:32 +02:00

394 lines
12 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 { 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 <mark> 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 <mark> 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 <mark> 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');
});
});