From a6b1e576deb440b97950f64150953f8a23b4421b Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 9 May 2026 20:58:25 +0200 Subject: [PATCH] test: cover users/[id], admin/ocr/global, geschichten/[id] page branches users/[id]: full-name derivation across all four branches (both/firstName-only/lastName-only/email fallback), avatar initials matrix, email/contact row visibility tied to data presence. admin/ocr/global: heading + back link, runs prop pass-through, defensive default for missing history fields. geschichten/[id]: title rendering, author full-name vs email fallback vs null, publishedAt suffix conditional, persons and documents sections gated on array length, edit/delete actions gated on canBlogWrite. Mocks the confirm service since it requires a ConfirmDialog mounted in layout. 26 tests across three files. Refs #496. Co-Authored-By: Claude Sonnet 4.6 --- .../admin/ocr/global/page.svelte.test.ts | 37 +++++ .../geschichten/[id]/page.svelte.test.ts | 146 ++++++++++++++++++ .../src/routes/users/[id]/page.svelte.test.ts | 106 +++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 frontend/src/routes/admin/ocr/global/page.svelte.test.ts create mode 100644 frontend/src/routes/geschichten/[id]/page.svelte.test.ts create mode 100644 frontend/src/routes/users/[id]/page.svelte.test.ts diff --git a/frontend/src/routes/admin/ocr/global/page.svelte.test.ts b/frontend/src/routes/admin/ocr/global/page.svelte.test.ts new file mode 100644 index 00000000..4b0868eb --- /dev/null +++ b/frontend/src/routes/admin/ocr/global/page.svelte.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import OcrGlobalPage from './+page.svelte'; + +afterEach(cleanup); + +describe('admin/ocr/global page', () => { + it('renders the heading and back link to /admin/ocr', async () => { + render(OcrGlobalPage, { + props: { data: { history: { runs: [], personNames: {} } } } + }); + + await expect.element(page.getByRole('heading', { name: /globaler verlauf/i })).toBeVisible(); + await expect + .element(page.getByRole('link', { name: /ocr/i })) + .toHaveAttribute('href', '/admin/ocr'); + }); + + it('passes the runs array through to TrainingHistory', async () => { + render(OcrGlobalPage, { + props: { + data: { history: { runs: [], personNames: { 'p-1': 'Anna Schmidt' } } } + } + }); + + await expect.element(page.getByRole('heading', { name: /globaler verlauf/i })).toBeVisible(); + }); + + it('handles a missing history.runs by defaulting to an empty list', async () => { + render(OcrGlobalPage, { + props: { data: { history: { runs: undefined, personNames: undefined } } } + }); + + await expect.element(page.getByRole('heading', { name: /globaler verlauf/i })).toBeVisible(); + }); +}); diff --git a/frontend/src/routes/geschichten/[id]/page.svelte.test.ts b/frontend/src/routes/geschichten/[id]/page.svelte.test.ts new file mode 100644 index 00000000..f119f0c3 --- /dev/null +++ b/frontend/src/routes/geschichten/[id]/page.svelte.test.ts @@ -0,0 +1,146 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +vi.mock('$lib/shared/services/confirm.svelte', () => ({ + getConfirmService: () => ({ confirm: async () => false }) +})); + +const { default: GeschichtePage } = await import('./+page.svelte'); + +afterEach(cleanup); + +const baseGeschichte = (overrides: Record = {}) => ({ + id: 'g1', + title: 'Die Reise nach Berlin', + body: '

Im Jahr 1923 fuhr Helene...

