import { expect, test, type Page } from '@playwright/test'; import { PDFDocument } from 'pdf-lib'; import { PAGE_NUMBER_MARGIN } from '../../src/lib/pdf-pagination'; import { createEncryptedPdf, createPdf, createPreRotatedPdf, dropFiles, expectPdfDownload, getPageContentText, getPageText, getPageTextAngle, getPageTextMatrices, getPageTextValues, pdfDropzone, trackPageErrors } from './helpers'; /** * Number of text operands equal to `label` on a page. The source documents carry * their own text, so this counts the stamped sheet label instead of comparing * the whole page. */ const countLabel = (document: PDFDocument, pageIndex: number, label: string) => getPageTextValues(document, pageIndex).filter((value) => value === label).length; const downloadPaginated = async (page: Page, fileName: RegExp) => { const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Paginieren & herunterladen' }).click(); return expectPdfDownload(await downloadPromise, fileName); }; test('pagination stamps every page with an Aktenzeichen and the configured start number', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/pagination'); await dropFiles(page, pdfDropzone(page), [ { name: 'akte.pdf', mimeType: 'application/pdf', buffer: await createPdf(3, 'Akte') } ]); await expect(page.getByText('Seite 1 von 3')).toBeVisible(); await page.locator('#pagination-start').fill('26'); await page.locator('#pagination-text').fill('123 Ja 60/26'); await expect(page.getByTestId('pagination-example')).toContainText( 'Beispiel: 123 Ja 60/26, Bl. 26' ); await page.getByRole('button', { name: 'Nächste Seite' }).click(); await expect(page.getByText('Seite 2 von 3')).toBeVisible(); await expect(page.getByTestId('pagination-preview')).toContainText('123 Ja 60/26, Bl. 27'); const bytes = await downloadPaginated(page, /^akte_paginiert\.pdf$/); const output = await PDFDocument.load(bytes); expect(output.getPageCount()).toBe(3); // Exactly one label per page: a duplicated draw would still pass `toContain`. expect(countLabel(output, 0, '123 Ja 60/26, Bl. 26')).toBe(1); expect(countLabel(output, 1, '123 Ja 60/26, Bl. 27')).toBe(1); expect(countLabel(output, 2, '123 Ja 60/26, Bl. 28')).toBe(1); expect(pageErrors).toEqual([]); }); test('pagination can keep the additional text on the first page only', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/pagination'); await dropFiles(page, pdfDropzone(page), [ { name: 'akte.pdf', mimeType: 'application/pdf', buffer: await createPdf(3, 'Akte') } ]); await page.locator('#pagination-start').fill('7'); await page.locator('#pagination-text').fill('AZ 7/26'); await page.locator('#pagination-format').selectOption('s'); await page.getByLabel('Nur auf der ersten Seite').check(); await expect(page.getByTestId('pagination-example')).toContainText('AZ 7/26, S. 7'); await page.getByRole('button', { name: 'Nächste Seite' }).click(); await expect(page.getByTestId('pagination-preview')).toHaveText('S. 8'); const bytes = await downloadPaginated(page, /^akte_paginiert\.pdf$/); const output = await PDFDocument.load(bytes); expect(output.getPageCount()).toBe(3); expect(getPageText(output, 0)).toContain('AZ 7/26, S. 7'); expect(getPageText(output, 1)).toContain('S. 8'); expect(getPageText(output, 1)).not.toContain('AZ 7/26'); expect(getPageText(output, 2)).toContain('S. 9'); expect(pageErrors).toEqual([]); }); test('pagination supports number-only labels and jumps to a page', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/pagination'); await dropFiles(page, pdfDropzone(page), [ { name: 'nummern.pdf', mimeType: 'application/pdf', buffer: await createPdf(11, 'Nummern') } ]); await page.locator('#pagination-start').fill('42'); await page.locator('#pagination-format').selectOption('nummer'); await expect(page.getByTestId('pagination-example')).toHaveText('Beispiel: 42'); await page.getByLabel('Springe zu Seite').fill('11'); await expect(page.getByText('Seite 11 von 11')).toBeVisible(); await expect(page.getByTestId('pagination-preview')).toHaveText('52'); const bytes = await downloadPaginated(page, /^nummern_paginiert\.pdf$/); const output = await PDFDocument.load(bytes); expect(output.getPageCount()).toBe(11); expect(getPageText(output, 0)).toContain('42'); expect(getPageText(output, 10)).toContain('52'); expect(pageErrors).toEqual([]); }); test('pagination keeps the page number upright on rotated scans', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/pagination'); await dropFiles(page, pdfDropzone(page), [ { name: 'gedreht.pdf', mimeType: 'application/pdf', buffer: await createPreRotatedPdf(2, 'Gedreht') } ]); const bytes = await downloadPaginated(page, /^gedreht_paginiert\.pdf$/); const output = await PDFDocument.load(bytes); expect(output.getPageCount()).toBe(2); // `createPreRotatedPdf` stores 420×594 pages and puts /Rotate 90 on page 1, so // the page is displayed as 594×420. The label is counter-rotated so it reads // horizontally on screen instead of sideways. // The stamp is the last text drawn on the page. const rotated = getPageTextMatrices(output, 0).at(-1)!; expect(Math.round(getPageTextAngle(rotated))).toBe(90); expect(countLabel(output, 0, 'Bl. 1')).toBe(1); // Visual bottom centre maps back into user space as x = stored width − margin. expect(rotated.x).toBeCloseTo(420 - PAGE_NUMBER_MARGIN, -1); // …and stays horizontally centred on the displayed width. expect(Math.abs(rotated.y - 594 / 2)).toBeLessThan(15); const plain = getPageTextMatrices(output, 1).at(-1)!; expect(Math.round(getPageTextAngle(plain))).toBe(0); expect(countLabel(output, 1, 'Bl. 2')).toBe(1); expect(pageErrors).toEqual([]); }); test('pagination offers the "Seite" format and applies it to every page', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/pagination'); await dropFiles(page, pdfDropzone(page), [ { name: 'formate.pdf', mimeType: 'application/pdf', buffer: await createPdf(2, 'Formate') } ]); await page.locator('#pagination-format').selectOption('seite'); await expect(page.getByTestId('pagination-example')).toHaveText('Beispiel: Seite 1'); const bytes = await downloadPaginated(page, /^formate_paginiert\.pdf$/); const output = await PDFDocument.load(bytes); expect(countLabel(output, 0, 'Seite 1')).toBe(1); expect(countLabel(output, 1, 'Seite 2')).toBe(1); expect(pageErrors).toEqual([]); }); test('pagination explains how to handle a password-protected PDF', async ({ page }) => { const pageErrors = trackPageErrors(page); await page.goto('/tools/pagination'); 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([]); }); test('merge hands the merged PDF over to pagination', 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 page.getByRole('button', { name: 'Zusammenfügen & paginieren' }).click(); await expect(page).toHaveURL(/\/tools\/pagination$/); await expect(page.getByText('Seite 1 von 3')).toBeVisible({ timeout: 15_000 }); const bytes = await downloadPaginated(page, /^zusammengefuegt_paginiert\.pdf$/); const output = await PDFDocument.load(bytes); expect(output.getPageCount()).toBe(3); expect(getPageText(output, 0)).toContain('Bl. 1'); expect(getPageText(output, 1)).toContain('Bl. 2'); expect(getPageText(output, 2)).toContain('Bl. 3'); expect(pageErrors).toEqual([]); });