tests/designChromeShapes.test.ts
Rama main · réplica de lectura del repositorio original.
// THE CHROME FIELDS — a masthead's shape, the page's ground, the shape of the
// end of the page, and how a menu item is drawn.
//
// Four enums across the whole chrome document, and every one of them is a place
// the two halves of this schema could drift. What is pinned here:
//
// 1. ABSENT IS THE OLD BEHAVIOUR. A `designs.json` written before these
// fields existed — which is every design on every instance that upgrades —
// must validate, and must come back drawing exactly what it drew
// yesterday. That is the whole reason they are enums with a default
// carried on `chrome` rather than a schema bump.
// 2. A VALUE THIS BUILD DOES NOT KNOW IS A NAMED 400. The path in the error is
// what the owner's notice prints and what makes a hand-edited file fixable
// without a debugger, so the tests read the path, not just the throw.
// 3. THE TWO VALIDATORS AGREE ON WHAT IS LEGAL AND DISAGREE ONLY ON WHAT TO
// DO ABOUT AN ILLEGAL VALUE. `normalizeChrome` renders a page;
// `validateChrome` says no out loud. A value one of them accepts and the
// other refuses is a design that saves and never loads, or loads and never
// saves.
// 4. `surface`, `scenery`, `ornament`, `shell` AND `frame` ARE TOP-LEVEL
// CHROME KEYS. They are
// the scalars that are not inside `header` / `footer` / `nav`, which is
// exactly the shape a loop over those three groups walks past — it did
// once, in check-presets, when `surface` was the only one. There are five
// now, so the same omission would be five times as quiet — and one of
// them, `shell`, decides where the chrome IS.
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
CHROME_FRAMES,
CHROME_SIGNATURES,
CHROME_ORNAMENTS,
CHROME_SCENERIES,
CHROME_SHELLS,
CHROME_SURFACES,
DesignError,
FOOTER_FORMS,
HEADER_LAYOUTS,
NAV_STYLES,
normalizeChrome,
stockChrome,
validateChrome,
} from "../shared/designChrome.ts";
import { validateDesign } from "../shared/design.ts";
/** A stored document with `chrome` swapped in. */
function doc(chrome: unknown): Record<string, unknown> {
return {
id: "d1",
name: "A design",
schema: 1,
theme: null,
site: { width: 720, density: "regular" },
chrome,
sections: [],
article: {},
createdMs: 1,
updatedMs: 1,
};
}
/** The path a rejection names, or "" if it did not reject. */
function refusal(chrome: unknown): string {
try {
validateChrome(chrome);
return "";
} catch (err) {
assert.ok(err instanceof DesignError, "a rejection must be a DesignError");
return err.path;
}
}
describe("design chrome: the shape fields", () => {
it("a chrome written before the fields existed keeps the shape it had", () => {
// Exactly what an older `designs.json` carries: the four groups, none of
// the new keys, no `surface` at all.
const old = {
nav: { items: [], fallback: "topics", showSearch: true, showThemeToggle: true, showLangSwitch: true },
typography: {},
header: { layout: "stacked", density: "regular", sticky: "nav" },
footer: { columns: [], copyright: "", align: "center" },
};
for (const chrome of [normalizeChrome(old), validateChrome(old)]) {
assert.equal(chrome.header.layout, "stacked");
assert.equal(chrome.nav.style, "plain");
assert.equal(chrome.footer.form, "columns");
assert.equal(chrome.surface, "flat");
// THE WHOLE PROMISE OF THE SCENERY RELEASE, in two lines. Every design
// on every instance that upgrades was authored without these keys, and
// "no sky, and the wordmark on the dividers" is precisely the page each
// of them drew yesterday.
assert.equal(chrome.scenery, "none");
assert.equal(chrome.ornament, "asterism");
// AND THE ROOM. `shell` is the field that moves walls rather than
// furniture, so an upgraded design coming back as anything but "stack"
// would be every existing site relaid out overnight.
assert.equal(chrome.shell, "stack");
assert.equal(chrome.frame, "plain");
}
});
it("the stock chrome is the old page, forever", () => {
const stock = stockChrome();
assert.equal(stock.surface, "flat");
assert.equal(stock.scenery, "none");
assert.equal(stock.ornament, "asterism");
assert.equal(stock.shell, "stack");
assert.equal(stock.frame, "plain");
assert.equal(stock.nav.style, "plain");
assert.equal(stock.footer.form, "columns");
assert.equal(stock.header.layout, "stacked");
});
it("every value in every vocabulary survives BOTH validators unchanged", () => {
for (const layout of HEADER_LAYOUTS) {
const raw = { header: { layout } };
assert.equal(normalizeChrome(raw).header.layout, layout);
assert.equal(validateChrome(raw).header.layout, layout);
}
for (const style of NAV_STYLES) {
const raw = { nav: { style } };
assert.equal(normalizeChrome(raw).nav.style, style);
assert.equal(validateChrome(raw).nav.style, style);
}
for (const form of FOOTER_FORMS) {
const raw = { footer: { form } };
assert.equal(normalizeChrome(raw).footer.form, form);
assert.equal(validateChrome(raw).footer.form, form);
}
for (const surface of CHROME_SURFACES) {
const raw = { surface };
assert.equal(normalizeChrome(raw).surface, surface);
assert.equal(validateChrome(raw).surface, surface);
}
for (const scenery of CHROME_SCENERIES) {
const raw = { scenery };
assert.equal(normalizeChrome(raw).scenery, scenery);
assert.equal(validateChrome(raw).scenery, scenery);
}
for (const ornament of CHROME_ORNAMENTS) {
const raw = { ornament };
assert.equal(normalizeChrome(raw).ornament, ornament);
assert.equal(validateChrome(raw).ornament, ornament);
}
for (const shell of CHROME_SHELLS) {
const raw = { shell };
assert.equal(normalizeChrome(raw).shell, shell);
assert.equal(validateChrome(raw).shell, shell);
}
for (const frame of CHROME_FRAMES) {
const raw = { frame };
assert.equal(normalizeChrome(raw).frame, frame);
assert.equal(validateChrome(raw).frame, frame);
}
});
it("the two grounds compose: a surface and a scenery are separate answers", () => {
// The reason this is a second key rather than five more values of the
// first. A texture of ink on the sheet and a field of light behind it are
// not alternatives, and a design that wants laid paper under a starfield
// must be able to say so in one document.
const both = validateChrome({ surface: "paper", scenery: "starfield" });
assert.equal(both.surface, "paper");
assert.equal(both.scenery, "starfield");
assert.deepEqual(normalizeChrome(structuredClone(both)), both);
});
it("the two masthead shapes this release adds are real values, not strings", () => {
// The point of the release, stated as a test: a newspaper plate and a
// magazine bar are choices the engine now has and did not.
assert.ok(HEADER_LAYOUTS.includes("rule"));
assert.ok(HEADER_LAYOUTS.includes("banner"));
assert.equal(validateChrome({ header: { layout: "banner" } }).header.layout, "banner");
});
it("a sidebar masthead is refused, by name, because it is deferred and not shipped", () => {
// Not a curiosity: "sidebar" is the value somebody will hand-edit in after
// reading a changelog, and the honest answer is a 400 naming the field
// rather than a page that silently draws the centred masthead.
assert.equal(refusal({ header: { layout: "sidebar" } }), "header.layout");
assert.equal(normalizeChrome({ header: { layout: "sidebar" } }).header.layout, "stacked");
});
it("an unknown value is a 400 that NAMES the field", () => {
assert.equal(refusal({ header: { layout: "diagonal" } }), "header.layout");
assert.equal(refusal({ nav: { style: "neon" } }), "nav.style");
assert.equal(refusal({ footer: { form: "sitemap" } }), "footer.form");
assert.equal(refusal({ surface: "linen" }), "surface");
// A non-string is the same refusal: the vocabulary is the check.
assert.equal(refusal({ surface: 3 }), "surface");
// The new vocabularies answer the same way, and the names matter: a reader
// who hand-edits `scenery: "starlight"` after a changelog gets the field
// back, not a page that quietly stands in nothing. (It was `nebula` here
// until `nebula` became a real world, which is the shape of test that
// stops being a test the moment the thing it names ships.)
assert.equal(refusal({ scenery: "starlight" }), "scenery");
assert.equal(refusal({ ornament: "logo" }), "ornament");
assert.equal(refusal({ scenery: 3 }), "scenery");
assert.equal(refusal({ shell: "sidebar" }), "shell");
assert.equal(refusal({ frame: "card" }), "frame");
assert.equal(refusal({ nav: { style: null } }), "nav.style");
});
it("the lenient half never throws and always lands on the default", () => {
for (const junk of [undefined, null, 7, "linen", {}, []]) {
assert.equal(normalizeChrome({ surface: junk }).surface, "flat");
assert.equal(normalizeChrome({ scenery: junk }).scenery, "none");
assert.equal(normalizeChrome({ ornament: junk }).ornament, "asterism");
assert.equal(normalizeChrome({ shell: junk }).shell, "stack");
assert.equal(normalizeChrome({ frame: junk }).frame, "plain");
assert.equal(normalizeChrome({ nav: { style: junk } }).nav.style, "plain");
assert.equal(normalizeChrome({ footer: { form: junk } }).footer.form, "columns");
assert.equal(normalizeChrome({ header: { layout: junk } }).header.layout, "stacked");
}
});
it("the whole document validator carries the chrome's rejection out", () => {
// `validateDesign` calls `validateChrome`, and the path it throws is what
// the route turns into a named 400 rather than a 500.
assert.throws(
() => validateDesign(doc({ surface: "linen" })),
(err: unknown) => err instanceof DesignError && err.path === "surface",
);
const ok = validateDesign(doc({ surface: "paper", footer: { form: "grand" }, nav: { style: "brackets" } }));
assert.equal(ok.chrome.surface, "paper");
assert.equal(ok.chrome.footer.form, "grand");
assert.equal(ok.chrome.nav.style, "brackets");
});
it("a design carrying every new field round-trips through a full save", () => {
// The shape a preset applied through the import route produces: written,
// validated, read back leniently, and identical both times.
const written = validateChrome({
surface: "ruled",
scenery: "topography",
ornament: "lozenge",
shell: "console",
frame: "window",
header: { layout: "rule", density: "tall" },
footer: { form: "colophon", align: "center" },
nav: { style: "underline" },
});
assert.deepEqual(normalizeChrome(structuredClone(written)), written);
});
});
describe("portable signature art direction", () => {
it("leaves older and custom designs unstyled by default", () => {
assert.equal(validateChrome({}).signature, "none");
assert.equal(normalizeChrome({ signature: "unrecognized" }).signature, "none");
assert.equal(refusal({ signature: "unrecognized" }), "signature");
});
it("preserves every art direction across export, rename and import", () => {
for (const signature of CHROME_SIGNATURES) {
const original = validateDesign(doc({ signature }));
const renamed = { ...original, id: "my-personal-copy", name: "My own name" };
const imported = validateDesign(JSON.parse(JSON.stringify(renamed)));
assert.equal(imported.chrome.signature, signature);
assert.equal(normalizeChrome(imported.chrome).signature, signature);
}
});
});