import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; import { describe, expect, it } from "vitest"; import type { CanvasLike } from "../pdf/flatten"; import { runAssembleJob } from "../pdf/render/assembleJob"; import { renderInvoicePdf } from "../pdf/render/invoice"; import { ensureSetup } from "../pdf/templates/harness"; import type { GoldenMap } from "../pdf/testing/goldens"; import goldens from "../pdf/testing/goldens.json"; import { loadCanvas } from "../pdf/testing/rasterize"; import { selfTestFixtures } from "../pdf/testing/selfTestFixtures"; import { reportToText, runSelfTest, type SelfTestDeps, type SelfTestDoc } from "./selfTest"; const canvasLib = await loadCanvas(); if (!canvasLib) console.warn("[selfTest.test] @napi-rs/canvas is not installed: skipping the self-test run under Node"); function nodeDeps(over: Partial = {}): SelfTestDeps { ensureSetup(); return { env: () => ({ userAgent: "node", isSecureContext: true, renderMode: "worker" }), fixtures: selfTestFixtures(), goldens: goldens as GoldenMap, render: async (f) => { const r = await renderInvoicePdf({ model: f.model, prefs: f.prefs }); return { bytes: r.bytes, pages: r.pages, issues: r.issues, fingerprint: r.fingerprint, ms: 1 }; }, openPdf: async (bytes) => { const task = getDocument({ data: new Uint8Array(bytes), useSystemFonts: false, disableFontFace: true }); const doc = await task.promise; return Object.assign(doc, { close: () => task.destroy() }) as unknown as SelfTestDoc; }, createCanvas: (w, h) => canvasLib!.createCanvas(w, h) as unknown as CanvasLike, encodeJpeg: async (c, q) => new Uint8Array(await (c as unknown as { encode(f: "jpeg", q: number): Promise }).encode("jpeg", Math.round(q * 100))), assemble: async (pages, title) => (await runAssembleJob({ id: 1, pages, title })).bytes, startFrameMonitor: () => () => 0, ...over, }; } describe.skipIf(!canvasLib)("runSelfTest under Node", () => { it("passes every check against the committed goldens", async () => { const updates: number[] = []; const report = await runSelfTest(nodeDeps(), (r) => updates.push(r.sections.length)); const failed = report.sections.flatMap((s) => s.checks.filter((c) => c.status === "fail").map((c) => `${s.title}: ${c.label}: ${c.detail}`)); expect(failed).toEqual([]); expect(report.passed && report.done).toBe(true); expect(updates.length).toBeGreaterThan(10); const labels = report.sections.flatMap((s) => s.checks.map((c) => c.label)); for (const l of ["Page count", "Layout audit", "Rasterise page 1", "Text extraction", "Layout fingerprint matches the Node golden", "Longest main-thread stall", "Assemble the image-only PDF"]) { expect(labels).toContain(l); } expect(reportToText(report)).toContain("RESULT: all checks passed"); }); it("reports a fingerprint difference clearly, with both values", async () => { const tampered: GoldenMap = JSON.parse(JSON.stringify(goldens)); const id = Object.keys(tampered)[0]; tampered[id].fingerprint = "0".repeat(64); const report = await runSelfTest(nodeDeps({ goldens: tampered, fixtures: selfTestFixtures().slice(0, 1) })); const check = report.sections.flatMap((s) => s.checks).find((c) => c.label.startsWith("Layout fingerprint"))!; expect(check.status).toBe("fail"); expect(check.detail).toContain("0".repeat(64)); expect(check.detail).toContain("this webview"); expect(report.passed).toBe(false); expect(reportToText(report)).toContain("RESULT: FAILED"); }); it("fails the text check when the page does not contain the expected strings, and a render error stops that fixture", async () => { const tampered: GoldenMap = JSON.parse(JSON.stringify(goldens)); const id = Object.keys(tampered)[0]; tampered[id].totalText = "₹9,999,999.99"; const a = await runSelfTest(nodeDeps({ goldens: tampered, fixtures: selfTestFixtures().slice(0, 1) })); expect(a.sections.flatMap((s) => s.checks).find((c) => c.label === "Text extraction")).toMatchObject({ status: "fail" }); const b = await runSelfTest(nodeDeps({ render: async () => Promise.reject(new Error("worker could not start")), fixtures: selfTestFixtures().slice(0, 1) })); const sec = b.sections.find((s) => s.title.startsWith("Classic, A4"))!; expect(sec.checks.map((c) => c.status)).toEqual(["fail"]); expect(sec.checks[0].detail).toContain("worker could not start"); expect(b.sections.some((s) => s.title.startsWith("300 DPI"))).toBe(false); }); it("fails the stall check when the longest frame gap exceeds the budget", async () => { const report = await runSelfTest(nodeDeps({ startFrameMonitor: () => () => 900 })); const stall = report.sections.flatMap((s) => s.checks).find((c) => c.label === "Longest main-thread stall")!; expect(stall.status).toBe("fail"); expect(stall.detail).toContain("900 ms"); }); });