From 8b37c918b3b2bc4e5f4aca1136e6b4fd5295919b Mon Sep 17 00:00:00 2001 From: openclaw-bot Date: Tue, 26 May 2026 04:42:26 +0000 Subject: [PATCH] =?UTF-8?q?test(#1361):=20RED=20=E2=80=94=20colorblind=20p?= =?UTF-8?q?reset=20MVP=20failing=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds test-issue-1361-cb-presets.js covering the MVP scope: - cb-presets.js module exposes window.MeshCorePresets - .list returns 5 presets (default/deut/prot/trit/achromat) - applyPreset(id) sets body[data-cb-preset] AND --mc-role-* vars - Persistence to localStorage('meshcore-cb-preset') - Re-init from localStorage on reload - Cross-tab sync via storage event - style.css has preset blocks for non-default presets - customize-v2.js wires the UI selector - index.html loads cb-presets.js before app.js - app.js initializes preset + handles storage event - WCAG contrast helper correctness on reference pairs - achromat preset has pure-luminance (r==g==b) values Test fails on 16 assertions today (no module, no UI, no CSS blocks). Stretch goals (Brettel/Vienot SVG sim, Reset button) intentionally NOT asserted — deferred per locked MVP scope. --- test-issue-1361-cb-presets.js | 215 ++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 test-issue-1361-cb-presets.js diff --git a/test-issue-1361-cb-presets.js b/test-issue-1361-cb-presets.js new file mode 100644 index 00000000..f9e5cb17 --- /dev/null +++ b/test-issue-1361-cb-presets.js @@ -0,0 +1,215 @@ +/** + * #1361 — Theme customizer: first-class colorblind-mode presets. + * + * MVP scope (locked): + * - 5 presets: default, deut, prot, trit, achromat + * - Each preset overrides --mc-role-* CSS vars + --mc-mb-* status vars + * - Achromatopsia uses pure luminance ramp (no hue) + * - Persisted to localStorage("meshcore-cb-preset"), survives reload, + * syncs across tabs via the `storage` event. + * - Customizer UI exposes a radio/dropdown to switch preset. + * - WCAG 1.4.3 / 1.4.11 validation helper exists and is correct on + * known reference pairs. + * + * Pure-string + vm.createContext assertions (mirrors test-issue-1356 / 1360 + * pattern) so this runs in the JS-unit-tests CI step without a browser. + * + * Stretch goals (live simulation overlay, "Reset to default Wong" button) + * are explicitly DEFERRED and intentionally NOT asserted here. + */ +'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 presetsPath = path.join(__dirname, 'public', 'cb-presets.js'); +const styleSrc = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8'); +const customSrc = fs.readFileSync(path.join(__dirname, 'public', 'customize-v2.js'), 'utf8'); +const appSrc = fs.readFileSync(path.join(__dirname, 'public', 'app.js'), 'utf8'); +const indexSrc = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8'); + +console.log('\n=== #1361 A: cb-presets.js module exists and is loadable ==='); +assert(fs.existsSync(presetsPath), 'public/cb-presets.js exists'); +const presetsSrc = fs.existsSync(presetsPath) ? fs.readFileSync(presetsPath, 'utf8') : ''; + +// Build a minimal browser-ish sandbox so we can run the IIFE module. +function makeSandbox() { + const root = { style: { _vars: {}, setProperty(k, v) { this._vars[k] = v; }, getPropertyValue(k) { return this._vars[k]; }, removeProperty(k) { delete this._vars[k]; } } }; + const body = { _attrs: {}, setAttribute(k, v) { this._attrs[k] = v; }, getAttribute(k) { return this._attrs[k] || null; }, removeAttribute(k) { delete this._attrs[k]; }, dataset: {} }; + const listeners = {}; + const storage = { + _data: {}, + getItem(k) { return Object.prototype.hasOwnProperty.call(this._data, k) ? this._data[k] : null; }, + setItem(k, v) { this._data[k] = String(v); }, + removeItem(k) { delete this._data[k]; }, + }; + const sandbox = { + window: null, + document: { + documentElement: root, + body: body, + getElementById(id) { return null; }, + createElement() { return { setAttribute() {}, appendChild() {}, style: {} }; }, + }, + localStorage: storage, + console: console, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + addEventListener(ev, cb) { (listeners[ev] = listeners[ev] || []).push(cb); }, + dispatchEvent(ev) { (listeners[ev.type] || []).forEach(function (cb) { cb(ev); }); return true; }, + CustomEvent: function (type, opts) { this.type = type; this.detail = opts && opts.detail; }, + Event: function (type) { this.type = type; }, + }; + sandbox.window = sandbox; + sandbox.document.body = body; + return { sandbox, root, body, storage, listeners }; +} + +let envOK = false, env; +try { + env = makeSandbox(); + vm.createContext(env.sandbox); + vm.runInContext(presetsSrc, env.sandbox); + envOK = true; +} catch (e) { + console.error(' ! cb-presets.js failed to load in vm sandbox: ' + e.message); +} + +console.log('\n=== #1361 B: MeshCorePresets.list — 5 documented presets ==='); +const MCP = envOK && env.sandbox.window && env.sandbox.window.MeshCorePresets; +assert(!!MCP, 'window.MeshCorePresets exists after script load'); +assert(MCP && Array.isArray(MCP.list), 'MeshCorePresets.list is an array'); +const expectedIds = ['default', 'deut', 'prot', 'trit', 'achromat']; +if (MCP && Array.isArray(MCP.list)) { + assert(MCP.list.length === 5, 'list contains exactly 5 presets (got ' + MCP.list.length + ')'); + const ids = MCP.list.map(function (p) { return p.id; }); + expectedIds.forEach(function (id) { + assert(ids.indexOf(id) >= 0, 'list contains preset id="' + id + '"'); + }); + MCP.list.forEach(function (p) { + assert(typeof p.label === 'string' && p.label.length > 0, 'preset "' + p.id + '" has non-empty label'); + assert(typeof p.description === 'string' && p.description.length > 0, 'preset "' + p.id + '" has 1-line description'); + assert(p.roleColors && typeof p.roleColors === 'object', 'preset "' + p.id + '" has roleColors map'); + ['repeater', 'companion', 'room', 'sensor', 'observer'].forEach(function (role) { + assert(typeof p.roleColors[role] === 'string' && /^#[0-9a-f]{6}$/i.test(p.roleColors[role]), + 'preset "' + p.id + '" has hex roleColors.' + role); + }); + }); +} + +console.log('\n=== #1361 C: applyPreset sets body[data-cb-preset] + CSS vars ==='); +assert(MCP && typeof MCP.applyPreset === 'function', 'applyPreset is a function'); +if (MCP && typeof MCP.applyPreset === 'function') { + ['default', 'deut', 'prot', 'trit', 'achromat'].forEach(function (id) { + MCP.applyPreset(id); + assert(env.body.getAttribute('data-cb-preset') === id, + 'applyPreset("' + id + '") sets body[data-cb-preset="' + id + '"]'); + // Verify the css var for repeater matches the preset's declared color + const declared = MCP.list.find(function (p) { return p.id === id; }).roleColors.repeater; + const got = env.root.style.getPropertyValue('--mc-role-repeater'); + assert(got && got.toLowerCase() === declared.toLowerCase(), + 'applyPreset("' + id + '") sets --mc-role-repeater=' + declared + ' (got ' + got + ')'); + }); +} + +console.log('\n=== #1361 D: persistence — localStorage("meshcore-cb-preset") ==='); +if (MCP) { + MCP.applyPreset('trit'); + assert(env.storage.getItem('meshcore-cb-preset') === 'trit', + 'applyPreset persists choice to localStorage key "meshcore-cb-preset"'); +} + +console.log('\n=== #1361 E: re-init from localStorage re-applies preset ==='); +// Fresh sandbox with localStorage pre-populated +{ + const env2 = makeSandbox(); + env2.storage.setItem('meshcore-cb-preset', 'achromat'); + vm.createContext(env2.sandbox); + try { + vm.runInContext(presetsSrc, env2.sandbox); + const MCP2 = env2.sandbox.window.MeshCorePresets; + // Module init OR explicit initFromStorage should re-apply + if (MCP2 && typeof MCP2.initFromStorage === 'function') MCP2.initFromStorage(); + assert(env2.body.getAttribute('data-cb-preset') === 'achromat', + 're-init from localStorage re-applies "achromat" preset to body data-attr'); + } catch (e) { + assert(false, 're-init sandbox load failed: ' + e.message); + } +} + +console.log('\n=== #1361 F: cross-tab sync via storage event ==='); +if (MCP) { + // Dispatch a synthetic storage event for our key + const ev = new env.sandbox.Event('storage'); + ev.key = 'meshcore-cb-preset'; + ev.newValue = 'prot'; + env.sandbox.dispatchEvent(ev); + assert(env.body.getAttribute('data-cb-preset') === 'prot', + 'storage event with newValue="prot" updates body[data-cb-preset="prot"]'); +} + +console.log('\n=== #1361 G: style.css has preset blocks for non-default presets ==='); +['deut', 'prot', 'trit', 'achromat'].forEach(function (id) { + const re = new RegExp('body\\[data-cb-preset=["\']' + id + '["\']\\][^{]*\\{[^}]*--mc-role-repeater', 'i'); + assert(re.test(styleSrc), + 'style.css has body[data-cb-preset="' + id + '"] block overriding --mc-role-repeater'); +}); + +console.log('\n=== #1361 H: customize-v2.js has Colorblind preset selector UI ==='); +assert(/data-cv2-cb-preset|cust-cb-preset|colorblind|Colorblind/i.test(customSrc), + 'customize-v2.js contains a Colorblind preset selector hook'); +assert(/MeshCorePresets|applyPreset|cb-preset/i.test(customSrc), + 'customize-v2.js wires the UI to MeshCorePresets.applyPreset'); + +console.log('\n=== #1361 I: index.html loads cb-presets.js BEFORE app.js ==='); +const cbIdx = indexSrc.indexOf('cb-presets.js'); +const appIdx = indexSrc.indexOf('app.js?'); +assert(cbIdx > 0, 'index.html includes