import { afterEach, describe, expect, it, vi } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import Page from './+page.svelte'; const tick = () => new Promise((r) => setTimeout(r, 0)); vi.mock('$app/navigation', () => ({ goto: vi.fn() })); // Silence fetch calls from PersonTypeahead when advanced filters are open vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) }) ); afterEach(cleanup); // ─── Test data ──────────────────────────────────────────────────────────────── const emptyData = { user: undefined, canWrite: true, canAnnotate: false, filters: { q: '', from: '', to: '', senderId: '', receiverId: '', tags: [] }, documents: [], initialValues: { senderName: '', receiverName: '' }, error: null }; const makeDoc = (overrides = {}) => ({ id: '1', title: 'Testbrief', originalFilename: 'testbrief.pdf', status: 'UPLOADED' as const, documentDate: '2024-03-15', location: 'Berlin', sender: { id: 'p1', firstName: 'Max', lastName: 'Mustermann' }, receivers: [{ id: 'p2', firstName: 'Anna', lastName: 'Musterfrau' }], tags: [{ id: 't1', name: 'Familie' }], filePath: '/files/testbrief.pdf', createdAt: '2024-03-15T10:00:00Z', updatedAt: '2024-03-15T10:00:00Z', ...overrides }); const dataWithDocs = { ...emptyData, documents: [makeDoc()] }; // ─── Search bar ─────────────────────────────────────────────────────────────── describe('Home page – search bar', () => { it('renders the full-text search input', async () => { render(Page, { data: emptyData }); await expect .element(page.getByPlaceholder('Suche in Titel, Inhalt, Ort...')) .toBeInTheDocument(); await page.screenshot({ path: 'test-results/screenshots/home-default.png' }); }); it('renders the filter toggle button', async () => { render(Page, { data: emptyData }); // Use exact match to avoid collision with the empty-state "Alle Filter löschen" button await expect .element(page.getByRole('button', { name: 'Filter', exact: true })) .toBeInTheDocument(); }); it('renders the reset link pointing to /', async () => { render(Page, { data: emptyData }); const resetLink = page.getByTitle('Filter zurücksetzen'); await expect.element(resetLink).toBeInTheDocument(); await expect.element(resetLink).toHaveAttribute('href', '/'); }); it('pre-fills the search input from filters.q', async () => { render(Page, { data: { ...emptyData, filters: { ...emptyData.filters, q: 'Urlaub' } } }); await expect .element(page.getByPlaceholder('Suche in Titel, Inhalt, Ort...')) .toHaveValue('Urlaub'); }); }); // ─── Advanced filters ───────────────────────────────────────────────────────── describe('Home page – advanced filters', () => { it('hides the advanced filters by default', async () => { render(Page, { data: emptyData }); // Date inputs are inside the {#if showAdvanced} block → not in DOM await tick(); expect(document.querySelector('input[id="from"]')).toBeNull(); expect(document.querySelector('input[id="to"]')).toBeNull(); }); it('toggles the advanced filter panel open on button click', async () => { render(Page, { data: emptyData }); await page.getByRole('button', { name: 'Filter', exact: true }).click(); await tick(); expect(document.querySelector('input[id="from"]')).not.toBeNull(); expect(document.querySelector('input[id="to"]')).not.toBeNull(); await page.screenshot({ path: 'test-results/screenshots/home-filters-open.png' }); }); it('collapses the advanced filter panel on second click', async () => { render(Page, { data: emptyData }); const btn = page.getByRole('button', { name: 'Filter', exact: true }); await btn.click(); // Wait for the input to appear before clicking again await expect.element(page.getByText('Schlagworte')).toBeInTheDocument(); await btn.click(); // Wait for slide transition to finish await expect.element(page.getByText('Schlagworte')).not.toBeInTheDocument(); }); it('renders the tag filter section when filters are open', async () => { render(Page, { data: emptyData }); await page.getByRole('button', { name: 'Filter', exact: true }).click(); await expect.element(page.getByText('Schlagworte')).toBeInTheDocument(); }); }); // ─── Document list ──────────────────────────────────────────────────────────── describe('Home page – document list', () => { it('shows empty state when there are no documents', async () => { render(Page, { data: emptyData }); await expect.element(page.getByText('Keine Dokumente gefunden')).toBeInTheDocument(); await page.screenshot({ path: 'test-results/screenshots/home-empty-state.png' }); }); it('renders a document with title, date, location, sender and receiver', async () => { render(Page, { data: dataWithDocs }); await expect.element(page.getByText('Testbrief')).toBeInTheDocument(); await expect.element(page.getByText('15. März 2024')).toBeInTheDocument(); await expect.element(page.getByText('Berlin')).toBeInTheDocument(); await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument(); await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument(); await page.screenshot({ path: 'test-results/screenshots/home-with-documents.png' }); }); it('renders a tag chip for each document tag', async () => { render(Page, { data: dataWithDocs }); await expect.element(page.getByRole('button', { name: 'Familie' })).toBeInTheDocument(); }); it('renders "Unbekannt" for sender when sender is null', async () => { const data = { ...emptyData, documents: [makeDoc({ sender: null })] }; render(Page, { data }); await expect.element(page.getByText('Unbekannt')).toBeInTheDocument(); }); it('renders original filename when title is empty', async () => { const data = { ...emptyData, documents: [makeDoc({ title: null })] }; render(Page, { data }); await expect.element(page.getByText('testbrief.pdf')).toBeInTheDocument(); }); it('links each document to its detail page', async () => { render(Page, { data: dataWithDocs }); const link = page.getByRole('link', { name: /Testbrief/ }); await expect.element(link).toHaveAttribute('href', '/documents/1'); }); it('renders the "Neues Dokument" link', async () => { render(Page, { data: emptyData }); const link = page.getByRole('link', { name: /Neues Dokument/i }); await expect.element(link).toBeInTheDocument(); await expect.element(link).toHaveAttribute('href', '/documents/new'); }); }); // ─── Keystroke preservation (issue #34) ────────────────────────────────────── describe('Home page – search input keystroke preservation', () => { it('does not overwrite the search input while the user is focused and stale data arrives', async () => { const { rerender } = render(Page, { data: emptyData }); const input = page.getByPlaceholder('Suche in Titel, Inhalt, Ort...'); // User types "abc" — input is focused await input.click(); await input.fill('abc'); // Simulate a navigation completing with stale data (q='a') while the user is still typing await rerender({ data: { ...emptyData, filters: { ...emptyData.filters, q: 'a' } } }); await tick(); // Input must still show what the user typed, not the stale URL value await expect.element(input).toHaveValue('abc'); }); }); // ─── Error state ────────────────────────────────────────────────────────────── describe('Home page – error state', () => { it('shows the error message when data.error is set', async () => { const data = { ...emptyData, error: 'Daten konnten nicht geladen werden.' }; render(Page, { data }); await expect.element(page.getByText('Daten konnten nicht geladen werden.')).toBeInTheDocument(); await page.screenshot({ path: 'test-results/screenshots/home-error.png' }); }); });