Files
familienarchiv/frontend/src/routes/page.svelte.spec.ts
Marcel 0e13fd194b
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
feat(search): show spinner in search input while navigation is in-flight
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 15:52:37 +02:00

276 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(), invalidateAll: 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,
isDashboard: false,
filters: { q: '', from: '', to: '', senderId: '', receiverId: '', tags: [] },
documents: [],
incompleteDocs: [],
recentDocs: [],
stats: null,
incompleteCount: 0,
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('Titel, Personen, Tags durchsuchen\u2026'))
.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('Titel, Personen, Tags durchsuchen\u2026'))
.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('Titel, Personen, Tags durchsuchen\u2026');
// 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');
});
});
// ─── Dashboard mode ───────────────────────────────────────────────────────────
describe('Home page dashboard mode', () => {
const dashboardData = {
...emptyData,
isDashboard: true,
canWrite: false,
incompleteDocs: [],
recentDocs: []
};
it('hides the right column when canWrite is false and incompleteDocs is empty', async () => {
render(Page, { data: dashboardData });
const rightCol = page.getByTestId('dashboard-right-column');
await expect.element(rightCol).not.toBeInTheDocument();
});
it('shows the right column when canWrite is true', async () => {
render(Page, { data: { ...dashboardData, canWrite: true } });
const rightCol = page.getByTestId('dashboard-right-column');
await expect.element(rightCol).toBeInTheDocument();
});
it('shows the right column when incompleteDocs is non-empty', async () => {
render(Page, {
data: {
...dashboardData,
canWrite: false,
incompleteDocs: [{ id: 'd1', title: 'Taufschein' }]
}
});
const rightCol = page.getByTestId('dashboard-right-column');
await expect.element(rightCol).toBeInTheDocument();
});
});
// ─── 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' });
});
});
// ─── Loading spinner ──────────────────────────────────────────────────────────
describe('Home page loading spinner', () => {
it('does not show spinner by default', async () => {
render(Page, { data: emptyData });
const spinner = page.getByRole('status');
await expect.element(spinner).not.toBeInTheDocument();
});
});
// ─── Sort controls ────────────────────────────────────────────────────────────
describe('Home page sort controls', () => {
it('pre-fills sort from filters.sort', async () => {
const data = {
...emptyData,
filters: { ...emptyData.filters, sort: 'TITLE', dir: 'asc', tagQ: '' }
};
render(Page, { data });
const select = page.getByRole('combobox');
await expect.element(select).toHaveValue('TITLE');
});
it('renders direction toggle with asc indicator when dir is asc', async () => {
const data = {
...emptyData,
filters: { ...emptyData.filters, sort: 'DATE', dir: 'asc', tagQ: '' }
};
render(Page, { data });
const btn = page.getByRole('button', { name: /aufsteigend/i });
await expect.element(btn).toBeInTheDocument();
});
});