Files
meshcore-analyzer/test-issue-1509-nav-active-bg.js
T
b74a64ccfa fix(ui): canonical payload label map across packets/live/packet-filter (#1799) (#1804)
## Summary

Replaces the three drifted per-surface payload-type label vocabularies
with a single canonical map keyed by firmware enum name.

Per the locked triage comment on #1799
([comment-4823975431](https://github.com/Kpa-clawbot/CoreScope/issues/1799#issuecomment-4823975431)):

> Create `public/payload-labels.js` exporting `{GRP_DATA: {short:'Group
Data', long:'Group data packet', enumId:6}, ...}`. Migrate `packets.js
typeMap`, `packet-filter.js FW_PAYLOAD_TYPES`, `live.js TYPE_COLORS
legend` to consume it. E2E that scrapes each surface and asserts label
equality.

## Changes

- **`public/payload-labels.js`** (new) — canonical map exposed as
`window.PayloadLabels` and `window.PayloadLabelsApi`. Keys are firmware
enum names; values carry `{short, long, enumId}` plus derived
`SHORT_BY_ID` / `FW_PAYLOAD_TYPES` / `TYPE_ALIASES` for legacy callers.
- **`public/packets.js`** — `TYPE_NAMES` + `typeMap` now read from
`PayloadLabelsApi.SHORT_BY_ID`. Literal kept only as a defensive
fallback for the case where the script tag fails to load.
- **`public/packet-filter.js`** — `FW_PAYLOAD_TYPES` + `TYPE_ALIASES`
now sourced from `PayloadLabelsApi`. Literal fallback retained so `node
test-packet-filter.js` still works headlessly.
- **`public/live.js`** — legend `<li>` rows are now generated from
`window.PayloadLabels` in stable order, killing the third-vocabulary
`Message — Group text` / `Direct — Direct message` drift the #1797
review surfaced.
- **`public/index.html`** — `<script src="payload-labels.js">` loaded
before `roles.js` / `packet-filter.js` / `packets.js`.
- **`test-issue-1799-label-vocab-e2e.js`** (new) — Playwright E2E.
Scrapes `#liveLegend` rows and the `/packets` type-filter checklist,
asserts each label matches `window.PayloadLabels[ENUM].short` for
`TXT_MSG`, `GRP_TXT`, `GRP_DATA`. Also verifies `window.PacketFilter`
still recognises the enum names.
- **`.github/workflows/deploy.yml`** — wired the new E2E into the
existing Playwright block.

## TDD trail

- Red commit `eb392d4` — adds the failing E2E only (asserts
`window.PayloadLabels` exists and labels match; both fail).
- Green commit `44e902a` — introduces the canonical map and migrates the
three surfaces.

## Verification

- `node test-packet-filter.js` — 92/92 pass with the new fallback
wiring.
- Preflight: `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh
origin/master` — clean.

Browser verified: E2E `test-issue-1799-label-vocab-e2e.js` exercises
`/live` legend + `/packets` type filter against a Playwright headless
Chromium; CI's Playwright block runs it on every push.

E2E assertion added: `test-issue-1799-label-vocab-e2e.js:139` —
`assert(fromLegend === canon, ...)` and `assert(fromPackets === canon,
...)` per enum.

Fixes #1799

---------

Co-authored-by: mc-bot <bot@corescope>
Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: clawbot <clawbot@kpa.com>
Co-authored-by: clawbot <bot@clawbot.local>
2026-06-30 05:48:47 -07:00

120 lines
4.8 KiB
JavaScript

/**
* Regression test for #1509: --nav-active-bg must be theme-overridable.
*
* The active nav pill uses `background: var(--nav-active-bg)` in style.css.
* Before #1509 the customizer's THEME_CSS_MAP did not include navActiveBg,
* so themes / per-operator overrides could not change the active-pill color
* even though every other nav color (navBg, navBg2, navText, navTextMuted)
* was themeable.
*
* This unit test asserts:
* 1. THEME_CSS_MAP exposes navActiveBg → '--nav-active-bg'
* 2. THEME_COLOR_KEYS implicitly picks it up (it derives from THEME_CSS_MAP)
* 3. applyCSS() actually writes the variable on documentElement when an
* override is present (proves end-to-end wiring, not just the map entry)
* 4. The 'default' preset seeds both light + dark themes with a navActiveBg
* default so themes don't fall back to the hardcoded rgba()
*
* Run: node test-issue-1509-nav-active-bg.js
*/
'use strict';
const vm = require('vm');
const fs = require('fs');
const assert = require('assert');
let passed = 0, failed = 0;
function test(name, fn) {
try { fn(); passed++; console.log(` ✅ ${name}`); }
catch (e) { failed++; console.log(` ❌ ${name}: ${e.message}`); }
}
function makeSandbox(opts) {
opts = opts || {};
const storage = {};
const localStorage = {
getItem(k) { return k in storage ? storage[k] : null; },
setItem(k, v) { storage[k] = String(v); },
removeItem(k) { delete storage[k]; },
clear() { for (const k in storage) delete storage[k]; }
};
// Capture CSS variable writes for assertion.
const cssProps = {};
const ctx = {
window: { addEventListener: () => {}, dispatchEvent: () => {}, SITE_CONFIG: {}, _SITE_CONFIG_ORIGINAL_HOME: null },
document: {
readyState: 'loading',
createElement: () => ({
id: '', textContent: '', innerHTML: '', className: '',
setAttribute: () => {}, appendChild: () => {},
style: {}, addEventListener: () => {},
querySelectorAll: () => [], querySelector: () => null,
}),
head: { appendChild: () => {} },
getElementById: () => null,
addEventListener: () => {},
querySelectorAll: () => [],
querySelector: () => null,
body: { style: { setProperty: () => {} } },
documentElement: {
style: {
setProperty: (k, v) => { cssProps[k] = v; },
removeProperty: (k) => { delete cssProps[k]; },
getPropertyValue: (k) => cssProps[k] || '',
},
dataset: { theme: opts.theme || 'light' },
getAttribute: () => (opts.theme || 'light'),
},
},
console,
localStorage,
setTimeout: (fn) => fn(),
clearTimeout: () => {},
Date, Math, Array, Object, JSON, String, Number, Boolean,
parseInt, parseFloat, isNaN, Infinity, NaN, undefined,
MutationObserver: class { observe() {} },
HashChangeEvent: class {},
CustomEvent: class CustomEvent { constructor(type, opts) { this.type = type; this.detail = opts && opts.detail; } },
getComputedStyle: () => ({ getPropertyValue: () => '' }),
};
ctx.window.localStorage = localStorage;
ctx.self = ctx.window;
return { ctx, cssProps };
}
function loadCustomizer(opts) {
const { ctx, cssProps } = makeSandbox(opts);
const labelsCode = fs.readFileSync('public/payload-labels.js', 'utf8');
const code = fs.readFileSync('public/customize-v2.js', 'utf8');
vm.createContext(ctx);
vm.runInContext(labelsCode, ctx, { filename: 'payload-labels.js' });
vm.runInContext(code, ctx, { filename: 'customize-v2.js' });
return { api: ctx.window._customizerV2, cssProps, ls: ctx.localStorage };
}
console.log('\n#1509 — themeable --nav-active-bg\n');
test('THEME_CSS_MAP maps navActiveBg → --nav-active-bg', () => {
const { api } = loadCustomizer();
assert.strictEqual(api.THEME_CSS_MAP.navActiveBg, '--nav-active-bg',
'THEME_CSS_MAP.navActiveBg should be "--nav-active-bg"');
});
test('applyCSS writes --nav-active-bg on documentElement when overridden (light)', () => {
const { api, cssProps, ls } = loadCustomizer({ theme: 'light' });
ls.setItem('cs-theme-overrides', JSON.stringify({ theme: { navActiveBg: '#abcdef' } }));
api.init({});
assert.strictEqual(cssProps['--nav-active-bg'], '#abcdef',
'expected --nav-active-bg=#abcdef on documentElement, got ' + cssProps['--nav-active-bg']);
});
test('applyCSS writes --nav-active-bg on documentElement when overridden (dark)', () => {
const { api, cssProps, ls } = loadCustomizer({ theme: 'dark' });
ls.setItem('cs-theme-overrides', JSON.stringify({ themeDark: { navActiveBg: '#112233' } }));
api.init({});
assert.strictEqual(cssProps['--nav-active-bg'], '#112233',
'expected --nav-active-bg=#112233 on documentElement in dark mode, got ' + cssProps['--nav-active-bg']);
});
console.log('\n' + passed + '/' + (passed + failed) + ' tests passed');
process.exit(failed > 0 ? 1 : 0);