From 1c44a514b49c3694f5ea9935befd8a6dce13ec85 Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Thu, 27 Aug 2026 12:21:09 +0200 Subject: [PATCH] [verified] fix: use system Chrome for PDF generation --- .../__tests__/pdfGenerationService.test.js | 28 +++++++++++++++++++ backend/src/services/pdfGenerationService.js | 19 +++++++++++++ 2 files changed, 47 insertions(+) diff --git a/backend/src/__tests__/pdfGenerationService.test.js b/backend/src/__tests__/pdfGenerationService.test.js index 9faaf55..392f684 100644 --- a/backend/src/__tests__/pdfGenerationService.test.js +++ b/backend/src/__tests__/pdfGenerationService.test.js @@ -114,6 +114,32 @@ describe('PdfGenerationService P0 contract', () => { expect(html).not.toContain('Ingen materialelinjer registreret'); }); + test('uses a configured Chrome executable for Puppeteer', () => { + const fs = require('fs'); + const previousPath = process.env.PUPPETEER_EXECUTABLE_PATH; + process.env.PUPPETEER_EXECUTABLE_PATH = '/custom/chrome'; + const exists = jest.spyOn(fs, 'existsSync').mockImplementation(filePath => filePath === '/custom/chrome'); + expect(service.resolvePuppeteerExecutablePath()).toBe('/custom/chrome'); + exists.mockRestore(); + if (previousPath === undefined) delete process.env.PUPPETEER_EXECUTABLE_PATH; + else process.env.PUPPETEER_EXECUTABLE_PATH = previousPath; + }); + + test('discovers Chrome from PATH', () => { + const fs = require('fs'); + const previousExecutable = process.env.PUPPETEER_EXECUTABLE_PATH; + const previousPath = process.env.PATH; + delete process.env.PUPPETEER_EXECUTABLE_PATH; + process.env.PATH = '/custom/bin'; + const exists = jest.spyOn(fs, 'existsSync').mockImplementation(filePath => filePath === '/custom/bin/google-chrome'); + expect(service.resolvePuppeteerExecutablePath()).toBe('/custom/bin/google-chrome'); + exists.mockRestore(); + if (previousPath === undefined) delete process.env.PATH; + else process.env.PATH = previousPath; + if (previousExecutable === undefined) delete process.env.PUPPETEER_EXECUTABLE_PATH; + else process.env.PUPPETEER_EXECUTABLE_PATH = previousExecutable; + }); + test('converts Puppeteer Uint8Array output to a raw PDF Buffer', async () => { const pdfBytes = new Uint8Array(Buffer.from('%PDF-1.7\nfixture')); const close = jest.fn(); @@ -124,11 +150,13 @@ describe('PdfGenerationService P0 contract', () => { }), close }); + jest.spyOn(service, 'resolvePuppeteerExecutablePath').mockReturnValue('/resolved/chrome'); const result = await service.generateQuotePdf(buildData()); expect(Buffer.isBuffer(result)).toBe(true); expect(result.subarray(0, 5).toString()).toBe('%PDF-'); + expect(puppeteer.launch).toHaveBeenCalledWith(expect.objectContaining({ executablePath: '/resolved/chrome' })); expect(close).toHaveBeenCalled(); }); diff --git a/backend/src/services/pdfGenerationService.js b/backend/src/services/pdfGenerationService.js index c17977e..887ef55 100644 --- a/backend/src/services/pdfGenerationService.js +++ b/backend/src/services/pdfGenerationService.js @@ -11,6 +11,23 @@ class PdfGenerationService { this.maxPages = 3; } + resolvePuppeteerExecutablePath() { + const browserNames = ['google-chrome-stable', 'google-chrome', 'chromium', 'chromium-browser']; + const pathCandidates = String(process.env.PATH || '') + .split(path.delimiter) + .filter(Boolean) + .flatMap(directory => browserNames.map(browserName => path.join(directory, browserName))); + const candidates = [ + process.env.PUPPETEER_EXECUTABLE_PATH, + ...pathCandidates, + '/usr/bin/google-chrome-stable', + '/usr/bin/google-chrome', + '/usr/bin/chromium', + '/usr/bin/chromium-browser' + ]; + return candidates.find(candidate => candidate && fs.existsSync(candidate)); + } + // Convert image to base64 async imageToBase64(imagePath) { try { @@ -768,8 +785,10 @@ class PdfGenerationService { const html = await this.generatePdfHtml(data); // Launch puppeteer with better error handling + const executablePath = this.resolvePuppeteerExecutablePath(); browser = await puppeteer.launch({ headless: 'new', + ...(executablePath ? { executablePath } : {}), args: [ '--no-sandbox', '--disable-setuid-sandbox',