Ctrl K
ComunidadGuías…

test/themes.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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");
  }
});