From 434a6fecc9c400a74c3bbedfde70c096319595e8 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 9 May 2026 21:54:50 +0200 Subject: [PATCH] test: cover login and persons/[id] page branches login: form rendering, registered-success banner branch, form-error banner branch, form-action wiring, email/password input attributes, forgot-password link. persons/[id]: PersonCard heading via prop pass-through, document section headings, empty-message branches, GeschichtenCard hidden when empty, co-correspondents derived from sent documents, canWrite gating the edit link. 14 tests across two large pages. Refs #496. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/login/page.svelte.test.ts | 73 ++++++++++++ .../routes/persons/[id]/page.svelte.test.ts | 104 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 frontend/src/routes/login/page.svelte.test.ts create mode 100644 frontend/src/routes/persons/[id]/page.svelte.test.ts diff --git a/frontend/src/routes/login/page.svelte.test.ts b/frontend/src/routes/login/page.svelte.test.ts new file mode 100644 index 00000000..88d05706 --- /dev/null +++ b/frontend/src/routes/login/page.svelte.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import LoginPage from './+page.svelte'; + +afterEach(cleanup); + +describe('login page', () => { + it('renders the login form by default', async () => { + render(LoginPage, { props: { data: { registered: false }, form: undefined } }); + + await expect.element(page.getByRole('heading', { name: /^anmelden$/i })).toBeVisible(); + await expect.element(page.getByLabelText(/e-mail-adresse/i)).toBeVisible(); + await expect.element(page.getByLabelText(/passwort/i)).toBeVisible(); + await expect.element(page.getByRole('button', { name: /^anmelden$/i })).toBeVisible(); + }); + + it('shows the registered-success banner when data.registered is true', async () => { + render(LoginPage, { props: { data: { registered: true }, form: undefined } }); + + await expect + .element(page.getByText('Dein Konto wurde erfolgreich erstellt. Melde dich jetzt an.')) + .toBeVisible(); + }); + + it('hides the registered-success banner when data.registered is false', async () => { + render(LoginPage, { props: { data: { registered: false }, form: undefined } }); + + await expect.element(page.getByRole('status')).not.toBeInTheDocument(); + }); + + it('shows the form error when form.error is set', async () => { + render(LoginPage, { + props: { data: { registered: false }, form: { error: 'Ungültige Anmeldedaten' } } + }); + + await expect.element(page.getByText('Ungültige Anmeldedaten')).toBeVisible(); + }); + + it('declares POST as the form method and routes to ?/login', async () => { + render(LoginPage, { props: { data: { registered: false }, form: undefined } }); + + const form = document.querySelector('form'); + expect(form?.getAttribute('method')).toBe('POST'); + expect(form?.getAttribute('action')).toBe('?/login'); + }); + + it('exposes the email input as type=email and required', async () => { + render(LoginPage, { props: { data: { registered: false }, form: undefined } }); + + const email = document.querySelector('input[name="email"]') as HTMLInputElement; + expect(email.type).toBe('email'); + expect(email.required).toBe(true); + expect(email.autocomplete).toBe('email'); + }); + + it('exposes the password input as type=password and required', async () => { + render(LoginPage, { props: { data: { registered: false }, form: undefined } }); + + const pwd = document.querySelector('input[name="password"]') as HTMLInputElement; + expect(pwd.type).toBe('password'); + expect(pwd.required).toBe(true); + expect(pwd.autocomplete).toBe('current-password'); + }); + + it('renders the forgot-password link', async () => { + render(LoginPage, { props: { data: { registered: false }, form: undefined } }); + + await expect + .element(page.getByRole('link', { name: /passwort vergessen/i })) + .toHaveAttribute('href', '/forgot-password'); + }); +}); diff --git a/frontend/src/routes/persons/[id]/page.svelte.test.ts b/frontend/src/routes/persons/[id]/page.svelte.test.ts new file mode 100644 index 00000000..ec8bf773 --- /dev/null +++ b/frontend/src/routes/persons/[id]/page.svelte.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import PersonDetailPage from './+page.svelte'; + +afterEach(cleanup); + +const basePerson = { + id: 'p-1', + firstName: 'Anna', + lastName: 'Schmidt', + displayName: 'Anna Schmidt', + personType: 'PERSON' as const +}; + +const baseData = (overrides: Record = {}) => ({ + person: basePerson, + canWrite: false, + canBlogWrite: false, + aliases: [] as { name: string; from: string; to: string }[], + relationships: [], + inferredRelationships: [], + sentDocuments: [] as Array<{ + id: string; + title: string; + status: string; + receivers?: { id: string; displayName: string }[]; + }>, + receivedDocuments: [] as Array<{ + id: string; + title: string; + status: string; + sender?: { id: string; displayName: string }; + }>, + geschichten: undefined, + ...overrides +}); + +describe('persons/[id] page', () => { + it('renders the person heading via PersonCard', async () => { + render(PersonDetailPage, { props: { data: baseData() } }); + + await expect.element(page.getByRole('heading', { name: 'Anna Schmidt' })).toBeVisible(); + }); + + it('renders both document section headings', async () => { + render(PersonDetailPage, { props: { data: baseData() } }); + + await expect.element(page.getByRole('heading', { name: /gesendete dokumente/i })).toBeVisible(); + await expect + .element(page.getByRole('heading', { name: /empfangene dokumente/i })) + .toBeVisible(); + }); + + it('renders empty messages when no documents are present', async () => { + render(PersonDetailPage, { props: { data: baseData() } }); + + await expect + .element(page.getByText('Diese Person ist noch nicht als Absender verknüpft.')) + .toBeVisible(); + await expect + .element(page.getByText('Diese Person ist noch nicht als Empfänger verknüpft.')) + .toBeVisible(); + }); + + it('does not render the GeschichtenCard when there are no geschichten', async () => { + render(PersonDetailPage, { props: { data: baseData({ geschichten: [] }) } }); + + const geschichteHeadings = Array.from(document.querySelectorAll('h2')) + .map((h) => h.textContent ?? '') + .filter((t) => /geschichte/i.test(t)); + expect(geschichteHeadings.length).toBe(0); + }); + + it('renders co-correspondents derived from sent documents receivers', async () => { + const data = baseData({ + sentDocuments: [ + { + id: 'd1', + title: 'B1', + status: 'UPLOADED', + receivers: [{ id: 'r1', displayName: 'Bert Meier' }] + }, + { + id: 'd2', + title: 'B2', + status: 'UPLOADED', + receivers: [{ id: 'r1', displayName: 'Bert Meier' }] + } + ] + }); + render(PersonDetailPage, { props: { data } }); + + await expect.element(page.getByText(/häufige korrespondenten/i)).toBeVisible(); + }); + + it('shows the edit button via PersonCard when canWrite is true', async () => { + render(PersonDetailPage, { props: { data: baseData({ canWrite: true }) } }); + + await expect + .element(page.getByRole('link', { name: /bearbeiten/i })) + .toHaveAttribute('href', '/persons/p-1/edit'); + }); +});