import { expect, test } from '@playwright/test'; import { unzipSync } from 'fflate'; import { readFile } from 'node:fs/promises'; import { PDFDocument } from 'pdf-lib'; import { createEncryptedPdf, createPdf, createStampPng, dropFiles, expectPdfDownload, getPageContentStreamCount, getPageContentText, pdfDropzone, trackPageErrors } from './helpers'; test('stamp presets fill the editable text and EILT is applied to every page', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/stamp'); await dropFiles(page, pdfDropzone(page), [ { name: 'akte.pdf', mimeType: 'application/pdf', buffer: await createPdf(3, 'Akte') } ]); const stampInput = page.locator('#stamp-text'); for (const preset of ['BEGLAUBIGT', 'AUSFERTIGUNG', 'KOPIE', 'EILT', 'ENTWURF', 'ERLEDIGT']) { await page.getByRole('button', { name: preset, exact: true }).click(); await expect(stampInput).toHaveValue(preset); } await page.getByRole('button', { name: 'EILT', exact: true }).click(); await page.locator('#stamp-rotation').fill('20'); await expect(page.getByTestId('stamp-preview')).toContainText('EILT'); await expect(page.getByTestId('stamp-preview')).not.toHaveCSS('transform', 'none'); await page.getByRole('button', { name: 'Stempel anwenden', exact: true }).click(); await expect(page.getByText('Fertig ✓')).toBeVisible({ timeout: 20_000 }); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'akte.pdf herunterladen' }).click(); const outputBytes = await expectPdfDownload(await downloadPromise, /^akte_stempel\.pdf$/); const output = await PDFDocument.load(outputBytes); expect(output.getPageCount()).toBe(3); for (let pageIndex = 0; pageIndex < output.getPageCount(); pageIndex += 1) { expect(getPageContentStreamCount(output, pageIndex)).toBeGreaterThan(1); } expect(pageErrors).toEqual([]); }); test('stamp custom text adds a German date and only changes selected pages', async ({ page }) => { const pageErrors = trackPageErrors(page); const inputBytes = await createPdf(3, 'Auswahl'); const input = await PDFDocument.load(inputBytes); const originalStreamCounts = input .getPages() .map((_, pageIndex) => getPageContentStreamCount(input, pageIndex)); await page.goto('/tools/stamp'); await dropFiles(page, pdfDropzone(page), [ { name: 'auswahl.pdf', mimeType: 'application/pdf', buffer: inputBytes } ]); await page.locator('#stamp-text').fill('nur hier'); await page.getByRole('checkbox', { name: 'Datum anhängen' }).check(); await expect(page.getByTestId('stamp-preview')).toHaveText(/NUR HIER \d{2}\.\d{2}\.\d{4}/); await page.getByRole('checkbox', { name: 'Auf alle Seiten anwenden' }).uncheck(); await page.getByRole('button', { name: /Seite 2/ }).click(); await page.getByRole('button', { name: 'Stempel anwenden', exact: true }).click(); await expect(page.getByText('Fertig ✓')).toBeVisible({ timeout: 20_000 }); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'auswahl.pdf herunterladen' }).click(); const outputBytes = await expectPdfDownload(await downloadPromise, /^auswahl_stempel\.pdf$/); const output = await PDFDocument.load(outputBytes); expect(output.getPageCount()).toBe(3); expect(getPageContentStreamCount(output, 0)).toBe(originalStreamCounts[0]); expect(getPageContentStreamCount(output, 1)).toBeGreaterThan(originalStreamCounts[1]); expect(getPageContentStreamCount(output, 2)).toBe(originalStreamCounts[2]); expect(pageErrors).toEqual([]); }); test('stamp uses an image instead of text and makes the border optional', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/stamp'); await dropFiles(page, pdfDropzone(page), [ { name: 'bildakte.pdf', mimeType: 'application/pdf', buffer: await createPdf(2, 'Bild') } ]); await page.locator('#stamp-image').setInputFiles({ name: 'stempel.png', mimeType: 'image/png', buffer: Buffer.from(await createStampPng(page), 'base64') }); await expect(page.getByText('stempel.png')).toBeVisible(); await expect(page.getByTestId('stamp-preview').locator('img')).toBeVisible(); await expect(page.locator('#stamp-text')).toHaveCount(0); await expect(page.getByRole('checkbox', { name: 'Rahmen um das Bild anzeigen' })).toBeVisible(); await expect( page.getByRole('checkbox', { name: 'Rahmen um das Bild anzeigen' }) ).not.toBeChecked(); await page.getByRole('button', { name: 'Stempel anwenden', exact: true }).click(); await expect(page.getByText('Fertig ✓')).toBeVisible({ timeout: 20_000 }); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'bildakte.pdf herunterladen' }).click(); const outputBytes = await expectPdfDownload(await downloadPromise, /^bildakte_stempel\.pdf$/); const output = await PDFDocument.load(outputBytes); expect(output.getPageCount()).toBe(2); // Image without frame: the image XObject is drawn, no stroked rectangle is present. for (const pageIndex of [0, 1]) { const content = getPageContentText(output, pageIndex); expect(content).toContain(' Do\n'); expect(content).not.toContain(' RG'); } await page.getByRole('checkbox', { name: 'Rahmen um das Bild anzeigen' }).check(); await expect(page.getByTestId('stamp-preview')).toHaveCSS('border-style', 'solid'); // Re-add the file so the changed option applies to a fresh pending row. await page.getByRole('button', { name: 'bildakte.pdf entfernen' }).click(); await dropFiles(page, pdfDropzone(page), [ { name: 'bildakte.pdf', mimeType: 'application/pdf', buffer: await createPdf(2, 'Bild') } ]); await expect(page.getByTestId('stamp-preview').locator('img')).toBeVisible(); await page.getByRole('button', { name: 'Stempel anwenden', exact: true }).click(); await expect(page.getByText('Fertig ✓')).toBeVisible({ timeout: 20_000 }); const borderedDownloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'bildakte.pdf herunterladen' }).click(); const borderedBytes = await expectPdfDownload( await borderedDownloadPromise, /^bildakte_stempel\.pdf$/ ); const bordered = await PDFDocument.load(borderedBytes); // The optional frame adds a stroked rectangle around the drawn image. for (const pageIndex of [0, 1]) { const content = getPageContentText(bordered, pageIndex); expect(content).toContain(' Do\n'); expect(content).toContain(' RG'); expect(content).toMatch(/\bh\nS\n/); } expect(pageErrors).toEqual([]); }); test('stamp batch applies to every file and bundles results in one ZIP', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/stamp'); 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') }, { name: 'kaputt.pdf', mimeType: 'application/pdf', buffer: Buffer.from('kein gueltiges PDF') }, { name: 'geschuetzt.pdf', mimeType: 'application/pdf', buffer: await createEncryptedPdf(1, 'Geschützt', 'test-passwort') } ]); await expect(page.getByText('Dateien (4)')).toBeVisible(); // With several files the stamp is applied to every page and no preview is loaded. await expect(page.getByRole('checkbox', { name: 'Auf alle Seiten anwenden' })).toHaveCount(0); await expect(page.getByRole('heading', { name: 'Vorschau' })).toHaveCount(0); await page.getByRole('button', { name: 'Stempel anwenden', exact: true }).click(); await expect(page.getByText('Fertig ✓')).toHaveCount(2, { timeout: 30_000 }); await expect(page.getByText('Fehler')).toHaveCount(2); await expect(page.getByText('Dieses PDF ist passwortgeschützt')).toBeVisible(); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Alle herunterladen' }).click(); const download = await downloadPromise; expect(download.suggestedFilename()).toMatch(/^stempel_\d{4}-\d{2}-\d{2}\.zip$/); const zipPath = await download.path(); expect(zipPath).not.toBeNull(); const entries = Object.keys(unzipSync(new Uint8Array(await readFile(zipPath!)))); expect(entries.sort()).toEqual(['eins_stempel.pdf', 'zwei_stempel.pdf']); expect(pageErrors).toEqual([]); }); test('stamp explains how to handle a password-protected PDF', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/stamp'); await dropFiles(page, pdfDropzone(page), [ { name: 'geschuetzt.pdf', mimeType: 'application/pdf', buffer: await createEncryptedPdf(1, 'Geschützt', 'test-passwort') } ]); await expect(page.getByRole('alert')).toContainText('passwortgeschützt'); await expect(page.getByRole('alert')).toContainText('Passwort entfernen'); expect(pageErrors).toEqual([]); });