aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/test-app-core.mjs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-02 23:01:54 -0400
committerCraig Jennings <c@cjennings.net>2026-07-02 23:01:54 -0400
commit630cfddc7060c7019815f8e82f87fb629aefebfa (patch)
treeb645a1c56616094eca74efdffe499a51250e1bb1 /scripts/theme-studio/test-app-core.mjs
parentf9b379ed52e4b5947bb2a2fc8d2c54c872e39791 (diff)
downloaddotemacs-630cfddc7060c7019815f8e82f87fb629aefebfa.tar.gz
dotemacs-630cfddc7060c7019815f8e82f87fb629aefebfa.zip
feat(theme-studio): explicit absolute-vs-relative face height kind
JSON collapses 2.0 to 2 on save, so a height's number type can't say whether it's a fixed 1/10pt value or a relative multiplier. The face model now carries an explicit heightMode field (abs/rel) through seed, save/load, and export. build-theme.el coerces :height from the kind: abs exports an integer, rel a float, so a relative 2.0 renders as 2.0, never 2. Faces saved before the field existed infer the kind once on load (JS: integer to abs, fractional to rel; Python keeps the authored type, so a float 2.0 seed stays relative) and persist it on the next save. The mode-line seed carries abs explicitly, and WIP.json's eight seeded heights are stamped with their kinds. Regenerating the theme from the stamped WIP.json produces an identical WIP-theme.el, so the round-trip holds.
Diffstat (limited to 'scripts/theme-studio/test-app-core.mjs')
-rw-r--r--scripts/theme-studio/test-app-core.mjs55
1 files changed, 52 insertions, 3 deletions
diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs
index 217ea0e6..c6e6650b 100644
--- a/scripts/theme-studio/test-app-core.mjs
+++ b/scripts/theme-studio/test-app-core.mjs
@@ -634,7 +634,7 @@ test('normalizePkgFace: Normal — fills every package face field', () => {
assert.deepEqual(normalizePkgFace({ fg: 'blue', bold: true, inherit: 'base' }, 'default', PAL), {
fg: '#67809c', bg: null, 'distant-fg': null, family: null, weight: 'bold',
slant: null, underline: null, strike: null, overline: null,
- inherit: 'base', height: 1, box: null, inverse: false, extend: false,
+ inherit: 'base', height: 1, heightMode: null, box: null, inverse: false, extend: false,
source: 'default',
});
});
@@ -687,7 +687,7 @@ test('buildPkgmap: Boundary — a face with no default dict still seeds blank',
assert.deepEqual(m.a.f, {
fg: null, bg: null, 'distant-fg': null, family: null, weight: null,
slant: null, underline: null, strike: null, overline: null,
- inherit: null, height: 1, box: null, inverse: false, extend: false,
+ inherit: null, height: 1, heightMode: null, box: null, inverse: false, extend: false,
source: 'default',
});
});
@@ -773,13 +773,62 @@ test('packagesForExport: Boundary — unset additive attrs are omitted', () => {
}
});
+// --- height kind (explicit absolute vs relative) -----------------------------
+
+test('migrateLegacyFace: Normal — infers heightMode from the number on load', () => {
+ assert.equal(migrateLegacyFace({ height: 130 }).heightMode, 'abs');
+ assert.equal(migrateLegacyFace({ height: 1.2 }).heightMode, 'rel');
+});
+
+test('migrateLegacyFace: Normal — an explicit heightMode wins over inference', () => {
+ // the integral-float case: JSON collapsed 2.0 to 2, but the stored kind rules
+ assert.equal(migrateLegacyFace({ height: 2, heightMode: 'rel' }).heightMode, 'rel');
+ assert.equal(migrateLegacyFace({ height: 1.5, heightMode: 'abs' }).heightMode, 'abs');
+});
+
+test('migrateLegacyFace: Boundary — no height, identity, or non-number infers no kind', () => {
+ assert.ok(!('heightMode' in migrateLegacyFace({})));
+ assert.ok(!('heightMode' in migrateLegacyFace({ height: null })));
+ assert.ok(!('heightMode' in migrateLegacyFace({ height: 1 })));
+ assert.ok(!('heightMode' in migrateLegacyFace({ height: 'big' })));
+});
+
+test('normalizePkgFace: Normal — heightMode survives normalization', () => {
+ const f = normalizePkgFace({ height: 2, heightMode: 'rel' }, 'user');
+ assert.equal(f.height, 2);
+ assert.equal(f.heightMode, 'rel');
+});
+
+test('normalizePkgFace: Boundary — unset height leaves heightMode null', () => {
+ assert.equal(normalizePkgFace({}, 'user').heightMode, null);
+});
+
+test('packagesForExport: Normal — heightMode rides along with a non-default height', () => {
+ const m = { a: { f: normalizePkgFace({ fg: '#67809c', height: 130, heightMode: 'abs' }, 'user') } };
+ const o = packagesForExport(m).a.f;
+ assert.equal(o.height, 130);
+ assert.equal(o.heightMode, 'abs');
+});
+
+test('packagesForExport: Boundary — no heightMode when height is the default 1', () => {
+ const m = { a: { f: normalizePkgFace({ fg: '#67809c' }, 'user') } };
+ assert.ok(!('heightMode' in packagesForExport(m).a.f));
+});
+
+test('packagesForExport: Normal — integral rel multiplier keeps its kind through JSON (round-trip)', () => {
+ const m = { a: { f: normalizePkgFace({ fg: 'blue', height: 2, heightMode: 'rel' }, 'user', PAL) } };
+ const back = normalizePkgFace(JSON.parse(JSON.stringify(packagesForExport(m))).a.f, 'user');
+ assert.equal(back.height, 2);
+ assert.equal(back.heightMode, 'rel');
+});
+
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, 'distant-fg': null, family: null, weight: null,
slant: null, underline: null, strike: null, overline: null,
- inherit: null, height: 1, box: null, inverse: false, extend: false,
+ inherit: null, height: 1, heightMode: null, box: null, inverse: false, extend: false,
source: 'user',
});
});