aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/test-seed-core.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/test-seed-core.mjs')
-rw-r--r--scripts/theme-studio/test-seed-core.mjs173
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');
+});