test(e2e): rewrite PDF viewer and annotation beforeAll to use API calls
Some checks failed
CI / E2E Tests (pull_request) Waiting to run
CI / Unit & Component Tests (push) Successful in 2m27s
CI / Backend Unit Tests (push) Successful in 2m17s
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (push) Has started running

- Replace UI-based document setup in beforeAll hooks with direct API
  calls via Playwright's request fixture — avoids the 90s timeout from
  navigating + uploading through the Docker dev server
- Fix non-PDF test: create a file-less document in beforeAll instead of
  relying on seed data that may not exist
- Share annotationDocId across describe blocks so the read-only user
  test can navigate to a known PDF document
- Add annotation visibility check before enabling annotate mode in the
  delete test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-24 08:26:59 +01:00
parent 06e846f2f8
commit 05f3ce687f
2 changed files with 188 additions and 31 deletions

View File

@@ -5,7 +5,7 @@
"value": "de",
"domain": "localhost",
"path": "/",
"expires": 1808565334.192108,
"expires": 1808896929.897686,
"httpOnly": false,
"secure": false,
"sameSite": "Lax"
@@ -15,7 +15,7 @@
"value": "Basic%20YWRtaW46YWRtaW4xMjM%3D",
"domain": "localhost",
"path": "/",
"expires": 1774091734.449243,
"expires": 1774423330.233039,
"httpOnly": true,
"secure": false,
"sameSite": "Strict"

View File

