Replace Playwright locator .click() calls with native DOM element.click() for all tests that trigger Svelte 5 delegated onclick handlers ($.delegated). Playwright's CDP-based synthetic events don't propagate through Svelte 5's document-level handle_event_propagation delegation mechanism, while native DOM .click() does. Also replace locator.click() with element.focus() for onfocus handler tests, and add cleanup() to afterEach in all spec files missing it to prevent test pollution between runs. Fix TagInput.svelte to use untrack() when reading bindable state after an await to avoid track_reactivity_loss errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
200 lines
8.3 KiB
TypeScript
200 lines
8.3 KiB
TypeScript
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,
|
||
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' });
|
||
});
|
||
});
|