Files
Voiced/src/pdf/engine/logo.slots.test.ts
T
xavierk c78bf1a851 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.
2026-10-05 10:18:53 +05:30

273 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { MIN_LOGO_HEIGHT, computeLogoPlacement, type SlotLogoInput } from "./logo";
import { SLOTS } from "../templates/slots.generated";
import { OPTIN_SLOTS, slotFor } from "../templates/slots.optin";
import { SLOT_PAGE, type Box, type TemplateId, type TemplateSlot } from "../templates/slots.types";
const measured = Object.entries(SLOTS).filter((e): e is [TemplateId, TemplateSlot] => e[1] !== null);
const optIn = Object.entries(OPTIN_SLOTS) as [TemplateId, TemplateSlot][];
const logo = (over: Partial<SlotLogoInput>): SlotLogoInput => ({
aspect: 1,
density: 0.5,
kind: "mark",
includesName: false,
hasKnockout: true,
...over,
});
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!);
const markTarget = (s: TemplateSlot): Box => (s.textOnly ? s.allowance : (s.mark ?? s.lockup)!);
const inside = (inner: Box, outer: Box, eps = 1e-6) =>
inner.x >= outer.x - eps && inner.y >= outer.y - eps && inner.x + inner.w <= outer.x + outer.w + eps && inner.y + inner.h <= outer.y + outer.h + eps;
const closeBox = (a: Box, b: Box, tol = 0.5) => {
for (const k of ["x", "y", "w", "h"] as const) expect(Math.abs(a[k] - b[k]), `${k}: ${a[k]} vs ${b[k]}`).toBeLessThanOrEqual(tol);
};
describe("slots.generated", () => {
it("has a measured slot for the nine templates with a reference placeholder, none for the rest", () => {
expect(measured.map(([id]) => id).sort()).toEqual(
["citrus-split", "cobalt-stripe", "crimson-grid", "monogram", "monolith", "serenity", "slate-band", "tangerine-ledger", "teal-swoosh"],
);
for (const id of ["classic", "marble", "highlighter", "linea", "purple-pop"] as const) expect(SLOTS[id]).toBeNull();
});
it("keeps every box on the page, with the mark inside the lockup", () => {
for (const [id, s] of measured) {
const page = { x: 0, y: 0, w: SLOT_PAGE.w, h: SLOT_PAGE.h };
expect(s.lockup && inside(s.lockup, page), id).toBe(true);
if (s.mark && s.lockup) expect(inside(s.mark, s.lockup, 0.01), id).toBe(true);
}
});
it("text-only placeholders (Monolith, Tangerine) carry a bigger allowance anchored at the text's edge", () => {
expect(measured.filter(([, s]) => s.textOnly).map(([id]) => id).sort()).toEqual(["monolith", "tangerine-ledger"]);
const mono = SLOTS.monolith!;
expect(mono.allowance).toEqual({ x: 400, y: 54.75, w: 160, h: 28 });
expect(mono.allowance.x + mono.allowance.w).toBeCloseTo(mono.lockup!.x + mono.lockup!.w, 6);
const tang = SLOTS["tangerine-ledger"]!;
expect(tang.allowance).toEqual({ x: 60.5, y: 62, w: 220, h: 36 });
expect(tang.allowance.x).toBeCloseTo(tang.lockup!.x, 6);
for (const [id, s] of measured) if (!s.textOnly) expect(s.allowance, id).toEqual(s.lockup);
});
it("only Slate Band is a dark surface", () => {
expect(measured.filter(([, s]) => s.darkSurface).map(([id]) => id)).toEqual(["slate-band"]);
});
});
describe("slots.optin", () => {
it("covers the four templates without a placeholder, all opt-in and off by default", () => {
expect(optIn.map(([id]) => id).sort()).toEqual(["highlighter", "linea", "marble", "purple-pop"]);
for (const [id, s] of optIn) {
expect(s.optIn, id).toBe(true);
expect(s.defaultOn, id).toBe(false);
}
});
it("slotFor prefers a measured slot and gives Classic none", () => {
expect(slotFor("monogram")).toBe(SLOTS.monogram);
expect(slotFor("marble")).toBe(OPTIN_SLOTS.marble);
expect(slotFor("classic")).toBeNull();
});
});
describe("computeLogoPlacement identity: the placeholder itself comes back as its own box", () => {
for (const [id, s] of measured) {
it(`${id} lockup`, () => {
const l = lockupTarget(s);
const p = computeLogoPlacement({
logo: logo({ aspect: l.w / l.h, density: s.lockupDensity, kind: "wordmark", includesName: true }),
slot: s,
});
expect(p.slotKind).toBe("lockup");
closeBox(p.box, l);
});
if (s.mark && !s.textOnly) {
it(`${id} mark`, () => {
const m = s.mark!;
const p = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: s.markDensity }), slot: s });
expect(p.slotKind).toBe("mark");
closeBox(p.box, m);
});
}
}
});
describe("computeLogoPlacement text-only allowances", () => {
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_LOGO, slot: s });
expect(p.h).toBeCloseTo(28, 6);
expect(p.w).toBeGreaterThanOrEqual(128);
expect(p.w).toBeLessThanOrEqual(130);
expect(p.x + p.w).toBeCloseTo(s.lockup!.x + s.lockup!.w, 6);
expect(p.y).toBeCloseTo(s.allowance.y, 6);
expect(p.slotKind).toBe("lockup");
});
it("a very wide logo in Monolith is capped by the 160 pt width", () => {
const s = SLOTS.monolith!;
const p = computeLogoPlacement({ logo: logo({ aspect: 10, density: 0.3, kind: "wordmark", includesName: true }), slot: s });
expect(p.w).toBeCloseTo(160, 6);
expect(p.h).toBeCloseTo(16, 6);
expect(p.x + p.w).toBeCloseTo(560, 6);
});
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_LOGO, slot: s });
expect(p.h).toBeCloseTo(36, 6);
expect(p.w).toBeCloseTo(36 * 4.62, 6);
expect(p.x).toBeCloseTo(60.5, 6);
});
it("a square mark in Tangerine is 36 x 36 and vertically centred in the allowance", () => {
const s = SLOTS["tangerine-ledger"]!;
const p = computeLogoPlacement({ logo: logo({ aspect: 1, density: 0.3 }), slot: s });
expect(p.w).toBeCloseTo(36, 6);
expect(p.y + p.h / 2).toBeCloseTo(62 + 18, 6);
});
it("the weight factor still uses the text's density: a dense logo shrinks to 70 % at most", () => {
const s = SLOTS["tangerine-ledger"]!;
const p = computeLogoPlacement({ logo: logo({ aspect: 4, density: 5, kind: "wordmark", includesName: true }), slot: s });
expect(p.h).toBeCloseTo(36 * 0.7, 6);
});
});
describe("computeLogoPlacement on every slot", () => {
const all: [TemplateId, TemplateSlot][] = [...measured, ...optIn];
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_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_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);
}
}
});
it("a square mark, a round emblem and a tall logo stay inside the mark box (the lockup box without one)", () => {
const shapes = [logo({ aspect: 1, density: 0.6 }), logo({ aspect: 1, density: 0.2, kind: "mark" }), logo({ aspect: 0.5, density: 0.5, kind: "tall" })];
for (const [id, s] of all) {
for (const l of shapes) {
const p = computeLogoPlacement({ logo: l, slot: s });
expect(inside(p.box, markTarget(s)), `${id} aspect ${l.aspect}`).toBe(true);
expect(p.w / p.h).toBeCloseTo(l.aspect, 6);
}
}
});
it("falls back to the lockup box for a mark-less slot even when the logo has no name", () => {
const s = SLOTS.monolith!;
expect(s.mark).toBeNull();
const p = computeLogoPlacement({ logo: logo({ aspect: 1 }), slot: s });
expect(p.slotKind).toBe("lockup");
expect(inside(p.box, s.allowance)).toBe(true);
});
it("a denser logo is drawn smaller, at most down to 70 %, and a lighter one is not enlarged", () => {
const s = SLOTS["cobalt-stripe"]!;
const m = s.mark!;
const dense = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: 1 }), slot: s });
expect(dense.h / m.h).toBeCloseTo(Math.max(0.7, Math.sqrt(s.markDensity)), 6);
const heavy = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: 50 }), slot: s });
expect(heavy.h / m.h).toBeCloseTo(0.7, 6);
const light = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: 0.01 }), slot: s });
expect(light.h).toBeCloseTo(m.h, 6);
});
it("aligns by the slot's anchor and vAlign", () => {
const s = SLOTS["crimson-grid"]!; // right, bottom
const m = s.mark!;
const p = computeLogoPlacement({ logo: logo({ aspect: 0.5, density: 0.1 }), slot: s });
expect(p.x + p.w).toBeCloseTo(m.x + m.w, 6);
expect(p.y + p.h).toBeCloseTo(m.y + m.h, 6);
const left = computeLogoPlacement({ logo: logo({ aspect: 0.5, density: 0.1 }), slot: { ...s, anchor: "left", vAlign: "top" } });
expect(left.x).toBeCloseTo(m.x, 6);
expect(left.y).toBeCloseTo(m.y, 6);
const mid = computeLogoPlacement({ logo: logo({ aspect: 0.5, density: 0.1 }), slot: { ...s, anchor: "center", vAlign: "middle" } });
expect(mid.x + mid.w / 2).toBeCloseTo(m.x + m.w / 2, 6);
expect(mid.y + mid.h / 2).toBeCloseTo(m.y + m.h / 2, 6);
});
});
describe("computeLogoPlacement surface", () => {
const slate = SLOTS["slate-band"]!;
it("uses the knockout on a dark surface, a white chip without one, and the print image elsewhere", () => {
expect(computeLogoPlacement({ logo: logo({ hasKnockout: true }), slot: slate }).variant).toBe("knockout");
const chip = computeLogoPlacement({ logo: logo({ hasKnockout: false }), slot: slate });
expect(chip.variant).toBe("chip");
expect(chip.chip).toEqual({ x: chip.x - 4, y: chip.y - 4, w: chip.w + 8, h: chip.h + 8 });
for (const [id, s] of measured) {
if (id === "slate-band") continue;
expect(computeLogoPlacement({ logo: logo({ hasKnockout: false }), slot: s }).variant, id).toBe("print");
}
expect(computeLogoPlacement({ logo: logo({}), slot: slate }).chip).toBeUndefined();
});
});
describe("computeLogoPlacement legibility floor", () => {
const slot = (h: number): TemplateSlot => ({
page: { ...SLOT_PAGE },
mark: { x: 50, y: 50, w: 100, h },
lockup: { x: 50, y: 50, w: 100, h },
markDensity: 0.25,
lockupDensity: 0.25,
anchor: "left",
vAlign: "middle",
darkSurface: false,
textOnly: false,
allowance: { x: 50, y: 50, w: 100, h },
});
it("raises a logo that the weight factor made too short to 14 pt", () => {
// contain gives 16 pt tall; density 1 against 0.25 shrinks it to 11.2 pt.
const p = computeLogoPlacement({ logo: logo({ aspect: 1, density: 1 }), slot: slot(16) });
expect(p.h).toBe(MIN_LOGO_HEIGHT);
expect(p.w).toBe(MIN_LOGO_HEIGHT);
});
it("never grows past the box: a box under 14 pt stays the limit", () => {
const p = computeLogoPlacement({ logo: logo({ aspect: 4, density: 0.25 }), slot: slot(9) });
expect(p.h).toBeCloseTo(9, 6);
expect(p.w).toBeCloseTo(36, 6);
});
it("lets the width follow the floor and clamps it to the box", () => {
const p = computeLogoPlacement({ logo: logo({ aspect: 10, density: 0.25 }), slot: slot(40) });
// contain: 100 x 10 (width-limited), floor wants 14 x 140, the box width wins.
expect(p.w).toBeCloseTo(100, 6);
expect(p.h).toBeCloseTo(10, 6);
});
});
describe("computeLogoPlacement page scale and available room", () => {
const letter = 612 / 595.28;
it("scales the box about its anchor: a right-aligned slot keeps its right edge", () => {
const s = SLOTS.monogram!;
const m = s.mark!;
const p = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: s.markDensity }), slot: s, scale: letter });
expect(p.w).toBeCloseTo(m.w * letter, 6);
expect(p.h).toBeCloseTo(m.h * letter, 6);
expect(p.x + p.w).toBeCloseTo(m.x + m.w, 6);
});
it("shrinks to the room the layout has", () => {
const s = SLOTS["teal-swoosh"]!;
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);
});
});