From 1ff8393ad61dc8b1f5c04a99580dd2181897140f Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 27 Apr 2026 15:07:37 +0200 Subject: [PATCH] test(stammbaum): E2E spec + extend person load mock - frontend/e2e/stammbaum.spec.ts covers four journeys: 1) /briefwechsel still resolves with a 2xx after the nav swap. 2) /stammbaum shows the page heading. 3) /stammbaum renders either the empty state (with the Personenliste link) or at least one node[role=button] in the SVG. 4) The person edit card surfaces the year-range error when Bis < Von. - persons/[id]/page.server.spec.ts gains two extra mockResolvedValueOnce entries per scenario to match the new relationships + inferred-relationships GETs that the page load now performs. Refs #358. Co-Authored-By: Claude Sonnet 4.6 --- frontend/e2e/stammbaum.spec.ts | 57 +++++++++++++++++++ .../routes/persons/[id]/page.server.spec.ts | 10 ++++ 2 files changed, 67 insertions(+) create mode 100644 frontend/e2e/stammbaum.spec.ts diff --git a/frontend/e2e/stammbaum.spec.ts b/frontend/e2e/stammbaum.spec.ts new file mode 100644 index 00000000..b54b48f2 --- /dev/null +++ b/frontend/e2e/stammbaum.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Stammbaum — issue #358', () => { + test('nav swap: /briefwechsel still renders without 404', async ({ page }) => { + // Plan journey 4: the /briefwechsel route must stay intact even though the + // AppNav now points at /stammbaum. + const response = await page.goto('/briefwechsel'); + expect(response?.status()).toBeLessThan(400); + await expect(page).toHaveURL(/\/briefwechsel/); + }); + + test('/stammbaum renders the page heading', async ({ page }) => { + await page.goto('/stammbaum'); + await expect(page.getByRole('heading', { name: 'Stammbaum' })).toBeVisible(); + }); + + test('/stammbaum either shows an empty state or at least one node', async ({ page }) => { + // Plan journey 3 (empty branch) and journey 1 (populated branch) covered jointly: + // the test passes whenever the page renders one of the two coherent states. + await page.goto('/stammbaum'); + const empty = page.getByRole('heading', { name: 'Noch keine Familienmitglieder' }); + const anyNode = page.locator('svg[role="img"][aria-label="Stammbaum"] g[role="button"]'); + await expect(async () => { + const emptyVisible = await empty.isVisible().catch(() => false); + const nodeCount = await anyNode.count(); + expect(emptyVisible || nodeCount > 0).toBe(true); + }).toPass(); + + if (await empty.isVisible().catch(() => false)) { + await expect(page.getByRole('link', { name: /Zur Personenliste/ })).toBeVisible(); + } + }); + + test('person edit Stammbaum card surfaces the year-range error', async ({ page }) => { + // Plan task 36: Bis < Von triggers the inline error and keeps the form unsubmitted. + // We pick the first person, open the edit page, expand the add-rel form, and + // inspect the validation message bound to the Bis field. + await page.goto('/persons'); + const firstPerson = page.locator('a[href^="/persons/"]').first(); + await firstPerson.click(); + await expect(page).toHaveURL(/\/persons\/[^/]+/); + await page.goto(page.url() + '/edit'); + + // Open the add-rel form + const addBtn = page.getByRole('button', { name: /Beziehung hinzufügen/i }); + await addBtn.click(); + + // Enter Von 1935, Bis 1920 → expect the year-range error + const fromInput = page.locator('input[name="fromYear"]'); + const toInput = page.locator('input[name="toYear"]'); + await fromInput.fill('1935'); + await toInput.fill('1920'); + + await expect(page.locator('#add-rel-year-error')).toBeVisible(); + await expect(page.locator('#add-rel-year-error')).toContainText(/Bis.*Von|nicht vor/i); + }); +}); diff --git a/frontend/src/routes/persons/[id]/page.server.spec.ts b/frontend/src/routes/persons/[id]/page.server.spec.ts index c41ef58b..fcf24eb7 100644 --- a/frontend/src/routes/persons/[id]/page.server.spec.ts +++ b/frontend/src/routes/persons/[id]/page.server.spec.ts @@ -27,6 +27,8 @@ describe('person detail load — happy path', () => { .mockResolvedValueOnce({ response: { ok: true }, data: [{ id: 'd1', title: 'Brief' }] }) .mockResolvedValueOnce({ response: { ok: true }, data: [] }) .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) } as ReturnType); const result = await load({ params: { id: 'p1' }, fetch: mockFetch, locals: mockLocals }); @@ -47,6 +49,8 @@ describe('person detail load — happy path', () => { .mockResolvedValueOnce({ response: { ok: true }, data: [] }) .mockResolvedValueOnce({ response: { ok: true }, data: [] }) .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) } as ReturnType); const result = await load({ params: { id: 'p1' }, fetch: mockFetch, locals: mockLocalsWriter }); @@ -65,6 +69,8 @@ describe('person detail load — happy path', () => { .mockResolvedValueOnce({ response: { ok: false }, data: null }) .mockResolvedValueOnce({ response: { ok: false }, data: null }) .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) } as ReturnType); const result = await load({ params: { id: 'p1' }, fetch: mockFetch, locals: mockLocals }); @@ -85,6 +91,8 @@ describe('person detail load — error paths', () => { .mockResolvedValueOnce({ response: { ok: true }, data: [] }) .mockResolvedValueOnce({ response: { ok: true }, data: [] }) .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) } as ReturnType); await expect( @@ -102,6 +110,8 @@ describe('person detail load — error paths', () => { .mockResolvedValueOnce({ response: { ok: true }, data: [] }) .mockResolvedValueOnce({ response: { ok: true }, data: [] }) .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) + .mockResolvedValueOnce({ response: { ok: true }, data: [] }) } as ReturnType); await expect(