diff options
Diffstat (limited to 'scripts/theme-studio/test-seed-core.mjs')
| -rw-r--r-- | scripts/theme-studio/test-seed-core.mjs | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/scripts/theme-studio/test-seed-core.mjs b/scripts/theme-studio/test-seed-core.mjs new file mode 100644 index 00000000..8ad6fc60 --- /dev/null +++ b/scripts/theme-studio/test-seed-core.mjs @@ -0,0 +1,173 @@ +// Unit tests for the seeding engine (seed-core.js): the seed model as data and +// the pure seed() operation. seed() projects theme-coloring-guide.org's role/seed +// table onto the three owned tiers (syntax, UI, org), reusing colormath.js OKLCH +// generation for the palette shades and heading ramp. +// Run: node --test scripts/theme-studio/ + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { buildModel, seed, ROLES } from './seed-core.js'; +import { oklchOf } from './colormath.js'; + +const here = fileURLToPath(new URL('.', import.meta.url)); +const HEX = /^#[0-9a-f]{6}$/; + +// --- the model: OKLCH-generated palette shades --------------------------- + +test('buildModel: every swatch is a valid in-gamut hex', () => { + const m = buildModel(); + for (const [name, hex] of Object.entries(m.swatch)) { + assert.match(hex, HEX, `swatch ${name} is not a hex: ${hex}`); + } +}); + +test('buildModel: builtin blue-grey is the blue hue at lower chroma and lightness', () => { + const m = buildModel(); + const blue = oklchOf(m.swatch.blue), grey = oklchOf(m.swatch['blue-grey']); + assert.ok(grey.C < blue.C, 'blue-grey should have lower chroma than blue'); + assert.ok(grey.L < blue.L, 'blue-grey should be darker than blue'); + assert.ok(Math.abs(grey.H - blue.H) < 15, 'blue-grey should keep the blue hue'); +}); + +test('buildModel: the heading ramp descends in lightness, level 1 strongest', () => { + const m = buildModel(); + assert.equal(m.ramp.length, 4); + for (let i = 1; i < m.ramp.length; i++) { + assert.ok(oklchOf(m.ramp[i - 1]).L > oklchOf(m.ramp[i]).L, + `ramp step ${i} should be darker than step ${i - 1}`); + } +}); + +// --- seed(): syntax tier ------------------------------------------------- + +test('seed: syntax builtin (bi) resolves to blue-grey', () => { + const m = buildModel(), s = seed(m).syntax; + assert.equal(s.bi.fg, m.swatch['blue-grey']); + assert.notEqual(s.bi.weight, 'bold'); +}); + +test('seed: syntax definition (fnd) is gold and bold; call (fnc) is quieter gold, not bold', () => { + const m = buildModel(), s = seed(m).syntax; + assert.equal(s.fnd.fg, m.swatch.gold); + assert.equal(s.fnd.weight, 'bold'); + assert.equal(s.fnc.fg, m.swatch['gold-quiet']); + assert.notEqual(s.fnc.weight, 'bold'); +}); + +test('seed: syntax base + structure + keyword + literal + string land on their swatches', () => { + const m = buildModel(), s = seed(m).syntax; + assert.equal(s.var.fg, m.swatch.fg); // base identity + assert.equal(s.p.fg, m.swatch.fg); + assert.equal(s.op.fg, m.swatch['muted-fg']); // structure + assert.equal(s.punc.fg, m.swatch['muted-fg']); + assert.equal(s.kw.fg, m.swatch.blue); // control + assert.equal(s.kw.weight, 'bold'); + assert.equal(s.num.fg, m.swatch.terracotta); // literal + assert.equal(s.con.fg, m.swatch.terracotta); + assert.equal(s.str.fg, m.swatch.sage); // string + assert.equal(s.ty.fg, m.swatch.regal); // type +}); + +test('seed: docstring and comment take italic; comment is the low-contrast lane', () => { + const m = buildModel(), s = seed(m).syntax; + assert.equal(s.doc.slant, 'italic'); + assert.equal(s.cm.slant, 'italic'); + assert.equal(s.cm.fg, m.swatch.comment); +}); + +test('seed: regexp and escape use the teal/bright-green lanes; bg is the ground', () => { + const m = buildModel(), s = seed(m).syntax; + assert.equal(s.re.fg, m.swatch.teal); + assert.equal(s.rxgb.fg, m.swatch.teal); + assert.equal(s.esc.fg, m.swatch['sage-bright']); + assert.equal(s.bg.fg, m.swatch.ground); +}); + +// --- seed(): UI tier ----------------------------------------------------- + +test('seed: transient state faces are background-only (no foreground)', () => { + const m = buildModel(), u = seed(m).ui; + for (const f of ['region', 'hl-line', 'highlight', 'show-paren-match']) { + assert.ok(u[f].bg, `${f} should carry a background tint`); + assert.ok(!u[f].fg, `${f} should not set a foreground`); + } +}); + +test('seed: link is blue and underlined (redundant encoding)', () => { + const m = buildModel(), u = seed(m).ui; + assert.equal(u.link.fg, m.swatch.blue); + assert.ok(u.link.underline, 'link should be underlined'); +}); + +test('seed: signal faces sit on the convention hues, with weight for redundancy', () => { + const m = buildModel(), u = seed(m).ui; + assert.equal(u.error.fg, m.swatch.red); + assert.equal(u.error.weight, 'bold'); + assert.equal(u.warning.fg, m.swatch.amber); + assert.equal(u.success.fg, m.swatch.green); + assert.equal(u['isearch-fail'].fg, m.swatch.red); +}); + +test('seed: active chrome differs from idle chrome', () => { + const m = buildModel(), u = seed(m).ui; + assert.notEqual(u['mode-line'].fg, u['mode-line-inactive'].fg); + assert.notEqual(u['line-number-current-line'].fg, u['line-number'].fg); +}); + +// --- seed(): org package tier ------------------------------------------- + +test('seed: packages carries only org-mode (non-org bespoke packages untouched)', () => { + const m = buildModel(), p = seed(m).packages; + assert.deepEqual(Object.keys(p), ['org-mode']); +}); + +test('seed: org headings ramp — level 1 strongest and bold, deeper levels quieter', () => { + const m = buildModel(), org = seed(m).packages['org-mode']; + assert.equal(org['org-level-1'].weight, 'bold'); + assert.ok(oklchOf(org['org-level-1'].fg).L > oklchOf(org['org-level-2'].fg).L, + 'org-level-1 should be lighter (stronger) than org-level-2'); +}); + +test('seed: org code-like faces reuse the syntax literal lane', () => { + const m = buildModel(), org = seed(m).packages['org-mode']; + assert.equal(org['org-code'].fg, m.swatch.terracotta); + assert.equal(org['org-code'].inherit, 'fixed-pitch'); +}); + +test('seed: org link underlined; org-done receded with strikethrough; org-todo warm', () => { + const m = buildModel(), org = seed(m).packages['org-mode']; + assert.ok(org['org-link'].underline, 'org-link should be underlined'); + assert.ok(org['org-done'].strike, 'org-done should be struck through'); + assert.equal(org['org-todo'].fg, m.swatch.red); +}); + +// --- purity -------------------------------------------------------------- + +test('seed: pure — two calls deep-equal and the model is not mutated', () => { + const m = buildModel(); + const before = JSON.stringify(m); + const a = seed(m), b = seed(m); + assert.deepEqual(a, b); + assert.equal(JSON.stringify(m), before, 'seed() must not mutate the model'); +}); + +test('ROLES: the table exposes the guide roles as data', () => { + assert.equal(ROLES.builtin.swatch, 'blue-grey'); + assert.equal(ROLES.def.swatch, 'gold'); + assert.equal(ROLES.def.weight, 'bold'); + assert.equal(ROLES.state.channel, 'bg'); + assert.equal(ROLES.sig_link.underline, true); +}); + +// --- inline-integrity ---------------------------------------------------- +// The page must carry seed-core.js's body (sans import/export) verbatim — the +// same strip generate.py applies. Requires `python3 generate.py`. +import { stripInlinedBody } from './inline-strip.mjs'; + +test('inline-integrity: theme-studio.html contains the seed-core.js body verbatim', () => { + const body = stripInlinedBody(readFileSync(here + 'seed-core.js', 'utf8')); + const html = readFileSync(here + 'theme-studio.html', 'utf8'); + assert.ok(html.includes(body), 'generated page is missing the seed-core.js body verbatim'); +}); |
