Files
familienarchiv/frontend/src/routes/korrespondenz/page.svelte.spec.ts
Marcel 5fd7e41492 fix(korrespondenz): dark theme, compact strip labels, year divider size, chevron alignment
- Add compact prop to PersonTypeahead: 7px uppercase label, 30px h input (matches spec FL/FI)
- Replace all hardcoded hex in 6 korrespondenz components with theme tokens (bg-surface,
  bg-muted, bg-canvas, border-line, text-ink, text-primary, text-accent, etc.)
- Fix year divider: text-[15px] font-black (spec: 15px/900)
- Fix log row chevron: items-center instead of items-start for vertical centering
- Fix recent-persons persistence: move persistRecentPerson to post-navigation $effect so
  senderName is resolved from server before stored in localStorage
- Add metadataComplete field to makeDoc() fixture to satisfy updated Document type
- Restore opacity-0 on swap button when only one person is set (matches spec + test)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 14:33:23 +02:00

241 lines
10 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 makeDoc = (overrides: Record<string, unknown> = {}) => ({
id: 'd1',
title: 'Testbrief',
originalFilename: 'testbrief.pdf',
status: 'UPLOADED' as const,
documentDate: '1923-04-12',
location: 'Berlin',
metadataComplete: false,
sender: { id: 'p1', firstName: 'Hans', lastName: 'Müller' },
receivers: [{ id: 'p2', firstName: 'Anna', lastName: 'Schmidt' }],
tags: [],
transcription: undefined,
filePath: undefined,
createdAt: '1923-04-12T00:00:00Z',
updatedAt: '1923-04-12T00:00:00Z',
...overrides
});
const withDocs = {
...withPersons,
documents: [makeDoc()]
};
// ─── Empty state (no senderId) ────────────────────────────────────────────────
describe('Korrespondenz page empty state', () => {
it('shows the search heading when no person is selected', async () => {
render(Page, { data: baseData });
await expect.element(page.getByText(/Korrespondenz durchsuchen/i)).toBeInTheDocument();
});
it('shows the empty-search button', async () => {
render(Page, { data: baseData });
await expect.element(page.getByTestId('conv-empty-search')).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();
});
});
// ─── Recent persons chips ─────────────────────────────────────────────────────
describe('Korrespondenz 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 });
// Empty state heading is still shown — no chip list crash
await expect.element(page.getByText(/Korrespondenz durchsuchen/i)).toBeInTheDocument();
localStorage.removeItem('korrespondenz_recent_persons');
});
});
// ─── Single-person hint bar ───────────────────────────────────────────────────
describe('Korrespondenz 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();
});
});
// ─── Filter controls disabled state ──────────────────────────────────────────
describe('Korrespondenz page filter strip Row 2 disabled state', () => {
it('renders filter controls with aria-disabled when no senderId', async () => {
render(Page, { data: baseData });
const strip = document.querySelector('[aria-disabled="true"]');
expect(strip).not.toBeNull();
});
});
// ─── Strip letter count ───────────────────────────────────────────────────────
describe('Korrespondenz 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('Korrespondenz 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('Korrespondenz 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();
// opacity-0 is applied via class when swapVisible is false
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());
});
});
// ─── Year dividers ────────────────────────────────────────────────────────────
describe('Korrespondenz 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('Korrespondenz 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();
});
});