mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-20 21:40:56 +00:00
Implements the two deferred a11y stretch goals from #1361 / PR #1378. ## What 1. **Brettel/Vienot 1997 dichromatic simulation overlay** — `public/index.html` ships inline `<svg>` defs with `<filter id="cb-deut|cb-prot|cb-trit|cb-achromat">` using `feColorMatrix`. Activation rule: `body[data-cb-sim="X"] { filter: url(#cb-X); }`. `public/customize-v2.js` renders a radio group (off/deut/prot/trit/achromat) under the existing CB preset section. Preview-only — **not persisted**, per the issue spec. 2. **Reset to default Wong button** — `data-cv2-cb-reset` button that calls `MeshCorePresets.applyPreset('default')` and removes `localStorage["meshcore-cb-preset"]`. Two helpers exposed on `window._customizerV2` for unit-test drive: `applyCbSim(id)` and `resetCbPreset()`. ## TDD (red → green) - **Red:** `49155723` — `test-issue-1380-cb-sim-overlay.js` + `test-issue-1380-cb-reset-button.js`. Both load `customize-v2.js` and (for reset) `cb-presets.js` in a vm sandbox; failure is assertion (not compile). - **Green:** `5d8f3c1f` — both tests pass (21 + 7 assertions). ## Files changed - `public/index.html` — inline SVG `<defs>` + 4-rule `<style>` block. - `public/customize-v2.js` — render fns `_renderCbSimSelector` + `_renderCbResetButton`, change/click handlers, helper exports. - `test-issue-1380-cb-sim-overlay.js` (new) — string-asserts on index.html SVG filters / CSS rules / customize-v2 hooks + vm.createContext drive of `applyCbSim`. - `test-issue-1380-cb-reset-button.js` (new) — vm.createContext seeds `meshcore-cb-preset=trit`, calls `resetCbPreset()`, asserts storage cleared + `body[data-cb-preset="default"]`. - `test-all.sh` + `.github/workflows/deploy.yml` — register both tests. ## Out of scope - No new preset palettes (locked from MVP). - No persistence for the sim overlay (preview-only per spec — `localStorage` intentionally untouched by sim radio). - No colorblind-sim JS library — pure inline SVG `feColorMatrix`. Browser verified: filter rule matches via CSS sandbox; visual confirmation deferred to operator (single-tab radio, no fetch). E2E DOM assertion lives in the cv2 vm tests. Fixes #1380 --------- Co-authored-by: openclaw-bot <bot@openclaw.local>
140 lines
5.1 KiB
JavaScript
140 lines
5.1 KiB
JavaScript
/**
|
|
* #1380 — Colorblind a11y stretch goal: "Reset to default Wong" button.
|
|
*
|
|
* Deferred from #1361 / PR #1378. This test enforces that the customizer
|
|
* exposes a Reset button which:
|
|
* - Calls MeshCorePresets.applyPreset('default'), AND
|
|
* - Removes localStorage["meshcore-cb-preset"] afterwards.
|
|
*
|
|
* Pure-string + vm.createContext assertions, mirrors test-issue-1361.
|
|
*/
|
|
'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(' \u2713 ' + msg); }
|
|
else { failed++; console.error(' \u2717 ' + msg); }
|
|
}
|
|
|
|
const customSrc = fs.readFileSync(path.join(__dirname, 'public', 'customize-v2.js'), 'utf8');
|
|
const presetsSrc = fs.readFileSync(path.join(__dirname, 'public', 'cb-presets.js'), 'utf8');
|
|
|
|
console.log('\n=== #1380 Reset A: customize-v2.js renders a Reset-to-Wong button ===');
|
|
assert(/data-cv2-cb-reset/.test(customSrc),
|
|
'customize-v2.js exposes a data-cv2-cb-reset hook on the reset button');
|
|
assert(/Reset[^<]*Wong/i.test(customSrc),
|
|
'customize-v2.js button copy mentions "Reset" and "Wong"');
|
|
|
|
console.log('\n=== #1380 Reset B: handler exposed for tests + behavior ===');
|
|
|
|
function makeSandbox() {
|
|
const stored = {};
|
|
const ls = {
|
|
getItem(k) { return Object.prototype.hasOwnProperty.call(stored, k) ? stored[k] : null; },
|
|
setItem(k, v) { stored[k] = String(v); },
|
|
removeItem(k) { delete stored[k]; },
|
|
};
|
|
const body = {
|
|
_attrs: {},
|
|
setAttribute(k, v) { this._attrs[k] = v; },
|
|
getAttribute(k) { return Object.prototype.hasOwnProperty.call(this._attrs, k) ? this._attrs[k] : null; },
|
|
removeAttribute(k) { delete this._attrs[k]; },
|
|
dataset: {},
|
|
};
|
|
const root = {
|
|
style: {
|
|
_v: {},
|
|
setProperty(k, v) { this._v[k] = v; },
|
|
removeProperty(k) { delete this._v[k]; },
|
|
getPropertyValue(k) { return this._v[k] || ''; },
|
|
},
|
|
dataset: { theme: 'light' },
|
|
getAttribute() { return 'light'; },
|
|
setAttribute() {},
|
|
};
|
|
const handlers = {};
|
|
const sandbox = {
|
|
window: null,
|
|
document: {
|
|
readyState: 'complete',
|
|
body: body,
|
|
documentElement: root,
|
|
head: { appendChild() {} },
|
|
getElementById() { return null; },
|
|
createElement() {
|
|
return {
|
|
id: '', textContent: '', innerHTML: '', className: '',
|
|
setAttribute() {}, appendChild() {}, style: {},
|
|
addEventListener() {}, querySelectorAll() { return []; }, querySelector() { return null; },
|
|
};
|
|
},
|
|
addEventListener(ev, cb) { (handlers[ev] = handlers[ev] || []).push(cb); },
|
|
querySelectorAll() { return []; },
|
|
querySelector() { return null; },
|
|
},
|
|
localStorage: ls,
|
|
console: console,
|
|
setTimeout(fn) { try { fn(); } catch (e) {} },
|
|
clearTimeout() {},
|
|
MutationObserver: class { observe() {} },
|
|
HashChangeEvent: class {},
|
|
CustomEvent: class { constructor(t, o) { this.type = t; this.detail = o && o.detail; } },
|
|
Event: class { constructor(t) { this.type = t; } },
|
|
getComputedStyle() { return { getPropertyValue() { return ''; } }; },
|
|
};
|
|
sandbox.window = {
|
|
addEventListener(ev, cb) { (handlers[ev] = handlers[ev] || []).push(cb); },
|
|
dispatchEvent(ev) { (handlers[ev.type] || []).forEach(function (cb) { try { cb(ev); } catch (_) {} }); return true; },
|
|
localStorage: ls,
|
|
SITE_CONFIG: {},
|
|
location: { hash: '', pathname: '/' },
|
|
CustomEvent: sandbox.CustomEvent,
|
|
Event: sandbox.Event,
|
|
matchMedia() { return { matches: false, addEventListener() {} }; },
|
|
};
|
|
sandbox.self = sandbox.window;
|
|
return { sandbox, body, ls };
|
|
}
|
|
|
|
let envOK = false, env, exposed;
|
|
try {
|
|
env = makeSandbox();
|
|
vm.createContext(env.sandbox);
|
|
// Load cb-presets first so MeshCorePresets exists.
|
|
vm.runInContext(presetsSrc, env.sandbox, { filename: 'cb-presets.js' });
|
|
vm.runInContext(customSrc, env.sandbox, { filename: 'customize-v2.js' });
|
|
exposed = env.sandbox.window._customizerV2;
|
|
envOK = !!exposed;
|
|
} catch (e) {
|
|
console.error(' ! load failed: ' + e.message);
|
|
}
|
|
|
|
assert(envOK, 'cb-presets.js + customize-v2.js both load in vm sandbox');
|
|
|
|
// Seed: pretend the user had picked "trit" earlier.
|
|
env.ls.setItem('meshcore-cb-preset', 'trit');
|
|
env.sandbox.window.MeshCorePresets.applyPreset('trit');
|
|
assert(env.ls.getItem('meshcore-cb-preset') === 'trit',
|
|
'seed: localStorage[meshcore-cb-preset] == "trit" before reset');
|
|
assert(env.body.getAttribute('data-cb-preset') === 'trit',
|
|
'seed: body[data-cb-preset] == "trit" before reset');
|
|
|
|
if (envOK && typeof exposed.resetCbPreset === 'function') {
|
|
exposed.resetCbPreset();
|
|
assert(env.ls.getItem('meshcore-cb-preset') === null,
|
|
'resetCbPreset() removes localStorage["meshcore-cb-preset"]');
|
|
// MeshCorePresets.applyPreset('default') sets body[data-cb-preset="default"].
|
|
assert(env.body.getAttribute('data-cb-preset') === 'default',
|
|
'resetCbPreset() applies the default Wong preset (body[data-cb-preset="default"])');
|
|
} else {
|
|
assert(false, 'customize-v2.js exposes resetCbPreset() helper');
|
|
}
|
|
|
|
console.log('\n=== #1380 Reset summary ===');
|
|
console.log(' passed: ' + passed + ' failed: ' + failed);
|
|
if (failed > 0) process.exit(1);
|