aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-19 11:42:52 -0400
committerCraig Jennings <c@cjennings.net>2026-06-19 11:42:52 -0400
commit7ef4737d3a06fb8b39eab3342658be76219ac6dc (patch)
tree5c90acb2d69a809ff970b4055ea654ff37df2b49
parent3bdf9b23204f4f908d3ef0353b2fe2eb5f9f3d2c (diff)
downloaddotemacs-7ef4737d3a06fb8b39eab3342658be76219ac6dc.tar.gz
dotemacs-7ef4737d3a06fb8b39eab3342658be76219ac6dc.zip
test(theme-studio): cover defensive branches and the palette generator
Added the uncovered fallback branches in app-core (migrateLegacyFace null input, normalizePkgFace's source fallback chain, mergePackagesInto's null/new-app guards, boxCss shading a relief from the bg when no box color is set) and in colormath (apca's equal-luminance return-0 and low-contrast clamp, isPureEndpointHex). New test-palette-generator-core.mjs drives planPaletteGenerator across every scheme, vibe, source mode, and the fill-gaps intents, since those internals are only reachable through the public planner. colormath branch 96 -> 99%, palette-generator-core funcs 97 -> 100%, node suite 237 tests. The remaining gaps are the deep palette-column edge branches, deferred as diminishing returns on already line-covered code.
-rw-r--r--scripts/theme-studio/test-app-core.mjs32
-rw-r--r--scripts/theme-studio/test-colormath.mjs31
-rw-r--r--scripts/theme-studio/test-palette-generator-core.mjs78
3 files changed, 140 insertions, 1 deletions
diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs
index e0d3c321a..c6473ae96 100644
--- a/scripts/theme-studio/test-app-core.mjs
+++ b/scripts/theme-studio/test-app-core.mjs
@@ -1103,3 +1103,35 @@ test('faceCss: Error — opts omitted still works', () => {
assert.equal(faceCss({}, '#111', null),
'color:#111;font-weight:normal;font-style:normal;text-decoration:none');
});
+
+// --- defensive / fallback branches -------------------------------------------
+
+test('migrateLegacyFace: Boundary — null/undefined input yields an empty object', () => {
+ assert.deepEqual(migrateLegacyFace(null), {});
+ assert.deepEqual(migrateLegacyFace(undefined), {});
+});
+
+test('normalizePkgFace: Normal — source falls back through arg, d.source, then "user"', () => {
+ assert.equal(normalizePkgFace({}, 'default').source, 'default'); // arg wins
+ assert.equal(normalizePkgFace({source: 'cleared'}).source, 'cleared'); // d.source
+ assert.equal(normalizePkgFace({}).source, 'user'); // default
+});
+
+test('mergePackagesInto: Boundary — null packages is a no-op', () => {
+ const map = {existing: {f: {fg: '#111'}}};
+ mergePackagesInto(map, null);
+ assert.deepEqual(Object.keys(map), ['existing']);
+});
+test('mergePackagesInto: Normal — a new app key is created', () => {
+ const map = {};
+ mergePackagesInto(map, {newapp: {'face-a': {fg: '#112233', source: 'user'}}});
+ assert.ok(map.newapp && map.newapp['face-a']);
+ assert.equal(map.newapp['face-a'].fg, '#112233');
+});
+
+test('boxCss: Boundary — released with no color but a bg shades from the bg', () => {
+ const fromBg = boxCss({style: 'released'}, '#808080');
+ // not the translucent no-bg fallback, and a real two-edge relief
+ assert.notEqual(fromBg, 'inset 1px 1px 0 #ffffff33,inset -1px -1px 0 #00000066');
+ assert.match(fromBg, /^inset 1px 1px 0 \S+,inset -1px -1px 0 \S+$/);
+});
diff --git a/scripts/theme-studio/test-colormath.mjs b/scripts/theme-studio/test-colormath.mjs
index 992d35bcc..ee40e3437 100644
--- a/scripts/theme-studio/test-colormath.mjs
+++ b/scripts/theme-studio/test-colormath.mjs
@@ -13,7 +13,7 @@ import {
srgb2oklab, oklab2oklch, oklch2oklab, oklch2hex, apca, deltaE,
hex2rgb, rl, contrast, rating, hsv2rgb, rgb2hsv, rgb2hex,
oklab2lrgb, inGamut, lrgb2hex, planeCell, paletteWarnings,
- reliefColors,
+ reliefColors, isPureEndpointHex,
} from './colormath.js';
const close = (a, b, eps = 0.005) => Math.abs(a - b) <= eps;
@@ -270,3 +270,32 @@ test('inline-integrity: theme-studio.html contains the colormath.js body verbati
const html = readFileSync(here + 'theme-studio.html', 'utf8');
assert.ok(html.includes(body), 'generated page is missing the colormath.js body verbatim');
});
+
+// --- apca contrast branches + isPureEndpointHex ------------------------------
+
+test('apca: Boundary — equal luminance returns 0 (below the delta-Y floor)', () => {
+ assert.equal(apca('#808080', '#808080'), 0);
+});
+test('apca: Normal — dark text on light background is positive (Ybg > Ytxt)', () => {
+ assert.ok(apca('#000000', '#ffffff') > 0);
+});
+test('apca: Normal — light text on dark background is negative (else branch)', () => {
+ assert.ok(apca('#ffffff', '#000000') < 0);
+});
+test('apca: Boundary — near-equal colors below the floor clamp to 0', () => {
+ assert.equal(apca('#808080', '#828282'), 0);
+});
+
+test('isPureEndpointHex: Normal — pure black and white are endpoints', () => {
+ assert.equal(isPureEndpointHex('#ffffff'), true);
+ assert.equal(isPureEndpointHex('#000000'), true);
+ assert.equal(isPureEndpointHex('#FFFFFF'), true);
+});
+test('isPureEndpointHex: Boundary — any other color is not an endpoint', () => {
+ assert.equal(isPureEndpointHex('#010101'), false);
+ assert.equal(isPureEndpointHex('#123456'), false);
+});
+test('isPureEndpointHex: Error — null/empty is not an endpoint', () => {
+ assert.equal(isPureEndpointHex(null), false);
+ assert.equal(isPureEndpointHex(''), false);
+});
diff --git a/scripts/theme-studio/test-palette-generator-core.mjs b/scripts/theme-studio/test-palette-generator-core.mjs
new file mode 100644
index 000000000..d3d725957
--- /dev/null
+++ b/scripts/theme-studio/test-palette-generator-core.mjs
@@ -0,0 +1,78 @@
+// Unit tests for the palette generator planner (palette-generator-core.js).
+// Only planPaletteGenerator and entriesForGeneratedColumn are exported, so the
+// internal scheme / vibe / source-mode / intent logic is exercised by driving
+// the planner across each of those input dimensions.
+// Run: node --test scripts/theme-studio/
+
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { planPaletteGenerator, entriesForGeneratedColumn } from './palette-generator-core.js';
+
+const GROUND = { bg: '#0d0b0a', fg: '#f0fef0' };
+const PAL = [['#0d0b0a', 'bg'], ['#f0fef0', 'fg'], ['#67809c', 'blue'], ['#e8bd30', 'gold']];
+const rng = () => 0.42; // deterministic, so failures repeat
+
+test('planPaletteGenerator: Normal — every scheme produces a valid plan', () => {
+ for (const scheme of ['random', 'analogous', 'triadic', 'manual']) {
+ const plan = planPaletteGenerator(PAL, GROUND, { scheme, accentCount: 4, spanCount: 0, rng });
+ assert.equal(plan.scheme, scheme);
+ assert.ok(Array.isArray(plan.columns), `${scheme} columns`);
+ assert.equal(typeof plan.summary.generated, 'number');
+ }
+});
+
+test('planPaletteGenerator: Normal — every vibe biases hues without error', () => {
+ for (const vibe of ['warm', 'cool', 'earthy', 'muted', 'pastel', 'deep',
+ 'jewel', 'neon', 'strange', 'balanced']) {
+ const plan = planPaletteGenerator(PAL, GROUND,
+ { scheme: 'analogous', vibe, accentCount: 5, spanCount: 0, rng });
+ assert.equal(plan.vibe, vibe);
+ assert.ok(Array.isArray(plan.columns), `${vibe} columns`);
+ }
+});
+
+test('planPaletteGenerator: Normal — every source mode resolves', () => {
+ for (const sourceMode of ['bg-fg', 'palette', 'none', 'selected']) {
+ const plan = planPaletteGenerator(PAL, GROUND,
+ { sourceMode, selectedHex: '#9b5fd0', scheme: 'analogous', accentCount: 3, spanCount: 0, rng });
+ assert.ok(['bg-fg', 'palette', 'none', 'selected'].includes(plan.sourceMode));
+ assert.ok(Array.isArray(plan.columns));
+ }
+});
+
+test('planPaletteGenerator: Boundary — selected source with no valid hex falls back to bg-fg', () => {
+ const plan = planPaletteGenerator(PAL, GROUND,
+ { sourceMode: 'selected', scheme: 'analogous', accentCount: 2, spanCount: 0, rng });
+ assert.equal(plan.sourceMode, 'bg-fg');
+});
+
+test('planPaletteGenerator: Normal — fill-gaps and fill-hue-gaps intents produce plans', () => {
+ for (const intent of ['fill-gaps', 'fill-hue-gaps']) {
+ const plan = planPaletteGenerator(PAL, GROUND, { intent, accentCount: 4, spanCount: 0, rng });
+ assert.equal(plan.intent, intent);
+ assert.ok(Array.isArray(plan.columns));
+ }
+});
+
+test('planPaletteGenerator: Boundary — an empty palette still plans', () => {
+ const plan = planPaletteGenerator([], { bg: '#000000', fg: '#ffffff' },
+ { scheme: 'analogous', accentCount: 3, spanCount: 0, rng });
+ assert.ok(Array.isArray(plan.columns));
+ assert.equal(typeof plan.summary.generated, 'number');
+});
+
+test('planPaletteGenerator: Boundary — spanCount expands a column into members', () => {
+ const plan = planPaletteGenerator(PAL, GROUND,
+ { scheme: 'analogous', accentCount: 2, spanCount: 2, rng });
+ if (plan.columns.length) assert.ok(plan.columns[0].members.length >= 1);
+});
+
+test('entriesForGeneratedColumn: Normal — maps a planned column to palette entries', () => {
+ const plan = planPaletteGenerator(PAL, GROUND,
+ { scheme: 'analogous', accentCount: 1, spanCount: 0, rng });
+ if (plan.columns.length) {
+ const entries = entriesForGeneratedColumn(plan.columns[0]);
+ assert.ok(Array.isArray(entries) && entries.length >= 1);
+ assert.ok(typeof entries[0][0] === 'string' && entries[0][0].startsWith('#'));
+ }
+});