[WIP] Add pdf2img integration for converting PDF to images in e2e tests (#774)

* Add pdf2img integration for converting PDF to images in e2e tests

* Refactor PDF generation tests to convert PDFs to images and validate against snapshots

* Refactor package.json files to remove "type": "module" and update import statements to CommonJS format

* Fix e2e test

* Add new image snapshots for e2e tests and remove obsolete snapshot
This commit is contained in:
Kyohei Fukuda
2025-03-01 16:24:01 +09:00
committed by GitHub
parent c76f3cae5e
commit afbfa87674
26 changed files with 160 additions and 141 deletions

View File

@@ -1,5 +1,4 @@
{
"type": "module",
"name": "@pdfme/common",
"version": "0.0.0",
"sideEffects": false,

View File

@@ -1,11 +1,6 @@
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const updateVersion = (version) => {
const filePath = path.join(__dirname, 'src/version.ts');

View File

@@ -1,5 +1,4 @@
{
"type": "module",
"name": "@pdfme/converter",
"version": "0.0.0",
"sideEffects": false,

View File

@@ -13,20 +13,22 @@ function dataURLToArrayBuffer(dataURL: string): ArrayBuffer {
// Decode the Base64 string to get the binary data
const byteString = atob(base64String);
// Create a typed array from the binary string
const arrayBuffer = new ArrayBuffer(byteString.length);
const uintArray = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uintArray[i] = byteString.charCodeAt(i);
}
return arrayBuffer;
}
export const pdf2img = async (pdf: ArrayBuffer, options: Pdf2ImgOptions = {}) =>
export const pdf2img = async (
pdf: ArrayBuffer,
options: Pdf2ImgOptions = {}
): Promise<ArrayBuffer[]> =>
_pdf2img(pdf, options, {
getDocument: (pdf) => pdfjsLib.getDocument(pdf).promise,
createCanvas: (width, height) => {
@@ -45,4 +47,4 @@ export const pdf2size = async (pdf: ArrayBuffer, options: Pdf2SizeOptions = {})
getDocument: (pdf) => pdfjsLib.getDocument(pdf).promise,
});
export { img2pdf } from './img2pdf.js';
export { img2pdf } from './img2pdf.js';

View File

@@ -1,15 +1,13 @@
import { createCanvas } from 'canvas';
import { pdf2img as _pdf2img, Pdf2ImgOptions } from './pdf2img.js';
import { pdf2size as _pdf2size, Pdf2SizeOptions } from './pdf2size.js';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
// @ts-expect-error
import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.entry.js';
// @ts-ignore
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf';
if (typeof window !== 'undefined' && pdfjsLib.GlobalWorkerOptions) {
pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJSWorker;
}
export const pdf2img = async (pdf: ArrayBuffer, options: Pdf2ImgOptions = {}) =>
export const pdf2img = async (
pdf: ArrayBuffer,
options: Pdf2ImgOptions = {}
): Promise<ArrayBuffer[]> =>
_pdf2img(pdf, options, {
getDocument: (pdf) => pdfjsLib.getDocument(pdf).promise,
createCanvas: (width, height) => createCanvas(width, height) as unknown as HTMLCanvasElement,

View File

@@ -1,5 +1,4 @@
{
"type": "module",
"name": "@pdfme/generator",
"version": "0.0.0",
"sideEffects": false,

View File

@@ -1,5 +1,4 @@
{
"type": "module",
"name": "@pdfme/manipulator",
"version": "0.0.0",
"sideEffects": false,

View File

@@ -1,5 +1,4 @@
{
"type": "module",
"name": "@pdfme/schemas",
"version": "0.0.0",
"sideEffects": false,

View File

@@ -1,5 +1,4 @@
{
"type": "module",
"name": "@pdfme/ui",
"version": "0.0.0",
"sideEffects": false,

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 207 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -1,5 +1,5 @@
import fs from 'fs';
import puppeteer, { Browser, Page } from 'puppeteer';
import { pdf2img } from '@pdfme/converter';
import { createRunner, parse, PuppeteerRunnerExtension } from '@puppeteer/replay';
import { execSync, ChildProcessWithoutNullStreams } from 'child_process';
import { spawn } from 'child_process';
@@ -8,10 +8,11 @@ import templateCreationRecord from './templateCreationRecord.json';
import formInputRecord from './formInputRecord.json';
const baseUrl = 'http://localhost:4173';
const timeout = 20000;
jest.setTimeout(timeout * 5);
const isRunningLocal = process.env.LOCAL === 'true';
const snapShotOpt: MatchImageSnapshotOptions = {
failureThreshold: 1,
failureThresholdType: 'percent',
@@ -21,37 +22,66 @@ const snapShotOpt: MatchImageSnapshotOptions = {
const viewport = { width: 1366, height: 768 };
const generatePdfAndTakeScreenshot = async (arg: { page: Page; browser: Browser }) => {
const { page, browser } = arg;
async function generatePdf(page: Page, browser: Browser): Promise<Buffer> {
await page.waitForSelector('#generate-pdf', { timeout });
await page.click('#generate-pdf');
const newTarget = await browser.waitForTarget((target) => target.url().startsWith('blob:'), {
timeout,
});
const newPage = await newTarget.page();
const newTarget = await browser.waitForTarget(
(target) => target.url().startsWith('blob:'),
{ timeout }
);
const newPage = await newTarget?.page();
if (!newPage) {
throw new Error('[generatePdfAndTakeScreenshot]: New page not found');
throw new Error('[generatePdf]: New page not found');
}
await newPage.setViewport(viewport);
await newPage.bringToFront();
await newPage.goto(newPage.url(), { waitUntil: 'networkidle2', timeout });
const screenshot = await newPage.screenshot({ encoding: 'base64' });
const pdfArray = await newPage.evaluate(async () => {
const response = await fetch(location.href);
const buffer = await response.arrayBuffer();
return Array.from(new Uint8Array(buffer));
});
// TODO ここから
// pdfダウンロードしてスナップショットテストしたい
const pdfBuffer = Buffer.from(pdfArray);
await newPage.close();
await page.bringToFront();
return pdfBuffer;
}
return screenshot;
};
async function pdfToImages(pdf: Buffer): Promise<Buffer[]> {
const arrayBuffer = pdf.buffer.slice(pdf.byteOffset, pdf.byteOffset + pdf.byteLength);
const arrayBuffers = await pdf2img(
{ data: arrayBuffer } as unknown as ArrayBuffer,
{ imageType: 'png' }
);
return arrayBuffers.map((buf) => Buffer.from(new Uint8Array(buf)));
}
async function captureAndCompareScreenshot(page: Page, label?: string) {
const screenshot = await page.screenshot({ encoding: 'base64' });
expect(screenshot).toMatchImageSnapshot({
...snapShotOpt,
customSnapshotIdentifier: label ? `${label}` : undefined,
});
}
async function generateAndComparePDF(page: Page, browser: Browser, labelPrefix: string) {
const pdfBuffer = await generatePdf(page, browser);
const pdfImages = await pdfToImages(pdfBuffer);
pdfImages.forEach((imageBuffer, idx) => {
expect(imageBuffer).toMatchImageSnapshot({
...snapShotOpt,
customSnapshotIdentifier: `${labelPrefix}-pdf-page-${idx}`,
});
});
}
describe('Playground E2E Tests', () => {
const isRunningLocal = process.env.LOCAL === 'true';
let browser: Browser | undefined;
let page: Page | undefined;
let previewProcess: ChildProcessWithoutNullStreams | undefined;
@@ -78,6 +108,7 @@ describe('Playground E2E Tests', () => {
await page.setRequestInterception(true);
await page.setViewport(viewport);
page.setDefaultNavigationTimeout(timeout);
page.on('request', (req) => {
const ignoreDomains = ['https://media.ethicalads.io/'];
if (ignoreDomains.some((d) => req.url().startsWith(d))) {
@@ -97,101 +128,84 @@ describe('Playground E2E Tests', () => {
}
});
test('E2E suite', async () => {
if (!browser) throw new Error('Browser not initialized');
if (!page) throw new Error('Page not initialized');
it('should select Invoice template and compare PDF snapshot', async () => {
if (!browser || !page) throw new Error('Browser/Page not initialized');
// 1. Navigate to templates list & click on Invoice template
await page.goto(`${baseUrl}/templates`);
await page.waitForSelector('#template-img-invoice', { timeout });
await page.click('#template-img-invoice');
// 2. Check that "INVOICE" text is present
await page.waitForFunction(() => {
const container = document.querySelector('div.flex-1.w-full');
return container ? container.textContent?.includes('INVOICE') : false;
}, { timeout });
// 3. Screenshot & compare
await captureAndCompareScreenshot(page, 'invoice-designer');
// 4. Generate PDF & compare
await generateAndComparePDF(page, browser, 'invoice');
});
it('should select Pedigree template and compare PDF snapshot', async () => {
if (!browser || !page) throw new Error('Browser/Page not initialized');
// 5. Return to template list screen
await page.click('#templates-nav');
await page.reload();
// 6. Select Pedigree template
await page.waitForSelector('#template-img-pedigree', { timeout });
await page.click('#template-img-pedigree');
await page.waitForFunction(() => {
const container = document.querySelector('div.flex-1.w-full');
return container ? container.textContent?.includes('Pet Name') : false;
}, { timeout });
// 7. Screenshot & compare
await captureAndCompareScreenshot(page, 'pedigree-designer');
// 8. Generate PDF & compare
await generateAndComparePDF(page, browser, 'pedigree');
});
it('should modify template, generate PDF and compare, then input form data', async () => {
if (!browser || !page) throw new Error('Browser/Page not initialized');
const extension = new PuppeteerRunnerExtension(browser, page, { timeout });
try {
console.log('1. Navigate to template list screen');
await page.goto(`${baseUrl}/templates`);
// 9. Press Reset button
await page.$eval('#reset-template', (el: Element) => (el as HTMLElement).click());
console.log('2. Click on Invoice template');
await page.waitForSelector('#template-img-invoice', { timeout });
await page.click('#template-img-invoice');
// 10. Replay templateCreationRecord operations to add elements
const templateCreationUserFlow = parse(templateCreationRecord);
const templateCreationRunner = await createRunner(templateCreationUserFlow, extension);
await templateCreationRunner.run();
await page.waitForFunction(
() => {
const container = document.querySelector('div.flex-1.w-full');
return container ? container.textContent?.includes('INVOICE') : false;
},
{ timeout }
);
// 11. Screenshot & compare
await captureAndCompareScreenshot(page, 'modified-template-designer');
console.log('3. Take screenshot in designer');
let screenshot = await page.screenshot({ encoding: 'base64' });
expect(screenshot).toMatchImageSnapshot(snapShotOpt);
// 12. Generate PDF & compare
await generateAndComparePDF(page, browser, 'modified-template');
console.log('4. Generate PDF and capture screenshot');
screenshot = await generatePdfAndTakeScreenshot({ page, browser });
expect(screenshot).toMatchImageSnapshot(snapShotOpt);
// 13. Save locally
await page.click('#save-local');
console.log('5. Return to template list screen');
await page.click('#templates-nav');
await page.reload();
// 14. Move to form viewer
await page.click('#form-viewer-nav');
await page.waitForFunction(() => {
const container = document.querySelector('div.flex-1.w-full');
return container ? container.textContent?.includes('Type Something...') : false;
}, { timeout });
console.log('6. Click on Pedigree template');
await page.waitForSelector('#template-img-pedigree', { timeout });
await page.click('#template-img-pedigree');
await page.waitForFunction(
() => {
const container = document.querySelector('div.flex-1.w-full');
return container ? container.textContent?.includes('Pet Name') : false;
},
{ timeout }
);
// 15. Input form data
const formInputUserFlow = parse(formInputRecord);
const formInputRunner = await createRunner(formInputUserFlow, extension);
await formInputRunner.run();
console.log('7. Take screenshot in designer');
screenshot = await page.screenshot({ encoding: 'base64' });
expect(screenshot).toMatchImageSnapshot(snapShotOpt);
console.log('8. Generate PDF and capture screenshot');
screenshot = await generatePdfAndTakeScreenshot({ page, browser });
expect(screenshot).toMatchImageSnapshot(snapShotOpt);
console.log('9. Press Reset button to reset template');
await page.$eval('#reset-template', (el: Element) => (el as HTMLElement).click());
console.log('10. Replay templateCreationRecord operations to add elements');
const templateCreationUserFlow = parse(templateCreationRecord);
const templateCreationRunner = await createRunner(templateCreationUserFlow, extension);
await templateCreationRunner.run();
console.log('11. Take another screenshot in designer');
screenshot = await page.screenshot({ encoding: 'base64' });
expect(screenshot).toMatchImageSnapshot(snapShotOpt);
console.log('12. Generate PDF, take screenshot, and compare with snapshot');
screenshot = await generatePdfAndTakeScreenshot({ page, browser });
expect(screenshot).toMatchImageSnapshot(snapShotOpt);
console.log('13. Save locally using Save Local button');
await page.click('#save-local');
console.log('14. Click on form-viewer-nav to navigate to form viewer');
await page.click('#form-viewer-nav');
await page.waitForFunction(
() => {
const container = document.querySelector('div.flex-1.w-full');
return container ? container.textContent?.includes('Type Something...') : false;
},
{ timeout }
);
console.log('15. Input form data following formInputRecord steps');
const formInputUserFlow = parse(formInputRecord);
const formInputRunner = await createRunner(formInputUserFlow, extension);
await formInputRunner.run();
console.log('16. Generate PDF, take screenshot, and compare with snapshot');
screenshot = await generatePdfAndTakeScreenshot({ page, browser });
expect(screenshot).toMatchImageSnapshot(snapShotOpt);
} catch (e) {
console.error(e);
const screenshot = await page.screenshot({ encoding: 'base64' });
fs.writeFileSync('e2e-error-screenshot.png', screenshot, 'base64');
throw e;
}
// 16. Generate PDF & compare
await generateAndComparePDF(page, browser, 'final-form');
});
});

View File

@@ -1,6 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/e2e/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};

View File

@@ -54,5 +54,26 @@
"ts-node": "^10.9.1",
"typescript": "^5.0.2",
"vite": "^4.4.5"
},
"jest": {
"resolver": "ts-jest-resolver",
"moduleFileExtensions": [
"js",
"ts"
],
"transform": {
"^.+\\.ts?$": [
"ts-jest",
{
"tsconfig": "tsconfig.json"
}
]
},
"testMatch": [
"**/*.test.ts"
],
"setupFilesAfterEnv": [
"<rootDir>/jest.setup.js"
]
}
}
}

View File

@@ -2,9 +2,9 @@ import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import pLimit from 'p-limit';
import { generate } from '../../packages/generator/dist/esm/src/index.js';
import { pdf2img } from '../../packages/converter/dist/esm/src/index.node.js';
import { getInputFromTemplate, getDefaultFont } from '../../packages/common/dist/esm/src/index.js';
import { generate } from '@pdfme/generator/cjs/src/index.js';
import { pdf2img } from '@pdfme/converter/cjs/src/index.node.js';
import { getInputFromTemplate, getDefaultFont } from '@pdfme/common/cjs/src/index.js';
import {
multiVariableText,
text,
@@ -21,7 +21,7 @@ import {
select,
checkbox,
radioGroup,
} from '../../packages/schemas/dist/esm/src/index.js';
} from '@pdfme/schemas/cjs/src/index.js';
const __dirname = path.dirname(new URL(import.meta.url).pathname);
@@ -97,6 +97,7 @@ async function createThumbnailFromTemplate(templatePath, thumbnailPath) {
fs.writeFileSync(thumbnailPath, Buffer.from(thumbnail));
} catch (err) {
console.error(`Failed to create thumbnail from ${templatePath}:`, err);
throw err;
}
}