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.
273 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|