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('', { width: 100, height: 50 }); await generatePDF('', { 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('', { width: 100, height: 50 }); const second = generatePDF('', { 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('', { width: 100, height: 50 }); expect(page.waitForFunction).toHaveBeenCalled(); }); it('measures auto height from the visible page', async () => { await generatePDF('', { 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('', { 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('', { width: 100, height: 50 }); expect(puppeteer.launch).toHaveBeenCalledTimes(1); }); });