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>
329 lines
13 KiB
TypeScript
329 lines
13 KiB
TypeScript
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();
|
||
});
|
||
});
|