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>
75 lines
1.8 KiB
TypeScript
75 lines
1.8 KiB
TypeScript
import { describe, it, expect, afterEach } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
import { page } from 'vitest/browser';
|
|
|
|
import AnnotationLayer from './AnnotationLayer.svelte';
|
|
|
|
afterEach(cleanup);
|
|
|
|
type Annotation = {
|
|
id: string;
|
|
documentId: string;
|
|
pageNumber: number;
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
color: string;
|
|
createdAt: string;
|
|
};
|
|
|
|
function makeAnnotation(id = 'ann-1'): Annotation {
|
|
return {
|
|
id,
|
|
documentId: 'doc-1',
|
|
pageNumber: 1,
|
|
x: 0.1,
|
|
y: 0.1,
|
|
width: 0.3,
|
|
height: 0.2,
|
|
color: '#ff0000',
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
describe('AnnotationLayer', () => {
|
|
it('renders a colored element for each annotation', async () => {
|
|
render(AnnotationLayer, {
|
|
annotations: [makeAnnotation('ann-1'), makeAnnotation('ann-2')],
|
|
canAnnotate: false,
|
|
color: '#ff0000',
|
|
onDraw: () => {},
|
|
onDelete: () => {}
|
|
});
|
|
|
|
await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
|
|
await expect.element(page.getByTestId('annotation-ann-2')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows a delete button for each annotation when canAnnotate is true', async () => {
|
|
render(AnnotationLayer, {
|
|
annotations: [makeAnnotation('ann-1')],
|
|
canAnnotate: true,
|
|
color: '#ff0000',
|
|
onDraw: () => {},
|
|
onDelete: () => {}
|
|
});
|
|
|
|
await expect
|
|
.element(page.getByRole('button', { name: /annotation löschen/i }))
|
|
.toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show delete buttons when canAnnotate is false', async () => {
|
|
render(AnnotationLayer, {
|
|
annotations: [makeAnnotation('ann-1')],
|
|
canAnnotate: false,
|
|
color: '#ff0000',
|
|
onDraw: () => {},
|
|
onDelete: () => {}
|
|
});
|
|
|
|
expect(page.getByRole('button', { name: /annotation löschen/i }).query()).toBeNull();
|
|
});
|
|
});
|