All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit introduces the `pdf-lib` library to facilitate PDF document manipulation. A new function, `mergePdfBuffers`, is added to handle the merging of multiple PDF buffers into a single document. Additionally, the PDF generation process is updated to support pagination, allowing for the export of each paginated page before merging. The changes enhance the PDF generation capabilities, ensuring that documents can be created with multiple pages while maintaining proper formatting and structure. Corresponding tests have been added to verify the new functionality.
171 lines
4.9 KiB
JavaScript
171 lines
4.9 KiB
JavaScript
import { jest } from '@jest/globals';
|
|
|
|
const page = {
|
|
setContent: jest.fn().mockResolvedValue(undefined),
|
|
waitForFunction: jest.fn().mockResolvedValue(undefined),
|
|
evaluate: jest.fn(),
|
|
pdf: jest.fn().mockResolvedValue(Buffer.from('pdf-bytes')),
|
|
close: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
|
|
const browserInstance = {
|
|
connected: true,
|
|
newPage: jest.fn().mockResolvedValue(page),
|
|
close: jest.fn().mockResolvedValue(undefined),
|
|
on: jest.fn(),
|
|
};
|
|
|
|
const puppeteer = {
|
|
launch: jest.fn().mockResolvedValue(browserInstance),
|
|
executablePath: jest.fn().mockReturnValue('/fake/puppeteer/chrome'),
|
|
};
|
|
|
|
const copiedPage = {};
|
|
const donorPdf = {
|
|
getPageIndices: jest.fn(() => [0]),
|
|
};
|
|
const mergedPdf = {
|
|
copyPages: jest.fn(async () => [copiedPage]),
|
|
addPage: jest.fn(),
|
|
save: jest.fn(async () => new Uint8Array([9, 8, 7])),
|
|
};
|
|
const PDFDocument = {
|
|
create: jest.fn(async () => mergedPdf),
|
|
load: jest.fn(async () => donorPdf),
|
|
};
|
|
|
|
jest.unstable_mockModule('puppeteer', () => ({
|
|
default: puppeteer,
|
|
}));
|
|
|
|
jest.unstable_mockModule('pdf-lib', () => ({
|
|
PDFDocument,
|
|
}));
|
|
|
|
jest.unstable_mockModule('../../config.js', () => ({
|
|
default: {
|
|
server: {
|
|
logLevel: 'info',
|
|
},
|
|
},
|
|
}));
|
|
|
|
jest.unstable_mockModule('log4js', () => ({
|
|
default: {
|
|
getLogger: () => ({
|
|
level: 'info',
|
|
debug: jest.fn(),
|
|
error: jest.fn(),
|
|
warn: jest.fn(),
|
|
info: jest.fn(),
|
|
}),
|
|
},
|
|
}));
|
|
|
|
const { generatePDF, startPDFBrowser, closePDFBrowser } = await import('../pdffactory.js');
|
|
|
|
function mockEvaluate({ pageCount = 1, height = 120 } = {}) {
|
|
page.evaluate.mockImplementation(async (fn) => {
|
|
const source = String(fn);
|
|
if (source.includes('getPageCount')) {
|
|
return pageCount;
|
|
}
|
|
if (source.includes('showPage')) {
|
|
return undefined;
|
|
}
|
|
if (source.includes('measureVisiblePageHeight') || source.includes('scrollHeight')) {
|
|
return height;
|
|
}
|
|
return pageCount;
|
|
});
|
|
}
|
|
|
|
describe('PDF Factory', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
browserInstance.connected = true;
|
|
browserInstance.newPage.mockResolvedValue(page);
|
|
puppeteer.launch.mockResolvedValue(browserInstance);
|
|
page.pdf.mockResolvedValue(Buffer.from('pdf-bytes'));
|
|
mockEvaluate({ pageCount: 1, height: 120 });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await closePDFBrowser();
|
|
});
|
|
|
|
it('launches puppeteer once and reuses it across renders', async () => {
|
|
await generatePDF('<html></html>', { width: 100, height: 50 });
|
|
await generatePDF('<html></html>', { width: 100, height: 50 });
|
|
|
|
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
|
expect(browserInstance.newPage).toHaveBeenCalledTimes(2);
|
|
expect(page.close).toHaveBeenCalledTimes(2);
|
|
expect(browserInstance.close).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
it('shares a single in-flight launch across concurrent renders', async () => {
|
|
let resolveLaunch;
|
|
puppeteer.launch.mockImplementation(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
resolveLaunch = () => resolve(browserInstance);
|
|
})
|
|
);
|
|
|
|
const first = generatePDF('<html></html>', { width: 100, height: 50 });
|
|
const second = generatePDF('<html></html>', { width: 100, height: 50 });
|
|
|
|
await Promise.resolve();
|
|
resolveLaunch();
|
|
|
|
await Promise.all([first, second]);
|
|
|
|
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('waits for page overflow pagination before measuring', async () => {
|
|
await generatePDF('<html></html>', { width: 100, height: 50 });
|
|
|
|
expect(page.waitForFunction).toHaveBeenCalled();
|
|
});
|
|
|
|
it('measures auto height from the visible page', async () => {
|
|
await generatePDF('<html></html>', { width: 80, height: 'auto' });
|
|
|
|
expect(page.waitForFunction).toHaveBeenCalled();
|
|
expect(page.evaluate).toHaveBeenCalled();
|
|
expect(page.pdf).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
width: '80mm',
|
|
height: '122px',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('exports each paginated page then merges with pdf-lib', async () => {
|
|
mockEvaluate({ pageCount: 2, height: 120 });
|
|
page.pdf
|
|
.mockResolvedValueOnce(Buffer.from('page-one'))
|
|
.mockResolvedValueOnce(Buffer.from('page-two'));
|
|
|
|
const result = await generatePDF('<html></html>', { width: 100, height: 50 });
|
|
|
|
expect(page.pdf).toHaveBeenCalledTimes(2);
|
|
expect(page.evaluate).toHaveBeenCalledWith(expect.any(Function), 0);
|
|
expect(page.evaluate).toHaveBeenCalledWith(expect.any(Function), 1);
|
|
expect(PDFDocument.create).toHaveBeenCalledTimes(1);
|
|
expect(PDFDocument.load).toHaveBeenCalledTimes(2);
|
|
expect(mergedPdf.addPage).toHaveBeenCalledTimes(2);
|
|
expect(result).toEqual(Buffer.from([9, 8, 7]));
|
|
});
|
|
|
|
it('can start the browser before the first render', async () => {
|
|
await startPDFBrowser();
|
|
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
|
|
|
await generatePDF('<html></html>', { width: 100, height: 50 });
|
|
expect(puppeteer.launch).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|