Skip to content
6 changes: 5 additions & 1 deletion public/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <body>, 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.
// <body> 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);
Expand Down
11 changes: 7 additions & 4 deletions test-analytics-channels-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
6 changes: 4 additions & 2 deletions test-channel-psk-ux.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) ||
Expand Down
13 changes: 9 additions & 4 deletions test-frontend-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; },
Expand Down Expand Up @@ -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'));
});

Expand Down
32 changes: 32 additions & 0 deletions test-issue-1407-cb-preset-propagation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <body> (style.css rule). ROLE_COLORS must resolve through <body>, 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];
}
// <body> 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);
Expand Down
26 changes: 23 additions & 3 deletions test-issue-1438-customizer-mcrole.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <body>, 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) {
Expand Down Expand Up @@ -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',
Expand Down
19 changes: 18 additions & 1 deletion test-issue-1446-cb-preset-cascade.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <body>, 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() {
Expand Down Expand Up @@ -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.
Expand Down
29 changes: 19 additions & 10 deletions test-issue-1470-node-tile-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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;
Expand All @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions test-issue-1648-m3-emoji-scan.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
'>🤖</div>',
];

function scanFile(rel) {
Expand Down
Loading