import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page, userEvent } 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: () => () => {} })); vi.mock('$lib/shared/cookies', () => ({ csrfFetch: vi.fn() })); import { createConfirmService, CONFIRM_KEY } from '$lib/shared/services/confirm.svelte.js'; import { csrfFetch } from '$lib/shared/cookies'; import { goto } from '$app/navigation'; import type { components } from '$lib/generated/api'; const { default: GeschichtePage } = await import('./+page.svelte'); afterEach(cleanup); beforeEach(() => { vi.clearAllMocks(); }); type GeschichteView = components['schemas']['GeschichteView']; const baseGeschichte = (overrides: Partial = {}): GeschichteView => ({ id: 'g1', title: 'Die Reise nach Berlin', body: '

Im Jahr 1923 fuhr Helene...

', type: 'STORY', status: 'PUBLISHED', author: { id: 'u1', displayName: 'Anna Schmidt' }, persons: [], items: [], createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', publishedAt: '2026-04-15T10:00:00Z', ...overrides }); const baseData = (overrides: Record = {}) => ({ user: undefined, canWrite: false, canAnnotate: false, geschichte: baseGeschichte(), canBlogWrite: false, ...overrides }); describe('geschichten/[id] page', () => { it('renders the geschichte title as the level-1 heading', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData() } }); await expect .element(page.getByRole('heading', { level: 1, name: /reise nach berlin/i })) .toBeVisible(); }); it('renders the article on a reading-sheet surface card (#797)', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData() } }); const article = document.querySelector('article'); expect(article).not.toBeNull(); // bg-sheet sits between the sand canvas and the white cards inside the article for (const cls of ['bg-sheet', 'border-line', 'rounded-sm', 'shadow-sm']) { expect(article!.className).toContain(cls); } }); it('renders the author full name from firstName + lastName', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData() } }); await expect.element(page.getByText(/Anna Schmidt/)).toBeVisible(); }); it('renders the server-computed author displayName verbatim', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData({ geschichte: baseGeschichte({ author: { id: 'u2', displayName: 'fallback@example.com' } }) }) } }); await expect.element(page.getByText(/fallback@example.com/)).toBeVisible(); }); it('renders an empty author when author is absent', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData({ geschichte: baseGeschichte({ author: undefined }) }) } }); await expect.element(page.getByRole('heading', { level: 1 })).toBeVisible(); }); it('renders the publishedAt date suffix when publishedAt is set', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), 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, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData({ geschichte: baseGeschichte({ publishedAt: undefined }) }) } }); 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, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), 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, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData({ geschichte: baseGeschichte({ persons: [ { id: 'p1', firstName: 'Helene', lastName: 'Schmidt' }, { id: 'p2', firstName: 'Karl', lastName: '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, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData() } }); await expect.element(page.getByText('Erwähnte Dokumente')).not.toBeInTheDocument(); }); it('renders the documents section when there are linked journey items', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData({ geschichte: baseGeschichte({ items: [ { id: 'item1', position: 0, document: { id: 'd1', title: 'Brief 1923', datePrecision: 'DAY', receiverCount: 0 }, note: 'Brief aus 1923' } ] }) }) } }); await expect.element(page.getByText('Erwähnte Dokumente')).toBeVisible(); await expect.element(page.getByText('Brief 1923')).toBeVisible(); await expect.element(page.getByText('Brief aus 1923')).toBeVisible(); expect(document.querySelector('a[href="/documents/d1"]')).not.toBeNull(); }); it('JOURNEY shows "zusammengestellt am" instead of "veröffentlicht am"', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData({ geschichte: baseGeschichte({ type: 'JOURNEY' }) }) } }); await expect.element(page.getByText(/zusammengestellt am/i)).toBeVisible(); await expect.element(page.getByText(/veröffentlicht am/i)).not.toBeInTheDocument(); }); it('renders the author avatar initials in the meta bar', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData() } }); await expect.element(page.getByText('AS', { exact: true })).toBeInTheDocument(); }); it('renders edit and delete actions when canBlogWrite is true', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), 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, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), 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(); }); it('STORY with items:[] renders rich-text body and no empty-state message', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData({ geschichte: baseGeschichte({ type: 'STORY', items: [] }) }) } }); await expect.element(page.getByText(/Im Jahr 1923/)).toBeVisible(); await expect.element(page.getByText(/Diese Lesereise ist noch leer/)).not.toBeInTheDocument(); }); it('type:undefined + non-empty body renders StoryReader and no empty-state', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { data: baseData({ geschichte: baseGeschichte({ type: undefined as unknown as 'STORY' | 'JOURNEY', items: [] }) }) } }); await expect.element(page.getByText(/Im Jahr 1923/)).toBeVisible(); await expect.element(page.getByText(/Diese Lesereise ist noch leer/)).not.toBeInTheDocument(); }); it('delete success: navigates to /geschichten after confirmed DELETE returns ok', async () => { vi.mocked(csrfFetch).mockResolvedValue(new Response(null, { status: 200 })); const confirmService = createConfirmService(); render(GeschichtePage, { context: new Map([[CONFIRM_KEY, confirmService]]), props: { data: baseData({ canBlogWrite: true }) } }); // Trigger delete — opens confirm dialog const deleteBtn = page.getByRole('button', { name: /löschen/i }); await userEvent.click(deleteBtn); // Settle the confirmation dialog confirmService.settle(true); // Wait for the async delete to complete, then check goto was called await vi.waitFor(() => { expect(vi.mocked(goto)).toHaveBeenCalledWith('/geschichten'); }); }); it('delete failure: shows error message when DELETE returns non-ok', async () => { vi.mocked(csrfFetch).mockResolvedValue( new Response(JSON.stringify({ code: 'FORBIDDEN' }), { status: 403, headers: { 'Content-Type': 'application/json' } }) ); const confirmService = createConfirmService(); render(GeschichtePage, { context: new Map([[CONFIRM_KEY, confirmService]]), props: { data: baseData({ canBlogWrite: true }) } }); // Trigger delete — opens confirm dialog const deleteBtn = page.getByRole('button', { name: /löschen/i }); await userEvent.click(deleteBtn); // Settle the confirmation dialog confirmService.settle(true); // Wait for the error to appear inline await expect.element(page.getByRole('alert')).toBeVisible(); expect(vi.mocked(goto)).not.toHaveBeenCalled(); }); });