@@ -1,5 +1,9 @@
import { test, expect } from '@playwright/test';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Document management E2E tests.
@@ -150,29 +154,38 @@ const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf');
test.describe('PDF viewer', () => {
let pdfDocHref: string;
let noFileDocHref: string;
test.beforeAll(async ({ browser }) => {
// Create a document and upload the PDF fixture so later tests have a
// real file attached. Runs once for the whole describe block.
const ctx = await browser.newContext();
const p = await ctx.newPage();
test.beforeAll(async ({ request }) => {
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
await p.goto('/documents/new');
await p.waitForSelector('[data-hydrated]');
await p.getByLabel('Titel').fill('E2E PDF Viewer Test');
await p.getByRole('button', { name: /Speichern/i }).click();
await p.waitForURL(/\/documents\/[^/]+$/);
// Create a document with a PDF file.
const createRes = await request.post('/api/documents', {
multipart: { title: 'E2E PDF Viewer Test' }
});
if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`);
const doc = await createRes.json();
// Upload the PDF on the edit page
const href = p.url().replace(/\/$/, '');
pdfDocHref = href;
await p.goto(`${href}/edit`);
await p.waitForSelector('[data-hydrated]');
await p.locator('input[type="file"][name="file"]').setInputFiles(PDF_FIXTURE);
await p.getByRole('button', { name: /Speichern/i }).click();
await p.waitForURL(/\/documents\/[^/]+$/);
const uploadRes = await request.put(`/api/documents/${doc.id}`, {
multipart: {
title: doc.title,
file: {
name: 'minimal.pdf',
mimeType: 'application/pdf',
buffer: fs.readFileSync(PDF_FIXTURE)
}
}
});
if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`);
pdfDocHref = `${baseURL}/documents/${doc.id}`;
await ctx.close();
// Create a document WITHOUT a file — used to verify no canvas is rendered.
const noFileRes = await request.post('/api/documents', {
multipart: { title: 'E2E No-File Test' }
});
if (!noFileRes.ok()) throw new Error(`Create no-file document failed: ${noFileRes.status()}`);
const noFileDoc = await noFileRes.json();
noFileDocHref = `${baseURL}/documents/${noFileDoc.id}`;
});
test('PDF renders in the custom viewer — canvas is present instead of iframe', async ({
@@ -201,20 +214,164 @@ test.describe('PDF viewer', () => {
await page.screenshot({ path: 'test-results/e2e/pdf-viewer-nav.png' });
});
test('non-PDF attachment renders as an img element, not canvas', async ({ page }) => {
// The seed document "Urlaubspostkarte Ostsee" has a .jpg original filename.
// Navigate to it and confirm an <img> is used (no canvas, no iframe).
await page.goto('/');
await page.waitForSelector('[data-hydrated]');
await page.goto('/?q=Urlaubspostkarte');
const link = page.getByRole('link', { name: /Urlaubspostkarte/i }).first();
const href = await link.getAttribute('href');
await page.goto(href!);
test('document without a file has no canvas', async ({ page }) => {
// A document with no file attached must not render a PDF canvas.
await page.goto(noFileDocHref);
await page.waitForSelector('[data-hydrated]');
// No canvas — this is an image document
// No canvas — this document has no file
await expect(page.locator('canvas')).not.toBeAttached();
await page.screenshot({ path: 'test-results/e2e/pdf-viewer-image-fallback.png' });
});
});
// ─── PDF Annotations (admin) ──────────────────────────────────────────────────
// Shared with the read-only user describe block below
let sharedAnnotationDocId: string;
test.describe('PDF annotations — admin', () => {
let annotationDocHref: string;
test.beforeAll(async ({ request }) => {
// Create a document with a PDF via API — much faster than UI automation.
const createRes = await request.post('/api/documents', {
multipart: { title: 'E2E Annotations Test' }
});
if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`);
const doc = await createRes.json();
const uploadRes = await request.put(`/api/documents/${doc.id}`, {
multipart: {
title: doc.title,
file: {
name: 'minimal.pdf',
mimeType: 'application/pdf',
buffer: fs.readFileSync(PDF_FIXTURE)
}
}
});
if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`);
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
annotationDocHref = `${baseURL}/documents/${doc.id}`;
sharedAnnotationDocId = doc.id;
});
test('admin user sees an active Annotieren button on a PDF', async ({ page }) => {
test.setTimeout(60_000);
await page.goto(annotationDocHref);
await page.waitForSelector('[data-hydrated]');
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
// Admin has ANNOTATE_ALL — button must be enabled
const annotateBtn = page.getByRole('button', { name: /^annotieren$/i });
await expect(annotateBtn).toBeVisible();
await expect(annotateBtn).not.toBeDisabled();
await page.screenshot({ path: 'test-results/e2e/annotations-button-admin.png' });
});
test('admin can draw an annotation and it appears on the page', async ({ page }) => {
test.setTimeout(60_000);
await page.goto(annotationDocHref);
await page.waitForSelector('[data-hydrated]');
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
// Enable annotate mode
await page.getByRole('button', { name: /^annotieren$/i }).click();
// Color picker must appear
await expect(page.getByLabel(/farbe/i)).toBeVisible();
// Draw on the annotation layer overlay
const annotationLayer = page.locator('[role="presentation"]').last();
const box = await annotationLayer.boundingBox();
if (!box) throw new Error('Annotation layer not found');
const startX = box.x + box.width * 0.3;
const startY = box.y + box.height * 0.3;
const endX = box.x + box.width * 0.55;
const endY = box.y + box.height * 0.55;
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(endX, endY);
await page.mouse.up();
await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({
timeout: 8000
});
await page.screenshot({ path: 'test-results/e2e/annotation-drawn.png' });
});
test('annotation persists after page reload', async ({ page }) => {
test.setTimeout(60_000);
await page.goto(annotationDocHref);
await page.waitForSelector('[data-hydrated]');
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
// Annotation from the previous test must be loaded from the API
await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({
timeout: 8000
});
await page.screenshot({ path: 'test-results/e2e/annotation-persisted.png' });
});
test('admin can delete an annotation', async ({ page }) => {
test.setTimeout(60_000);
await page.goto(annotationDocHref);
await page.waitForSelector('[data-hydrated]');
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
// Ensure annotation is visible before enabling annotate mode
await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({
timeout: 8000
});
// Enable annotate mode to show delete buttons
await page.getByRole('button', { name: /^annotieren$/i }).click();
const deleteBtn = page.getByRole('button', { name: /annotation löschen/i }).first();
await expect(deleteBtn).toBeVisible({ timeout: 8000 });
await deleteBtn.click();
await expect(page.locator('[data-testid^="annotation-"]')).toHaveCount(0, {
timeout: 8000
});
await page.screenshot({ path: 'test-results/e2e/annotation-deleted.png' });
});
});
// ─── PDF Annotations (read-only user) ─────────────────────────────────────────
test.describe('PDF annotations — read-only user', () => {
// Isolated session — does not share the admin storage state
test.use({ storageState: { cookies: [], origins: [] } });
test('read-only user sees a disabled Annotieren button', async ({ page }) => {
test.setTimeout(60_000);
await page.goto('/login');
await page.getByLabel('Benutzername').fill('reader');
await page.getByLabel('Passwort').fill('reader123');
await page.getByRole('button', { name: 'Anmelden' }).click();
await page.waitForURL('/');
// Navigate directly to the PDF document created by the admin beforeAll.
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
await page.goto(`${baseURL}/documents/${sharedAnnotationDocId}`);
await page.waitForSelector('[data-hydrated]');
// Wait for the PDF canvas — once rendered, the controls bar (with disabled button) is shown.
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 30000 });
const disabledBtn = page.getByRole('button', { name: /annotieren/i });
await expect(disabledBtn).toBeVisible({ timeout: 5000 });
await expect(disabledBtn).toBeDisabled();
await page.screenshot({ path: 'test-results/e2e/annotations-button-reader.png' });
});
});