Remove dead code and unused imports, drop useless assignments, and tune rules in eslint.config.js (underscore-prefixed unused names, intentional BOM/NBSP, React Compiler rules from eslint-plugin-react-hooks 7 as warnings).
74 lines
3.7 KiB
TypeScript
74 lines
3.7 KiB
TypeScript
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { loadCanvas, pagePng } from "../testing/rasterize";
|
|
import { renderInvoicePdf } from "../render/invoice";
|
|
import { FIXTURES, buildFixture, ensureSetup, PAGE_SIZE_IDS, prefsFor, renderFixture, type FixtureSpec } from "./harness";
|
|
import { TEMPLATES } from "./registry";
|
|
|
|
/**
|
|
* Template harness: every registered template x {A4, Letter} x the fixture matrix, plus an item-count
|
|
* sweep. A failure here means the template or a primitive is wrong, not the assertion.
|
|
*/
|
|
|
|
import { checkRendered } from "./harness.check";
|
|
|
|
const OUT_DIR = process.env.PDF_OUT_DIR ?? "";
|
|
describe("template harness: fixtures", () => {
|
|
for (const template of Object.values(TEMPLATES)) {
|
|
describe(template.id, () => {
|
|
for (const size of PAGE_SIZE_IDS) {
|
|
for (const spec of FIXTURES) {
|
|
it(`${spec.name} on ${size}`, async () => {
|
|
const prefs = prefsFor(size, { templateId: template.id });
|
|
const r = await renderFixture(spec, prefs);
|
|
checkRendered(r, `${template.id}/${spec.name}/${size}`);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("template harness: determinism", () => {
|
|
for (const template of Object.values(TEMPLATES)) {
|
|
for (const size of PAGE_SIZE_IDS) {
|
|
for (const name of ["cgst-sgst", "rows-60", "cancelled", "unregistered"]) {
|
|
it(`${template.id}/${name}/${size}: same input gives identical bytes and fingerprint`, async () => {
|
|
ensureSetup();
|
|
const spec = FIXTURES.find((f) => f.name === name) as FixtureSpec;
|
|
const { model } = buildFixture(spec);
|
|
const prefs = prefsFor(size, { templateId: template.id });
|
|
const a = await renderInvoicePdf({ model, prefs });
|
|
const b = await renderInvoicePdf({ model, prefs });
|
|
expect(Buffer.from(a.bytes).equals(Buffer.from(b.bytes))).toBe(true);
|
|
expect(b.fingerprint).toBe(a.fingerprint);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
describe.skipIf(!OUT_DIR)("template harness: samples for visual review", () => {
|
|
const samples: Array<{ file: string; spec: FixtureSpec; size: "A4" | "LETTER"; pages: number }> = [
|
|
{ file: "classic-A4-tax", spec: { name: "s1", items: 3, signature: true, invoice: { poNumber: "PO-2026-0042", notes: "Payment due within 30 days.\nLate payments attract 2% monthly interest." } }, size: "A4", pages: 1 },
|
|
{ file: "classic-Letter-tax", spec: { name: "s2", items: 3, signature: true, invoice: { poNumber: "PO-2026-0042", notes: "Payment due within 30 days.\nLate payments attract 2% monthly interest." } }, size: "LETTER", pages: 1 },
|
|
{ file: "classic-A4-unregistered", spec: { name: "s3", registered: false, items: 3, invoice: { notes: "Thank you for your business." } }, size: "A4", pages: 1 },
|
|
{ file: "classic-A4-60rows", spec: { name: "s4", items: 60, signature: true }, size: "A4", pages: 2 },
|
|
{ file: "classic-Letter-60rows", spec: { name: "s5", items: 60, signature: true }, size: "LETTER", pages: 2 },
|
|
{ file: "classic-A4-cancelled", spec: { name: "s6", items: 3, invoice: { status: "cancelled" }, signature: true }, size: "A4", pages: 1 },
|
|
];
|
|
for (const s of samples) {
|
|
it(s.file, async () => {
|
|
mkdirSync(OUT_DIR, { recursive: true });
|
|
const r = await renderFixture(s.spec, prefsFor(s.size));
|
|
writeFileSync(path.join(OUT_DIR, `${s.file}.pdf`), r.result.bytes);
|
|
const canvas = await loadCanvas();
|
|
if (!canvas) return;
|
|
for (let p = 1; p <= Math.min(s.pages, r.result.pages); p++) {
|
|
writeFileSync(path.join(OUT_DIR, `${s.file}-p${p}.png`), await pagePng(canvas, r.result.bytes, p, 1.5));
|
|
}
|
|
});
|
|
}
|
|
});
|