Backend: - Add ANNOTATE_ALL permission - Add ANNOTATION_NOT_FOUND and ANNOTATION_OVERLAP error codes - V10 migration: document_annotations table with page/rect/color/owner - DocumentAnnotation entity, AnnotationRepository, CreateAnnotationDTO - AnnotationService: overlap detection (rectangle intersection), ownership enforcement on delete - AnnotationController: GET (authenticated), POST/DELETE (ANNOTATE_ALL) - 15 new tests (AnnotationServiceTest, AnnotationControllerTest) — TDD red/green Frontend: - AnnotationLayer.svelte: pointer-event drawing, colored rect overlays, delete buttons - PdfViewer.svelte: annotate toggle, color picker, loads/saves/deletes annotations via API - Disabled annotate button with tooltip for users without ANNOTATE_ALL - canAnnotate exposed from layout server, passed to PdfViewer - errors.ts + de/en/es translations for new error codes - 3 new unit tests for AnnotationLayer — TDD red/green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
73 lines
2.6 KiB
TypeScript
73 lines
2.6 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() }));
|
||
|
||
const makePerson = (overrides = {}) => ({
|
||
id: '1',
|
||
firstName: 'Max',
|
||
lastName: 'Mustermann',
|
||
...overrides
|
||
});
|
||
|
||
const emptyData = { user: undefined, canWrite: true, canAnnotate: false, q: '', persons: [] };
|
||
const dataWithPersons = { ...emptyData, persons: [makePerson()] };
|
||
|
||
afterEach(cleanup);
|
||
|
||
// ─── Rendering ────────────────────────────────────────────────────────────────
|
||
|
||
describe('Persons page – rendering', () => {
|
||
it('renders the search input', async () => {
|
||
render(Page, { data: emptyData });
|
||
await expect.element(page.getByRole('textbox')).toBeInTheDocument();
|
||
});
|
||
|
||
it('pre-fills the search input from data.q', async () => {
|
||
render(Page, { data: { ...emptyData, q: 'Müller' } });
|
||
await expect.element(page.getByRole('textbox')).toHaveValue('Müller');
|
||
});
|
||
|
||
it('shows empty state when no persons', async () => {
|
||
render(Page, { data: emptyData });
|
||
await expect.element(page.getByText('Keine Personen gefunden')).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders person cards', async () => {
|
||
render(Page, { data: dataWithPersons });
|
||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||
});
|
||
|
||
it('links person card to detail page', async () => {
|
||
render(Page, { data: dataWithPersons });
|
||
await expect
|
||
.element(page.getByRole('link', { name: /Max Mustermann/ }))
|
||
.toHaveAttribute('href', '/persons/1');
|
||
});
|
||
});
|
||
|
||
// ─── Keystroke preservation (issue #34) ──────────────────────────────────────
|
||
|
||
describe('Persons 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.getByRole('textbox');
|
||
|
||
// 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, q: 'a' } });
|
||
await tick();
|
||
|
||
// Input must still show what the user typed, not the stale URL value
|
||
await expect.element(input).toHaveValue('abc');
|
||
});
|
||
});
|