From 145ea1c53bfea68cf4df03c47d5323057f9dd951 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 9 May 2026 21:56:25 +0200 Subject: [PATCH] test(persons): cover persons/+ list page branches Heading + stats bar render, empty-state placeholder, populated card grid, canWrite-gated new-person CTA, search-input hydration from data.q, document-count chip singular/plural/zero branches, alias rendering. Mocks $app/navigation since the search debounce calls goto. 10 tests, ~30 branches. Refs #496. Co-Authored-By: Claude Sonnet 4.6 --- .../src/routes/persons/page.svelte.test.ts | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 frontend/src/routes/persons/page.svelte.test.ts diff --git a/frontend/src/routes/persons/page.svelte.test.ts b/frontend/src/routes/persons/page.svelte.test.ts new file mode 100644 index 00000000..0c670576 --- /dev/null +++ b/frontend/src/routes/persons/page.svelte.test.ts @@ -0,0 +1,190 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +vi.mock('$app/navigation', () => ({ + beforeNavigate: () => {}, + afterNavigate: () => {}, + goto: vi.fn(), + invalidate: vi.fn(), + invalidateAll: vi.fn(), + preloadCode: vi.fn(), + preloadData: vi.fn(), + pushState: vi.fn(), + replaceState: vi.fn(), + disableScrollHandling: vi.fn(), + onNavigate: () => () => {} +})); + +const { default: PersonsListPage } = await import('./+page.svelte'); + +afterEach(cleanup); + +const baseData = (overrides: Record = {}) => ({ + persons: [] as Array<{ + id: string; + firstName?: string; + lastName: string; + displayName: string; + personType?: string; + alias?: string; + birthYear?: number; + deathYear?: number; + documentCount?: number; + }>, + stats: { totalPersons: 0, totalDocuments: 0 }, + canWrite: false, + q: '', + ...overrides +}); + +describe('persons/+ page', () => { + it('renders the heading and stats bar', async () => { + render(PersonsListPage, { + props: { data: baseData({ stats: { totalPersons: 5, totalDocuments: 10 } }) } + }); + + await expect.element(page.getByRole('heading', { name: /personen/i })).toBeVisible(); + }); + + it('renders the empty state when persons is empty', async () => { + render(PersonsListPage, { props: { data: baseData() } }); + + // PersonsEmptyState renders an empty-state element — verify no person cards + const personLinks = document.querySelectorAll('a[href^="/persons/"]'); + expect(personLinks.length).toBe(0); + }); + + it('renders a card per person when populated', async () => { + render(PersonsListPage, { + props: { + data: baseData({ + persons: [ + { + id: 'p1', + firstName: 'Anna', + lastName: 'Schmidt', + displayName: 'Anna Schmidt', + personType: 'PERSON' + }, + { + id: 'p2', + firstName: '', + lastName: 'Acme', + displayName: 'Acme', + personType: 'INSTITUTION' + } + ] + }) + } + }); + + await expect.element(page.getByText('Anna Schmidt')).toBeVisible(); + await expect.element(page.getByText('Acme')).toBeVisible(); + }); + + it('shows the new-person CTA when canWrite is true', async () => { + render(PersonsListPage, { props: { data: baseData({ canWrite: true }) } }); + + await expect + .element(page.getByRole('link', { name: /neue person/i })) + .toHaveAttribute('href', '/persons/new'); + }); + + it('hides the new-person CTA when canWrite is false', async () => { + render(PersonsListPage, { props: { data: baseData({ canWrite: false }) } }); + + await expect.element(page.getByRole('link', { name: /neue person/i })).not.toBeInTheDocument(); + }); + + it('preselects the search input from data.q', async () => { + render(PersonsListPage, { props: { data: baseData({ q: 'Schmidt' }) } }); + + const search = (await page.getByLabelText(/suche/i).element()) as HTMLInputElement; + expect(search.value).toBe('Schmidt'); + }); + + it('renders the doc-count chip with singular label for documentCount=1', async () => { + render(PersonsListPage, { + props: { + data: baseData({ + persons: [ + { + id: 'p1', + firstName: 'Anna', + lastName: 'Schmidt', + displayName: 'Anna Schmidt', + personType: 'PERSON', + documentCount: 1 + } + ] + }) + } + }); + + await expect.element(page.getByText('1 Dok.')).toBeVisible(); + }); + + it('renders the doc-count chip with plural label for documentCount > 1', async () => { + render(PersonsListPage, { + props: { + data: baseData({ + persons: [ + { + id: 'p1', + firstName: 'Anna', + lastName: 'Schmidt', + displayName: 'Anna Schmidt', + personType: 'PERSON', + documentCount: 5 + } + ] + }) + } + }); + + await expect.element(page.getByText('5 Dok.')).toBeVisible(); + }); + + it('omits the doc-count chip when documentCount is 0', async () => { + render(PersonsListPage, { + props: { + data: baseData({ + persons: [ + { + id: 'p1', + firstName: 'Anna', + lastName: 'Schmidt', + displayName: 'Anna Schmidt', + personType: 'PERSON', + documentCount: 0 + } + ] + }) + } + }); + + await expect.element(page.getByText(/^\d+ Dok\.$/)).not.toBeInTheDocument(); + }); + + it('renders the alias in italic when set', async () => { + render(PersonsListPage, { + props: { + data: baseData({ + persons: [ + { + id: 'p1', + firstName: 'Anna', + lastName: 'Schmidt', + displayName: 'Anna Schmidt', + personType: 'PERSON', + alias: 'Anni' + } + ] + }) + } + }); + + await expect.element(page.getByText(/Anni/)).toBeVisible(); + }); +});