Files
familienarchiv/frontend/src/routes/briefwechsel/page.svelte.spec.ts
Marcel 3b83141ddf test(briefwechsel): makePerson factory + per-row tile assertion
Consolidates the hansPerson / annaPerson fixture into a makePerson()
factory matching the makeDoc convention, adds an assertion that
the bilateral list renders one ConversationThumbnail tile per
document (catches a broken {#each} keying wired around the
DistributionBar), and decouples the DistributionBar aria-label
assertion from the German locale now that i18n lands via Paraglide.

Refs #305
Fixes @saraholt concerns 3 + 4 from PR review

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 20:34:00 +02:00

329 lines
13 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 { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import Page from './+page.svelte';
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
afterEach(cleanup);
// ─── Test data ────────────────────────────────────────────────────────────────
const baseData = {
user: undefined,
canWrite: true,
canAnnotate: false,
documents: [],
initialValues: { senderName: '', receiverName: '' },
filters: { senderId: '', receiverId: '', from: '', to: '', dir: 'DESC' as const }
};
const withSender = {
...baseData,
initialValues: { senderName: 'Hans Müller', receiverName: '' },
filters: { ...baseData.filters, senderId: 'p1' }
};
const withPersons = {
...baseData,
initialValues: { senderName: 'Hans Müller', receiverName: 'Anna Schmidt' },
filters: { ...baseData.filters, senderId: 'p1', receiverId: 'p2' }
};
const makePerson = (overrides: Record<string, unknown> = {}) => ({
id: 'p1',
firstName: 'Hans',
lastName: 'Müller',
personType: 'PERSON' as const,
displayName: 'Hans Müller',
...overrides
});
const hansPerson = makePerson();
const annaPerson = makePerson({
id: 'p2',
firstName: 'Anna',
lastName: 'Schmidt',
displayName: 'Anna Schmidt'
});
const makeDoc = (overrides: Record<string, unknown> = {}) => ({
id: 'd1',
title: 'Testbrief',
originalFilename: 'testbrief.pdf',
status: 'UPLOADED' as const,
documentDate: '1923-04-12',
location: 'Berlin',
metadataComplete: false,
scriptType: 'UNKNOWN' as const,
sender: makePerson(),
receivers: [
makePerson({
id: 'p2',
firstName: 'Anna',
lastName: 'Schmidt',
displayName: 'Anna Schmidt'
})
],
tags: [],
transcription: undefined,
filePath: undefined,
createdAt: '1923-04-12T00:00:00Z',
updatedAt: '1923-04-12T00:00:00Z',
...overrides
});
const withDocs = {
...withPersons,
documents: [makeDoc()]
};
// ─── Hero state (no senderId) ────────────────────────────────────────────────
describe('Briefwechsel page hero state', () => {
it('shows the hero when no person is selected', async () => {
render(Page, { data: baseData });
await expect.element(page.getByTestId('conv-hero')).toBeInTheDocument();
});
it('shows the discovery headline', async () => {
render(Page, { data: baseData });
await expect.element(page.getByText(/Wessen Briefe möchten Sie lesen/i)).toBeInTheDocument();
});
it('does not show the person bar in hero state', async () => {
render(Page, { data: baseData });
await expect.element(page.getByTestId('conv-hero')).toBeInTheDocument();
await expect.element(page.getByTestId('conv-person-bar')).not.toBeInTheDocument();
});
it('does not show filter controls in hero state', async () => {
render(Page, { data: baseData });
await expect.element(page.getByTestId('conv-hero')).toBeInTheDocument();
await expect.element(page.getByTestId('conv-filter-controls')).not.toBeInTheDocument();
});
it('does not show the new document link when no person is selected', async () => {
render(Page, { data: baseData });
await expect.element(page.getByTestId('conv-new-doc-link')).not.toBeInTheDocument();
});
it('does not show a year divider when no person is selected', async () => {
render(Page, { data: baseData });
await expect.element(page.getByTestId('year-divider')).not.toBeInTheDocument();
});
});
// ─── Results state (senderId set) ────────────────────────────────────────────
describe('Briefwechsel page results state', () => {
it('does not show the hero when senderId is set', async () => {
render(Page, { data: withSender });
await expect.element(page.getByTestId('conv-person-bar')).toBeInTheDocument();
await expect.element(page.getByTestId('conv-hero')).not.toBeInTheDocument();
});
it('shows the person bar when senderId is set', async () => {
render(Page, { data: withSender });
await expect.element(page.getByTestId('conv-person-bar')).toBeInTheDocument();
});
it('hides filter controls by default (collapsible)', async () => {
render(Page, { data: withSender });
await expect.element(page.getByTestId('conv-person-bar')).toBeInTheDocument();
await expect.element(page.getByTestId('conv-filter-controls')).not.toBeInTheDocument();
});
});
// ─── Recent persons chips ─────────────────────────────────────────────────────
describe('Briefwechsel page recent persons', () => {
it('shows recent person chips from localStorage', async () => {
localStorage.setItem(
'korrespondenz_recent_persons',
JSON.stringify([{ id: 'r1', name: 'Clara Braun' }])
);
render(Page, { data: baseData });
await expect.element(page.getByText('Clara Braun')).toBeInTheDocument();
localStorage.removeItem('korrespondenz_recent_persons');
});
it('does not crash when localStorage contains corrupt JSON', async () => {
localStorage.setItem('korrespondenz_recent_persons', '}{not valid json');
render(Page, { data: baseData });
await expect.element(page.getByText(/Wessen Briefe möchten Sie lesen/i)).toBeInTheDocument();
localStorage.removeItem('korrespondenz_recent_persons');
});
});
// ─── Single-person hint bar ───────────────────────────────────────────────────
describe('Briefwechsel page single-person hint bar', () => {
it('shows hint bar when only senderId is set', async () => {
render(Page, { data: withSender });
await expect.element(page.getByText(/Alle Briefe von Hans Müller/i)).toBeInTheDocument();
});
it('does not show hint bar when both persons are set', async () => {
render(Page, { data: { ...withPersons, documents: [makeDoc()] } });
await expect.element(page.getByText(/Alle Briefe von Hans Müller/i)).not.toBeInTheDocument();
});
it('does not show hint bar when no person is set', async () => {
render(Page, { data: baseData });
await expect.element(page.getByText(/Alle Briefe von/i)).not.toBeInTheDocument();
});
});
// ─── Strip letter count ───────────────────────────────────────────────────────
describe('Briefwechsel page strip letter count', () => {
it('shows 0 Briefe when senderId is set but no documents', async () => {
render(Page, { data: withSender });
await expect.element(page.getByTestId('conv-strip-count')).toHaveTextContent('0 Briefe');
});
it('shows correct count when documents are loaded', async () => {
render(Page, { data: { ...withPersons, documents: [makeDoc()] } });
await expect.element(page.getByTestId('conv-strip-count')).toHaveTextContent('1 Briefe');
});
});
// ─── No results ───────────────────────────────────────────────────────────────
describe('Briefwechsel page no results', () => {
it('shows "no documents found" when a person is selected but there are no documents', async () => {
render(Page, { data: withSender });
await expect.element(page.getByText(/Keine Dokumente gefunden/i)).toBeInTheDocument();
});
});
// ─── Swap button ──────────────────────────────────────────────────────────────
describe('Briefwechsel page swap button', () => {
it('swap button is invisible when only one person is set', async () => {
render(Page, { data: withSender });
const btn = document.querySelector<HTMLElement>('[data-testid="conv-swap-btn"]');
expect(btn).not.toBeNull();
expect(btn!.className).toMatch(/opacity-0/);
});
it('swap button is visible when both persons are set', async () => {
render(Page, { data: withPersons });
const btn = document.querySelector<HTMLElement>('[data-testid="conv-swap-btn"]');
expect(btn).not.toBeNull();
expect(btn!.className).not.toMatch(/opacity-0/);
});
it('calls goto with swapped sender and receiver when clicked', async () => {
const { goto } = await import('$app/navigation');
vi.mocked(goto).mockClear();
render(Page, { data: withPersons });
document.querySelector<HTMLElement>('[data-testid="conv-swap-btn"]')!.click();
expect(goto).toHaveBeenCalledWith(expect.stringContaining('senderId=p2'), expect.anything());
expect(goto).toHaveBeenCalledWith(expect.stringContaining('receiverId=p1'), expect.anything());
});
});
// ─── Distribution bar (bilateral only) ────────────────────────────────────────
describe('Briefwechsel page distribution bar', () => {
it('renders the DistributionBar when both persons are set and there are documents', async () => {
const data = {
...withPersons,
documents: [
makeDoc({ id: 'out1', sender: hansPerson, receivers: [annaPerson] }),
makeDoc({ id: 'in1', sender: annaPerson, receivers: [hansPerson] }),
makeDoc({ id: 'in2', sender: annaPerson, receivers: [hansPerson] })
]
};
render(Page, { data });
const bar = document.querySelector('[role="img"]');
expect(bar).not.toBeNull();
const label = bar!.getAttribute('aria-label') ?? '';
expect(label).toContain('Hans Müller');
expect(label).toContain('Anna Schmidt');
expect(label).toMatch(/\b1\b/);
expect(label).toMatch(/\b2\b/);
});
it('does not render the DistributionBar in single-person mode', async () => {
render(Page, { data: { ...withSender, documents: [makeDoc()] } });
const bar = document.querySelector('[role="img"]');
expect(bar).toBeNull();
});
it('renders a ConversationThumbnail tile for each document in the list', async () => {
// A broken `{#each}` wiring in ConversationTimeline would silently stop
// rendering rows while the DistributionBar above it kept working. Assert
// the per-row tile so that class of regression is caught.
const data = {
...withPersons,
documents: [makeDoc({ id: 'd-a' }), makeDoc({ id: 'd-b' }), makeDoc({ id: 'd-c' })]
};
render(Page, { data });
const tiles = document.querySelectorAll('[data-testid="conv-thumb-tile"]');
expect(tiles).toHaveLength(3);
});
});
// ─── Year dividers ────────────────────────────────────────────────────────────
describe('Briefwechsel page year dividers', () => {
it('renders a year divider for the first document', async () => {
render(Page, { data: withDocs });
await expect.element(page.getByTestId('year-divider').first()).toHaveTextContent('1923');
});
it('renders a divider for each new year in the document list', async () => {
const data = {
...withPersons,
documents: [
makeDoc({ documentDate: '1923-04-12' }),
makeDoc({ id: 'd2', documentDate: '1965-08-03' })
]
};
render(Page, { data });
await expect.element(page.getByTestId('year-divider').first()).toHaveTextContent('1923');
await expect.element(page.getByTestId('year-divider').nth(1)).toHaveTextContent('1965');
});
it('does not render a second divider for documents from the same year', async () => {
const data = {
...withPersons,
documents: [
makeDoc({ documentDate: '1923-04-12' }),
makeDoc({ id: 'd2', documentDate: '1923-09-01' })
]
};
render(Page, { data });
await expect.element(page.getByTestId('year-divider').first()).toHaveTextContent('1923');
await expect.element(page.getByTestId('year-divider').nth(1)).not.toBeInTheDocument();
});
});
// ─── New document link ────────────────────────────────────────────────────────
describe('Briefwechsel page new document link', () => {
it('shows the link with correct href for a write user (bilateral)', async () => {
render(Page, { data: { ...withDocs, canWrite: true } });
const link = page.getByTestId('conv-new-doc-link');
await expect.element(link).toBeInTheDocument();
await expect.element(link).toHaveAttribute('href', expect.stringContaining('senderId=p1'));
await expect.element(link).toHaveAttribute('href', expect.stringContaining('receiverId=p2'));
});
it('shows the link with correct href for single-person mode', async () => {
render(Page, { data: { ...withSender, documents: [makeDoc()], canWrite: true } });
const link = page.getByTestId('conv-new-doc-link');
await expect.element(link).toBeInTheDocument();
await expect.element(link).toHaveAttribute('href', expect.stringContaining('senderId=p1'));
await expect.element(link).not.toHaveAttribute('href', expect.stringContaining('receiverId'));
});
it('hides the link for a read-only user', async () => {
render(Page, { data: { ...withDocs, canWrite: false } });
await expect.element(page.getByTestId('conv-new-doc-link')).not.toBeInTheDocument();
});
});