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 => ({ 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); }); });