tests/designFonts.test.ts
Rama main · réplica de lectura del repositorio original.
// PER-DESIGN FONT PAIRING — the contract between a design that names a face
// and a server that has to serve it.
//
// Three seams, and every one of them is a place the two halves could drift:
//
// 1. THE VALIDATORS. A face this build does not have is refused out loud on
// the write side and dropped quietly on the read side, which is the same
// split every other design field already has.
// 2. THE FAMILY NAME. `typographyVars()` writes a family name into a
// visitor's inline style with no knowledge of settings.json; the server
// emits a family under exactly that name. If the two ever computed it
// differently the page would silently fall back and nothing would say so
// — which is precisely the failure mode a test has to pin.
// 3. THE UNION. One stylesheet carries the instance's four slots AND the
// active design's faces, and the design's Latin face composes with the
// INSTANCE's Arabic slot, per character, exactly as a settings-chosen
// face does.
import assert from "node:assert/strict";
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { after, before, describe, it } from "node:test";
import {
designFontRefs,
normalizeChrome,
stockChrome,
typographyVars,
validateChrome,
type TypographyDesign,
} from "../shared/designChrome.ts";
import {
designFontFamily,
designFontRef,
parseDesignFontRefs,
} from "../shared/fontCatalog.ts";
import { stockDesign } from "../shared/design.ts";
import {
buildDesignFontCss,
buildFontCss,
designCatalogIds,
siteFontsSignature,
SYSTEM_SLOTS,
type FontSlots,
} from "../server/fonts.ts";
import { activeDesignFontRefs } from "../server/designs.ts";
import { initSite } from "../server/site.ts";
import { initVault } from "../server/vault.ts";
import { makeDir, makeVault, removeVault } from "./helpers/vault.ts";
const data = makeDir();
const root = makeVault({ "Home.md": "# Home\n" });
/** A family the CSS builder will believe in: `readFaces()` reads meta.json and
* nothing else, so a cache is one JSON file. The ranges are the shapes Google
* actually emits, because the composite's whole job is narrowing them. */
function fakeCache(id: string, family: string, ranges: Record<string, string>): void {
const dir = path.join(data, "fonts", "catalog", id);
mkdirSync(dir, { recursive: true });
const faces = Object.entries(ranges).map(([subset, range]) => ({
file: `${subset}-400.woff2`,
weight: "400",
style: "normal",
range,
subset,
}));
writeFileSync(
path.join(dir, "meta.json"),
`${JSON.stringify({ id, family, fetched: new Date().toISOString(), faces }, null, 2)}\n`,
);
}
before(() => {
initSite({ ASTROLABE_DATA: data });
initVault(root);
fakeCache("eb-garamond", "EB Garamond", { latin: "U+0000-00FF, U+0131" });
fakeCache("jetbrains-mono", "JetBrains Mono", { latin: "U+0000-00FF" });
fakeCache("lora", "Lora", { latin: "U+0000-00FF" });
fakeCache("amiri", "Amiri", { arabic: "U+0600-06FF, U+FE70-FEFC", latin: "U+0000-00FF" });
fakeCache("literata", "Literata", { latin: "U+0000-00FF", arabic: "U+0600-06FF" });
});
after(() => removeVault(root));
const typo = (patch: Partial<TypographyDesign>): TypographyDesign => ({
...stockChrome().typography,
...patch,
});
describe("design fonts: the validators", () => {
it("accepts a catalog id on both sides", () => {
const chrome = validateChrome({ typography: { headingFont: "eb-garamond" } });
assert.equal(chrome.typography.headingFont, "eb-garamond");
assert.equal(
normalizeChrome({ typography: { headingFont: "eb-garamond" } }).typography.headingFont,
"eb-garamond",
);
});
it("REFUSES an unknown id, naming the field", () => {
assert.throws(
() => validateChrome({ typography: { headingFont: "eb-garamon" } }),
(err: Error & { path?: string; code?: string }) =>
err.path === "typography.headingFont" && err.code === "design_bad_value",
);
});
it("refuses an uploaded face — a design travels and a file does not", () => {
assert.throws(() => validateChrome({ typography: { bodyFont: "custom:mine.woff2" } }));
});
it("DROPS an unknown id on the read side rather than throwing", () => {
const chrome = normalizeChrome({ typography: { bodyFont: "nope", monoFont: "fira-code" } });
assert.equal(chrome.typography.bodyFont, undefined);
assert.equal(chrome.typography.monoFont, "fira-code");
assert.ok(!("bodyFont" in chrome.typography), "absent means the key is gone, not undefined");
});
it("null clears a face back to the instance's stack", () => {
const chrome = validateChrome({ typography: { headingFont: null } });
assert.ok(!("headingFont" in chrome.typography));
});
it("a design that names nothing is unchanged by both", () => {
const stock = stockChrome().typography;
assert.deepEqual(validateChrome({}).typography, stock);
assert.deepEqual(normalizeChrome({}).typography, stock);
});
});
describe("design fonts: what typographyVars emits", () => {
it("ABSENT is the stock token, exactly as before the feature", () => {
const vars = typographyVars(typo({}));
assert.equal(vars["--dsg-head-font"], "var(--font-serif)");
assert.equal(vars["--dsg-body-font"], "var(--font-serif)");
assert.equal(vars["--dsg-mono-font"], "var(--font-mono)");
});
it("PRESENT is the resolved family with the token behind it", () => {
const vars = typographyVars(typo({ headingFont: "eb-garamond" }));
assert.equal(
vars["--dsg-head-font"],
`"${designFontFamily("eb-garamond", "prose")}", var(--font-serif)`,
);
// The fallback is the feature: a face that never downloads leaves the page
// exactly as it was.
assert.ok(vars["--dsg-head-font"].endsWith("var(--font-serif)"));
});
it("the family follows the STACK the role stands in", () => {
const sans = typographyVars(typo({ headingFamily: "sans", headingFont: "inter" }));
assert.equal(sans["--dsg-head-font"], `"${designFontFamily("inter", "ui")}", var(--font-ui)`);
});
it("a mono role with no face of its own resolves to monoFont", () => {
const vars = typographyVars(
typo({ headingFamily: "mono", bodyFamily: "mono", monoFont: "jetbrains-mono" }),
);
const family = `"${designFontFamily("jetbrains-mono", "mono")}", var(--font-mono)`;
assert.equal(vars["--dsg-head-font"], family);
assert.equal(vars["--dsg-body-font"], family);
assert.equal(vars["--dsg-mono-font"], family);
});
it("a role's own face still outranks the mono stack's", () => {
const vars = typographyVars(
typo({ headingFamily: "mono", headingFont: "fira-code", monoFont: "jetbrains-mono" }),
);
assert.equal(
vars["--dsg-head-font"],
`"${designFontFamily("fira-code", "mono")}", var(--font-mono)`,
);
});
});
describe("design fonts: which faces a design asks for", () => {
it("names each (id, slot) pair once", () => {
const refs = designFontRefs(typo({ headingFont: "lora", bodyFont: "lora" }));
assert.equal(refs.length, 1);
assert.deepEqual(refs[0], designFontRef("lora", "prose"));
});
it("the same face in two stacks is two families", () => {
const refs = designFontRefs(
typo({ headingFont: "lora", bodyFamily: "sans", bodyFont: "lora" }),
);
assert.deepEqual(
refs.map((r) => r.family).sort(),
[designFontFamily("lora", "prose"), designFontFamily("lora", "ui")].sort(),
);
});
it("a mono face is asked for by the roles that inherit it", () => {
const refs = designFontRefs(typo({ bodyFamily: "mono", monoFont: "jetbrains-mono" }));
assert.deepEqual(refs.map((r) => r.family), [designFontFamily("jetbrains-mono", "mono")]);
});
it("the wire form survives the round trip, and drops what it cannot use", () => {
const refs = designFontRefs(typo({ headingFont: "lora", monoFont: "fira-code" }));
const spec = refs.map((r) => `${r.slot}:${r.id}`).join(",");
assert.deepEqual(parseDesignFontRefs(spec), refs);
assert.deepEqual(parseDesignFontRefs("prose:not-a-font,junk,arabic:amiri"), []);
});
});
describe("design fonts: the served stylesheet", () => {
it("emits a composite under the family typographyVars names", async () => {
const css = await buildDesignFontCss(
designFontRefs(typo({ headingFont: "eb-garamond" })),
SYSTEM_SLOTS,
);
assert.match(css, new RegExp(`font-family: "${designFontFamily("eb-garamond", "prose")}"`));
assert.match(css, /url\("\/api\/fonts\/catalog\/eb-garamond\/latin-400\.woff2"\)/);
// Never an external host, on this route as on every other.
assert.ok(!css.includes("fonts.gstatic.com"));
});
it("a design's LATIN face composes with the INSTANCE's Arabic slot", async () => {
const slots: FontSlots = { ...SYSTEM_SLOTS, arabic: "amiri" };
const css = await buildDesignFontCss(
designFontRefs(typo({ headingFont: "eb-garamond" })),
slots,
);
const family = designFontFamily("eb-garamond", "prose");
// Both halves under ONE family, Arabic first and narrowed to Arabic
// codepoints — the per-character pick the instance-wide system makes.
assert.equal(css.match(new RegExp(`font-family: "${family}"`, "g"))?.length, 2);
// The Arabic half is declared FIRST and carries the measured optical-size
// compensation, exactly as it does for a settings-chosen face.
assert.ok(css.indexOf("amiri/arabic-400") < css.indexOf("eb-garamond/latin-400"));
assert.match(css, /size-adjust: 138%/);
assert.match(css, /unicode-range: U\+0600-06FF, U\+FE70-FEFC/);
// …and only the Arabic half is compensated: the number describes THAT
// family's body height, and is meaningless on the Latin one.
assert.equal(css.match(/size-adjust/g)?.length, 1);
});
it("carves the Arabic codepoints out of the Latin half, so the pick is by coverage", async () => {
// `literata` is faked with an Arabic subset it does not really have — the
// shape of a Latin family whose Google slices overlap the Arabic blocks.
// Standing beside a chosen Arabic face, that subset must not be emitted at
// all, or the browser would pick by declaration order instead.
const slots: FontSlots = { ...SYSTEM_SLOTS, arabic: "amiri" };
const css = await buildDesignFontCss(
designFontRefs(typo({ headingFont: "literata" })),
slots,
);
assert.match(css, /literata\/latin-400\.woff2/);
assert.ok(!css.includes("literata/arabic-400.woff2"));
});
it("a design's ARABIC face becomes the Arabic half, pairing with the instance's Latin", async () => {
const slots: FontSlots = { ...SYSTEM_SLOTS, prose: "lora" };
const refs = designFontRefs(typo({ headingFont: "amiri" }));
const css = await buildDesignFontCss(refs, slots);
assert.match(css, /url\("\/api\/fonts\/catalog\/amiri\/arabic-400\.woff2"\)/);
assert.match(css, /url\("\/api\/fonts\/catalog\/lora\/latin-400\.woff2"\)/);
// The counterpart has to be CACHED for that to work, so it is named.
assert.deepEqual(designCatalogIds(refs, slots).sort(), ["amiri", "lora"]);
});
it("a face that is not on disk emits nothing — the fallback answers", async () => {
const css = await buildDesignFontCss(
designFontRefs(typo({ headingFont: "merriweather" })),
SYSTEM_SLOTS,
);
assert.equal(css.trim(), "");
});
it("the sheet is the UNION: instance slots plus the design's faces", async () => {
const slots: FontSlots = { ...SYSTEM_SLOTS, prose: "lora" };
const refs = designFontRefs(typo({ headingFont: "eb-garamond" }));
const css =
(await buildFontCss(slots, { prefix: "Astrolabe", root: true })) +
(await buildDesignFontCss(refs, slots));
assert.match(css, /font-family: "AstrolabeProse"/);
assert.match(css, /--font-serif: "AstrolabeProse"/);
assert.match(css, new RegExp(`font-family: "${designFontFamily("eb-garamond", "prose")}"`));
// The design's family is NEVER remapped onto an instance token: a design's
// type reaches its page through --dsg-*, never by moving :root.
assert.ok(!/--font-serif: "AstrolabeDsg/.test(css));
});
});
describe("design fonts: the cache-buster", () => {
it("is empty when neither the instance nor the design names a face", () => {
assert.equal(siteFontsSignature(SYSTEM_SLOTS, []), "");
});
it("is present for a design's faces even with every slot on system", () => {
const sig = siteFontsSignature(SYSTEM_SLOTS, designFontRefs(typo({ bodyFont: "lora" })));
assert.notEqual(sig, "");
assert.match(sig, /prose:lora/);
});
it("moves when the design's faces move", () => {
const a = siteFontsSignature(SYSTEM_SLOTS, designFontRefs(typo({ bodyFont: "lora" })));
const b = siteFontsSignature(SYSTEM_SLOTS, designFontRefs(typo({ bodyFont: "literata" })));
assert.notEqual(a, b);
});
});
describe("design fonts: which design is the public one", () => {
it("reads the ACTIVE design's faces, and answers [] when there is none", () => {
writeFileSync(
path.join(data, "designs.json"),
`${JSON.stringify({ schema: 1, activeId: null, designs: [], themes: [] }, null, 2)}\n`,
);
assert.deepEqual(activeDesignFontRefs(), []);
const quiet = stockDesign("quiet", "Quiet");
const loud = stockDesign("loud", "Loud");
loud.chrome.typography.headingFont = "eb-garamond";
writeFileSync(
path.join(data, "designs.json"),
`${JSON.stringify(
{ schema: 1, activeId: "loud", designs: [quiet, loud], themes: [] },
null,
2,
)}\n`,
);
assert.deepEqual(activeDesignFontRefs(), [designFontRef("eb-garamond", "prose")]);
// A design that is merely STORED is not the public site's typography.
writeFileSync(
path.join(data, "designs.json"),
`${JSON.stringify(
{ schema: 1, activeId: "quiet", designs: [quiet, loud], themes: [] },
null,
2,
)}\n`,
);
assert.deepEqual(activeDesignFontRefs(), []);
});
});