diff --git a/public/templates/citrus-split.png b/public/templates/citrus-split.png new file mode 100644 index 0000000..09942f8 Binary files /dev/null and b/public/templates/citrus-split.png differ diff --git a/public/templates/classic.png b/public/templates/classic.png new file mode 100644 index 0000000..2cea5db Binary files /dev/null and b/public/templates/classic.png differ diff --git a/public/templates/cobalt-stripe.png b/public/templates/cobalt-stripe.png new file mode 100644 index 0000000..33889d6 Binary files /dev/null and b/public/templates/cobalt-stripe.png differ diff --git a/public/templates/crimson-grid.png b/public/templates/crimson-grid.png new file mode 100644 index 0000000..0c136dc Binary files /dev/null and b/public/templates/crimson-grid.png differ diff --git a/public/templates/highlighter.png b/public/templates/highlighter.png new file mode 100644 index 0000000..94a66a9 Binary files /dev/null and b/public/templates/highlighter.png differ diff --git a/public/templates/linea.png b/public/templates/linea.png new file mode 100644 index 0000000..4b70a67 Binary files /dev/null and b/public/templates/linea.png differ diff --git a/public/templates/marble.png b/public/templates/marble.png new file mode 100644 index 0000000..46bee38 Binary files /dev/null and b/public/templates/marble.png differ diff --git a/public/templates/monogram.png b/public/templates/monogram.png new file mode 100644 index 0000000..7e18da8 Binary files /dev/null and b/public/templates/monogram.png differ diff --git a/public/templates/monolith.png b/public/templates/monolith.png new file mode 100644 index 0000000..bf46291 Binary files /dev/null and b/public/templates/monolith.png differ diff --git a/public/templates/purple-pop.png b/public/templates/purple-pop.png new file mode 100644 index 0000000..d9b9778 Binary files /dev/null and b/public/templates/purple-pop.png differ diff --git a/public/templates/serenity.png b/public/templates/serenity.png new file mode 100644 index 0000000..cd87d84 Binary files /dev/null and b/public/templates/serenity.png differ diff --git a/public/templates/slate-band.png b/public/templates/slate-band.png new file mode 100644 index 0000000..d8c529b Binary files /dev/null and b/public/templates/slate-band.png differ diff --git a/public/templates/tangerine-ledger.png b/public/templates/tangerine-ledger.png new file mode 100644 index 0000000..1b8808c Binary files /dev/null and b/public/templates/tangerine-ledger.png differ diff --git a/public/templates/teal-swoosh.png b/public/templates/teal-swoosh.png new file mode 100644 index 0000000..eeb91c1 Binary files /dev/null and b/public/templates/teal-swoosh.png differ diff --git a/scripts/logo/make-sample-logo.mjs b/scripts/logo/make-sample-logo.mjs new file mode 100644 index 0000000..bcc657e --- /dev/null +++ b/scripts/logo/make-sample-logo.mjs @@ -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")); diff --git a/src-tauri/src/gst.rs b/src-tauri/src/gst.rs index ac578fb..951a03f 100644 --- a/src-tauri/src/gst.rs +++ b/src-tauri/src/gst.rs @@ -18,7 +18,7 @@ pub const STATES: &[(&str, &str)] = &[ ("09", "Uttar Pradesh"), ("10", "Bihar"), ("11", "Sikkim"), - ("12", "Sampleachal Pradesh"), + ("12", "Arunachal Pradesh"), ("13", "Nagaland"), ("14", "Manipur"), ("15", "Mizoram"), diff --git a/src-tauri/src/integrations/erpnext/client.rs b/src-tauri/src/integrations/erpnext/client.rs index 52078ac..41d8407 100644 --- a/src-tauri/src/integrations/erpnext/client.rs +++ b/src-tauri/src/integrations/erpnext/client.rs @@ -404,7 +404,7 @@ mod tests { .await .unwrap(); 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] diff --git a/src-tauri/src/integrations/erpnext/push/tests.rs b/src-tauri/src/integrations/erpnext/push/tests.rs index 4f9ca27..70f0ce1 100644 --- a/src-tauri/src/integrations/erpnext/push/tests.rs +++ b/src-tauri/src/integrations/erpnext/push/tests.rs @@ -820,7 +820,7 @@ fn sync_status_list_and_open_url() { 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!(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. 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"); diff --git a/src-tauri/src/logo.rs b/src-tauri/src/logo.rs index ae4fd90..e04a360 100644 --- a/src-tauri/src/logo.rs +++ b/src-tauri/src/logo.rs @@ -540,8 +540,8 @@ mod tests { let out = process_logo(&bytes).unwrap(); let m = &out.metrics; assert_eq!((m.source_width, m.source_height), (2172, 724)); - assert!((4.5..=4.7).contains(&m.aspect), "aspect {}", m.aspect); - assert!((0.40..=0.46).contains(&m.ink_density), "density {}", m.ink_density); + assert!((5.3..=5.6).contains(&m.aspect), "aspect {}", m.aspect); + assert!((0.50..=0.60).contains(&m.ink_density), "density {}", m.ink_density); assert_eq!(m.kind, LogoKind::Wordmark); assert!(out.print_png.len() <= 120 * 1024, "print is {} bytes", out.print_png.len()); assert!(out.knockout_png.is_some()); diff --git a/src/lib/types.ts b/src/lib/types.ts index ac89252..fafdfe2 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -273,7 +273,7 @@ export const INDIAN_STATES: { code: string; name: string }[] = [ { code: "09", name: "Uttar Pradesh" }, { code: "10", name: "Bihar" }, { code: "11", name: "Sikkim" }, - { code: "12", name: "Sampleachal Pradesh" }, + { code: "12", name: "Arunachal Pradesh" }, { code: "13", name: "Nagaland" }, { code: "14", name: "Manipur" }, { code: "15", name: "Mizoram" }, diff --git a/src/pdf/blocks/blocks.test.tsx b/src/pdf/blocks/blocks.test.tsx index b6f48bb..bf2fbad 100644 --- a/src/pdf/blocks/blocks.test.tsx +++ b/src/pdf/blocks/blocks.test.tsx @@ -210,10 +210,10 @@ describe("Row", () => { }); 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 () => { - const email = "vendor.name@examplestudios.co.in"; + const email = "anon.longsurname.voiceover@examplestudios.co.in"; let info!: FitInfo; const pdf = await renderPdfText( frame( diff --git a/src/pdf/engine/logo.slots.test.ts b/src/pdf/engine/logo.slots.test.ts index 957ea1e..231edd7 100644 --- a/src/pdf/engine/logo.slots.test.ts +++ b/src/pdf/engine/logo.slots.test.ts @@ -15,7 +15,7 @@ const logo = (over: Partial): SlotLogoInput => ({ hasKnockout: true, ...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. */ 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", () => { - 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 p = computeLogoPlacement({ logo: Sample_P, slot: s }); + const p = computeLogoPlacement({ logo: SAMPLE_LOGO, slot: s }); expect(p.h).toBeCloseTo(28, 6); expect(p.w).toBeGreaterThanOrEqual(128); expect(p.w).toBeLessThanOrEqual(130); @@ -117,9 +117,9 @@ describe("computeLogoPlacement text-only allowances", () => { 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 p = computeLogoPlacement({ logo: Sample_P, slot: s }); + const p = computeLogoPlacement({ logo: SAMPLE_LOGO, slot: s }); expect(p.h).toBeCloseTo(36, 6); expect(p.w).toBeCloseTo(36 * 4.62, 6); expect(p.x).toBeCloseTo(60.5, 6); @@ -142,13 +142,13 @@ describe("computeLogoPlacement text-only allowances", () => { describe("computeLogoPlacement on every slot", () => { 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) { - const p = computeLogoPlacement({ logo: Sample_P, slot: s }); + const p = computeLogoPlacement({ logo: SAMPLE_LOGO, slot: s }); expect(p.slotKind).toBe("lockup"); expect(inside(p.box, lockupTarget(s)), id).toBe(true); 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; 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", () => { 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.x).toBeCloseTo(s.lockup!.x, 6); }); diff --git a/src/pdf/engine/logo.test.ts b/src/pdf/engine/logo.test.ts index da5fb46..5cd7389 100644 --- a/src/pdf/engine/logo.test.ts +++ b/src/pdf/engine/logo.test.ts @@ -5,7 +5,7 @@ const BOX = { w: 170, h: 54 }; const base = { box: BOX, availableWidth: 299, minTextWidth: 160 }; 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 }); expect(p.w).toBeCloseTo(170, 6); expect(p.h).toBeCloseTo(170 / 4.6, 6); diff --git a/src/pdf/engine/measure.test.tsx b/src/pdf/engine/measure.test.tsx index f5d48ad..b274163 100644 --- a/src/pdf/engine/measure.test.tsx +++ b/src/pdf/engine/measure.test.tsx @@ -12,7 +12,7 @@ const SAMPLES: string[] = [ "Voice-over session 12 - narration for module 3", "Total ₹1,23,456.78", "AVATAR WAVE To Ty Yo", - "vendor.name@examplestudios.co.in", + "anon.longsurname.voiceover@examplestudios.co.in", "29ABCDE1234F1Z5", "office fi fl ffi final", "BILLED TO", @@ -130,7 +130,7 @@ describe("measurer helpers", () => { }); 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); }); }); diff --git a/src/pdf/templates/band/band.test.tsx b/src/pdf/templates/band/band.test.tsx index 2be26ed..ce3dd1f 100644 --- a/src/pdf/templates/band/band.test.tsx +++ b/src/pdf/templates/band/band.test.tsx @@ -173,7 +173,7 @@ describe("Slate Band: dark band and logo", () => { for (const [i, page] of r.pdf.pages.entries()) { const text = page.items.map((t) => t.str).join(" "); 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"); } }); }); diff --git a/src/pdf/templates/cobalt/cobalt.test.tsx b/src/pdf/templates/cobalt/cobalt.test.tsx index d40ea59..81ce9f9 100644 --- a/src/pdf/templates/cobalt/cobalt.test.tsx +++ b/src/pdf/templates/cobalt/cobalt.test.tsx @@ -316,7 +316,7 @@ describe("closing and footer", () => { 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(" "); 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 () => { diff --git a/src/pdf/templates/harness.ts b/src/pdf/templates/harness.ts index edda96f..c610116 100644 --- a/src/pdf/templates/harness.ts +++ b/src/pdf/templates/harness.ts @@ -31,8 +31,8 @@ const FIXTURE_CLIENT_GSTIN = "29ABCDE1234F1Z5"; const baseVendor: Settings = { vendorName: "Test Vendor", - vendorAddress: "1 Test Road, Bangalore - 560001", - vendorEmail: "vendor@example.com", + vendorAddress: "27 Example Apartments, Sample Layout, Test Nagar, Bangalore - 560001", + vendorEmail: "vendor.test@example.in", vendorPhone: "9000000000", vendorPan: "ABCDE1234F", vendorGstin: "", @@ -165,7 +165,7 @@ export function fixtureLogo(kind: LogoKind): LogoInput | null { let hit = cache.get(kind); if (!hit) { 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 [width, height] = [png.readUInt32BE(16), png.readUInt32BE(20)]; 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, }; } 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 }; } else { 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", 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)", vendorName: "Venkatachalam Longsurname Ramasubramanian Voice Studios Pvt", }, diff --git a/src/pdf/templates/highlighter/highlighter.test.tsx b/src/pdf/templates/highlighter/highlighter.test.tsx index 204cbdc..74cb457 100644 --- a/src/pdf/templates/highlighter/highlighter.test.tsx +++ b/src/pdf/templates/highlighter/highlighter.test.tsx @@ -321,7 +321,7 @@ describe("totals and closing", () => { const last = r.result.pages - 1; const closing = byId(r, CLOSING_ID, last); 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); const sign = byId(r, SLOT_ID.signatory, last); expect(sign.box.w).toBeCloseTo(V.closing.ruledW, 0); diff --git a/src/pdf/templates/ledger/ledger.test.tsx b/src/pdf/templates/ledger/ledger.test.tsx index e5c596f..eb1487c 100644 --- a/src/pdf/templates/ledger/ledger.test.tsx +++ b/src/pdf/templates/ledger/ledger.test.tsx @@ -119,14 +119,14 @@ describe("what the header does with a logo", () => { 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); // 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 [mark] = imageBoxes(square); const markBox = SLOTS["crimson-grid"]!.mark!; expect(mark.box.h).toBeLessThanOrEqual(markBox.h + 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 () => { @@ -148,7 +148,7 @@ describe("what the header does with a logo", () => { const r = await renderFixture({ ...withLogo, logo: "none" }, prefsFor("A4", { templateId: t.id })); 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. - 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.filter((i) => i.bottom < 150).length, t.id).toBe(1); expect(r.pdf.raw, t.id).toContain("Poppins"); diff --git a/src/pdf/templates/marble/marble.test.tsx b/src/pdf/templates/marble/marble.test.tsx index d3aa545..38786a8 100644 --- a/src/pdf/templates/marble/marble.test.tsx +++ b/src/pdf/templates/marble/marble.test.tsx @@ -157,7 +157,7 @@ describe("header", () => { 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 name = items(r, "Sample P")[0]; + const name = items(r, "TEST VENDOR")[0]; expect(name.bottom).toBeCloseTo(124.9, 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 () => { const r = await renderFixture({ name: "o", items: 2, logo: "real" }, prefsFor("A4", { templateId: "marble" })); 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 () => { @@ -250,7 +250,7 @@ describe("logo", () => { }); 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()); expect(header(wide)).toHaveLength(0); 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 () => { 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 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)); diff --git a/src/pdf/templates/neutral/neutral.test.tsx b/src/pdf/templates/neutral/neutral.test.tsx index fe22a0c..a6c51e7 100644 --- a/src/pdf/templates/neutral/neutral.test.tsx +++ b/src/pdf/templates/neutral/neutral.test.tsx @@ -122,8 +122,8 @@ describe("page geometry", () => { describe("helpers", () => { it("initialsOf takes the first and last word", () => { - expect(initialsOf("Test Vendor")).toBe("AP"); - expect(initialsOf("Venkatachalam Longsurname Ramasubramanian Voice Studios Pvt")).toBe("AP"); + expect(initialsOf("Test Vendor")).toBe("TV"); + expect(initialsOf("Venkatachalam Longsurname Ramasubramanian Voice Studios Pvt")).toBe("VP"); expect(initialsOf("Bongbetic")).toBe("B"); expect(initialsOf(" ")).toBe(""); 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 })); expect(imageCount(none), t.id).toBe(0); 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. expect(nodes(none.result.core.layout[0].nodes).filter((n) => n.type === "svg" && !n.decor), t.id).toHaveLength(0); } diff --git a/src/pdf/templates/northline/northline.test.tsx b/src/pdf/templates/northline/northline.test.tsx index 9f5a89a..60e2aa6 100644 --- a/src/pdf/templates/northline/northline.test.tsx +++ b/src/pdf/templates/northline/northline.test.tsx @@ -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); const text = textsOf(r, r.result.pages - 1); 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 () => { diff --git a/src/pdf/templates/slots.optin.ts b/src/pdf/templates/slots.optin.ts index 12723e6..b7504a7 100644 --- a/src/pdf/templates/slots.optin.ts +++ b/src/pdf/templates/slots.optin.ts @@ -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 * 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. */ const TYPICAL_DENSITY = 0.4; diff --git a/src/pdf/testing/fixtures/sample-wordmark-logo.png b/src/pdf/testing/fixtures/sample-wordmark-logo.png new file mode 100644 index 0000000..6ed5e7c Binary files /dev/null and b/src/pdf/testing/fixtures/sample-wordmark-logo.png differ diff --git a/src/pdf/testing/goldens.json b/src/pdf/testing/goldens.json index e47bdd0..cc4e47d 100644 --- a/src/pdf/testing/goldens.json +++ b/src/pdf/testing/goldens.json @@ -1,23 +1,23 @@ { "classic-a4-tax-3": { "pages": 1, - "fingerprint": "35b03b20aeec3dcbb808300b6e8f0b1867dfd69d5fd6f69aaee891c354955092", + "fingerprint": "87b12a5c1b1a9b45ac6a8fd5862c8e7b5de8d203731fe4e485872dea5bbb07cd", "invoiceNumber": "ST/2026-001", "totalText": "₹8,625.80", "pageLabel": "Page 1 of 1", "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" }, "classic-letter-unreg-60": { "pages": 3, - "fingerprint": "78d60e1b72dac0e40fea96cbf34c35cfeb637dbe0bc9754dcecba72231197e75", + "fingerprint": "6999be0875f78e2c87aee1fd0ffa1dfe651faa7a7c0a9b4c2f78c1bdff53c7d4", "invoiceNumber": "ST/2026-002", "totalText": "₹1,56,000.00", "pageLabel": "Page 1 of 3", "textHashes": [ - "1728b53a177e385584d2dc33b2e71607f860886632f32a5911364f86f345c950", + "4a0cf8a7c1565037edd4d9941251e030fd9e6fca25845b971465e5cf9d916936", "870e9a70c0546550d002e5eb5728f342b1d235094c6b645ddda68a6306b33f7b", "fca30345bc73289cdcad200f888f917ff9a5386cdd64e41ea7dccc64f2b7e5e0" ],