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-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); 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) || 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')); }); 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); 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. 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; 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) {