Files
familienarchiv/frontend/src/routes/persons/page.svelte.spec.ts
Marcel 9e7861fa03 feat(geschichten): frontend foundation — canBlogWrite, sanitize util, nav, i18n
- Derives canBlogWrite in +layout.server.ts the same way as canAnnotate.
- Adds Geschichten link to AppNav (desktop + mobile, between Stammbaum and Admin).
- Adds error_geschichte_not_found mapping to errors.ts and translation keys
  for the Geschichten index, detail, editor, and confirmation copy in
  de/en/es.
- Adds isomorphic-dompurify-backed safeHtml() helper with allow-list
  matching the backend OWASP policy (p/br/strong/em/h2/h3/ul/ol/li),
  plus Vitest spec.
- Updates legacy spec test data so the new required canBlogWrite layout
  prop type-checks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 17:43:29 +02:00

104 lines
3.5 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';
const tick = () => new Promise((r) => setTimeout(r, 0));
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
const makePerson = (overrides = {}) => ({
id: '1',
firstName: 'Max',
lastName: 'Mustermann',
displayName: 'Max Mustermann',
documentCount: 0,
...overrides
});
const defaultStats = { totalPersons: 0, totalDocuments: 0 };
const emptyData = {
user: undefined,
canWrite: true,
canAnnotate: false,
canBlogWrite: false,
q: '',
persons: [],
stats: defaultStats
};
const dataWithPersons = {
...emptyData,
persons: [makePerson()],
stats: { totalPersons: 1, totalDocuments: 3 }
};
afterEach(cleanup);
// ─── Rendering ────────────────────────────────────────────────────────────────
describe('Persons page rendering', () => {
it('renders the search input', async () => {
render(Page, { data: emptyData });
await expect.element(page.getByRole('textbox')).toBeInTheDocument();
});
it('pre-fills the search input from data.q', async () => {
render(Page, { data: { ...emptyData, q: 'Müller' } });
await expect.element(page.getByRole('textbox')).toHaveValue('Müller');
});
it('shows empty state when no persons', async () => {
render(Page, { data: emptyData });
await expect.element(page.getByText('Keine Personen gefunden')).toBeInTheDocument();
});
it('renders person cards', async () => {
render(Page, { data: dataWithPersons });
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
});
it('links person card to detail page', async () => {
render(Page, { data: dataWithPersons });
await expect
.element(page.getByRole('link', { name: /Max Mustermann/ }))
.toHaveAttribute('href', '/persons/1');
});
it('shows alias in italic when provided', async () => {
render(Page, { data: { ...emptyData, persons: [makePerson({ alias: 'Maxi' })] } });
await expect.element(page.getByText('„Maxi"')).toBeInTheDocument();
});
it('shows life date range when birthYear is provided', async () => {
render(Page, { data: { ...emptyData, persons: [makePerson({ birthYear: 1900 })] } });
await expect.element(page.getByText('* 1900')).toBeInTheDocument();
});
it('shows stats bar with person and document counts', async () => {
render(Page, { data: dataWithPersons });
await expect.element(page.getByText(/1 Person/)).toBeInTheDocument();
await expect.element(page.getByText(/3 Dokumente/)).toBeInTheDocument();
});
});
// ─── Keystroke preservation (issue #34) ──────────────────────────────────────
describe('Persons page search input keystroke preservation', () => {
it('does not overwrite the search input while the user is focused and stale data arrives', async () => {
const { rerender } = render(Page, { data: emptyData });
const input = page.getByRole('textbox');
// User types "abc" — input is focused
await input.click();
await input.fill('abc');
// Simulate a navigation completing with stale data (q='a') while the user is still typing
await rerender({ data: { ...emptyData, q: 'a' } });
await tick();
// Input must still show what the user typed, not the stale URL value
await expect.element(input).toHaveValue('abc');
});
});