From 37b56cc3b0ff10d2dc2fc062698a477cbb4a6000 Mon Sep 17 00:00:00 2001 From: dborup Date: Fri, 2 Oct 2026 19:09:06 +0200 Subject: [PATCH 1/7] test(frontend-helpers): follow the Phosphor favStar markup, stub tbody.contains (#174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit favStar() was migrated from the ★/☆ text glyphs to a Phosphor sprite in #1648 M2, so the two favStar assertions grepping for the glyphs failed. Assert the sprite id (ph-star-fill / ph-star), the "on" class and aria-pressed instead. The nodes WS sandbox lacked element.contains(), which renderRows() has called since #1616, so 13 "Failed to load nodes: tbody.contains is not a function" errors were logged during the run. Add the stub. Relates to #174 Co-Authored-By: Claude Sonnet 5.5 --- test-frontend-helpers.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index 5658dc517..9bddf5242 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -1818,6 +1818,7 @@ console.log('\n=== nodes.js: WS handler runtime behavior ==='); style: {}, dataset: {}, classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } }, addEventListener() {}, + contains() { return false; }, // nodes.js renderRows() asks tbody.contains(document.activeElement) (#1616) querySelectorAll() { return []; }, querySelector() { return null; }, getAttribute() { return null; }, @@ -5248,16 +5249,20 @@ console.log('\n=== app.js: favorites ==='); test('favStar returns filled star for favorite', () => { ctx.localStorage.setItem('meshcore-favorites', '["pk1"]'); const html = ctx.favStar('pk1'); - assert.ok(html.includes('★')); - assert.ok(html.includes('on')); + // Phosphor migration (#1648 M2): the glyph is now an SVG sprite reference. + assert.ok(html.includes('#ph-star-fill')); + assert.ok(/class="fav-star[^"]* on"/.test(html)); + assert.ok(html.includes('aria-pressed="true"')); assert.ok(html.includes('Remove from favorites')); }); test('favStar returns empty star for non-favorite', () => { ctx.localStorage.setItem('meshcore-favorites', '[]'); const html = ctx.favStar('pk1'); - assert.ok(html.includes('☆')); - assert.ok(!html.includes(' on')); + assert.ok(html.includes('#ph-star"')); + assert.ok(!html.includes('#ph-star-fill')); + assert.ok(!/class="fav-star[^"]* on"/.test(html)); + assert.ok(html.includes('aria-pressed="false"')); assert.ok(html.includes('Add to favorites')); }); From 5233f009ac3bddd8240d9a6b60a92a2ebfebaa55 Mon Sep 17 00:00:00 2001 From: dborup Date: Fri, 2 Oct 2026 19:09:24 +0200 Subject: [PATCH 2/7] =?UTF-8?q?test(channel-psk-ux):=20assert=20the=20Phos?= =?UTF-8?q?phor=20lock=20icons=20instead=20of=20the=20=F0=9F=94=93=20glyph?= =?UTF-8?q?=20(#174)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit channels.js renders the user-added/encrypted badge with Phosphor sprite icons since #1648 M2 (ph-lock-open vs ph-lock), so grepping the source for the 🔓 glyph failed. Assert the badgeIcon mapping instead: user-added rows use ph-lock-open, server-encrypted rows use ph-lock. Relates to #174 Co-Authored-By: Claude Sonnet 5.5 --- test-channel-psk-ux.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test-channel-psk-ux.js b/test-channel-psk-ux.js index 2adb7dd22..cbc69f421 100644 --- a/test-channel-psk-ux.js +++ b/test-channel-psk-ux.js @@ -90,8 +90,10 @@ async function run() { assert(chSrc.includes('ch-user-added'), 'renderChannelList emits ch-user-added marker for keyed channels'); // Distinct icon - assert(chSrc.includes('🔓'), - 'user-added rows use a distinct unlocked icon (🔓) from server-encrypted (🔒)'); + // The 🔓/🔒 glyphs were migrated to Phosphor sprite icons (#1648 M2): + // user-added → ph-lock-open, server-encrypted → ph-lock. + assert(/const badgeIcon = isUserAdded \?[^;]*#ph-lock-open"[^;]*: \(isEncrypted \?[^;]*#ph-lock"/.test(chSrc), + 'user-added rows use a distinct unlocked icon (ph-lock-open) from server-encrypted (ph-lock)'); // addUserChannel accepts label assert(/addUserChannel\s*\(\s*val\s*,\s*\w*label/i.test(chSrc) || From cd43bc5b733a9543ee2b7128ed6ab0b2b8199bae Mon Sep 17 00:00:00 2001 From: dborup Date: Fri, 2 Oct 2026 19:09:46 +0200 Subject: [PATCH 3/7] test(analytics-channels): channels.js intentionally no longer links to analytics (#174) The "Channel Analytics" chip on the Channels page (#1042) was dropped on purpose by the chat-app redesign (#1376); test-issue-1367-channels-chat-app-e2e.js asserts the rendered header has no analytics link. The old source-grep assertion therefore failed. Invert it into a source-level guard that the #/analytics link and .ch-analytics-link chip stay gone. Relates to #174 Co-Authored-By: Claude Sonnet 5.5 --- test-analytics-channels-integration.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test-analytics-channels-integration.js b/test-analytics-channels-integration.js index 1b27dd5e9..4dbe64fb3 100644 --- a/test-analytics-channels-integration.js +++ b/test-analytics-channels-integration.js @@ -174,15 +174,18 @@ assert(iT > 0 && iP > iT && iE > iP, // Within "encrypted" section, ch64 (300 msgs) appears (only one entry). assert(tbody.indexOf('0x40') > iEnc, 'encrypted section contains 0x40'); -console.log('\n=== Channels page links to Analytics ==='); +// The Channels page used to carry a "Channel Analytics" chip linking to +// #/analytics (#1042). The #1367 chat-app redesign (#1376) dropped it on +// purpose; test-issue-1367-channels-chat-app-e2e.js covers the rendered header. +// Keep a source-level guard so the chip does not creep back in unnoticed. +console.log('\n=== Channels page no longer links to Analytics (#1376) ==='); const channelsSrc = fs.readFileSync( path.join(__dirname, 'public/channels.js'), 'utf8' ); -assert(/#\/analytics/.test(channelsSrc) && - /Channel Analytics|channel analytics/i.test(channelsSrc), - 'channels.js sidebar links to #/analytics with "Channel Analytics" text'); +assert(!/#\/analytics/.test(channelsSrc) && !/ch-analytics-link/.test(channelsSrc), + 'channels.js has no #/analytics link or .ch-analytics-link chip (dropped in #1376)'); console.log('\n' + (failed ? '✗ ' + failed + ' failed, ' : '') + passed + ' passed'); process.exit(failed ? 1 : 0); From da45d67ca7c7a98296f02c75607fc44b36ea39cb Mon Sep 17 00:00:00 2001 From: dborup Date: Fri, 2 Oct 2026 19:10:17 +0200 Subject: [PATCH 4/7] test(1648-m3): allowlist the two ping-bot emoji in channels.js, narrowly (#174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ping bot (2026-07-23) added two emoji to channels.js after the M3 sweep: - '🏓 pong! ' is the text of the CoreScopeBot reply, i.e. message content rather than UI iconography. - the 🤖 avatar is UI iconography, but the Phosphor sprite has no robot symbol yet, so it cannot be swapped without touching channels.js and the sprite. Allowlist it as a known exception with a TODO. Both entries match whole substrings, so any other emoji in channels.js (or a changed bot avatar/reply) still fails the scan. Relates to #174 Co-Authored-By: Claude Sonnet 5.5 --- test-issue-1648-m3-emoji-scan.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test-issue-1648-m3-emoji-scan.js b/test-issue-1648-m3-emoji-scan.js index 0767602e2..a6effc2ec 100644 --- a/test-issue-1648-m3-emoji-scan.js +++ b/test-issue-1648-m3-emoji-scan.js @@ -68,6 +68,15 @@ const ALLOW_SUBSTRINGS = [ '// PATH_SYMBOLS_LEGEND', '// the ✕ disappears', '// userAdded so the', + // channels.js ping bot (2026-07-23, after the M3 sweep). The 🏓 is message + // *content* — the text of the CoreScopeBot reply, like the emoji in the + // home.js prose above — not UI chrome. + "text: '🏓 pong! '", + // channels.js ping-bot message avatar: still the 🤖 glyph because the sprite + // has no robot symbol yet. Known exception, matched narrowly so any other + // avatar/icon emoji still fails. TODO: add ph-robot to phosphor-sprite.svg, + // swap the avatar and drop this entry. + '>🤖', ]; function scanFile(rel) { From dfe1ce821a3775b73ba8dc5afaf0758abacbec0d Mon Sep 17 00:00:00 2001 From: dborup Date: Fri, 2 Oct 2026 19:13:26 +0200 Subject: [PATCH 5/7] test(customizer): read the preset colour through the body cascade, not the root inline (#174) Since #1449 customize-v2 applyCSS removes the documentElement --mc-role-{role} write when a CB preset is active and the role has no user override, so the preset colour is delivered only by the body[data-cb-preset="X"] rule in style.css. The #1438 FINAL C and #1446 scenario 5 assertions still read the documentElement inline style, which is now empty, and failed. Model the browser cascade in the sandbox (body inline > body[data-cb-preset] rule parsed from style.css > root inline) and assert the effective value. The server colour must still stay out of the documentElement --mc-role-* write while a preset is active. Relates to #174 Co-Authored-By: Claude Sonnet 5.5 --- test-issue-1438-customizer-mcrole.js | 26 +++++++++++++++++++++++--- test-issue-1446-cb-preset-cascade.js | 19 ++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/test-issue-1438-customizer-mcrole.js b/test-issue-1438-customizer-mcrole.js index 6689d8ce8..ed98c905b 100644 --- a/test-issue-1438-customizer-mcrole.js +++ b/test-issue-1438-customizer-mcrole.js @@ -40,6 +40,24 @@ function assert(cond, msg) { const cv2Src = fs.readFileSync(path.join(__dirname, 'public', 'customize-v2.js'), 'utf8'); const rolesSrc = fs.readFileSync(path.join(__dirname, 'public', 'roles.js'), 'utf8'); const presetsSrc = fs.readFileSync(path.join(__dirname, 'public', 'cb-presets.js'), 'utf8'); +const styleSrc = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8'); + +// Effective --mc-role-{role} for a descendant of , modelled on the +// browser cascade: body inline (user override) > body[data-cb-preset="X"] +// rule in style.css > documentElement inline. Since #1449, applyCSS removes the +// documentElement write when a preset is active and the role has no user +// override, so the preset colour is only visible through the stylesheet rule. +function effectiveRoleVar(env, css, role) { + const bodyInline = env.body.style.getPropertyValue('--mc-role-' + role); + if (bodyInline) return bodyInline.toLowerCase(); + const preset = env.body.getAttribute('data-cb-preset'); + if (preset && preset !== 'none') { + const block = css.match(new RegExp('body\\[data-cb-preset="' + preset + '"\\]\\s*\\{([^}]*)\\}')); + const m = block && block[1].match(new RegExp('--mc-role-' + role + ':\\s*(#[0-9a-fA-F]{3,8})')); + if (m) return m[1].toLowerCase(); + } + return env.root.style.getPropertyValue('--mc-role-' + role).toLowerCase(); +} // ─── Extract the nodeColors-processing block from customize-v2.js. ─── function extractBlock(src, anchor) { @@ -164,11 +182,13 @@ console.log('\n=== #1438 FINAL C: server-only key does NOT clobber --mc-role-* ( vm.runInContext(setup, env.sandbox); // --mc-role-companion must remain the preset's value (no clobber from server). - const got = env.root.style.getPropertyValue('--mc-role-companion').toLowerCase(); + const got = effectiveRoleVar(env, styleSrc, 'companion'); + assert(env.root.style.getPropertyValue('--mc-role-companion').toLowerCase() !== '#2563eb', + 'documentElement --mc-role-companion is NOT the server-config legacy #2563eb'); assert(got !== '#2563eb', - '--mc-role-companion is NOT the server-config legacy #2563eb (got ' + got + ')'); + 'effective --mc-role-companion is NOT the server-config legacy #2563eb (got ' + got + ')'); assert(got === '#648fff', - '--mc-role-companion still reflects the active preset #648FFF (got ' + got + ')'); + 'effective --mc-role-companion still reflects the active preset #648FFF (got ' + got + ')'); // --node-companion CAN take the server value (legacy compat is fine here). assert(env.root.style.getPropertyValue('--node-companion').toLowerCase() === '#2563eb', diff --git a/test-issue-1446-cb-preset-cascade.js b/test-issue-1446-cb-preset-cascade.js index 033a0b30f..65932662a 100644 --- a/test-issue-1446-cb-preset-cascade.js +++ b/test-issue-1446-cb-preset-cascade.js @@ -30,6 +30,23 @@ const rolesSrc = fs.readFileSync(path.join(ROOT, 'public', 'roles.js'), 'utf8' const presetsSrc = fs.readFileSync(path.join(ROOT, 'public', 'cb-presets.js'), 'utf8'); const styleSrc = fs.readFileSync(path.join(ROOT, 'public', 'style.css'), 'utf8'); +// Effective --mc-role-{role} for a descendant of , modelled on the +// browser cascade: body inline (user override) > body[data-cb-preset="X"] +// rule in style.css > documentElement inline. Since #1449, applyCSS removes the +// documentElement write when a preset is active and the role has no user +// override, so the preset colour is only visible through the stylesheet rule. +function effectiveRoleVar(env, css, role) { + const bodyInline = env.body.style.getPropertyValue('--mc-role-' + role); + if (bodyInline) return bodyInline.toLowerCase(); + const preset = env.body.getAttribute('data-cb-preset'); + if (preset && preset !== 'none') { + const block = css.match(new RegExp('body\\[data-cb-preset="' + preset + '"\\]\\s*\\{([^}]*)\\}')); + const m = block && block[1].match(new RegExp('--mc-role-' + role + ':\\s*(#[0-9a-fA-F]{3,8})')); + if (m) return m[1].toLowerCase(); + } + return env.root.style.getPropertyValue('--mc-role-' + role).toLowerCase(); +} + function makeSandbox(localStorageMap) { localStorageMap = localStorageMap || {}; function makeStyle() { @@ -187,7 +204,7 @@ console.log('\n=== #1446 Scenario 5: clear preset → reverts to server config / vm.runInContext(cv2Src, env.sandbox); env.sandbox.window._customizerV2.init({ nodeColors: { repeater: '#aaaaaa' } }); // Confirm deut is active first. - const repWithPreset = env.root.style.getPropertyValue('--mc-role-repeater').toLowerCase(); + const repWithPreset = effectiveRoleVar(env, styleSrc, 'repeater'); assert(repWithPreset === '#fe6100', 'precondition: deut active → --mc-role-repeater = #FE6100 (got: ' + JSON.stringify(repWithPreset) + ')'); // Now clear the preset. From 387145a1d0a02aa56b55dc3d3d1fc390d5c66b70 Mon Sep 17 00:00:00 2001 From: dborup Date: Fri, 2 Oct 2026 19:14:03 +0200 Subject: [PATCH 6/7] test(1470): use the renamed carto-voyager-dark provider id and lazy provider urls (#174) #1533 renamed the dark Voyager provider 'voyager-inverted' to 'carto-voyager-dark' and made provider.url a lazy function (#1614). MC_setDarkTileProvider() silently returns false for an unknown id, so the test stayed on the default carto-dark and every voyager assertion failed. Select the current id, assert the setter accepted it so a future rename fails loudly at the cause, and resolve provider.url before matching. Relates to #174 Co-Authored-By: Claude Sonnet 5.5 --- test-issue-1470-node-tile-helper.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/test-issue-1470-node-tile-helper.js b/test-issue-1470-node-tile-helper.js index 7ff2854ac..9e6a9cd09 100644 --- a/test-issue-1470-node-tile-helper.js +++ b/test-issue-1470-node-tile-helper.js @@ -9,7 +9,7 @@ * - Extract the _applyTilesToNodeMap function source from public/nodes.js * by regex and run it in the same sandbox. * - Mock L.tileLayer / L.map to capture the URL + options the helper passes - * and the .addTo() target. Assert that selecting voyager-inverted in the + * and the .addTo() target. Assert that selecting carto-voyager-dark in the * customizer ends up calling tileLayer with the voyager URL AND setting * the tile pane filter to the provider.invertFilter string. * @@ -134,6 +134,13 @@ function extractApplyTilesHelper() { return nodesSrc.slice(start, i); } +// The provider id used to be 'voyager-inverted'; #1533 renamed it to +// 'carto-voyager-dark'. MC_setDarkTileProvider() silently returns false for an +// unknown id, which left the default carto-dark active, so assert it took. +function assertProviderSet(ctx, id) { + assert.strictEqual(ctx.window.MC_setDarkTileProvider(id), true, 'MC_setDarkTileProvider(' + id + ') accepted'); +} + console.log('── #1470 node-detail inset-map tile-provider routing ──'); test('roles.js + providers: getActiveTileProvider returns null in light mode', () => { @@ -148,29 +155,31 @@ test('roles.js + providers: getActiveTileProvider returns selected provider in d const ctx = makeSandbox({ theme: 'dark' }); loadInto(ctx, 'public/map-tile-providers.js'); loadInto(ctx, 'public/roles.js'); - ctx.window.MC_setDarkTileProvider('voyager-inverted'); + assertProviderSet(ctx, 'carto-voyager-dark'); const p = ctx.window.getActiveTileProvider(); assert.ok(p, 'provider returned in dark mode'); - assert.ok(typeof p.url === 'string' && /voyager/.test(p.url), - 'provider url is voyager — got ' + JSON.stringify(p.url)); + // #1614: provider urls are lazy functions that resolve to the template string. + const providerUrl = typeof p.url === 'function' ? p.url() : p.url; + assert.ok(typeof providerUrl === 'string' && /voyager/.test(providerUrl), + 'provider url is voyager — got ' + JSON.stringify(providerUrl)); assert.ok(typeof p.invertFilter === 'string' && /invert\(/.test(p.invertFilter), - 'voyager-inverted invertFilter present — got ' + JSON.stringify(p.invertFilter)); + 'carto-voyager-dark invertFilter present — got ' + JSON.stringify(p.invertFilter)); }); -test('roles.js: getTileUrl returns voyager URL in dark mode when voyager-inverted selected', () => { +test('roles.js: getTileUrl returns voyager URL in dark mode when carto-voyager-dark selected', () => { const ctx = makeSandbox({ theme: 'dark' }); loadInto(ctx, 'public/map-tile-providers.js'); loadInto(ctx, 'public/roles.js'); - ctx.window.MC_setDarkTileProvider('voyager-inverted'); + assertProviderSet(ctx, 'carto-voyager-dark'); const url = ctx.window.getTileUrl(); assert.ok(/voyager/.test(url), 'getTileUrl returns voyager URL — got ' + url); }); -test('_applyTilesToNodeMap: dark + voyager-inverted → tileLayer(voyagerURL) + invert filter', () => { +test('_applyTilesToNodeMap: dark + carto-voyager-dark → tileLayer(voyagerURL) + invert filter', () => { const ctx = makeSandbox({ theme: 'dark' }); loadInto(ctx, 'public/map-tile-providers.js'); loadInto(ctx, 'public/roles.js'); - ctx.window.MC_setDarkTileProvider('voyager-inverted'); + assertProviderSet(ctx, 'carto-voyager-dark'); const mock = makeLeafletMock(); ctx.L = mock.L; @@ -190,7 +199,7 @@ test('_applyTilesToNodeMap: dark + carto-dark (non-inverted) → no invert filte const ctx = makeSandbox({ theme: 'dark' }); loadInto(ctx, 'public/map-tile-providers.js'); loadInto(ctx, 'public/roles.js'); - ctx.window.MC_setDarkTileProvider('carto-dark'); + assertProviderSet(ctx, 'carto-dark'); const mock = makeLeafletMock(); ctx.L = mock.L; From f3af9d680b6f1f651bf6b3f0750ff6116d5ef93c Mon Sep 17 00:00:00 2001 From: dborup Date: Fri, 2 Oct 2026 19:14:47 +0200 Subject: [PATCH 7/7] fix(roles): resolve ROLE_COLORS through so an active CB preset reaches JS consumers (#174) Found while repairing the #1438/#1446 customizer tests. Since #1449, customize-v2 applyCSS removes the documentElement --mc-role-{role} write when a CB preset is active and the role has no user override; the preset colour is then delivered only by the body[data-cb-preset="X"] rule in style.css. The live ROLE_COLORS getter (#1407) resolved the variable on documentElement, which never sees a body-scoped rule, so under a preset it kept returning the Wong default while CSS-var consumers showed the preset. Observed in the browser with meshcore-cb-preset=deut: body computes --mc-role-repeater #FE6100, documentElement #D55E00 and ROLE_COLORS.repeater #D55E00 (also after MeshCorePresets.applyPreset('prot') at runtime). ROLE_COLORS is read by analytics, map, nodes, live and the route views. Resolve the variable on (falling back to documentElement). inherits every :root value, so reads without a preset are unchanged. Regression test added to test-issue-1407-cb-preset-propagation.js. Relates to #174 Co-Authored-By: Claude Sonnet 5.5 --- public/roles.js | 6 ++++- test-issue-1407-cb-preset-propagation.js | 32 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/public/roles.js b/public/roles.js index 3d874247e..95bd336d6 100644 --- a/public/roles.js +++ b/public/roles.js @@ -32,7 +32,11 @@ if (typeof document === 'undefined' || !document.documentElement) return fallback; var v = ''; if (typeof getComputedStyle === 'function') { - v = getComputedStyle(document.documentElement).getPropertyValue(name); + // Resolve on , not documentElement: an active CB preset delivers + // its colours through body[data-cb-preset="X"] (style.css), and since + // #1449 customize-v2 no longer mirrors them onto documentElement. + // inherits every :root value, so non-preset reads are unchanged. + v = getComputedStyle(document.body || document.documentElement).getPropertyValue(name); } if (!v && document.documentElement.style && typeof document.documentElement.style.getPropertyValue === 'function') { v = document.documentElement.style.getPropertyValue(name); diff --git a/test-issue-1407-cb-preset-propagation.js b/test-issue-1407-cb-preset-propagation.js index 28a30ce7c..0201b528d 100644 --- a/test-issue-1407-cb-preset-propagation.js +++ b/test-issue-1407-cb-preset-propagation.js @@ -211,6 +211,38 @@ if (pillRuleMatch) { const inlineHardcoded = /color:\s*#1a1a1a/.test(mapSrc); assert(!inlineHardcoded, 'public/map.js does not hardcode color:#1a1a1a on .mc-pill inline style'); +console.log('\n=== #1407 G: ROLE_COLORS follows the preset delivered by body[data-cb-preset] ==='); +// Regression: since #1449 customize-v2 removes the documentElement +// --mc-role-* write while a preset is active, so the preset colour only exists +// on (style.css rule). ROLE_COLORS must resolve through , otherwise +// JS consumers (map, analytics, live) keep the Wong default under a CB preset. +{ + const genv = makeSandbox(); + const deutBlock = styleSrc.match(/body\[data-cb-preset="deut"\]\s*\{([^}]*)\}/); + const wongRepeater = '#D55E00'; + genv.sandbox.getComputedStyle = function (el) { + return { + getPropertyValue: function (k) { + if (el === genv.body && genv.body.getAttribute('data-cb-preset') === 'deut' && deutBlock) { + const m = deutBlock[1].match(new RegExp(k + ':\\s*(#[0-9a-fA-F]{6})')); + if (m) return m[1]; + } + // inherits :root; nothing is written inline on documentElement. + return k === '--mc-role-repeater' ? wongRepeater : ''; + } + }; + }; + vm.createContext(genv.sandbox); + vm.runInContext(rolesSrc, genv.sandbox); + assert(!!deutBlock, 'style.css has a body[data-cb-preset="deut"] block'); + assert(String(genv.sandbox.window.ROLE_COLORS.repeater).toLowerCase() === '#d55e00', + 'no preset: ROLE_COLORS.repeater is the :root Wong default'); + genv.body.setAttribute('data-cb-preset', 'deut'); + const viaBody = String(genv.sandbox.window.ROLE_COLORS.repeater).toLowerCase(); + assert(viaBody === '#fe6100', + 'preset deut active via body[data-cb-preset] only: ROLE_COLORS.repeater === #FE6100 (got ' + viaBody + ')'); +} + console.log('\n=== Summary ==='); console.log(' passed: ' + passed); console.log(' failed: ' + failed);