test/themes.test.js
Rama main · réplica de lectura del repositorio original.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";
import { listThemes, loadTheme } from "../src/theme.js";
/**
* The theme contract as a permanent guard. Every theme must honour the token
* shape the renderer and the model both assume; a theme that validates but
* breaks the contract would surface here rather than as a subtle render bug.
*/
test("every theme honours the token contract", async () => {
const names = await listThemes();
assert.ok(names.length >= 5, `expected at least the reference themes, got ${names.length}`);
for (const name of names) {
const t = await loadTheme(name);
const raw = YAML.parse(await readFile(path.join(process.cwd(), "themes", `${name}.yaml`), "utf8"));
const at = (cond, msg) => assert.ok(cond, `${name}: ${msg}`);
at(raw.name === name, "raw name matches filename");
at(raw.label, "label present");
at(raw.voice, "voice present (the model reads it)");
for (const k of ["bg", "surface", "ink", "ink_muted", "accent", "on_accent"]) {
at(t.palette?.[k], `palette.${k}`);
}
for (const s of ["title", "section"]) {
at(t.surfaces?.[s], `surfaces.${s} explicit`);
for (const k of ["bg", "ink", "muted"]) at(t.surfaces[s][k], `surfaces.${s}.${k}`);
}
for (const k of ["display", "heading", "subhead", "body", "caption", "eyebrow", "stat"]) {
at(t.type?.[k], `type.${k}`);
at(t.type[k].family && t.type[k].size, `type.${k}.family/size`);
}
at(t.grid?.margin && t.grid?.band, "grid.margin and grid.band");
at(t.shape?.radius?.card != null, "shape.radius.card");
at(typeof t.plate?.enabled, "boolean", "plate.enabled is a boolean");
// The decorative background layer is theme-owned native shapes. Each decor
// entry must carry a shape, geometry and fill, and the shape must be one
// the renderer knows how to draw.
const decor = t.tokens?.background?.decor;
if (decor != null) {
at(Array.isArray(decor), "background.decor is an array when present");
const known = new Set(["ellipse", "rect", "roundRect", "triangle"]);
for (const d of decor) {
at(known.has(d.shape), `decor shape "${d.shape}" is known`);
at(Number.isFinite(d.x) && Number.isFinite(d.y) && Number.isFinite(d.w) && Number.isFinite(d.h), "decor geometry is numeric");
at(typeof d.fill === "string" && /^#[0-9a-fA-F]{3,8}$/.test(d.fill), `decor fill "${d.fill}" is hex`);
}
}
// The three-layer rule, machine-checked: the model's voice must never see
// a colour, a font name or geometry.
const voice = JSON.stringify(raw.voice ?? {});
at(!/\#[0-9a-fA-F]{3,8}\b/.test(voice), "voice contains no hex colours");
at(!/\b(family|font|px|pt|margin|spacing)\b/i.test(voice), "voice contains no font/geometry vocabulary");
}
});