mirror of
https://github.com/suitenumerique/docs.git
synced 2026-04-26 01:25:05 +02:00
Compare commits
2 Commits
feature/do
...
feat/e2e-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0144044c55 | ||
|
|
a6da37e231 |
@@ -32,6 +32,7 @@ and this project adheres to
|
|||||||
|
|
||||||
- ✨(export) enable ODT export for documents #1524
|
- ✨(export) enable ODT export for documents #1524
|
||||||
- ✨(frontend) improve mobile UX by showing subdocs count #1540
|
- ✨(frontend) improve mobile UX by showing subdocs count #1540
|
||||||
|
- ✅(e2e) add test to compare generated PDF against reference template #1648
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panojpg.jpeg
Normal file
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panojpg.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panopng.png
Normal file
BIN
src/frontend/apps/e2e/__tests__/app-impress/assets/panopng.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 839 KiB |
@@ -1,3 +1,4 @@
|
|||||||
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
@@ -13,6 +14,35 @@ import {
|
|||||||
import { openSuggestionMenu, writeInEditor } from './utils-editor';
|
import { openSuggestionMenu, writeInEditor } from './utils-editor';
|
||||||
import { createRootSubPage } from './utils-sub-pages';
|
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 }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
});
|
});
|
||||||
@@ -551,4 +581,118 @@ test.describe('Doc Export', () => {
|
|||||||
const download = await downloadPromise;
|
const download = await downloadPromise;
|
||||||
expect(download.suggestedFilename()).toBe(`${docChild}.odt`);
|
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) => {
|
||||||
|
// PDF generation for a large, image-heavy document can be slow in CI.
|
||||||
|
// Give this regression test a higher timeout budget than the default.
|
||||||
|
testInfo.setTimeout(120000);
|
||||||
|
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();
|
||||||
|
// After reloading, just ensure the editor container is present before exporting.
|
||||||
|
await expect(page.locator('.--docs--editor-container')).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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
29
src/frontend/apps/e2e/print-datauris.js
Normal file
29
src/frontend/apps/e2e/print-datauris.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//utilitary script to print the datauris of the targeted assets
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ASSETS_ROOT = path.resolve(__dirname, '__tests__/app-impress/assets');
|
||||||
|
|
||||||
|
function saveDataUrl(file, mime, outName) {
|
||||||
|
const abs = path.join(ASSETS_ROOT, file);
|
||||||
|
const base64 = fs.readFileSync(abs).toString('base64');
|
||||||
|
const dataUrl = `data:${mime};base64,${base64}`;
|
||||||
|
const outPath = path.join(ASSETS_ROOT, outName);
|
||||||
|
fs.writeFileSync(outPath, dataUrl, 'utf8');
|
||||||
|
console.log(`Wrote ${outName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PNG
|
||||||
|
saveDataUrl('panopng.png', 'image/png', 'pano-png-dataurl.txt');
|
||||||
|
|
||||||
|
// JPG
|
||||||
|
saveDataUrl('panojpg.jpeg', 'image/jpeg', 'pano-jpg-dataurl.txt');
|
||||||
|
|
||||||
|
// SVG
|
||||||
|
const svgPath = path.join(ASSETS_ROOT, 'test.svg');
|
||||||
|
const svgText = fs.readFileSync(svgPath, 'utf8');
|
||||||
|
const svgDataUrl =
|
||||||
|
'data:image/svg+xml;base64,' + Buffer.from(svgText).toString('base64');
|
||||||
|
fs.writeFileSync(path.join(ASSETS_ROOT, 'test-svg-dataurl.txt'), svgDataUrl, 'utf8');
|
||||||
|
console.log('Wrote test-svg-dataurl.txt');
|
||||||
Reference in New Issue
Block a user