import { expect, type Download, type Locator, type Page } from '@playwright/test'; import { decodePDFRawStream, degrees, PDFArray, PDFDocument, PDFRawStream, StandardFonts, rgb } from 'pdf-lib'; import { PDFDocument as EncryptablePdfDocument } from '@cantoo/pdf-lib'; import { zipSync } from 'fflate'; import { readFile } from 'node:fs/promises'; export const createPdf = async (pageCount: number, label: string) => { const document = await PDFDocument.create(); const font = await document.embedFont(StandardFonts.Helvetica); for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) { const page = document.addPage([420, 594]); page.drawText(`${label} – Seite ${pageNumber}`, { x: 40, y: 520, size: 20, font, color: rgb(0.1, 0.2, 0.4) }); } return Buffer.from(await document.save()); }; export const createStampPng = (page: Page) => page.evaluate( async () => new Promise((resolve, reject) => { const canvas = document.createElement('canvas'); canvas.width = 32; canvas.height = 32; const context = canvas.getContext('2d'); if (!context) { reject(new Error('Kein 2D-Kontext')); return; } context.fillStyle = '#cc1a1a'; context.fillRect(0, 0, 32, 32); canvas.toBlob(async (blob) => { if (!blob) { reject(new Error('PNG konnte nicht erzeugt werden')); return; } const bytes = new Uint8Array(await blob.arrayBuffer()); let binary = ''; bytes.forEach((byte) => { binary += String.fromCharCode(byte); }); resolve(btoa(binary)); }, 'image/png'); }) ); export const dropFiles = async ( page: Page, target: Locator | 'window', files: Array<{ name: string; mimeType: string; buffer: Buffer }> ) => { await page.locator('[data-dropzone-ready="true"]').first().waitFor(); const dataTransfer = await page.evaluateHandle( (droppedFiles) => { const transfer = new DataTransfer(); for (const droppedFile of droppedFiles) { const bytes = Uint8Array.from(atob(droppedFile.base64), (character) => character.charCodeAt(0) ); transfer.items.add( new File([bytes], droppedFile.name, { type: droppedFile.mimeType, lastModified: Date.now() }) ); } return transfer; }, files.map((file) => ({ name: file.name, mimeType: file.mimeType, base64: file.buffer.toString('base64') })) ); if (target === 'window') { await page.evaluate((transfer) => { window.dispatchEvent( new DragEvent('drop', { dataTransfer: transfer, bubbles: true, cancelable: true }) ); }, dataTransfer); } else { await target.dispatchEvent('drop', { dataTransfer }); } await dataTransfer.dispose(); }; export const expectPdfDownload = async (download: Download, name: RegExp) => { expect(download.suggestedFilename()).toMatch(name); const path = await download.path(); expect(path).not.toBeNull(); const bytes = await readFile(path!); expect(bytes.subarray(0, 5).toString()).toBe('%PDF-'); return bytes; }; export const getPageContentStreamCount = (document: PDFDocument, pageIndex: number) => { const contents = document.getPage(pageIndex).node.Contents(); if (!contents) return 0; return contents instanceof PDFArray ? contents.size() : 1; }; export const getPageContentText = (document: PDFDocument, pageIndex: number) => { const contents = document.getPage(pageIndex).node.Contents(); const refs = contents instanceof PDFArray ? contents.asArray() : contents ? [contents] : []; return refs .map((ref) => document.context.lookup(ref)) .filter((stream): stream is PDFRawStream => stream instanceof PDFRawStream) .map((stream) => Buffer.from(decodePDFRawStream(stream).decode()).toString('latin1')) .join('\n'); }; export const createEncryptedPdf = async (pageCount: number, label: string, password: string) => { const document = await EncryptablePdfDocument.load(await createPdf(pageCount, label)); document.encrypt({ userPassword: password, ownerPassword: 'owner-passwort', permissions: { printing: true, copying: true } }); return Buffer.from(await document.save()); }; export const createPreRotatedPdf = async (pageCount: number, label: string) => { const document = await PDFDocument.create(); const font = await document.embedFont(StandardFonts.Helvetica); for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) { const page = document.addPage([420, 594]); page.drawText(`${label} – Seite ${pageNumber}`, { x: 40, y: 520, size: 20, font, color: rgb(0.1, 0.2, 0.4) }); if (pageNumber === 1) page.setRotation(degrees(90)); } return Buffer.from(await document.save()); }; export const trackPageErrors = (page: Page) => { const errors: Error[] = []; page.on('pageerror', (error) => errors.push(error)); return errors; }; export const pdfDropzone = (page: Page) => page.getByRole('button', { name: 'PDF-Dateien hinzufügen' }); export const zipDropzone = (page: Page) => page.getByRole('button', { name: 'beA-ZIP-Dateien hinzufügen' }); export const createZip = async (label: string, pageCount = 1) => Buffer.from(zipSync({ [`${label}.pdf`]: await createPdf(pageCount, label) })); export const newZipButton = (page: Page) => page.getByRole('button', { name: 'Neue ZIP bearbeiten' }); export const settingsButton = (page: Page) => page.getByRole('button', { name: 'Einstellungen', exact: true }); export const replaceZipInput = (page: Page) => page.locator('input[data-zip-picker="replace-zip"]'); export const appendZipInput = (page: Page) => page.locator('input[data-zip-picker="append-zip"]'); export const workspaceStatus = (page: Page, pattern: RegExp) => page.getByText(pattern); export const waitForLoadedWorkspace = async ( page: Page, status: RegExp, options?: { timeout?: number } ) => { await expect(newZipButton(page)).toBeVisible(); await expect(workspaceStatus(page, status)).toBeVisible({ timeout: options?.timeout ?? 15_000 }); }; export const replaceThroughDialog = async (page: Page, zip: Buffer, confirm: boolean) => { await newZipButton(page).click(); await replaceZipInput(page).setInputFiles([ { name: 'archiv-b.zip', mimeType: 'application/zip', buffer: zip } ]); await expect(page.getByText('Aktuelle Bearbeitung ersetzen?')).toBeVisible(); if (confirm) { await page.getByRole('button', { name: 'Neue ZIP öffnen' }).click(); } else { await page.getByRole('button', { name: 'Abbrechen' }).click(); } await expect(page.getByText('Aktuelle Bearbeitung ersetzen?')).toBeHidden(); };