diff options
Diffstat (limited to 'scripts/theme-studio/test-app-core.mjs')
| -rw-r--r-- | scripts/theme-studio/test-app-core.mjs | 207 |
1 files changed, 182 insertions, 25 deletions
diff --git a/scripts/theme-studio/test-app-core.mjs b/scripts/theme-studio/test-app-core.mjs index 457f04d17..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, migrateLegacyFace, 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, 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'; @@ -817,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'); }); @@ -899,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 @@ -1014,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); +}); |
