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.
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -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"));
|
||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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 reference ring or stamp is ever drawn.
|
// No reference 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 () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
|
After Width: | Height: | Size: 30 KiB |
@@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||