Files
familienarchiv/frontend/src/routes/documents/page.svelte.spec.ts
Marcel 7f23e88b69
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m33s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Unit & Component Tests (push) Failing after 2m40s
CI / OCR Service Tests (push) Successful in 36s
CI / Backend Unit Tests (push) Failing after 2m54s
CI / Backend Unit Tests (pull_request) Failing after 2m58s
fix(documents): address review cycle 2 — a11y, CSS injection, debounce tests
- ContributorStack: text-xs for WCAG 1.4.4 (was text-[10px]), safeColor()
  validation to block CSS injection via actor.color, role="img" aria-label
  on empty placeholder, {#each} keyed by index
- ContributorStack spec: update empty-state assertion to getByRole('img')
- DocumentRow spec: add stopPropagation regression test for tag click
- documents/page.svelte.spec.ts: new — debounce, URL building, initial state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 00:40:48 +02:00

122 lines
3.5 KiB
TypeScript

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<string, unknown> = {}) {
return {
items: [],
total: 0,
q: '',
from: '',
to: '',
senderId: '',
receiverId: '',
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 })
);
});
});