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.mjs354
1 files changed, 315 insertions, 39 deletions
diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs
index 8f62ae55a..217ea0e6b 100644
--- a/scripts/theme-studio/test-app-core.mjs
+++ b/scripts/theme-studio/test-app-core.mjs
@@ -7,9 +7,11 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import {
- nameToHex, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, dropdownRowTextColor, paletteOptionList, spanNeighborHex, slugify,
+ nameToHex, migrateLegacyFace, normalizePkgFace, buildPkgmap, packagesForExport, mergePackagesInto, effResolve, resolveSyntaxFg, resolveUiAttr, paletteOptionList, spanNeighborHex, slugify,
clearPalettePlan, deletePaletteColumnPlan, groundColumnMembersFromPalette, areAllLocked, lockToggleLabel, toggleLockSet,
- galleryModel, appViewKeysSorted, faceBoxNonDefaults, stepViewIndex,
+ galleryModel, appViewKeysSorted, faceBoxNonDefaults, overflowNonDefault, stepViewIndex,
+ cssWeight, faceDecoration, boxCss, faceCss, composeHoverTitle,
+ clampHeight, HEIGHT_MIN, HEIGHT_MAX,
} from './app-core.js';
import { planPaletteGenerator, entriesForGeneratedColumn } from './palette-generator-core.js';
import { oklch2hex, deltaE } from './colormath.js';
@@ -621,7 +623,7 @@ test('buildPkgmap: Normal — seeds faces, resolving names and applying defaults
] } };
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'].weight, 'bold'); // legacy bold migrated on seed
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');
@@ -630,16 +632,63 @@ test('buildPkgmap: Normal — seeds faces, resolving names and applying defaults
test('normalizePkgFace: Normal — fills every package face field', () => {
assert.deepEqual(normalizePkgFace({ fg: 'blue', bold: true, inherit: 'base' }, 'default', PAL), {
- fg: '#67809c', bg: null, bold: true, italic: false, underline: false,
- strike: false, inherit: 'base', height: 1, box: null, source: 'default',
+ 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,
+ source: 'default',
});
});
+test('migrateLegacyFace: Normal — legacy booleans become the new shape', () => {
+ assert.deepEqual(
+ migrateLegacyFace({ bold: true, italic: true, underline: true, strike: true }),
+ { weight: 'bold', slant: 'italic', underline: { style: 'line', color: null }, strike: { color: null } },
+ );
+});
+
+test('migrateLegacyFace: Boundary — false booleans clear, explicit weight/slant win', () => {
+ const m = migrateLegacyFace({ bold: false, italic: false, underline: false, strike: false });
+ assert.ok(!('weight' in m), 'bold:false sets no weight');
+ assert.ok(!('slant' in m), 'italic:false sets no slant');
+ assert.equal(m.underline, null);
+ assert.equal(m.strike, null);
+ assert.ok(!('bold' in m) && !('italic' in m), 'legacy booleans are removed');
+ // an explicit weight/slant already set is not overwritten by the legacy flag
+ assert.equal(migrateLegacyFace({ bold: true, weight: 'light' }).weight, 'light');
+ assert.equal(migrateLegacyFace({ italic: true, slant: 'oblique' }).slant, 'oblique');
+});
+
+test('migrateLegacyFace: Boundary — a new-shape face passes through unchanged (idempotent)', () => {
+ const f = { weight: 'semibold', slant: 'oblique', underline: { style: 'wave', color: '#abcdef' }, strike: { color: null } };
+ assert.deepEqual(migrateLegacyFace(f), f);
+ assert.deepEqual(migrateLegacyFace(migrateLegacyFace(f)), f);
+});
+
+test('normalizePkgFace: Normal — carries the additive attribute model', () => {
+ const f = normalizePkgFace({
+ fg: 'blue', 'distant-fg': '#222222', family: 'Iosevka',
+ overline: { color: '#abcdef' }, inverse: true, extend: 1, height: 1.4,
+ }, 'user', PAL);
+ assert.equal(f['distant-fg'], '#222222');
+ assert.equal(f.family, 'Iosevka');
+ assert.deepEqual(f.overline, { color: '#abcdef' });
+ assert.equal(f.inverse, true);
+ assert.equal(f.extend, true); // coerced to boolean
+ assert.equal(f.height, 1.4);
+});
+
+test('normalizePkgFace: Boundary — distant-fg resolves through the palette', () => {
+ const f = normalizePkgFace({ 'distant-fg': 'blue' }, 'user', PAL);
+ assert.equal(f['distant-fg'], '#67809c');
+});
+
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, box: null, source: 'default',
+ 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,
+ source: 'default',
});
});
@@ -670,15 +719,29 @@ test('effResolve: Error — an inherit cycle terminates at null, no overflow', (
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',
+ fg: '#67809c', bg: null, weight: 'bold', slant: null, underline: null,
+ strike: null, inherit: null, height: 1, source: 'user',
} } };
const out = packagesForExport(m);
assert.equal(out.a.f.fg, '#67809c');
+ assert.equal(out.a.f.weight, 'bold');
assert.equal(out.a.f.source, 'user');
+ assert.ok(!('slant' in out.a.f), 'unset slant is omitted');
assert.ok(!('height' in out.a.f), 'height 1 is omitted');
});
+test('packagesForExport: Normal — emits weight/slant/underline/strike only when set', () => {
+ const m = { a: { f: normalizePkgFace({
+ fg: '#67809c', weight: 'semibold', slant: 'oblique',
+ underline: { style: 'wave', color: '#abcdef' }, strike: { color: null },
+ }, 'user') } };
+ const o = packagesForExport(m).a.f;
+ assert.equal(o.weight, 'semibold');
+ assert.equal(o.slant, 'oblique');
+ assert.deepEqual(o.underline, { style: 'wave', color: '#abcdef' });
+ assert.deepEqual(o.strike, { color: null });
+});
+
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);
@@ -689,15 +752,47 @@ test('packagesForExport: Error — faces with an unknown source are skipped', ()
assert.deepEqual(packagesForExport(m), {});
});
+test('packagesForExport: Normal — emits additive attrs only when set', () => {
+ const m = { a: { f: normalizePkgFace({
+ fg: '#67809c', 'distant-fg': '#222222', family: 'Iosevka',
+ overline: { color: '#abcdef' }, inverse: true, extend: true,
+ }, 'user') } };
+ const o = packagesForExport(m).a.f;
+ assert.equal(o['distant-fg'], '#222222');
+ assert.equal(o.family, 'Iosevka');
+ assert.deepEqual(o.overline, { color: '#abcdef' });
+ assert.equal(o.inverse, true);
+ assert.equal(o.extend, true);
+});
+
+test('packagesForExport: Boundary — unset additive attrs are omitted', () => {
+ const m = { a: { f: normalizePkgFace({ fg: '#67809c' }, 'user') } };
+ const o = packagesForExport(m).a.f;
+ for (const k of ['distant-fg', 'family', 'overline', 'inverse', 'extend']) {
+ assert.ok(!(k in o), k + ' is omitted when unset');
+ }
+});
+
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, box: null, source: 'user',
+ 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,
+ source: 'user',
});
});
+test('mergePackagesInto: Normal — migrates a legacy preset face on import', () => {
+ const m = {};
+ mergePackagesInto(m, { a: { f: { fg: '#112233', bold: true, italic: true, underline: true } } });
+ assert.equal(m.a.f.weight, 'bold');
+ assert.equal(m.a.f.slant, 'italic');
+ assert.deepEqual(m.a.f.underline, { style: 'line', color: null });
+ assert.ok(!('bold' in m.a.f) && !('italic' in m.a.f), 'legacy booleans dropped');
+});
+
test('mergePackagesInto: Boundary — undefined pkgs is a no-op', () => {
const m = { a: { f: { fg: '#000000' } } };
mergePackagesInto(m, undefined);
@@ -724,35 +819,34 @@ test('slugify: Error — an all-disallowed name falls back to "theme"', () => {
// 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') || l.startsWith('import'))).join('\n').replace(/\s+$/, '');
+import { stripInlinedBody } from './inline-strip.mjs';
test('inline-integrity: theme-studio.html contains the app-core.js body verbatim', () => {
- const body = stripExports(readFileSync(here + 'app-core.js', 'utf8'));
+ const body = stripInlinedBody(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');
});
test('inline-integrity: theme-studio.html contains palette-generator-core.js verbatim', () => {
- const body = stripExports(readFileSync(here + 'palette-generator-core.js', 'utf8'));
+ const body = stripInlinedBody(readFileSync(here + 'palette-generator-core.js', 'utf8'));
const html = readFileSync(here + 'theme-studio.html', 'utf8');
assert.ok(html.includes(body), 'generated page is missing palette-generator-core.js verbatim');
});
test('inline-integrity: theme-studio.html contains palette-generator-ui.js verbatim', () => {
- const body = stripExports(readFileSync(here + 'palette-generator-ui.js', 'utf8'));
+ const body = stripInlinedBody(readFileSync(here + 'palette-generator-ui.js', 'utf8'));
const html = readFileSync(here + 'theme-studio.html', 'utf8');
assert.ok(html.includes(body), 'generated page is missing palette-generator-ui.js verbatim');
});
test('inline-integrity: theme-studio.html contains palette-actions.js verbatim', () => {
- const body = stripExports(readFileSync(here + 'palette-actions.js', 'utf8'));
+ const body = stripInlinedBody(readFileSync(here + 'palette-actions.js', 'utf8'));
const html = readFileSync(here + 'theme-studio.html', 'utf8');
assert.ok(html.includes(body), 'generated page is missing palette-actions.js verbatim');
});
test('inline-integrity: theme-studio.html contains browser-gates.js verbatim', () => {
- const body = stripExports(readFileSync(here + 'browser-gates.js', 'utf8'));
+ const body = stripInlinedBody(readFileSync(here + 'browser-gates.js', 'utf8'));
const html = readFileSync(here + 'theme-studio.html', 'utf8');
assert.ok(html.includes(body), 'generated page is missing browser-gates.js verbatim');
});
@@ -806,23 +900,6 @@ test('resolveUiAttr: a face with no inherit and an unset attribute returns null'
assert.equal(resolveUiAttr('region', 'bg', { 'region': { bg: null } }), null);
});
-// dropdownRowTextColor: a popup row showing a real palette color inherits the
-// popup foreground (legible on the fixed dark popup); only the filled default
-// row uses a contrast color against its own background. textOn is stubbed so the
-// test asserts the decision, not the contrast math.
-const stubTextOn = (h) => (h === '#000000' ? '#fff' : '#000');
-test('dropdownRowTextColor: a real palette color inherits the popup fg (empty)', () => {
- assert.equal(dropdownRowTextColor('#2a3a5a', '#2a3a5a', stubTextOn), '');
-});
-test('dropdownRowTextColor: a dark swatch still inherits (regression: blues were unreadable)', () => {
- assert.equal(dropdownRowTextColor('#000000', '#000000', stubTextOn), '');
-});
-test('dropdownRowTextColor: the filled default row contrasts against its fill', () => {
- assert.equal(dropdownRowTextColor('', '#cdced1', stubTextOn), '#000');
-});
-test('dropdownRowTextColor: a default row with no fill inherits (empty)', () => {
- assert.equal(dropdownRowTextColor('', '', stubTextOn), '');
-});
// appViewKeysSorted: the assignment-view dropdown lists package apps
// alphabetically by display label, independent of the APPS build order
@@ -864,10 +941,36 @@ test('faceBoxNonDefaults: a set fg over an empty default flags fg', () => {
assert.equal(faceBoxNonDefaults({ fg: '#8ea85e' }, {}).fg, true);
assert.equal(faceBoxNonDefaults({}, {}).fg, false);
});
-test('faceBoxNonDefaults: any style attr differing flags the style box once', () => {
- assert.equal(faceBoxNonDefaults({ bold: true }, { bold: false }).style, true);
- assert.equal(faceBoxNonDefaults({ strike: true }, {}).style, true);
- assert.equal(faceBoxNonDefaults({ bold: true }, { bold: true }).style, false);
+test('faceBoxNonDefaults: an in-row style attr differing flags the style box once', () => {
+ assert.equal(faceBoxNonDefaults({ weight: 'bold' }, { weight: null }).style, true);
+ assert.equal(faceBoxNonDefaults({ slant: 'italic' }, {}).style, true);
+ assert.equal(faceBoxNonDefaults({ strike: { color: null } }, {}).style, true);
+ // underline lives in the expander now, so it does not flag the in-row style box
+ assert.equal(faceBoxNonDefaults({ underline: { style: 'line', color: null } }, {}).style, false);
+ assert.equal(faceBoxNonDefaults({ weight: 'bold' }, { weight: 'bold' }).style, false);
+});
+
+test('overflowNonDefault: Normal — flags an expander attr that differs from default', () => {
+ assert.equal(overflowNonDefault({ family: 'Iosevka' }, {}, false), true);
+ assert.equal(overflowNonDefault({ underline: { style: 'wave', color: null } }, {}, false), true);
+ assert.equal(overflowNonDefault({ inverse: true }, {}, false), true);
+ assert.equal(overflowNonDefault({ 'distant-fg': '#222222' }, {}, false), true);
+});
+
+test('overflowNonDefault: Boundary — matching attrs and in-row attrs do not flag', () => {
+ // identical overflow attrs -> no flag
+ const f = { family: 'Iosevka', overline: { color: '#abc' }, inverse: true };
+ assert.equal(overflowNonDefault(f, f, false), false);
+ // weight/slant/strike are in-row, not the expander's concern
+ assert.equal(overflowNonDefault({ weight: 'bold', slant: 'italic', strike: { color: null } }, {}, false), false);
+});
+
+test('overflowNonDefault: Boundary — inherit/height only count when shown in the expander', () => {
+ // packages keep inherit/height inline (showInheritHeight false) -> not flagged here
+ assert.equal(overflowNonDefault({ inherit: 'shadow', height: 1.4 }, {}, false), false);
+ // ui/syntax expose them in the expander (showInheritHeight true) -> flagged
+ assert.equal(overflowNonDefault({ inherit: 'shadow' }, {}, true), true);
+ assert.equal(overflowNonDefault({ height: 1.4 }, {}, true), true);
});
test('faceBoxNonDefaults: inherit and box differences are flagged', () => {
assert.equal(faceBoxNonDefaults({ inherit: 'bold' }, { inherit: null }).inherit, true);
@@ -895,3 +998,176 @@ test('stepViewIndex: a single option or empty list stays put', () => {
assert.equal(stepViewIndex(3, 0, -1), 3);
assert.equal(stepViewIndex(0, 0, 1), 0);
});
+
+// --- face CSS rendering helpers (promoted from app.js into app-core) ----------
+
+test('cssWeight: Normal — each weight name maps to its CSS number', () => {
+ assert.equal(cssWeight('light'), 300);
+ assert.equal(cssWeight('normal'), 400);
+ assert.equal(cssWeight('medium'), 500);
+ assert.equal(cssWeight('semibold'), 600);
+ assert.equal(cssWeight('bold'), 700);
+ assert.equal(cssWeight('heavy'), 900);
+});
+test('cssWeight: Boundary — null/undefined/empty fall back to "normal"', () => {
+ assert.equal(cssWeight(null), 'normal');
+ assert.equal(cssWeight(undefined), 'normal');
+ assert.equal(cssWeight(''), 'normal');
+});
+test('cssWeight: Error — unknown name or a number falls back to "normal"', () => {
+ assert.equal(cssWeight('ultrablack'), 'normal');
+ assert.equal(cssWeight(700), 'normal');
+});
+
+test('faceDecoration: Normal — underline, strike, or both', () => {
+ assert.equal(faceDecoration({underline:{style:'line',color:null}}), 'underline');
+ assert.equal(faceDecoration({strike:{color:null}}), 'line-through');
+ assert.equal(faceDecoration({underline:{style:'line'}, strike:{color:null}}),
+ 'underline line-through');
+});
+test('faceDecoration: Boundary — neither set yields "none"', () => {
+ assert.equal(faceDecoration({}), 'none');
+ assert.equal(faceDecoration({underline:null, strike:null}), 'none');
+});
+test('faceDecoration: Error — falsy underline/strike are ignored', () => {
+ assert.equal(faceDecoration({underline:false, strike:false}), 'none');
+});
+
+test('boxCss: Normal — line box uses the box color', () => {
+ assert.equal(boxCss({style:'line', color:'#aabbcc'}), 'inset 0 0 0 1px #aabbcc');
+});
+test('boxCss: Normal — pressed is released with the relief edges swapped', () => {
+ const rel = boxCss({style:'released', width:1, color:'#808080'});
+ const pre = boxCss({style:'pressed', width:1, color:'#808080'});
+ assert.match(rel, /^inset 1px 1px 0 \S+,inset -1px -1px 0 \S+$/);
+ assert.notEqual(rel, pre);
+ const [, ra, rz] = rel.match(/inset 1px 1px 0 (\S+?),inset -1px -1px 0 (\S+)/);
+ const [, pa, pz] = pre.match(/inset 1px 1px 0 (\S+?),inset -1px -1px 0 (\S+)/);
+ assert.equal(pa, rz);
+ assert.equal(pz, ra);
+});
+test('boxCss: Boundary — width respected; missing color uses currentColor', () => {
+ assert.equal(boxCss({style:'line', width:3, color:'#123456'}), 'inset 0 0 0 3px #123456');
+ assert.equal(boxCss({style:'line'}), 'inset 0 0 0 1px currentColor');
+});
+test('boxCss: Boundary — released/pressed with no color and no bg use the fallback', () => {
+ assert.equal(boxCss({style:'released'}),
+ 'inset 1px 1px 0 #ffffff33,inset -1px -1px 0 #00000066');
+ assert.equal(boxCss({style:'pressed'}),
+ 'inset 1px 1px 0 #00000066,inset -1px -1px 0 #ffffff33');
+});
+test('boxCss: Error — null or styleless box yields the empty string', () => {
+ assert.equal(boxCss(null), '');
+ assert.equal(boxCss({}), '');
+ assert.equal(boxCss({color:'#ffffff'}), '');
+});
+
+test('faceCss: Normal — minimal face is color plus defaults', () => {
+ assert.equal(faceCss({}, '#111111', null, {}),
+ 'color:#111111;font-weight:normal;font-style:normal;text-decoration:none');
+});
+test('faceCss: Normal — background, weight, slant, decoration reflected', () => {
+ assert.equal(
+ faceCss({weight:'bold', slant:'italic', underline:{style:'line'}}, '#111', '#222', {}),
+ 'color:#111;background:#222;font-weight:700;font-style:italic;text-decoration:underline');
+});
+test('faceCss: Boundary — noBg suppresses background; null bg omits it', () => {
+ assert.equal(faceCss({}, '#111', '#222', {noBg:true}),
+ 'color:#111;font-weight:normal;font-style:normal;text-decoration:none');
+ assert.equal(faceCss({}, '#111', null, {}),
+ 'color:#111;font-weight:normal;font-style:normal;text-decoration:none');
+});
+test('faceCss: Boundary — font-size precedes box-shadow', () => {
+ assert.equal(
+ faceCss({box:{style:'line',color:'#abcabc'}}, '#111', null, {fontSize:1.15, boxBg:'#000'}),
+ 'color:#111;font-weight:normal;font-style:normal;text-decoration:none;font-size:1.15em;box-shadow:inset 0 0 0 1px #abcabc');
+});
+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+$/);
+});
+
+test('composeHoverTitle: Normal — docstring sits on top of existing base text', () => {
+ assert.equal(composeHoverTitle('A face doc.', 'mode-line'),
+ 'A face doc.\n\nmode-line');
+});
+test('composeHoverTitle: Boundary — doc only (no base) returns the doc', () => {
+ assert.equal(composeHoverTitle('A face doc.', ''), 'A face doc.');
+ assert.equal(composeHoverTitle('A face doc.', null), 'A face doc.');
+});
+test('composeHoverTitle: Boundary — base only (no doc) returns the base unchanged', () => {
+ assert.equal(composeHoverTitle('', 'mode-line'), 'mode-line');
+ assert.equal(composeHoverTitle(undefined, 'mode-line'), 'mode-line');
+});
+test('composeHoverTitle: Error — neither doc nor base returns empty string', () => {
+ assert.equal(composeHoverTitle(null, null), '');
+ assert.equal(composeHoverTitle(undefined, ''), '');
+});
+
+// --- clampHeight: coerce a height-field value to null (unset) or an in-range number ---
+test('clampHeight: bounds are the agreed Emacs-floor / studio-ceiling pair', () => {
+ assert.equal(HEIGHT_MIN, 0.1);
+ assert.equal(HEIGHT_MAX, 2.0);
+});
+test('clampHeight: Normal — an in-range value passes through unchanged', () => {
+ assert.equal(clampHeight('1.2'), 1.2);
+ assert.equal(clampHeight('0.5'), 0.5);
+ assert.equal(clampHeight(1.0), 1.0);
+});
+test('clampHeight: Boundary — the exact min and max are kept', () => {
+ assert.equal(clampHeight('0.1'), 0.1);
+ assert.equal(clampHeight('2.0'), 2.0);
+ assert.equal(clampHeight(0.1), 0.1);
+});
+test('clampHeight: Boundary — out-of-range snaps to the nearer bound', () => {
+ assert.equal(clampHeight('5'), 2.0); // above max
+ assert.equal(clampHeight('0.05'), 0.1); // below the Emacs floor
+ assert.equal(clampHeight('0'), 0.1); // zero is not unset; it clamps up
+ assert.equal(clampHeight('-3'), 0.1); // negative clamps up
+});
+test('clampHeight: Boundary — blank or whitespace is unset (null)', () => {
+ assert.equal(clampHeight(''), null);
+ assert.equal(clampHeight(' '), null);
+ assert.equal(clampHeight(null), null);
+ assert.equal(clampHeight(undefined), null);
+});
+test('clampHeight: Error — non-numeric text is unset (null), not NaN', () => {
+ assert.equal(clampHeight('abc'), null);
+ assert.equal(clampHeight('1.2x'), 1.2); // parseFloat reads the leading number
+});
+test('clampHeight: caller may override the bounds', () => {
+ assert.equal(clampHeight('5', 0.1, 3.0), 3.0);
+ assert.equal(clampHeight('0.2', 0.5, 3.0), 0.5);
+});