198 lines
8.0 KiB
TypeScript
198 lines
8.0 KiB
TypeScript
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([]);
|
||
});
|