', + publishedAt: '2026-04-15T10:00:00Z' as string | null, + author: { firstName: 'Anna', lastName: 'Schmidt', email: 'anna@example.com' } as { + firstName?: string; + lastName?: string; + email: string; + } | null, + persons: [] as { id: string; displayName: string }[], + documents: [] as { id: string; title: string; documentDate?: string | null }[], + ...overrides +}); + +const baseData = (overrides: Record = {}) => ({ + geschichte: baseGeschichte(), + canBlogWrite: false, + ...overrides +}); + +describe('geschichten/[id] page', () => { + it('renders the geschichte title as the level-1 heading', async () => { + render(GeschichtePage, { props: { data: baseData() } }); + + await expect + .element(page.getByRole('heading', { level: 1, name: /reise nach berlin/i })) + .toBeVisible(); + }); + + it('renders the author full name from firstName + lastName', async () => { + render(GeschichtePage, { props: { data: baseData() } }); + + await expect.element(page.getByText(/Anna Schmidt/)).toBeVisible(); + }); + + it('falls back to author email when no name is set', async () => { + render(GeschichtePage, { + props: { + data: baseData({ + geschichte: baseGeschichte({ + author: { firstName: undefined, lastName: undefined, email: 'fallback@example.com' } + }) + }) + } + }); + + await expect.element(page.getByText(/fallback@example.com/)).toBeVisible(); + }); + + it('renders an empty author when author is null', async () => { + render(GeschichtePage, { + props: { data: baseData({ geschichte: baseGeschichte({ author: null }) }) } + }); + + await expect.element(page.getByRole('heading', { level: 1 })).toBeVisible(); + }); + + it('renders the publishedAt date suffix when publishedAt is set', async () => { + render(GeschichtePage, { props: { data: baseData() } }); + + await expect.element(page.getByText(/veröffentlicht am/i)).toBeVisible(); + }); + + it('omits the publishedAt suffix when publishedAt is null', async () => { + render(GeschichtePage, { + props: { data: baseData({ geschichte: baseGeschichte({ publishedAt: null }) }) } + }); + + await expect.element(page.getByText(/veröffentlicht am/i)).not.toBeInTheDocument(); + }); + + it('omits the persons section when there are no linked persons', async () => { + render(GeschichtePage, { props: { data: baseData() } }); + + await expect.element(page.getByText(/Personen in dieser Geschichte/i)).not.toBeInTheDocument(); + }); + + it('renders the persons section when there are linked persons', async () => { + render(GeschichtePage, { + props: { + data: baseData({ + geschichte: baseGeschichte({ + persons: [ + { id: 'p1', displayName: 'Helene Schmidt' }, + { id: 'p2', displayName: 'Karl Müller' } + ] + }) + }) + } + }); + + await expect.element(page.getByText('Personen in dieser Geschichte')).toBeVisible(); + await expect.element(page.getByText('Helene Schmidt')).toBeVisible(); + await expect.element(page.getByText('Karl Müller')).toBeVisible(); + }); + + it('omits the documents section when there are no linked documents', async () => { + render(GeschichtePage, { props: { data: baseData() } }); + + await expect.element(page.getByText('Erwähnte Dokumente')).not.toBeInTheDocument(); + }); + + it('renders the documents section when there are linked documents', async () => { + render(GeschichtePage, { + props: { + data: baseData({ + geschichte: baseGeschichte({ + documents: [{ id: 'd1', title: 'Brief 1923', documentDate: '1923-04-15' }] + }) + }) + } + }); + + await expect.element(page.getByText('Erwähnte Dokumente')).toBeVisible(); + await expect.element(page.getByText('Brief 1923')).toBeVisible(); + }); + + it('renders edit and delete actions when canBlogWrite is true', async () => { + render(GeschichtePage, { props: { data: baseData({ canBlogWrite: true }) } }); + + await expect + .element(page.getByRole('link', { name: /bearbeiten/i })) + .toHaveAttribute('href', '/geschichten/g1/edit'); + await expect.element(page.getByRole('button', { name: /löschen/i })).toBeVisible(); + }); + + it('hides edit and delete actions when canBlogWrite is false', async () => { + render(GeschichtePage, { props: { data: baseData({ canBlogWrite: false }) } }); + + await expect.element(page.getByRole('link', { name: /bearbeiten/i })).not.toBeInTheDocument(); + await expect.element(page.getByRole('button', { name: /löschen/i })).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/routes/users/[id]/page.svelte.test.ts b/frontend/src/routes/users/[id]/page.svelte.test.ts new file mode 100644 index 00000000..5e7fc31d --- /dev/null +++ b/frontend/src/routes/users/[id]/page.svelte.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import UserProfilePage from './+page.svelte'; + +afterEach(cleanup); + +const baseData = (overrides: Record = {}) => ({ + profileUser: { + firstName: 'Anna', + lastName: 'Schmidt', + email: 'anna@example.com', + contact: 'Telefon: 030-12345', + ...overrides + } +}); + +describe('users/[id] page', () => { + it('renders the heading and back link', async () => { + render(UserProfilePage, { props: { data: baseData() } }); + + await expect.element(page.getByRole('heading', { level: 1 })).toBeVisible(); + await expect.element(page.getByRole('link', { name: /zurück/i })).toHaveAttribute('href', '/'); + }); + + it('renders the full name when both firstName and lastName are present', async () => { + render(UserProfilePage, { props: { data: baseData() } }); + + await expect.element(page.getByRole('heading', { level: 2 })).toHaveTextContent('Anna Schmidt'); + }); + + it('renders firstName-only when lastName is missing', async () => { + render(UserProfilePage, { + props: { data: baseData({ firstName: 'Anna', lastName: '' }) } + }); + + await expect.element(page.getByRole('heading', { level: 2 })).toHaveTextContent('Anna'); + }); + + it('renders lastName-only when firstName is missing', async () => { + render(UserProfilePage, { + props: { data: baseData({ firstName: '', lastName: 'Schmidt' }) } + }); + + await expect.element(page.getByRole('heading', { level: 2 })).toHaveTextContent('Schmidt'); + }); + + it('falls back to email when both firstName and lastName are missing', async () => { + render(UserProfilePage, { + props: { data: baseData({ firstName: '', lastName: '', email: 'fallback@example.com' }) } + }); + + await expect + .element(page.getByRole('heading', { level: 2 })) + .toHaveTextContent('fallback@example.com'); + }); + + it('renders avatar initials when both names are present', async () => { + render(UserProfilePage, { props: { data: baseData() } }); + + await expect.element(page.getByText('AS')).toBeVisible(); + }); + + it('renders avatar firstName initial when only firstName is present', async () => { + render(UserProfilePage, { + props: { data: baseData({ firstName: 'Anna', lastName: '' }) } + }); + + const avatarInitial = document.querySelector('.h-16.w-16 span.font-serif'); + expect(avatarInitial?.textContent).toBe('A'); + }); + + it('renders avatar lastName initial when only lastName is present', async () => { + render(UserProfilePage, { + props: { data: baseData({ firstName: '', lastName: 'Schmidt' }) } + }); + + const avatarInitial = document.querySelector('.h-16.w-16 span.font-serif'); + expect(avatarInitial?.textContent).toBe('S'); + }); + + it('omits the email row when email is empty', async () => { + render(UserProfilePage, { + props: { data: baseData({ email: '' }) } + }); + + await expect.element(page.getByText('E-Mail')).not.toBeInTheDocument(); + }); + + it('omits the contact row when contact is empty', async () => { + render(UserProfilePage, { + props: { data: baseData({ contact: '' }) } + }); + + await expect.element(page.getByText('Kontakt')).not.toBeInTheDocument(); + }); + + it('renders both email and contact rows when populated', async () => { + render(UserProfilePage, { props: { data: baseData() } }); + + await expect.element(page.getByText('E-Mail')).toBeVisible(); + await expect.element(page.getByText('Kontakt')).toBeVisible(); + await expect.element(page.getByText('anna@example.com')).toBeVisible(); + await expect.element(page.getByText('Telefon: 030-12345')).toBeVisible(); + }); +});