import { expect, test, type Download, type Locator, type Page } from '@playwright/test'; import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'; import { zipSync } from 'fflate'; import { readFile } from 'node:fs/promises'; 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()); }; 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(); }; 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; }; const trackPageErrors = (page: Page) => { const errors: Error[] = []; page.on('pageerror', (error) => errors.push(error)); return errors; }; const pdfDropzone = (page: Page) => page.getByRole('button', { name: 'PDF-Dateien hinzufügen' }); test('start page exposes only working tools and forwards a dropped ZIP to beA', async ({ page }) => { const pageErrors = trackPageErrors(page); const pdf = await createPdf(1, 'beA'); const zip = Buffer.from(zipSync({ 'anlage.pdf': pdf })); await page.goto('/'); await expect(page.getByRole('heading', { name: 'PDF-Werkzeuge' })).toBeVisible(); await expect(page.getByRole('link', { name: /Signieren/ })).toHaveCount(0); await dropFiles(page, 'window', [ { name: 'bea-archiv.zip', mimeType: 'application/zip', buffer: zip } ]); await expect(page).toHaveURL(/\/tools\/bea$/); await expect(page.getByText('1 lose Datei')).toBeVisible({ timeout: 15_000 }); expect(pageErrors).toEqual([]); }); test('short tool pages keep the footer flush with the viewport bottom', async ({ page }) => { const expectFooterAtViewportBottom = async (viewport: { width: number; height: number }) => { await page.setViewportSize(viewport); await page.goto('/tools/watermark'); const footerBottom = await page.locator('footer').evaluate((footer) => { return footer.getBoundingClientRect().bottom; }); expect(Math.abs(footerBottom - viewport.height)).toBeLessThanOrEqual(1); }; await expectFooterAtViewportBottom({ width: 1440, height: 900 }); await expectFooterAtViewportBottom({ width: 390, height: 844 }); }); test('merge accepts dropped PDFs and downloads a merged PDF', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/merge'); await dropFiles(page, pdfDropzone(page), [ { name: 'eins.pdf', mimeType: 'application/pdf', buffer: await createPdf(1, 'Eins') }, { name: 'zwei.pdf', mimeType: 'application/pdf', buffer: await createPdf(2, 'Zwei') } ]); await expect(page.getByText('2 Dateien ausgewählt')).toBeVisible(); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'PDFs zusammenfügen' }).click(); await expectPdfDownload(await downloadPromise, /^zusammengefuegt\.pdf$/); expect(pageErrors).toEqual([]); }); test('separate renders dropped PDF pages and removes a selected page', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/separate'); await dropFiles(page, pdfDropzone(page), [ { name: 'seiten.pdf', mimeType: 'application/pdf', buffer: await createPdf(3, 'Trennen') } ]); await expect(page.getByRole('button', { name: /Seite 1/ })).toBeVisible({ timeout: 15_000 }); await page.getByRole('button', { name: /Seite 2/ }).click(); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Seiten entfernen & herunterladen' }).click(); await expectPdfDownload(await downloadPromise, /^seiten_ohne_seiten\.pdf$/); expect(pageErrors).toEqual([]); }); test('watermark accepts numeric controls and downloads text and image watermarks', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/watermark'); await dropFiles(page, pdfDropzone(page), [ { name: 'marke.pdf', mimeType: 'application/pdf', buffer: await createPdf(1, 'Marke') } ]); await expect(page.getByRole('heading', { name: 'Wasserzeichen-Einstellungen' })).toBeVisible({ timeout: 15_000 }); await page.locator('#watermark-color').fill('#00ff00'); await page.locator('#watermark-opacity').fill('0.45'); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Wasserzeichen anwenden & herunterladen' }).click(); await expectPdfDownload(await downloadPromise, /^marke_wasserzeichen\.pdf$/); await page.getByLabel('Bild', { exact: true }).check(); await dropFiles(page, page.getByRole('button', { name: 'Wasserzeichenbild hinzufügen' }), [ { name: 'logo.png', mimeType: 'image/png', buffer: Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Z5rAAAAAASUVORK5CYII=', 'base64' ) } ]); const imageDownloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Wasserzeichen anwenden & herunterladen' }).click(); await expectPdfDownload(await imageDownloadPromise, /^marke_wasserzeichen\.pdf$/); await expect(page.getByText('Fehler beim Anwenden')).toHaveCount(0); expect(pageErrors).toEqual([]); }); test('encrypt creates a protected PDF and decrypt rebuilds it with the password', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/encrypt'); await dropFiles(page, pdfDropzone(page), [ { name: 'geheim.pdf', mimeType: 'application/pdf', buffer: await createPdf(1, 'Geheim') } ]); await page.locator('#encrypt-password').fill('test-passwort'); await page.locator('#encrypt-password-confirmation').fill('test-passwort'); const encryptedDownloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Passwort setzen & herunterladen' }).click(); const encryptedDownload = await encryptedDownloadPromise; const encryptedBytes = await expectPdfDownload(encryptedDownload, /^geheim_geschuetzt\.pdf$/); await page.goto('/tools/decrypt'); await dropFiles(page, pdfDropzone(page), [ { name: 'geheim_geschuetzt.pdf', mimeType: 'application/pdf', buffer: encryptedBytes } ]); await page.locator('#decrypt-password').fill('test-passwort'); const decryptedDownloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Passwort entfernen & herunterladen' }).click(); await expectPdfDownload(await decryptedDownloadPromise, /^geheim_geschuetzt_entsperrt\.pdf$/); expect(pageErrors).toEqual([]); }); test('compress previews result size before downloading', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/compress'); await dropFiles(page, pdfDropzone(page), [ { name: 'gross.pdf', mimeType: 'application/pdf', buffer: await createPdf(2, 'Komprimieren') } ]); await page.getByRole('button', { name: 'Komprimieren', exact: true }).click(); await expect(page.getByText(/Ergebnisgröße:/)).toBeVisible({ timeout: 20_000 }); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'PDF herunterladen' }).click(); await expectPdfDownload(await downloadPromise, /^gross_komprimiert\.pdf$/); expect(pageErrors).toEqual([]); }); test('convert bundles a multi-page PDF as a correctly named ZIP', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/convert'); await dropFiles(page, pdfDropzone(page), [ { name: 'bilder.pdf', mimeType: 'application/pdf', buffer: await createPdf(2, 'Bilder') } ]); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Konvertieren & herunterladen' }).click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe('bilder_bilder.zip'); expect(download.suggestedFilename()).not.toContain('.pdf'); expect(pageErrors).toEqual([]); });