Crimson Grid, Tangerine Ledger, Slate Band and Teal Swoosh are rebuilt from the reference PDFs with grid and band tables that survive page breaks, full-bleed fixed bands, pennant tabs, gradient corner swooshes with keep-outs, Carbon icon circles, and the user's logo in the measured slot (knockout or white chip on the dark Slate band). Adds static Poppins 500-700 and Inter fonts. Thumbnails regenerated for all eight templates; the item-count sweeps run in their own test file.
52 lines
1.7 KiB
TypeScript
52 lines
1.7 KiB
TypeScript
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const ROOT = process.cwd();
|
|
const TEMPLATES_DIR = path.join(ROOT, "src/pdf/templates");
|
|
|
|
/**
|
|
* Model fields that carry regulated data. Templates must not read them: the data may leave the model
|
|
* only through the Slot components, so the contract test can see every place it is printed.
|
|
*/
|
|
const REGULATED = [
|
|
/\.gstin\b/,
|
|
/\.showGstin\b/,
|
|
/\.posLabel\b/,
|
|
/\.hsnSac\b/,
|
|
/\.taxRows\b/,
|
|
/\.amountInWords\b/,
|
|
/\.reverseCharge\b/,
|
|
/\.signature\b/,
|
|
/\.slots\b/,
|
|
];
|
|
|
|
function sources(dir: string): string[] {
|
|
return readdirSync(dir).flatMap((name) => {
|
|
const full = path.join(dir, name);
|
|
if (statSync(full).isDirectory()) return sources(full);
|
|
return /\.(ts|tsx)$/.test(name) && !/\.test\.|harness(\.check)?\.ts$/.test(name) ? [full] : [];
|
|
});
|
|
}
|
|
|
|
describe("templates read regulated data only through Slot.*", () => {
|
|
it("scans the classic template", () => {
|
|
expect(sources(TEMPLATES_DIR).some((f) => f.endsWith("classic/Layout.tsx"))).toBe(true);
|
|
});
|
|
|
|
it("finds no direct access to the regulated model fields", () => {
|
|
const hits: string[] = [];
|
|
for (const file of sources(TEMPLATES_DIR)) {
|
|
readFileSync(file, "utf8")
|
|
.split("\n")
|
|
.forEach((line, i) => {
|
|
if (/^\s*(\/\/|\*|\/\*)/.test(line)) return;
|
|
// The slot API itself is the sanctioned way in: Slot.TaxRows, slotHeights.amountInWords, ...
|
|
const code = line.replace(/\b(Slot|slotHeights)\.\w+/g, "");
|
|
for (const re of REGULATED) if (re.test(code)) hits.push(`${path.relative(ROOT, file)}:${i + 1} ${line.trim()}`);
|
|
});
|
|
}
|
|
expect(hits).toEqual([]);
|
|
});
|
|
});
|