openapi-typescript pulled the Stammbaum schemas: Person now has
familyMember (required), plus PersonNodeDTO, NetworkDTO, RelationshipDTO,
InferredRelationshipDTO, InferredRelationshipWithPersonDTO,
CreateRelationshipRequest, FamilyMemberPatchDTO. Routes:
/api/network, /api/persons/{id}/relationships,
/api/persons/{id}/inferred-relationships,
/api/persons/{aId}/relationship-to/{bId}, and the family-member PATCH.
Test fixtures in PersonMultiSelect, briefwechsel page, and DocumentList
specs gained familyMember: false where they otherwise typed Person
end-to-end. Pre-existing "missing lastName/personType" fixture errors
in DocumentRow.spec are out of scope.
Refs #358.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
306 lines
10 KiB
TypeScript
306 lines
10 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 DocumentSearchItem = components['schemas']['DocumentSearchItem'];
|
||
|
||
function makeItem(overrides: Partial<DocumentSearchItem> = {}): 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');
|
||
});
|
||
});
|