- 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>
104 lines
3.5 KiB
TypeScript
104 lines
3.5 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';
|
||
|
||
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');
|
||
});
|
||
});
|