diff --git a/src/__tests__/pdf-options-builder.test.ts b/src/__tests__/pdf-options-builder.test.ts new file mode 100644 index 0000000..30641aa --- /dev/null +++ b/src/__tests__/pdf-options-builder.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from "vitest"; +import { buildPdfOptions, PdfRenderOptions } from "../services/browser.js"; + +describe("buildPdfOptions", () => { + it("returns sensible defaults when no options given", () => { + const result = buildPdfOptions({}); + expect(result).toEqual({ + format: "A4", + landscape: false, + printBackground: true, + margin: { top: "0", right: "0", bottom: "0", left: "0" }, + }); + }); + + it("passes through all provided options", () => { + const opts: PdfRenderOptions = { + format: "Letter", + landscape: true, + printBackground: false, + margin: { top: "10mm", bottom: "10mm" }, + scale: 1.5, + pageRanges: "1-3", + preferCSSPageSize: true, + width: "210mm", + height: "297mm", + headerTemplate: "Header", + footerTemplate: "Footer", + displayHeaderFooter: true, + }; + const result = buildPdfOptions(opts); + expect(result.format).toBe("Letter"); + expect(result.landscape).toBe(true); + expect(result.printBackground).toBe(false); + expect(result.margin).toEqual({ top: "10mm", bottom: "10mm" }); + expect(result.scale).toBe(1.5); + expect(result.pageRanges).toBe("1-3"); + expect(result.preferCSSPageSize).toBe(true); + expect(result.width).toBe("210mm"); + expect(result.height).toBe("297mm"); + expect(result.headerTemplate).toBe("Header"); + expect(result.footerTemplate).toBe("Footer"); + expect(result.displayHeaderFooter).toBe(true); + }); + + it("omits undefined optional fields from output", () => { + const result = buildPdfOptions({ format: "A4" }); + expect(result).not.toHaveProperty("scale"); + expect(result).not.toHaveProperty("pageRanges"); + expect(result).not.toHaveProperty("preferCSSPageSize"); + expect(result).not.toHaveProperty("width"); + expect(result).not.toHaveProperty("height"); + expect(result).not.toHaveProperty("headerTemplate"); + expect(result).not.toHaveProperty("footerTemplate"); + }); + + it("handles printBackground false explicitly", () => { + const result = buildPdfOptions({ printBackground: false }); + expect(result.printBackground).toBe(false); + }); + + it("defaults displayHeaderFooter to false only when explicitly set", () => { + const r1 = buildPdfOptions({}); + expect(r1).not.toHaveProperty("displayHeaderFooter"); + + const r2 = buildPdfOptions({ displayHeaderFooter: false }); + expect(r2.displayHeaderFooter).toBe(false); + }); +}); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index 543b715..8bfbc29 100644 --- a/src/__tests__/setup.ts +++ b/src/__tests__/setup.ts @@ -53,7 +53,10 @@ vi.mock("../services/keys.js", () => { }); // Mock browser service -vi.mock("../services/browser.js", () => ({ +vi.mock("../services/browser.js", async (importOriginal) => { + const actual = await importOriginal>(); + return { + buildPdfOptions: actual.buildPdfOptions, initBrowser: vi.fn().mockResolvedValue(undefined), closeBrowser: vi.fn().mockResolvedValue(undefined), renderPdf: vi.fn().mockResolvedValue({ pdf: Buffer.from("%PDF-1.4 mock pdf content here"), durationMs: 10 }), @@ -68,7 +71,8 @@ vi.mock("../services/browser.js", () => ({ uptimeMs: 10000, browsers: [], }), -})); +}; +}); // Mock verification service vi.mock("../services/verification.js", () => ({ diff --git a/src/services/browser.ts b/src/services/browser.ts index 0263cfb..41d4622 100644 --- a/src/services/browser.ts +++ b/src/services/browser.ts @@ -221,7 +221,7 @@ export async function closeBrowser(): Promise { instances.length = 0; } -import type { PaperFormat, PuppeteerLifeCycleEvent } from "puppeteer"; +import type { PaperFormat, PDFOptions, PuppeteerLifeCycleEvent } from "puppeteer"; export interface PdfRenderOptions { format?: PaperFormat; @@ -238,6 +238,27 @@ export interface PdfRenderOptions { height?: string; } +/** Build a Puppeteer-compatible PDFOptions object from user-supplied render options. */ +export function buildPdfOptions(options: PdfRenderOptions): Record { + const result: Record = { + format: options.format || "A4", + landscape: options.landscape || false, + printBackground: options.printBackground !== false, + margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" }, + }; + + if (options.headerTemplate !== undefined) result.headerTemplate = options.headerTemplate; + if (options.footerTemplate !== undefined) result.footerTemplate = options.footerTemplate; + if (options.displayHeaderFooter !== undefined) result.displayHeaderFooter = options.displayHeaderFooter; + if (options.scale !== undefined) result.scale = options.scale; + if (options.pageRanges) result.pageRanges = options.pageRanges; + if (options.preferCSSPageSize !== undefined) result.preferCSSPageSize = options.preferCSSPageSize; + if (options.width) result.width = options.width; + if (options.height) result.height = options.height; + + return result; +} + export async function renderPdf( html: string, options: PdfRenderOptions = {} @@ -251,20 +272,7 @@ export async function renderPdf( (async () => { await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 15_000 }); await page.addStyleTag({ content: "* { margin: 0; padding: 0; } body { margin: 0; }" }); - const pdf = await page.pdf({ - format: options.format || "A4", - landscape: options.landscape || false, - printBackground: options.printBackground !== false, - margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" }, - headerTemplate: options.headerTemplate, - footerTemplate: options.footerTemplate, - displayHeaderFooter: options.displayHeaderFooter || false, - ...(options.scale !== undefined && { scale: options.scale }), - ...(options.pageRanges && { pageRanges: options.pageRanges }), - ...(options.preferCSSPageSize !== undefined && { preferCSSPageSize: options.preferCSSPageSize }), - ...(options.width && { width: options.width }), - ...(options.height && { height: options.height }), - }); + const pdf = await page.pdf(buildPdfOptions(options) as Parameters[0]); return Buffer.from(pdf); })(), new Promise((_, reject) => { @@ -330,20 +338,7 @@ export async function renderUrlPdf( waitUntil: options.waitUntil || "domcontentloaded", timeout: 30_000, }); - const pdf = await page.pdf({ - format: options.format || "A4", - landscape: options.landscape || false, - printBackground: options.printBackground !== false, - margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" }, - ...(options.headerTemplate && { headerTemplate: options.headerTemplate }), - ...(options.footerTemplate && { footerTemplate: options.footerTemplate }), - ...(options.displayHeaderFooter !== undefined && { displayHeaderFooter: options.displayHeaderFooter }), - ...(options.scale !== undefined && { scale: options.scale }), - ...(options.pageRanges && { pageRanges: options.pageRanges }), - ...(options.preferCSSPageSize !== undefined && { preferCSSPageSize: options.preferCSSPageSize }), - ...(options.width && { width: options.width }), - ...(options.height && { height: options.height }), - }); + const pdf = await page.pdf(buildPdfOptions(options) as Parameters[0]); return Buffer.from(pdf); })(), new Promise((_, reject) => {