aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/test-app-core.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/test-app-core.mjs')
-rw-r--r--scripts/theme-studio/test-app-core.mjs140
1 files changed, 140 insertions, 0 deletions
diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs
new file mode 100644
index 00000000..0befeb43
--- /dev/null
+++ b/scripts/theme-studio/test-app-core.mjs
@@ -0,0 +1,140 @@
+// Unit tests for the pure app logic (app-core.js): the package-face model and
+// the dropdown option list. These are the functions Stage 7 made importable.
+// 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 {
+ nameToHex, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, optList,
+} from './app-core.js';
+
+const here = fileURLToPath(new URL('.', import.meta.url));
+const PAL = [['#67809c', 'blue'], ['#e8bd30', 'gold']];
+
+test('nameToHex: Normal — resolves a palette name to its hex', () => {
+ assert.equal(nameToHex('blue', PAL), '#67809c');
+ assert.equal(nameToHex('gold', PAL), '#e8bd30');
+});
+
+test('nameToHex: Normal — a raw #hex passes through unchanged', () => {
+ assert.equal(nameToHex('#abcdef', PAL), '#abcdef');
+});
+
+test('nameToHex: Boundary/Error — null, empty, and unknown names give null', () => {
+ assert.equal(nameToHex(null, PAL), null);
+ assert.equal(nameToHex('', PAL), null);
+ assert.equal(nameToHex(undefined, PAL), null);
+ assert.equal(nameToHex('chartreuse', PAL), null);
+});
+
+test('optList: Normal — default entry then the whole palette', () => {
+ assert.deepEqual(optList('#67809c', PAL), [['', '— default —'], ...PAL]);
+});
+
+test('optList: Boundary — empty cur is "have", so no (gone) entry', () => {
+ assert.deepEqual(optList('', PAL), [['', '— default —'], ...PAL]);
+});
+
+test('optList: Error — a cur not in the palette is surfaced as (gone) first', () => {
+ const list = optList('#123456', PAL);
+ assert.deepEqual(list[0], ['', '— default —']);
+ assert.deepEqual(list[1], ['#123456', '(gone) #123456']);
+ assert.deepEqual(list.slice(2), PAL);
+});
+
+test('buildPkgmap: Normal — seeds faces, resolving names and applying defaults', () => {
+ const apps = { 'org-mode': { faces: [
+ ['org-todo', 'todo', { fg: 'blue', bold: true }],
+ ['org-done', 'done', { inherit: 'org-todo' }],
+ ] } };
+ const m = buildPkgmap(apps, PAL);
+ assert.equal(m['org-mode']['org-todo'].fg, '#67809c');
+ assert.equal(m['org-mode']['org-todo'].bold, true);
+ assert.equal(m['org-mode']['org-todo'].source, 'default');
+ assert.equal(m['org-mode']['org-todo'].height, 1);
+ assert.equal(m['org-mode']['org-done'].inherit, 'org-todo');
+ assert.equal(m['org-mode']['org-done'].fg, null);
+});
+
+test('buildPkgmap: Boundary — a face with no default dict still seeds blank', () => {
+ const m = buildPkgmap({ a: { faces: [['f', 'f']] } }, PAL);
+ assert.deepEqual(m.a.f, {
+ fg: null, bg: null, bold: false, italic: false, underline: false,
+ strike: false, inherit: null, height: 1, source: 'default',
+ });
+});
+
+test('effResolve: Normal — a face with a value returns it', () => {
+ const m = { a: { f: { fg: '#67809c', inherit: null } } };
+ assert.equal(effResolve(m, 'a', 'f', 'fg'), '#67809c');
+});
+
+test('effResolve: Normal — follows the inherit chain when unset', () => {
+ const m = { a: {
+ base: { bg: '#0d0b0a', inherit: null },
+ mid: { bg: null, inherit: 'base' },
+ leaf: { bg: null, inherit: 'mid' },
+ } };
+ assert.equal(effResolve(m, 'a', 'leaf', 'bg'), '#0d0b0a');
+});
+
+test('effResolve: Boundary — unset with no inherit, or a missing face, gives null', () => {
+ const m = { a: { f: { fg: null, inherit: null } } };
+ assert.equal(effResolve(m, 'a', 'f', 'fg'), null);
+ assert.equal(effResolve(m, 'a', 'nope', 'fg'), null);
+});
+
+test('effResolve: Error — an inherit cycle terminates at null, no overflow', () => {
+ const m = { a: { x: { fg: null, inherit: 'y' }, y: { fg: null, inherit: 'x' } } };
+ assert.equal(effResolve(m, 'a', 'x', 'fg'), null);
+});
+
+test('packagesForExport: Normal — exports sourced faces, omits height 1', () => {
+ const m = { a: { f: {
+ fg: '#67809c', bg: null, bold: true, italic: false, underline: false,
+ strike: false, inherit: null, height: 1, source: 'user',
+ } } };
+ const out = packagesForExport(m);
+ assert.equal(out.a.f.fg, '#67809c');
+ assert.equal(out.a.f.source, 'user');
+ assert.ok(!('height' in out.a.f), 'height 1 is omitted');
+});
+
+test('packagesForExport: Boundary — keeps a non-default height', () => {
+ const m = { a: { f: { fg: null, bg: null, source: 'user', height: 1.2 } } };
+ assert.equal(packagesForExport(m).a.f.height, 1.2);
+});
+
+test('packagesForExport: Error — faces with an unknown source are skipped', () => {
+ const m = { a: { f: { fg: '#67809c', source: 'system' } } };
+ assert.deepEqual(packagesForExport(m), {});
+});
+
+test('mergePackagesInto: Normal — fills missing fields with defaults', () => {
+ const m = {};
+ mergePackagesInto(m, { a: { f: { fg: '#112233' } } });
+ assert.deepEqual(m.a.f, {
+ fg: '#112233', bg: null, bold: false, italic: false, underline: false,
+ strike: false, inherit: null, height: 1, source: 'user',
+ });
+});
+
+test('mergePackagesInto: Boundary — undefined pkgs is a no-op', () => {
+ const m = { a: { f: { fg: '#000000' } } };
+ mergePackagesInto(m, undefined);
+ assert.deepEqual(m, { a: { f: { fg: '#000000' } } });
+});
+
+// Guards the one-source-of-truth contract, same as the colormath integrity test:
+// the page must carry app-core.js's body (sans exports) verbatim. Requires
+// `python3 generate.py` to have run first.
+const stripExports = (s) =>
+ s.split('\n').filter((l) => !l.startsWith('export')).join('\n').replace(/\s+$/, '');
+
+test('inline-integrity: theme-studio.html contains the app-core.js body verbatim', () => {
+ const body = stripExports(readFileSync(here + 'app-core.js', 'utf8'));
+ const html = readFileSync(here + 'theme-studio.html', 'utf8');
+ assert.ok(html.includes(body), 'generated page is missing the app-core.js body verbatim');
+});