mirror of
https://github.com/suitenumerique/docs.git
synced 2026-04-25 17:15:01 +02:00
✅(frontend) add test to compare generated PDF against reference template
ensures no regression by validating PDF content matches the expected pattern Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
@@ -32,6 +32,7 @@ and this project adheres to
|
||||
|
||||
- ✨(export) enable ODT export for documents #1524
|
||||
- ✨(frontend) improve mobile UX by showing subdocs count #1540
|
||||
- ✅(e2e) add test to compare generated PDF against reference template #1648
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { expect, test } from '@playwright/test';
|
||||
@@ -13,6 +14,35 @@ import {
|
||||
import { openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||
import { createRootSubPage } from './utils-sub-pages';
|
||||
|
||||
const REGRESSION_FIXTURE_CONTENT = fs.readFileSync(
|
||||
path.join(__dirname, 'assets/content.txt'),
|
||||
'utf-8',
|
||||
);
|
||||
const REGRESSION_SNAPSHOT_NAME = 'doc-export-regression.pdf';
|
||||
const REGRESSION_DOC_TITLE = 'doc-export-regression-reference';
|
||||
|
||||
/**
|
||||
* Playwright snapshots store the raw PDF bytes. However, each export embeds
|
||||
* dynamic metadata (timestamps, font-subset identifiers, etc.) that would make
|
||||
* the snapshot differ at every run. To ensure deterministic comparisons we
|
||||
* strip/neutralize those fields before matching against the reference PDF.
|
||||
*/
|
||||
const sanitizePdfBuffer = (buffer: Buffer) => {
|
||||
const pdfText = buffer.toString('latin1');
|
||||
const neutralized = pdfText
|
||||
// Remove per-export timestamps
|
||||
.replace(/\/CreationDate\s*\(.*?\)/g, '/CreationDate ()')
|
||||
.replace(/\/ModDate\s*\(.*?\)/g, '/ModDate ()')
|
||||
// Remove file identifiers
|
||||
.replace(/\/ID\s*\[<[^>]+>\s*<[^>]+>\]/g, '/ID [<0><0>]')
|
||||
.replace(/D:\d{14}Z/g, 'D:00000000000000Z')
|
||||
// Remove subset font prefixes generated by PDF renderer
|
||||
.replace(/\b[A-Z]{6}\+(Inter18pt-[A-Za-z]+)\b/g, 'STATIC+$1')
|
||||
.replace(/\b[A-Z]{6}\+(GeistMono-[A-Za-z]+)\b/g, 'STATIC+$1');
|
||||
|
||||
return Buffer.from(neutralized, 'latin1');
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
@@ -551,4 +581,119 @@ test.describe('Doc Export', () => {
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${docChild}.odt`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Regression guard for the full PDF export pipeline.
|
||||
*
|
||||
* Usage reminder:
|
||||
* 1. `npx playwright test __tests__/app-impress/doc-export.spec.ts --update-snapshots -g "full document" --project=chromium`
|
||||
* -> refresh the reference PDF whenever we intentionally change the export output.
|
||||
* 2. `npx playwright test __tests__/app-impress/doc-export.spec.ts -g "full document" --project=chromium`
|
||||
* -> CI (and local runs without --update-snapshots) will compare the PDF to the reference
|
||||
* and fail on any byte-level difference once the dynamic metadata has been sanitized.
|
||||
*/
|
||||
test('it keeps the full document PDF export identical to the reference snapshot', async ({
|
||||
page,
|
||||
browserName,
|
||||
}, testInfo) => {
|
||||
const snapshotPath = testInfo.snapshotPath(REGRESSION_SNAPSHOT_NAME);
|
||||
|
||||
test.skip(
|
||||
!fs.existsSync(snapshotPath) &&
|
||||
testInfo.config.updateSnapshots === 'none',
|
||||
`Missing PDF snapshot at ${snapshotPath}. Run Playwright with --update-snapshots to record it.`,
|
||||
);
|
||||
|
||||
// We must use a deterministic title so that block content (and thus the
|
||||
// exported PDF) stays identical between runs.
|
||||
await createDoc(page, 'doc-export-regression', browserName, 1);
|
||||
const titleInput = page.getByRole('textbox', { name: 'Document title' });
|
||||
await expect(titleInput).toBeVisible();
|
||||
await titleInput.fill(REGRESSION_DOC_TITLE);
|
||||
await titleInput.blur();
|
||||
await verifyDocName(page, REGRESSION_DOC_TITLE);
|
||||
const regressionDoc = REGRESSION_DOC_TITLE;
|
||||
|
||||
const docId = page
|
||||
.url()
|
||||
.split('/docs/')[1]
|
||||
?.split('/')
|
||||
.filter(Boolean)
|
||||
.shift();
|
||||
|
||||
expect(docId).toBeTruthy();
|
||||
|
||||
// Inject the pre-crafted blocknote document via the REST API to avoid
|
||||
// rebuilding it through the UI (which would be slow and flaky).
|
||||
const cookies = await page.context().cookies();
|
||||
const csrfToken = cookies.find(
|
||||
(cookie) => cookie.name === 'csrftoken',
|
||||
)?.value;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
|
||||
if (csrfToken) {
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
}
|
||||
|
||||
const updateResponse = await page.request.patch(
|
||||
`http://localhost:8071/api/v1.0/documents/${docId}/`,
|
||||
{
|
||||
headers,
|
||||
data: {
|
||||
content: REGRESSION_FIXTURE_CONTENT,
|
||||
websocket: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!updateResponse.ok()) {
|
||||
throw new Error(
|
||||
`Failed to seed document content. Status: ${updateResponse.status()}, body: ${await updateResponse.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
await page.reload();
|
||||
|
||||
const headingLocator = page
|
||||
.locator('.--docs--editor-container')
|
||||
.getByText('Titre h1 repliable', { exact: true })
|
||||
.first();
|
||||
|
||||
await headingLocator.scrollIntoViewIfNeeded();
|
||||
await expect(headingLocator).toBeVisible({ timeout: 15000 });
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Export the document',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByTestId('doc-open-modal-download-button'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId('doc-export-download-button')).toBeEnabled({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// Export to PDF and confirm the generated bytes match the reference file.
|
||||
const downloadPromise = page.waitForEvent('download', {
|
||||
timeout: 60000,
|
||||
predicate: (download) =>
|
||||
download.suggestedFilename().includes(`${regressionDoc}.pdf`),
|
||||
});
|
||||
|
||||
void page.getByTestId('doc-export-download-button').click();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe(`${regressionDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const normalizedPdfBuffer = sanitizePdfBuffer(pdfBuffer);
|
||||
|
||||
expect(normalizedPdfBuffer).toMatchSnapshot(REGRESSION_SNAPSHOT_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user