import { afterEach, describe, expect, it, vi } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; vi.mock('$app/navigation', () => ({ goto: vi.fn() })); vi.mock('$app/state', () => ({ navigating: { to: null } })); import Page from './+page.svelte'; import type { PageData } from './$types'; afterEach(() => { cleanup(); vi.clearAllMocks(); }); function person(id: string, displayName: string) { return { id, firstName: displayName.split(' ')[0] ?? displayName, lastName: displayName.split(' ').slice(1).join(' ') || 'X', displayName, personType: 'PERSON' }; } function makeData(overrides: Partial = {}): PageData { return { geschichten: [], drafts: [], personFilters: [], documentFilter: null, canBlogWrite: false, ...overrides } as unknown as PageData; } function makeDocumentFilter(overrides: { id?: string; title?: string | null } = {}): { id: string; title: string | null; } { return { id: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', title: 'Brief an Oma', ...overrides }; } describe('geschichten page — multi-person filter chips', () => { it('renders one chip per person in personFilters', async () => { render(Page, { data: makeData({ personFilters: [person('a', 'Anna A'), person('b', 'Bertha B')] as PageData['personFilters'] }) }); await expect .element(page.getByRole('button', { name: /Anna A aus Filter entfernen/ })) .toBeVisible(); await expect .element(page.getByRole('button', { name: /Bertha B aus Filter entfernen/ })) .toBeVisible(); }); it('renders the "All" pill in pressed state when no filters are active', async () => { render(Page, { data: makeData() }); await expect .element(page.getByRole('button', { name: 'Alle' })) .toHaveAttribute('aria-pressed', 'true'); }); it('renders the "All" pill in unpressed state when at least one filter is active', async () => { render(Page, { data: makeData({ personFilters: [person('a', 'Anna A')] as PageData['personFilters'] }) }); await expect .element(page.getByRole('button', { name: 'Alle' })) .toHaveAttribute('aria-pressed', 'false'); }); it('clicking × on a chip removes only that person from the URL', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); // Seed window.location so the chip-removal logic builds the new URL deterministically. const originalHref = window.location.href; window.history.replaceState({}, '', '/geschichten?personId=a&personId=b'); render(Page, { data: makeData({ personFilters: [person('a', 'Anna A'), person('b', 'Bertha B')] as PageData['personFilters'] }) }); const chipBtn = (await page .getByRole('button', { name: /Anna A aus Filter entfernen/ }) .element()) as HTMLElement; chipBtn.click(); await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce()); const url = vi.mocked(goto).mock.calls[0][0] as string; expect(url).toContain('personId=b'); expect(url).not.toContain('personId=a'); window.history.replaceState({}, '', originalHref); }); it('JOURNEY row in the list shows the REISE badge (integration: page passes type through)', async () => { render(Page, { data: makeData({ geschichten: [ { id: 'g1', title: 'Lesereise Berlin', type: 'JOURNEY' } ] as PageData['geschichten'] }) }); const badge = document.querySelector('[data-testid="journey-badge"]'); expect(badge).not.toBeNull(); }); it('shows the "+ Person wählen" button even when filters are already active', async () => { render(Page, { data: makeData({ personFilters: [person('a', 'Anna A')] as PageData['personFilters'] }) }); await expect.element(page.getByRole('button', { name: /Person wählen/ })).toBeVisible(); }); describe('document filter chip', () => { it('renders the document chip when documentFilter is set', async () => { render(Page, { data: makeData({ documentFilter: makeDocumentFilter() as PageData['documentFilter'] }) }); await expect.element(page.getByText(/Gefiltert nach Brief/)).toBeVisible(); await expect.element(page.getByText(/Brief an Oma/)).toBeVisible(); }); it('does not render the document chip when documentFilter is null', async () => { render(Page, { data: makeData() }); await expect.element(page.getByText(/Gefiltert nach Brief/)).not.toBeInTheDocument(); }); it('clicking the document chip remove button navigates without documentId', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); window.history.replaceState( {}, '', '/geschichten?documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ); render(Page, { data: makeData({ documentFilter: makeDocumentFilter() as PageData['documentFilter'] }) }); const removeBtn = (await page .getByRole('button', { name: /Brief an Oma aus Filter entfernen/ }) .element()) as HTMLElement; removeBtn.click(); await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce()); const url = vi.mocked(goto).mock.calls[0][0] as string; expect(url).not.toContain('documentId'); }); it('document chip removal preserves active person filters', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); window.history.replaceState( {}, '', '/geschichten?personId=p1&documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ); render(Page, { data: makeData({ personFilters: [person('p1', 'Anna A')] as PageData['personFilters'], documentFilter: makeDocumentFilter() as PageData['documentFilter'] }) }); const removeBtn = (await page .getByRole('button', { name: /Brief an Oma aus Filter entfernen/ }) .element()) as HTMLElement; removeBtn.click(); await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce()); const url = vi.mocked(goto).mock.calls[0][0] as string; expect(url).toContain('personId=p1'); expect(url).not.toContain('documentId'); }); it('marks the "All" pill as unpressed when document filter is active', async () => { render(Page, { data: makeData({ documentFilter: makeDocumentFilter() as PageData['documentFilter'] }) }); await expect .element(page.getByRole('button', { name: 'Alle' })) .toHaveAttribute('aria-pressed', 'false'); }); }); it('removing a person chip preserves an active document filter in the URL', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); window.history.replaceState( {}, '', '/geschichten?personId=a&documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ); render(Page, { data: makeData({ personFilters: [person('a', 'Anna A')] as PageData['personFilters'], documentFilter: makeDocumentFilter() as PageData['documentFilter'] }) }); const chipBtn = (await page .getByRole('button', { name: /Anna A aus Filter entfernen/ }) .element()) as HTMLElement; chipBtn.click(); await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce()); const url = vi.mocked(goto).mock.calls[0][0] as string; expect(url).not.toContain('personId=a'); expect(url).toContain('documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'); window.history.replaceState({}, '', '/'); }); it('clearAll removes both person and document filters from the URL', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); window.history.replaceState( {}, '', '/geschichten?personId=a&documentId=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' ); render(Page, { data: makeData({ personFilters: [person('a', 'Anna A')] as PageData['personFilters'], documentFilter: makeDocumentFilter() as PageData['documentFilter'] }) }); const allBtn = (await page.getByRole('button', { name: 'Alle' }).element()) as HTMLElement; allBtn.click(); await vi.waitFor(() => expect(goto).toHaveBeenCalledOnce()); const url = vi.mocked(goto).mock.calls[0][0] as string; expect(url).not.toContain('personId'); expect(url).not.toContain('documentId'); window.history.replaceState({}, '', '/'); }); describe('empty state precedence', () => { it('shows geschichten_empty_for_document when only document filter is active', async () => { render(Page, { data: makeData({ geschichten: [], documentFilter: makeDocumentFilter() as PageData['documentFilter'] }) }); await expect.element(page.getByText('Noch keine Geschichten zu diesem Brief')).toBeVisible(); }); it('shows geschichten_empty_for_persons when only person filter is active', async () => { render(Page, { data: makeData({ geschichten: [], personFilters: [person('a', 'Anna A')] as PageData['personFilters'] }) }); await expect.element(page.getByText(/Keine Geschichten für Anna A gefunden/)).toBeVisible(); }); it('shows geschichten_empty_no_filter when no filter is active', async () => { render(Page, { data: makeData({ geschichten: [] }) }); await expect .element(page.getByText('Es gibt noch keine veröffentlichten Geschichten.')) .toBeVisible(); }); it('person-wins: shows persons message when both person and document filters are active', async () => { render(Page, { data: makeData({ geschichten: [], personFilters: [person('a', 'Anna A')] as PageData['personFilters'], documentFilter: makeDocumentFilter() as PageData['documentFilter'] }) }); await expect.element(page.getByText(/Keine Geschichten für Anna A gefunden/)).toBeVisible(); await expect .element(page.getByText('Noch keine Geschichten zu diesem Brief')) .not.toBeInTheDocument(); }); it('chip renders alongside results (empty state not shown when results exist)', async () => { render(Page, { data: makeData({ geschichten: [ { id: 'g1', title: 'Lesereise Berlin', type: 'JOURNEY' } ] as PageData['geschichten'], documentFilter: makeDocumentFilter() as PageData['documentFilter'] }) }); await expect.element(page.getByText(/Gefiltert nach Brief/)).toBeVisible(); await expect.element(page.getByText(/Lesereise Berlin/)).toBeVisible(); await expect .element(page.getByText('Noch keine Geschichten zu diesem Brief')) .not.toBeInTheDocument(); }); }); it('renders all filter pills with a 44px touch target (h-11)', async () => { render(Page, { data: makeData({ personFilters: [person('a', 'Anna A')] as PageData['personFilters'] }) }); // All three pill variants must use h-11 (44px) per the senior-author touch-target rule const all = page.getByRole('button', { name: 'Alle' }); const chip = page.getByRole('button', { name: /Anna A aus Filter entfernen/ }); const add = page.getByRole('button', { name: /Person wählen/ }); const allEl = (await all.element()) as HTMLElement; const chipEl = (await chip.element()) as HTMLElement; const addEl = (await add.element()) as HTMLElement; expect(allEl.className).toContain('h-11'); expect(chipEl.className).toContain('h-11'); expect(addEl.className).toContain('h-11'); }); }); // ─── Entwürfe section ───────────────────────────────────────────────────────── describe('geschichten page — Entwürfe section', () => { const draft = () => ({ id: 'draft-1', title: 'Mein Entwurf', body: '

test

', type: 'STORY', status: 'DRAFT', author: { firstName: 'Max', lastName: 'Muster' }, publishedAt: null }) as unknown as PageData['geschichten'][0]; it('Entwürfe section is hidden when drafts array is empty', async () => { render(Page, { data: makeData({ drafts: [] }) }); const heading = Array.from(document.querySelectorAll('h2')).find( (h) => h.textContent?.includes('Entwürfe') || h.textContent?.includes('Drafts') ); expect(heading).toBeUndefined(); }); it('Entwürfe section is visible when drafts are present', async () => { render(Page, { data: makeData({ drafts: [draft()] as PageData['geschichten'] }) }); const heading = Array.from(document.querySelectorAll('h2')).find( (h) => h.textContent?.includes('Entwürfe') || h.textContent?.includes('Drafts') ); expect(heading).not.toBeUndefined(); }); it('renders a row for each draft story', async () => { render(Page, { data: makeData({ drafts: [draft()] as PageData['geschichten'] }) }); const link = document.querySelector('a[href="/geschichten/draft-1"]'); expect(link).not.toBeNull(); }); it('draft row shows the draft badge', async () => { render(Page, { data: makeData({ drafts: [draft()] as PageData['geschichten'] }) }); const badge = document.querySelector('[data-testid="draft-badge"]'); expect(badge).not.toBeNull(); }); });