Replace the sample data with synthetic fixtures and regenerate goldens and thumbnails

The template harness, tests and thumbnails now use a neutral vendor, bank account and
a generated sample wordmark (scripts/logo/make-sample-logo.mjs). Layout goldens and the
14 picker thumbnails are regenerated. Fixture strings keep their original lengths where
layout tests depend on them.
This commit is contained in:
2026-10-05 10:18:53 +05:30
parent edcb3d2b13
commit ede8eeda90
35 changed files with 64 additions and 44 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+20
View File
@@ -0,0 +1,20 @@
// Renders the synthetic wordmark used as the sample logo in tests, thumbnails and scripts
// (src/pdf/testing/fixtures/sample-wordmark-logo.png). Run: node scripts/logo/make-sample-logo.mjs
import { writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { GlobalFonts, createCanvas } from "@napi-rs/canvas";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const W = 2172;
const H = 724;
const size = Number(process.argv[2] ?? 300);
GlobalFonts.registerFromPath(path.join(ROOT, process.argv[3] ?? "public/fonts/montserrat/Montserrat-ExtraBold.ttf"), "SampleMark");
const canvas = createCanvas(W, H);
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#1f3a6e";
ctx.font = `${size}px SampleMark`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(process.argv[4] ?? "STUDIO", W / 2, H / 2);
writeFileSync(path.join(ROOT, "src/pdf/testing/fixtures/sample-wordmark-logo.png"), canvas.toBuffer("image/png"));
+1 -1
View File
@@ -18,7 +18,7 @@ pub const STATES: &[(&str, &str)] = &[
("09", "Uttar Pradesh"), ("09", "Uttar Pradesh"),
("10", "Bihar"), ("10", "Bihar"),
("11", "Sikkim"), ("11", "Sikkim"),
("12", "Sampleachal Pradesh"), ("12", "Arunachal Pradesh"),
("13", "Nagaland"), ("13", "Nagaland"),
("14", "Manipur"), ("14", "Manipur"),
("15", "Mizoram"), ("15", "Mizoram"),
+1 -1
View File
@@ -404,7 +404,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
let raw = mock.requests.lock().unwrap()[0].clone(); let raw = mock.requests.lock().unwrap()[0].clone();
assert!(raw.starts_with("GET /api/resource/Sales%20Invoice/AP%2F2026-001?limit_page_length=0 "), "{raw}"); assert!(raw.starts_with("GET /api/resource/Sales%20Invoice/INV%2F2026-001?limit_page_length=0 "), "{raw}");
} }
#[tokio::test] #[tokio::test]
@@ -820,7 +820,7 @@ fn sync_status_list_and_open_url() {
let s = sync_status(&conn, inv.id).unwrap(); let s = sync_status(&conn, inv.id).unwrap();
assert_eq!((s.status.as_str(), s.remote_name.as_str(), s.remote_docstatus, s.attached), ("synced", "INV/2026-001", 1, true)); assert_eq!((s.status.as_str(), s.remote_name.as_str(), s.remote_docstatus, s.attached), ("synced", "INV/2026-001", 1, true));
assert_eq!(sync_statuses(&conn).unwrap(), vec![s]); assert_eq!(sync_statuses(&conn).unwrap(), vec![s]);
assert_eq!(open_url(&conn, inv.id).unwrap(), "http://127.0.0.1:8000/app/sales-invoice/AP%2F2026-001"); assert_eq!(open_url(&conn, inv.id).unwrap(), "http://127.0.0.1:8000/app/sales-invoice/INV%2F2026-001");
// Names with spaces and other characters stay one path segment. // Names with spaces and other characters stay one path segment.
conn.execute("UPDATE erpnext_sync SET remote_name = 'SINV 26/1?x' WHERE invoice_id = ?1", params![inv.id]).unwrap(); conn.execute("UPDATE erpnext_sync SET remote_name = 'SINV 26/1?x' WHERE invoice_id = ?1", params![inv.id]).unwrap();
assert_eq!(open_url(&conn, inv.id).unwrap(), "http://127.0.0.1:8000/app/sales-invoice/SINV%2026%2F1%3Fx"); assert_eq!(open_url(&conn, inv.id).unwrap(), "http://127.0.0.1:8000/app/sales-invoice/SINV%2026%2F1%3Fx");
+2 -2
View File
@@ -540,8 +540,8 @@ mod tests {
let out = process_logo(&bytes).unwrap(); let out = process_logo(&bytes).unwrap();
let m = &out.metrics; let m = &out.metrics;
assert_eq!((m.source_width, m.source_height), (2172, 724)); assert_eq!((m.source_width, m.source_height), (2172, 724));
assert!((4.5..=4.7).contains(&m.aspect), "aspect {}", m.aspect); assert!((5.3..=5.6).contains(&m.aspect), "aspect {}", m.aspect);
assert!((0.40..=0.46).contains(&m.ink_density), "density {}", m.ink_density); assert!((0.50..=0.60).contains(&m.ink_density), "density {}", m.ink_density);
assert_eq!(m.kind, LogoKind::Wordmark); assert_eq!(m.kind, LogoKind::Wordmark);
assert!(out.print_png.len() <= 120 * 1024, "print is {} bytes", out.print_png.len()); assert!(out.print_png.len() <= 120 * 1024, "print is {} bytes", out.print_png.len());
assert!(out.knockout_png.is_some()); assert!(out.knockout_png.is_some());
+1 -1
View File
@@ -273,7 +273,7 @@ export const INDIAN_STATES: { code: string; name: string }[] = [
{ code: "09", name: "Uttar Pradesh" }, { code: "09", name: "Uttar Pradesh" },
{ code: "10", name: "Bihar" }, { code: "10", name: "Bihar" },
{ code: "11", name: "Sikkim" }, { code: "11", name: "Sikkim" },
{ code: "12", name: "Sampleachal Pradesh" }, { code: "12", name: "Arunachal Pradesh" },
{ code: "13", name: "Nagaland" }, { code: "13", name: "Nagaland" },
{ code: "14", name: "Manipur" }, { code: "14", name: "Manipur" },
{ code: "15", name: "Mizoram" }, { code: "15", name: "Mizoram" },
+2 -2
View File
@@ -210,10 +210,10 @@ describe("Row", () => {
}); });
describe("FitText and IdText", () => { describe("FitText and IdText", () => {
const EMAIL60 = "Sample.longsurname.voiceover.studio.department@examplemedia.co.in".slice(0, 60); const EMAIL60 = "anon.longsurname.voiceover.studio.department@examplemedia.co.in".slice(0, 60);
it("shrinks a medium e-mail to fit and keeps it on one line", async () => { it("shrinks a medium e-mail to fit and keeps it on one line", async () => {
const email = "vendor.name@examplestudios.co.in"; const email = "anon.longsurname.voiceover@examplestudios.co.in";
let info!: FitInfo; let info!: FitInfo;
const pdf = await renderPdfText( const pdf = await renderPdfText(
frame( frame(
+9 -9
View File
@@ -15,7 +15,7 @@ const logo = (over: Partial<SlotLogoInput>): SlotLogoInput => ({
hasKnockout: true, hasKnockout: true,
...over, ...over,
}); });
const Sample_P = logo({ aspect: 4.62, density: 0.437, kind: "wordmark", includesName: true }); const SAMPLE_LOGO = logo({ aspect: 4.62, density: 0.437, kind: "wordmark", includesName: true });
/** The box a logo is fitted to: the allowance for a text-only slot, else the lockup (or mark) box. */ /** The box a logo is fitted to: the allowance for a text-only slot, else the lockup (or mark) box. */
const lockupTarget = (s: TemplateSlot): Box => (s.textOnly ? s.allowance : s.lockup!); const lockupTarget = (s: TemplateSlot): Box => (s.textOnly ? s.allowance : s.lockup!);
@@ -98,9 +98,9 @@ describe("computeLogoPlacement identity: the placeholder itself comes back as it
}); });
describe("computeLogoPlacement text-only allowances", () => { describe("computeLogoPlacement text-only allowances", () => {
it("Test Vendor in Monolith is 28 pt tall, about 129 wide, and keeps the right edge", () => { it("The sample logo in Monolith is 28 pt tall, about 129 wide, and keeps the right edge", () => {
const s = SLOTS.monolith!; const s = SLOTS.monolith!;
const p = computeLogoPlacement({ logo: Sample_P, slot: s }); const p = computeLogoPlacement({ logo: SAMPLE_LOGO, slot: s });
expect(p.h).toBeCloseTo(28, 6); expect(p.h).toBeCloseTo(28, 6);
expect(p.w).toBeGreaterThanOrEqual(128); expect(p.w).toBeGreaterThanOrEqual(128);
expect(p.w).toBeLessThanOrEqual(130); expect(p.w).toBeLessThanOrEqual(130);
@@ -117,9 +117,9 @@ describe("computeLogoPlacement text-only allowances", () => {
expect(p.x + p.w).toBeCloseTo(560, 6); expect(p.x + p.w).toBeCloseTo(560, 6);
}); });
it("Test Vendor in Tangerine fills 36 pt of height (about 166 wide) at the left edge", () => { it("The sample logo in Tangerine fills 36 pt of height (about 166 wide) at the left edge", () => {
const s = SLOTS["tangerine-ledger"]!; const s = SLOTS["tangerine-ledger"]!;
const p = computeLogoPlacement({ logo: Sample_P, slot: s }); const p = computeLogoPlacement({ logo: SAMPLE_LOGO, slot: s });
expect(p.h).toBeCloseTo(36, 6); expect(p.h).toBeCloseTo(36, 6);
expect(p.w).toBeCloseTo(36 * 4.62, 6); expect(p.w).toBeCloseTo(36 * 4.62, 6);
expect(p.x).toBeCloseTo(60.5, 6); expect(p.x).toBeCloseTo(60.5, 6);
@@ -142,13 +142,13 @@ describe("computeLogoPlacement text-only allowances", () => {
describe("computeLogoPlacement on every slot", () => { describe("computeLogoPlacement on every slot", () => {
const all: [TemplateId, TemplateSlot][] = [...measured, ...optIn]; const all: [TemplateId, TemplateSlot][] = [...measured, ...optIn];
it("Test Vendor never exceeds its box and is never heavier than the placeholder ink", () => { it("The sample logo never exceeds its box and is never heavier than the placeholder ink", () => {
for (const [id, s] of all) { for (const [id, s] of all) {
const p = computeLogoPlacement({ logo: Sample_P, slot: s }); const p = computeLogoPlacement({ logo: SAMPLE_LOGO, slot: s });
expect(p.slotKind).toBe("lockup"); expect(p.slotKind).toBe("lockup");
expect(inside(p.box, lockupTarget(s)), id).toBe(true); expect(inside(p.box, lockupTarget(s)), id).toBe(true);
if (!s.optIn && !s.textOnly) { if (!s.optIn && !s.textOnly) {
const ink = p.w * p.h * Sample_P.density; const ink = p.w * p.h * SAMPLE_LOGO.density;
const placeholderInk = s.lockup!.w * s.lockup!.h * s.lockupDensity; const placeholderInk = s.lockup!.w * s.lockup!.h * s.lockupDensity;
expect(ink, `${id}: ${ink.toFixed(1)} vs ${placeholderInk.toFixed(1)}`).toBeLessThanOrEqual(placeholderInk * 1.05); expect(ink, `${id}: ${ink.toFixed(1)} vs ${placeholderInk.toFixed(1)}`).toBeLessThanOrEqual(placeholderInk * 1.05);
} }
@@ -265,7 +265,7 @@ describe("computeLogoPlacement page scale and available room", () => {
it("shrinks to the room the layout has", () => { it("shrinks to the room the layout has", () => {
const s = SLOTS["teal-swoosh"]!; const s = SLOTS["teal-swoosh"]!;
const p = computeLogoPlacement({ logo: Sample_P, slot: s, available: { w: 80, h: 100 } }); const p = computeLogoPlacement({ logo: SAMPLE_LOGO, slot: s, available: { w: 80, h: 100 } });
expect(p.w).toBeLessThanOrEqual(80 + 1e-9); expect(p.w).toBeLessThanOrEqual(80 + 1e-9);
expect(p.x).toBeCloseTo(s.lockup!.x, 6); expect(p.x).toBeCloseTo(s.lockup!.x, 6);
}); });
+1 -1
View File
@@ -5,7 +5,7 @@ const BOX = { w: 170, h: 54 };
const base = { box: BOX, availableWidth: 299, minTextWidth: 160 }; const base = { box: BOX, availableWidth: 299, minTextWidth: 160 };
describe("computeClassicLogoPlacement", () => { describe("computeClassicLogoPlacement", () => {
it("the Test Vendor trimmed 4.6:1 logo is width-limited and stacks above the text", () => { it("the sample trimmed 4.6:1 logo is width-limited and stacks above the text", () => {
const p = computeClassicLogoPlacement({ ...base, aspect: 4.6 }); const p = computeClassicLogoPlacement({ ...base, aspect: 4.6 });
expect(p.w).toBeCloseTo(170, 6); expect(p.w).toBeCloseTo(170, 6);
expect(p.h).toBeCloseTo(170 / 4.6, 6); expect(p.h).toBeCloseTo(170 / 4.6, 6);
+2 -2
View File
@@ -12,7 +12,7 @@ const SAMPLES: string[] = [
"Voice-over session 12 - narration for module 3", "Voice-over session 12 - narration for module 3",
"Total ₹1,23,456.78", "Total ₹1,23,456.78",
"AVATAR WAVE To Ty Yo", "AVATAR WAVE To Ty Yo",
"vendor.name@examplestudios.co.in", "anon.longsurname.voiceover@examplestudios.co.in",
"29ABCDE1234F1Z5", "29ABCDE1234F1Z5",
"office fi fl ffi final", "office fi fl ffi final",
"BILLED TO", "BILLED TO",
@@ -130,7 +130,7 @@ describe("measurer helpers", () => {
}); });
it("round-trips chunkIdentifier output", () => { it("round-trips chunkIdentifier output", () => {
const id = "vendor.name@examplestudios.co.in"; const id = "anon.longsurname.voiceover@examplestudios.co.in";
expect(chunkIdentifier(id, 12).join("")).toBe(id); expect(chunkIdentifier(id, 12).join("")).toBe(id);
}); });
}); });
+1 -1
View File
@@ -173,7 +173,7 @@ describe("Slate Band: dark band and logo", () => {
for (const [i, page] of r.pdf.pages.entries()) { for (const [i, page] of r.pdf.pages.entries()) {
const text = page.items.map((t) => t.str).join(" "); const text = page.items.map((t) => t.str).join(" ");
expect(text, `page ${i + 1} phone`).toContain("9000000000"); expect(text, `page ${i + 1} phone`).toContain("9000000000");
expect(text, `page ${i + 1} e-mail`).toContain("vendor@example.com"); expect(text, `page ${i + 1} e-mail`).toContain("vendor.test@example.in");
} }
}); });
}); });
+1 -1
View File
@@ -316,7 +316,7 @@ describe("closing and footer", () => {
expect(closing.box.y + closing.box.h).toBeCloseTo(frame.page.height - frame.pad.bottom, 0); expect(closing.box.y + closing.box.h).toBeCloseTo(frame.page.height - frame.pad.bottom, 0);
const text = r.pdf.pages[last].items.map((i) => i.str).join(" "); const text = r.pdf.pages[last].items.map((i) => i.str).join(" ");
expect(text).toContain("9000000000"); expect(text).toContain("9000000000");
expect(text).toContain("vendor@example.com"); expect(text).toContain("vendor.test@example.in");
}); });
it("a pasted signature image replaces the script: right-aligned, at most 36 pt tall, with the signatory name and designation under it", async () => { it("a pasted signature image replaces the script: right-aligned, at most 36 pt tall, with the signatory name and designation under it", async () => {
+5 -5
View File
@@ -31,8 +31,8 @@ const FIXTURE_CLIENT_GSTIN = "29ABCDE1234F1Z5";
const baseVendor: Settings = { const baseVendor: Settings = {
vendorName: "Test Vendor", vendorName: "Test Vendor",
vendorAddress: "1 Test Road, Bangalore - 560001", vendorAddress: "27 Example Apartments, Sample Layout, Test Nagar, Bangalore - 560001",
vendorEmail: "vendor@example.com", vendorEmail: "vendor.test@example.in",
vendorPhone: "9000000000", vendorPhone: "9000000000",
vendorPan: "ABCDE1234F", vendorPan: "ABCDE1234F",
vendorGstin: "", vendorGstin: "",
@@ -165,7 +165,7 @@ export function fixtureLogo(kind: LogoKind): LogoInput | null {
let hit = cache.get(kind); let hit = cache.get(kind);
if (!hit) { if (!hit) {
if (kind === "real") { if (kind === "real") {
// 2172 x 724: the untrimmed Test Vendor logo, a 3:1 aspect. // 2172 x 724: the untrimmed sample logo, a 3:1 aspect.
const png = readFileSync(path.resolve(process.cwd(), "src/pdf/testing/fixtures/sample-wordmark-logo.png")); const png = readFileSync(path.resolve(process.cwd(), "src/pdf/testing/fixtures/sample-wordmark-logo.png"));
const [width, height] = [png.readUInt32BE(16), png.readUInt32BE(20)]; const [width, height] = [png.readUInt32BE(16), png.readUInt32BE(20)];
hit = { dataUri: dataUri(png), width, height, aspect: width / height, density: 0.21, kind: "wordmark", includesName: true }; hit = { dataUri: dataUri(png), width, height, aspect: width / height, density: 0.21, kind: "wordmark", includesName: true };
@@ -183,7 +183,7 @@ export function fixtureLogo(kind: LogoKind): LogoInput | null {
includesName: true, includesName: true,
}; };
} else if (kind === "wide") { } else if (kind === "wide") {
// The trimmed Test Vendor ink box is 4.6:1. // The trimmed sample ink box is 4.6:1.
hit = { dataUri: dataUri(solidPng(460, 100)), width: 460, height: 100, aspect: 4.6, density: 1, kind: "wordmark", includesName: true }; hit = { dataUri: dataUri(solidPng(460, 100)), width: 460, height: 100, aspect: 4.6, density: 1, kind: "wordmark", includesName: true };
} else { } else {
hit = { dataUri: dataUri(solidPng(120, 120)), width: 120, height: 120, aspect: 1, density: 1, kind: "mark", includesName: false }; hit = { dataUri: dataUri(solidPng(120, 120)), width: 120, height: 120, aspect: 1, density: 1, kind: "mark", includesName: false };
@@ -281,7 +281,7 @@ export const FIXTURES: FixtureSpec[] = [
{ {
name: "id-tokens", name: "id-tokens",
vendor: { vendor: {
vendorEmail: "Sample.longsurname.voiceover.studio.bangalore@examplestudios-india.co.in", vendorEmail: "anon.longsurname.voiceover.studio.bangalore@examplestudios-india.co.in",
vendorPhone: "+91 97382 84459 / +91 80 4123 9988 (studio)", vendorPhone: "+91 97382 84459 / +91 80 4123 9988 (studio)",
vendorName: "Venkatachalam Longsurname Ramasubramanian Voice Studios Pvt", vendorName: "Venkatachalam Longsurname Ramasubramanian Voice Studios Pvt",
}, },
@@ -321,7 +321,7 @@ describe("totals and closing", () => {
const last = r.result.pages - 1; const last = r.result.pages - 1;
const closing = byId(r, CLOSING_ID, last); const closing = byId(r, CLOSING_ID, last);
const lines = textLines(closing).map((l) => l.text); const lines = textLines(closing).map((l) => l.text);
expect(lines).toEqual(expect.arrayContaining(["THANK YOU", "9000000000", "vendor@example.com", "Payment due within 30 days."])); expect(lines).toEqual(expect.arrayContaining(["THANK YOU", "9000000000", "vendor.test@example.in", "Payment due within 30 days."]));
expect(onPage(r, last).filter((n) => n.type === "svg" && n.box.y >= closing.box.y)).toHaveLength(3); expect(onPage(r, last).filter((n) => n.type === "svg" && n.box.y >= closing.box.y)).toHaveLength(3);
const sign = byId(r, SLOT_ID.signatory, last); const sign = byId(r, SLOT_ID.signatory, last);
expect(sign.box.w).toBeCloseTo(V.closing.ruledW, 0); expect(sign.box.w).toBeCloseTo(V.closing.ruledW, 0);
+3 -3
View File
@@ -119,14 +119,14 @@ describe("what the header does with a logo", () => {
expect(img.box.w).toBeLessThanOrEqual(lockup.w + 0.01); expect(img.box.w).toBeLessThanOrEqual(lockup.w + 0.01);
expect(img.box.x + img.box.w).toBeLessThanOrEqual(frameForPrefs(wide.prefs).page.width - frameForPrefs(wide.prefs).pad.right + 0.5); expect(img.box.x + img.box.w).toBeLessThanOrEqual(frameForPrefs(wide.prefs).page.width - frameForPrefs(wide.prefs).pad.right + 0.5);
// The business name is not repeated as text in the header (the party heading still prints it once). // The business name is not repeated as text in the header (the party heading still prints it once).
expect(textOf(wide)[0].match(/Sample P/g)?.length).toBe(1); expect(textOf(wide)[0].match(/TEST VENDOR/g)?.length).toBe(1);
const square = await renderFixture({ ...withLogo, logo: "square" }, prefsFor("A4", { templateId: "crimson-grid" })); const square = await renderFixture({ ...withLogo, logo: "square" }, prefsFor("A4", { templateId: "crimson-grid" }));
const [mark] = imageBoxes(square); const [mark] = imageBoxes(square);
const markBox = SLOTS["crimson-grid"]!.mark!; const markBox = SLOTS["crimson-grid"]!.mark!;
expect(mark.box.h).toBeLessThanOrEqual(markBox.h + 0.01); expect(mark.box.h).toBeLessThanOrEqual(markBox.h + 0.01);
expect(mark.box.w).toBeLessThanOrEqual(markBox.w + 0.01); expect(mark.box.w).toBeLessThanOrEqual(markBox.w + 0.01);
expect(textOf(square)[0].match(/Sample P/g)?.length).toBe(2); expect(textOf(square)[0].match(/TEST VENDOR/g)?.length).toBe(2);
}); });
it("Tangerine: the logo stays in its 220 x 36 allowance at the left", async () => { it("Tangerine: the logo stays in its 220 x 36 allowance at the left", async () => {
@@ -148,7 +148,7 @@ describe("what the header does with a logo", () => {
const r = await renderFixture({ ...withLogo, logo: "none" }, prefsFor("A4", { templateId: t.id })); const r = await renderFixture({ ...withLogo, logo: "none" }, prefsFor("A4", { templateId: t.id }));
expect(imageBoxes(r), t.id).toHaveLength(0); expect(imageBoxes(r), t.id).toHaveLength(0);
// The brand text in the header, the party heading and the footer: three prints of the name, one of them at the top. // The brand text in the header, the party heading and the footer: three prints of the name, one of them at the top.
const names = r.pdf.pages[0].items.filter((i) => /^Sample p$/i.test(i.str.trim())); const names = r.pdf.pages[0].items.filter((i) => /^test vendor$/i.test(i.str.trim()));
expect(names.length, t.id).toBe(3); expect(names.length, t.id).toBe(3);
expect(names.filter((i) => i.bottom < 150).length, t.id).toBe(1); expect(names.filter((i) => i.bottom < 150).length, t.id).toBe(1);
expect(r.pdf.raw, t.id).toContain("Poppins"); expect(r.pdf.raw, t.id).toContain("Poppins");
+4 -4
View File
@@ -157,7 +157,7 @@ describe("header", () => {
it("the vendor name stands in the empty left corner, level with the title, in Lustria caps", async () => { it("the vendor name stands in the empty left corner, level with the title, in Lustria caps", async () => {
const r = await renderFixture(PLAIN, prefsFor("A4", { templateId: "marble" })); const r = await renderFixture(PLAIN, prefsFor("A4", { templateId: "marble" }));
const name = items(r, "Sample P")[0]; const name = items(r, "TEST VENDOR")[0];
expect(name.bottom).toBeCloseTo(124.9, 0); expect(name.bottom).toBeCloseTo(124.9, 0);
expect(name.x0).toBeCloseTo(38.5, 0); expect(name.x0).toBeCloseTo(38.5, 0);
}); });
@@ -218,7 +218,7 @@ describe("logo", () => {
it("without the opt-in no logo is drawn, even when the invoice has one", async () => { it("without the opt-in no logo is drawn, even when the invoice has one", async () => {
const r = await renderFixture({ name: "o", items: 2, logo: "real" }, prefsFor("A4", { templateId: "marble" })); const r = await renderFixture({ name: "o", items: 2, logo: "real" }, prefsFor("A4", { templateId: "marble" }));
expect(imageBoxes(r)).toHaveLength(0); expect(imageBoxes(r)).toHaveLength(0);
expect(items(r, "Sample P")).toHaveLength(1); expect(items(r, "TEST VENDOR")).toHaveLength(1);
}); });
it("every opted-in logo fits the 150 x 50 slot, starts at the text inset and rests on its bottom edge", async () => { it("every opted-in logo fits the 150 x 50 slot, starts at the text inset and rests on its bottom edge", async () => {
@@ -250,7 +250,7 @@ describe("logo", () => {
}); });
it("a wordmark replaces the vendor name text; a mark keeps it under the mark", async () => { it("a wordmark replaces the vendor name text; a mark keeps it under the mark", async () => {
const header = (r: R) => r.pdf.pages[0].items.filter((i) => squash(i.str) === "SampleP" && i.bottom < 200); const header = (r: R) => r.pdf.pages[0].items.filter((i) => squash(i.str) === "TESTVENDOR" && i.bottom < 200);
const wide = await renderFixture({ name: "o", items: 2, logo: "real" }, optIn()); const wide = await renderFixture({ name: "o", items: 2, logo: "real" }, optIn());
expect(header(wide)).toHaveLength(0); expect(header(wide)).toHaveLength(0);
const square = await renderFixture({ name: "o", items: 2, logo: "square" }, optIn()); const square = await renderFixture({ name: "o", items: 2, logo: "square" }, optIn());
@@ -416,7 +416,7 @@ describe("long text", () => {
it("the foot band's text stays inside the band whatever the address, and its two columns never overlap", async () => { it("the foot band's text stays inside the band whatever the address, and its two columns never overlap", async () => {
for (const size of ["A4", "LETTER"] as const) { for (const size of ["A4", "LETTER"] as const) {
const r = await renderFixture({ name: "long", items: 3, vendor: { vendorAddress: Array.from({ length: 8 }, (_, i) => `Address line ${i + 1} of a very long Indian address with a locality name`).join("\n"), vendorEmail: "Sample.longsurname.voiceover.studio.bangalore@examplestudios-india.co.in", vendorName: "Supercalifragilisticexpialidocious-Pneumonoultramicroscopic-1234567890" } }, prefsFor(size, { templateId: "marble" })); const r = await renderFixture({ name: "long", items: 3, vendor: { vendorAddress: Array.from({ length: 8 }, (_, i) => `Address line ${i + 1} of a very long Indian address with a locality name`).join("\n"), vendorEmail: "anon.longsurname.voiceover.studio.bangalore@examplestudios-india.co.in", vendorName: "Supercalifragilisticexpialidocious-Pneumonoultramicroscopic-1234567890" } }, prefsFor(size, { templateId: "marble" }));
const frame = frameForPrefs(r.prefs); const frame = frameForPrefs(r.prefs);
const decorText = onPage(r, 0).filter((n) => n.decor && n.type === "text" && n.box.y > frame.page.height - V.bands.bottom); const decorText = onPage(r, 0).filter((n) => n.decor && n.type === "text" && n.box.y > frame.page.height - V.bands.bottom);
const lines = decorText.flatMap((n) => textLines(n)); const lines = decorText.flatMap((n) => textLines(n));
+3 -3
View File
@@ -122,8 +122,8 @@ describe("page geometry", () => {
describe("helpers", () => { describe("helpers", () => {
it("initialsOf takes the first and last word", () => { it("initialsOf takes the first and last word", () => {
expect(initialsOf("Test Vendor")).toBe("AP"); expect(initialsOf("Test Vendor")).toBe("TV");
expect(initialsOf("Venkatachalam Longsurname Ramasubramanian Voice Studios Pvt")).toBe("AP"); expect(initialsOf("Venkatachalam Longsurname Ramasubramanian Voice Studios Pvt")).toBe("VP");
expect(initialsOf("Bongbetic")).toBe("B"); expect(initialsOf("Bongbetic")).toBe("B");
expect(initialsOf(" ")).toBe(""); expect(initialsOf(" ")).toBe("");
expect(initialsOf("& Co")).toBe("C"); expect(initialsOf("& Co")).toBe("C");
@@ -151,7 +151,7 @@ describe("what the layout does with a logo", () => {
const none = await renderFixture({ ...withLogo, logo: "none" }, prefsFor("A4", { templateId: t.id })); const none = await renderFixture({ ...withLogo, logo: "none" }, prefsFor("A4", { templateId: t.id }));
expect(imageCount(none), t.id).toBe(0); expect(imageCount(none), t.id).toBe(0);
const text = none.pdf.pages[0].items.map((i) => i.str).join(" "); const text = none.pdf.pages[0].items.map((i) => i.str).join(" ");
if (t.id === "monogram") expect(text).toContain("AP"); if (t.id === "monogram") expect(text).toContain("TV");
// No Canva ring or stamp is ever drawn. // No Canva ring or stamp is ever drawn.
expect(nodes(none.result.core.layout[0].nodes).filter((n) => n.type === "svg" && !n.decor), t.id).toHaveLength(0); expect(nodes(none.result.core.layout[0].nodes).filter((n) => n.type === "svg" && !n.decor), t.id).toHaveLength(0);
} }
@@ -340,7 +340,7 @@ describe("Citrus Split", () => {
for (const i of icons) expect(i.box.x + i.box.w).toBeLessThanOrEqual(frame.page.width - frame.pad.right + 0.5); for (const i of icons) expect(i.box.x + i.box.w).toBeLessThanOrEqual(frame.page.width - frame.pad.right + 0.5);
const text = textsOf(r, r.result.pages - 1); const text = textsOf(r, r.result.pages - 1);
expect(text).toContain("9000000000"); expect(text).toContain("9000000000");
expect(text).toContain("vendor@example.com"); expect(text).toContain("vendor.test@example.in");
}); });
it("the logo stays inside its measured lockup box; a wordmark replaces the name, a mark keeps it", async () => { it("the logo stays inside its measured lockup box; a wordmark replaces the name, a mark keeps it", async () => {
+1 -1
View File
@@ -6,7 +6,7 @@ import { SLOTS } from "./slots.generated";
* is a free area named in docs/templates/NN-*.md and is an opt-in position, off by default (the plan * is a free area named in docs/templates/NN-*.md and is an opt-in position, off by default (the plan
* keeps Linea logo-free so it still matches format.pdf). Hand-written; not produced by logo:measure. * keeps Linea logo-free so it still matches format.pdf). Hand-written; not produced by logo:measure.
* *
* With no placeholder ink to match, the density is a typical logo's (Test Vendor is 0.437), so the weight * With no placeholder ink to match, the density is a typical logo's (the sample logo is 0.437), so the weight
* factor in computeLogoPlacement only shrinks logos that are denser than that. * factor in computeLogoPlacement only shrinks logos that are denser than that.
*/ */
const TYPICAL_DENSITY = 0.4; const TYPICAL_DENSITY = 0.4;
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+4 -4
View File
@@ -1,23 +1,23 @@
{ {
"classic-a4-tax-3": { "classic-a4-tax-3": {
"pages": 1, "pages": 1,
"fingerprint": "35b03b20aeec3dcbb808300b6e8f0b1867dfd69d5fd6f69aaee891c354955092", "fingerprint": "87b12a5c1b1a9b45ac6a8fd5862c8e7b5de8d203731fe4e485872dea5bbb07cd",
"invoiceNumber": "ST/2026-001", "invoiceNumber": "ST/2026-001",
"totalText": "₹8,625.80", "totalText": "₹8,625.80",
"pageLabel": "Page 1 of 1", "pageLabel": "Page 1 of 1",
"textHashes": [ "textHashes": [
"855dd2d104e80ff599210d69f643be0ccb281e6583cbcea2dafa5fdcb09f03fc" "ddda2419b84480c00d8ed76088898277711f448ef11c2383a48494ecd0e4c524"
], ],
"firstPageText": "Selftest Studio 1 Test Lane, Test Town, Bangalore - 560005 selftest@example.com 9000000000 PAN: AAAAA0000A GSTIN: 29AAAAA0000A1Z5 TAX INVOICE Invoice No. ST/2026-001 Date 15 Sept 2026 Due Date 15 Oct 2026 B I L L E D T O Acme Learning Pvt Ltd 42 MG Road Bangalore 560001 GSTIN: 29ABCDE1234F1Z5 Place of Supply: 29 – Karnataka S . N O D E S C R I P TI O N H S N /S AC U N IT P R I C E QT Y A M O U N T 1 Voice-over session 1 - narration for module 1 998397 ₹1,200.00 per hour 1 ₹1,200.00 2 Voice-over session 2 - narration for module 2 998397 — 1 ₹2,510.00 3 Voice-over session 3 - narration for module 3 998397 ₹1,200.00 per hour 3 ₹3,600.00 Subtotal ₹7,310.00 Taxable value ₹7,310.00 CGST (9%) ₹657.90 SGST (9%) ₹657.90 Total ₹8,625.80 Amount in words: Indian Rupees Eight Thousand Six Hundred Twenty Five and Eighty Paise Only Tax is payable on reverse charge: No PAY T O Selftest Studio State Bank of India Account No.: 00000000000 Branch: 000000 IFSC: SBIN0000000 Selftest Studio Proprietor Computer-generated invoice; no signature is required. Selftest Studio Generated with Voiced — a Bongbetic product Page 1 of 1" "firstPageText": "Selftest Studio 1 Test Lane, Test Town, Bangalore - 560005 selftest@example.com 9000000000 PAN: AAAAA0000A GSTIN: 29AAAAA0000A1Z5 TAX INVOICE Invoice No. ST/2026-001 Date 15 Sept 2026 Due Date 15 Oct 2026 B I L L E D T O Acme Learning Pvt Ltd 42 MG Road Bangalore 560001 GSTIN: 29ABCDE1234F1Z5 Place of Supply: 29 – Karnataka S . N O D E S C R I P TI O N H S N /S AC U N IT P R I C E QT Y A M O U N T 1 Voice-over session 1 - narration for module 1 998397 ₹1,200.00 per hour 1 ₹1,200.00 2 Voice-over session 2 - narration for module 2 998397 — 1 ₹2,510.00 3 Voice-over session 3 - narration for module 3 998397 ₹1,200.00 per hour 3 ₹3,600.00 Subtotal ₹7,310.00 Taxable value ₹7,310.00 CGST (9%) ₹657.90 SGST (9%) ₹657.90 Total ₹8,625.80 Amount in words: Indian Rupees Eight Thousand Six Hundred Twenty Five and Eighty Paise Only Tax is payable on reverse charge: No PAY T O Selftest Studio State Bank of India Account No.: 00000000000 Branch: 000000 IFSC: SBIN0000000 Selftest Studio Proprietor Computer-generated invoice; no signature is required. Selftest Studio Generated with Voiced — a Bongbetic product Page 1 of 1"
}, },
"classic-letter-unreg-60": { "classic-letter-unreg-60": {
"pages": 3, "pages": 3,
"fingerprint": "78d60e1b72dac0e40fea96cbf34c35cfeb637dbe0bc9754dcecba72231197e75", "fingerprint": "6999be0875f78e2c87aee1fd0ffa1dfe651faa7a7c0a9b4c2f78c1bdff53c7d4",
"invoiceNumber": "ST/2026-002", "invoiceNumber": "ST/2026-002",
"totalText": "₹1,56,000.00", "totalText": "₹1,56,000.00",
"pageLabel": "Page 1 of 3", "pageLabel": "Page 1 of 3",
"textHashes": [ "textHashes": [
"1728b53a177e385584d2dc33b2e71607f860886632f32a5911364f86f345c950", "4a0cf8a7c1565037edd4d9941251e030fd9e6fca25845b971465e5cf9d916936",
"870e9a70c0546550d002e5eb5728f342b1d235094c6b645ddda68a6306b33f7b", "870e9a70c0546550d002e5eb5728f342b1d235094c6b645ddda68a6306b33f7b",
"fca30345bc73289cdcad200f888f917ff9a5386cdd64e41ea7dccc64f2b7e5e0" "fca30345bc73289cdcad200f888f917ff9a5386cdd64e41ea7dccc64f2b7e5e0"
], ],