Files
Voiced/src/lib/selfTest.test.ts
T
xavierk b6d999b3e8 Add searchable and flattened export, history re-export and diagnostics self-test
issueAndArchive keeps an issued invoice even when rendering or archiving
fails (Issued - not archived, with a Render & archive action). The export
menu offers a searchable PDF or a 300 DPI image-only PDF whose metadata is
the invoice number alone; flatten failures offer Retry or searchable export
and never silently lower the resolution. History re-exports the original
as issued by default and warns when a re-render's layout fingerprint
differs. The render client gains a FIFO assemble lane that previews cannot
supersede. A Diagnostics panel runs fixtures in the real worker and
compares against goldens generated from Node.
2026-10-04 06:11:23 +05:30

88 lines
4.9 KiB
TypeScript

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> = {}): 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<Buffer> }).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");
});
});