mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-05 17:41:18 +00:00
604c3552c7
## Summary Closes the final gap left by #1439 (marker SVG `fill="var(--mc-role-X)"` migration) and #1441 (body.style write in `setRoleColorOverride`). Both prior PRs made marker SVGs read from `--mc-role-{role}` CSS vars, and made the LIVE customizer pick path write that var via `setRoleColorOverride`. But the second leg of the round-trip was still broken: **On page reload**, `customize-v2.js applyCSS()` replays `userOverrides.nodeColors` from localStorage and writes only `--node-{role}` (the legacy var). `setRoleColorOverride` is **not** replayed. Result: marker fills revert to the active preset's colors even though the operator's custom hex is still in localStorage. ## Fix Extend the per-role loop in `applyCSS` to write **both** `--node-{role}` (legacy compat) and `--mc-role-{role}` (the var marker SVGs now read). ```js for (var role in nc) { root.setProperty('--node-' + role, nc[role]); root.setProperty('--mc-role-' + role, nc[role]); // NEW } ``` `public/customize.js` `setRoleColorOverride` path: already correct in `roles.js` (#1441 wrote the body.style hop with the explicit #1438 comment). No change needed there — the gap was specifically the reload-time replay in customize-v2. ## Test New `test-issue-1438-customizer-mcrole.js` — source-invariant assertions on the loop body. Red commit fails on the `--mc-role-` assertion; green commit passes 4/4. Added to `test-all.sh`. ## Verification plan Post-merge hot-deploy + CDP verify on `analyzer-stg.00id.net`: 1. `setOverride('nodeColors','repeater','#ff00ff')` → `applyCSS(computeEffective())` 2. Assert `getComputedStyle(documentElement).getPropertyValue('--mc-role-repeater') === '#ff00ff'` 3. Sample a repeater marker SVG, assert `getComputedStyle(...).fill === 'rgb(255, 0, 255)'` 4. Screenshot Closes #1438. --------- Co-authored-by: openclaw-bot <bot@openclaw.local>
181 lines
7.6 KiB
JavaScript
181 lines
7.6 KiB
JavaScript
/**
|
|
* #1438 FINAL — customizer-v2 applyCSS must write --mc-role-{role}
|
|
* alongside the legacy --node-{role} WHEN the value comes from
|
|
* userOverrides (not from server defaults).
|
|
*
|
|
* The earlier closing chain (#1439 marker SVG migration, #1441 body.style
|
|
* tweaks) did NOT extend the per-role write loop. Result:
|
|
*
|
|
* - Operator opens customizer, picks a custom color per-role. The pick
|
|
* goes through setRoleColorOverride() (roles.js) which DOES write
|
|
* --mc-role-X correctly → marker SVGs recolor live. ✅
|
|
* - Operator reloads the page. customize-v2.js applyCSS replays from
|
|
* localStorage userOverrides.nodeColors via the
|
|
* `for (var role in nc) { root.setProperty('--node-' + role, ...) }`
|
|
* loop. setRoleColorOverride is NOT replayed. Result: --mc-role-X
|
|
* falls back to preset defaults; marker SVGs revert to preset
|
|
* colors even though localStorage still holds the user pick. ❌
|
|
*
|
|
* Fix: extend the loop in customize-v2.js applyCSS to write
|
|
* --mc-role-{role} when the role is present in userOverrides.nodeColors.
|
|
* Do NOT write --mc-role-* for keys that only exist in server config
|
|
* (that would re-introduce the #1412 ROLE_COLORS / preset-propagation
|
|
* regression — server-config writes must stay legacy-vars-only).
|
|
*
|
|
* This test runs the extracted block in a vm sandbox so reverting the
|
|
* fix in customize-v2.js breaks it.
|
|
*/
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
|
|
let passed = 0, failed = 0;
|
|
function assert(cond, msg) {
|
|
if (cond) { passed++; console.log(' ✓ ' + msg); }
|
|
else { failed++; console.error(' ✗ ' + 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');
|
|
|
|
// ─── Extract the nodeColors-processing block from customize-v2.js. ───
|
|
function extractBlock(src, anchor) {
|
|
const idx = src.indexOf(anchor);
|
|
if (idx === -1) throw new Error('anchor not found: ' + anchor);
|
|
const start = src.indexOf('{', idx);
|
|
if (start === -1) throw new Error('open brace not found after anchor');
|
|
let depth = 0, end = -1;
|
|
for (let i = start; i < src.length; i++) {
|
|
if (src[i] === '{') depth++;
|
|
else if (src[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
}
|
|
if (end === -1) throw new Error('matching close brace not found');
|
|
return src.slice(idx, end + 1);
|
|
}
|
|
const blockA = extractBlock(cv2Src, 'var nc = effectiveConfig.nodeColors;');
|
|
|
|
function makeSandbox() {
|
|
const root = {
|
|
style: {
|
|
_vars: {},
|
|
setProperty(k, v) { this._vars[k] = String(v); },
|
|
getPropertyValue(k) { return this._vars[k] || ''; },
|
|
removeProperty(k) { delete this._vars[k]; }
|
|
},
|
|
getAttribute() { return null; },
|
|
setAttribute() {}
|
|
};
|
|
const body = {
|
|
_attrs: {},
|
|
setAttribute(k, v) { this._attrs[k] = v; },
|
|
getAttribute(k) { return this._attrs[k] || null; },
|
|
removeAttribute(k) { delete this._attrs[k]; },
|
|
dataset: {},
|
|
style: {
|
|
_vars: {},
|
|
setProperty(k, v) { this._vars[k] = String(v); },
|
|
getPropertyValue(k) { return this._vars[k] || ''; },
|
|
removeProperty(k) { delete this._vars[k]; }
|
|
}
|
|
};
|
|
const sandbox = {
|
|
window: null,
|
|
document: {
|
|
documentElement: root,
|
|
body: body,
|
|
readyState: 'complete',
|
|
getElementById() { return null; },
|
|
createElement() { return { style: {}, setAttribute() {}, appendChild() {} }; },
|
|
head: { appendChild() {} },
|
|
addEventListener() {},
|
|
},
|
|
console: console,
|
|
setTimeout: setTimeout,
|
|
clearTimeout: clearTimeout,
|
|
addEventListener() {},
|
|
dispatchEvent() { return true; },
|
|
fetch: function () { return { then: function () { return { then: function () { return { catch: function () {} }; }, catch: function () {} }; } }; },
|
|
matchMedia: function () { return { matches: false }; },
|
|
CustomEvent: function (type, opts) { this.type = type; this.detail = opts && opts.detail; },
|
|
Event: function (type) { this.type = type; },
|
|
getComputedStyle: function () {
|
|
return { getPropertyValue: function (k) { return (root.style._vars[k] || ''); } };
|
|
}
|
|
};
|
|
sandbox.window = sandbox;
|
|
return { sandbox, root, body };
|
|
}
|
|
|
|
console.log('\n=== #1438 FINAL A: source invariant — --mc-role-{role} write present ===');
|
|
assert(/setProperty\(\s*['"]--mc-role-['"]\s*\+\s*role/.test(blockA),
|
|
'loop body writes --mc-role-{role}');
|
|
assert(/setProperty\(\s*['"]--node-['"]\s*\+\s*role/.test(blockA),
|
|
'loop body still writes legacy --node-{role}');
|
|
|
|
console.log('\n=== #1438 FINAL B: user override → --mc-role-{role} reflects user hex on reload ===');
|
|
{
|
|
const env = makeSandbox();
|
|
vm.createContext(env.sandbox);
|
|
vm.runInContext(rolesSrc, env.sandbox);
|
|
vm.runInContext(presetsSrc, env.sandbox);
|
|
env.sandbox.window.MeshCorePresets.applyPreset('deut');
|
|
|
|
// Precondition: preset wrote --mc-role-repeater to IBM orange.
|
|
assert(env.root.style.getPropertyValue('--mc-role-repeater').toLowerCase() === '#fe6100',
|
|
'precondition: applyPreset("deut") wrote --mc-role-repeater = #FE6100');
|
|
|
|
// Simulate reload: customize-v2 applyCSS replays from localStorage.
|
|
// effectiveConfig.nodeColors merges server + user; userOverrides is the
|
|
// user-only slice.
|
|
const setup =
|
|
'var root = document.documentElement.style;\n' +
|
|
'var userOverrides = { nodeColors: { repeater: "#ff00ff" } };\n' +
|
|
'var effectiveConfig = { nodeColors: { repeater: "#ff00ff", companion: "#56B4E9" } };\n' +
|
|
blockA + '\n';
|
|
vm.runInContext(setup, env.sandbox);
|
|
|
|
// Operator's pick MUST be in --mc-role-repeater now.
|
|
const got = env.root.style.getPropertyValue('--mc-role-repeater').toLowerCase();
|
|
assert(got === '#ff00ff',
|
|
'after applyCSS replay, --mc-role-repeater === user pick #ff00ff (got ' + got + ')');
|
|
|
|
// And --node-repeater for legacy compat.
|
|
assert(env.root.style.getPropertyValue('--node-repeater').toLowerCase() === '#ff00ff',
|
|
'--node-repeater legacy var also written (got ' + env.root.style.getPropertyValue('--node-repeater') + ')');
|
|
}
|
|
|
|
console.log('\n=== #1438 FINAL C: server-only key does NOT clobber --mc-role-* (preserves #1412) ===');
|
|
{
|
|
const env = makeSandbox();
|
|
vm.createContext(env.sandbox);
|
|
vm.runInContext(rolesSrc, env.sandbox);
|
|
vm.runInContext(presetsSrc, env.sandbox);
|
|
env.sandbox.window.MeshCorePresets.applyPreset('deut');
|
|
|
|
// companion has NO user override; server config has its own legacy hex.
|
|
const setup =
|
|
'var root = document.documentElement.style;\n' +
|
|
'var userOverrides = { nodeColors: { repeater: "#ff00ff" } };\n' +
|
|
'var effectiveConfig = { nodeColors: { repeater: "#ff00ff", companion: "#2563eb" } };\n' +
|
|
blockA + '\n';
|
|
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();
|
|
assert(got !== '#2563eb',
|
|
'--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 + ')');
|
|
|
|
// --node-companion CAN take the server value (legacy compat is fine here).
|
|
assert(env.root.style.getPropertyValue('--node-companion').toLowerCase() === '#2563eb',
|
|
'--node-companion legacy var takes server value (got ' + env.root.style.getPropertyValue('--node-companion') + ')');
|
|
}
|
|
|
|
console.log('\n──────────────────────────');
|
|
console.log('passed: ' + passed + ', failed: ' + failed);
|
|
process.exit(failed === 0 ? 0 : 1);
|