import { afterEach, beforeEach, 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'; afterEach(() => { cleanup(); vi.useRealTimers(); }); const SEARCH_LABEL = 'Titel, Personen, Tags durchsuchen…'; function makeData(overrides: Record = {}) { return { items: [], total: 0, q: '', from: '', to: '', senderId: '', receiverId: '', initialSenderName: '', initialReceiverName: '', tags: [], sort: 'DATE', dir: 'desc', tagQ: '', tagOp: 'AND', canWrite: false, error: null, ...overrides }; } // ─── Initial state from server data ─────────────────────────────────────────── describe('documents page — initial state', () => { it('pre-fills the search input from data.q', async () => { render(Page, { data: makeData({ q: 'Geburtstag' }) }); await expect .element(page.getByRole('textbox', { name: SEARCH_LABEL })) .toHaveValue('Geburtstag'); }); it('leaves the search input empty when data.q is not set', async () => { render(Page, { data: makeData() }); await expect.element(page.getByRole('textbox', { name: SEARCH_LABEL })).toHaveValue(''); }); }); // ─── URL building via triggerSearch ─────────────────────────────────────────── describe('documents page — URL building', () => { beforeEach(() => vi.useFakeTimers()); it('calls goto with /documents?q=… after the 500 ms debounce', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); render(Page, { data: makeData() }); const input = page.getByRole('textbox', { name: SEARCH_LABEL }); await input.fill('Urlaub'); expect(goto).not.toHaveBeenCalled(); vi.advanceTimersByTime(500); expect(goto).toHaveBeenCalledOnce(); const [url] = vi.mocked(goto).mock.calls[0]; expect(url).toContain('q=Urlaub'); expect(url).toMatch(/^\/documents\?/); }); it('omits q from the URL when the search field is empty', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); render(Page, { data: makeData() }); const input = page.getByRole('textbox', { name: SEARCH_LABEL }); await input.fill(''); vi.advanceTimersByTime(500); const [url] = vi.mocked(goto).mock.calls[0] ?? ['']; expect(url).not.toContain('q='); }); it('second keystroke within 500 ms cancels the first timer — goto called only once', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); render(Page, { data: makeData() }); const input = page.getByRole('textbox', { name: SEARCH_LABEL }); await input.fill('U'); vi.advanceTimersByTime(200); await input.fill('Urlaub'); vi.advanceTimersByTime(500); expect(goto).toHaveBeenCalledOnce(); }); it('passes keepFocus and noScroll options to goto', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); render(Page, { data: makeData() }); const input = page.getByRole('textbox', { name: SEARCH_LABEL }); await input.fill('Brief'); vi.advanceTimersByTime(500); expect(goto).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ keepFocus: true, noScroll: true }) ); }); it('filter change does not carry the current page — goto URL drops page param', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); // User is mid-way through results at page 5; change the search text. render(Page, { data: makeData({ q: 'old', pageNumber: 5 }) }); const input = page.getByRole('textbox', { name: SEARCH_LABEL }); await input.fill('Brief'); vi.advanceTimersByTime(500); const [url] = vi.mocked(goto).mock.calls[0]; expect(url).toContain('q=Brief'); expect(url).not.toContain('page='); }); }); // ─── Sender / receiver name display ────────────────────────────────────────── describe('documents page — sender/receiver display', () => { it('pre-fills sender typeahead from initialSenderName when senderId filter is active', async () => { render(Page, { data: makeData({ senderId: '11111111-1111-1111-1111-111111111111', initialSenderName: 'Max Mustermann' }) }); // Advanced filters are auto-shown when senderId is set const inputs = page.getByPlaceholder('Namen tippen...'); await expect.element(inputs.first()).toHaveValue('Max Mustermann'); }); }); // ─── Timeline density widget wiring (#385) ──────────────────────────────────── describe('documents page — timeline density widget', () => { it('renders the timeline widget when density data is present', async () => { render(Page, { data: makeData({ density: [{ month: '1915-08', count: 3 }], minDate: '1915-08-01', maxDate: '1915-08-31' }) }); await expect.element(page.getByTestId('timeline-density-filter')).toBeInTheDocument(); }); it('hides the timeline widget when density is null (mobile / calendar view)', async () => { render(Page, { data: makeData({ density: null, minDate: null, maxDate: null }) }); expect(document.querySelector('[data-testid="timeline-density-filter"]')).toBeNull(); }); it('clicking a timeline bar navigates with from/to set to that month boundary', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); render(Page, { data: makeData({ density: [{ month: '1915-08', count: 3 }], minDate: '1915-08-01', maxDate: '1915-08-31' }) }); const bar = document.querySelector('[data-testid="timeline-bar"]') as HTMLButtonElement; bar.dispatchEvent(new MouseEvent('click', { bubbles: true })); expect(goto).toHaveBeenCalledOnce(); const [url] = vi.mocked(goto).mock.calls[0]; expect(url).toContain('from=1915-08-01'); expect(url).toContain('to=1915-08-31'); }); it('the standalone zoom-in button no longer exists (drag replaces it)', async () => { render(Page, { data: makeData({ density: [{ month: '1915-08', count: 3 }], minDate: '1915-08-01', maxDate: '1915-08-31', from: '1915-08-01', to: '1915-08-31' }) }); expect(document.querySelector('[data-testid="timeline-zoom-in"]')).toBeNull(); }); it('clicking reset-zoom drops zoomFrom/zoomTo from the URL', async () => { const { goto } = await import('$app/navigation'); vi.mocked(goto).mockClear(); render(Page, { data: makeData({ density: [{ month: '1915-08', count: 3 }], minDate: '1915-08-01', maxDate: '1915-08-31', zoomFrom: '1915-08-01', zoomTo: '1915-08-31' }) }); const resetBtn = document.querySelector( '[data-testid="timeline-zoom-reset"]' ) as HTMLButtonElement; resetBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })); expect(goto).toHaveBeenCalledOnce(); const [url] = vi.mocked(goto).mock.calls[0]; expect(url).not.toContain('zoomFrom='); expect(url).not.toContain('zoomTo='); }); });