mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-12 18:25:57 +00:00
Partial fix for #1858. Clears four blockers; does **not** close the issue. ## Why partial matters here `test-all.sh` aborts at the first failing script. So the 9 red tests the issue counts are not 9 problems a contributor can work through — the run stops at `test-frontend-helpers.js` and everything after it is invisible. You have to fix them in order just to see the next one. That is what this does, as far as I could go without guessing. ## Three assertions that outlived their features Each was left behind by a merged PR that changed the thing being asserted. None could pass again without reverting that work. | Test | Asserted | Reality | |---|---|---| | `test-frontend-helpers.js` | `★` / `☆` glyphs in `favStar` | #1648 replaced them with `#ph-star-fill` / `#ph-star` sprite refs | | `test-channel-psk-ux.js` | literal `🔓` | same migration → `#ph-lock-open` | | `test-analytics-channels-integration.js` | a `#/analytics` chip in `channels.js` | #1367 / PR #1376 deliberately dropped it | The `favStar` rewrite also tightens two assertions that were not doing their job: `html.includes('on')` matched the word "favorites", and the negative case checked `!html.includes(' on')` rather than the class. Both now assert `aria-pressed` and the class explicitly. For the analytics chip I removed the assertion rather than weakening it, and left the reason in place so it does not get "restored" by someone reading a bare deletion: ```js // The "Channels page links to Analytics" assertion that used to live here was // removed: #1367 / PR #1376 ... deliberately deleted the `#/analytics` chip. ``` ## A Windows-only bug in the emoji sweep `test-issue-1648-m6-final-sweep.js` reports **64 violations on Windows and 1 on Linux**. `walkFiles` builds each path with `path.relative`, which yields `public\cb-presets.js` on Windows. Every entry in `tests/emoji-allowlist.txt` — and the ignore list in the same function — is written with `/`. So `matchesGlob` matched nothing, and every deliberately-annotated line (WCAG contrast `✓`/`✗` notes, `⌈⌉` ceiling brackets in audio-lab) was reported as a violation. One line, normalising to forward slashes. 64 → 1. The remaining 1 is the `🗺️` in `public/rx-coverage.js` that **PR #1860 already fixes**, and `test-issue-1648-m6-lint-self.js` cascades from it (`repo MUST be clean before anti-tautology probe`). Both go green when #1860 merges. Not duplicated here. ## Still red, deliberately untouched Four tests fail for reasons that need a behavioural decision rather than a test edit: - `test-issue-1438-customizer-mcrole.js` — `--mc-role-companion` empty - `test-issue-1446-cb-preset-cascade.js` — see below - `test-issue-1470-node-tile-helper.js` — `getActiveTileProvider` returns `undefined` for voyager; `getTileUrl` yields the `dark_all` URL instead - `test-issue-1485-live-anim-z.js` — animLayer hosts 2 animation shapes, ≥3 expected ### #1446 may not be a stale test Worth a second look before anyone edits it: - **Scenario 6** (roles.js + cb-presets.js, stored preset `deut`) **passes** — `--mc-role-repeater` = `#fe6100`. - **Scenario 5** is the same setup plus `customize-v2.js` and `_customizerV2.init({nodeColors:{repeater:'#aaaaaa'}})`, and the var comes back **empty** — not the preset colour, not the server colour. If that is the real behaviour, a colourblind user with a saved preset loses their role colours the moment the customizer initialises against a server config carrying `nodeColors`. I left it alone: the three-way cascade (preset vs server config vs user override) is exactly what #1446 defined, and picking a side changes colours for the users the feature exists for. That is your call, not mine. ## Not addressed: the structural half Three hand-maintained test lists (267 files, 159 in `deploy.yml`, 53 in `test-all.sh`, no CI invocation of the latter) is the root cause the issue identifies, and it is a workflow decision rather than a bug fix. Happy to do it in whatever shape you want — the obvious one being CI calling `test-all.sh` and the two lists collapsing into one — but that changes what gates merges, so it should be your choice. ## Verification ``` node test-frontend-helpers.js → 627 passed, 0 failed node test-channel-psk-ux.js → 20 passed, 0 failed node test-analytics-channels-integration.js → 23 passed node test-issue-1648-m6-final-sweep.js → 64 violations → 1 ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
230 lines
9.3 KiB
JavaScript
230 lines
9.3 KiB
JavaScript
/* test-issue-1470-node-tile-helper.js — behavioral test for the
|
|
* _applyTilesToNodeMap helper shipped in #1471 and roles.js
|
|
* getActiveTileProvider() / getTileUrl() integration with the
|
|
* MC_TILE_PROVIDERS registry.
|
|
*
|
|
* Strategy:
|
|
* - vm-load public/map-tile-providers.js + public/roles.js into a single
|
|
* sandbox with mocked localStorage + document(theme=dark).
|
|
* - 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 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.
|
|
*
|
|
* No source-grep on the production fix. The assertions exercise the helper
|
|
* code path end-to-end. Reverting the fix (re-hardcoding OSM) breaks this.
|
|
*/
|
|
'use strict';
|
|
const vm = require('vm');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
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 makeStorage() {
|
|
const store = {};
|
|
return {
|
|
getItem(k) { return Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null; },
|
|
setItem(k, v) { store[k] = String(v); },
|
|
removeItem(k) { delete store[k]; },
|
|
clear() { for (const k of Object.keys(store)) delete store[k]; },
|
|
};
|
|
}
|
|
|
|
function makeLeafletMock() {
|
|
const calls = [];
|
|
const tilePane = { style: { filter: '' } };
|
|
const fakeMap = {
|
|
_tileLayers: [],
|
|
getPane: (name) => name === 'tilePane' ? tilePane : null,
|
|
};
|
|
const L = {
|
|
tileLayer(url, opts) {
|
|
const layer = {
|
|
url, opts,
|
|
addTo(map) {
|
|
calls.push({ kind: 'tileLayer', url, opts, map });
|
|
map._tileLayers.push(this);
|
|
return this;
|
|
},
|
|
};
|
|
return layer;
|
|
},
|
|
map() { return fakeMap; },
|
|
marker() { return { addTo: () => ({ bindPopup: () => {} }) }; },
|
|
};
|
|
return { L, calls, fakeMap, tilePane };
|
|
}
|
|
|
|
function makeSandbox(opts) {
|
|
opts = opts || {};
|
|
const ctx = {
|
|
console,
|
|
setTimeout, clearTimeout,
|
|
JSON, Date, Math, Object, Array, String, Number, Boolean, Set, Map,
|
|
fetch: () => Promise.resolve({ ok: false, json: () => Promise.resolve({}) }),
|
|
localStorage: makeStorage(),
|
|
document: {
|
|
documentElement: {
|
|
getAttribute: () => opts.theme || 'dark',
|
|
style: { getPropertyValue: () => '' },
|
|
},
|
|
querySelector: () => null,
|
|
querySelectorAll: () => [],
|
|
getElementById: () => null,
|
|
createElement: () => ({ style: {}, appendChild: () => {}, setAttribute: () => {}, addEventListener: () => {} }),
|
|
addEventListener: () => {},
|
|
body: { appendChild: () => {}, style: {} },
|
|
head: { appendChild: () => {} },
|
|
readyState: 'complete',
|
|
},
|
|
window: {
|
|
addEventListener: () => {},
|
|
dispatchEvent: () => true,
|
|
matchMedia: () => ({ matches: opts.prefersDark !== false, addEventListener: () => {} }),
|
|
},
|
|
CustomEvent: function (type, init) { this.type = type; this.detail = (init && init.detail) || null; },
|
|
};
|
|
ctx.window.localStorage = ctx.localStorage;
|
|
ctx.window.document = ctx.document;
|
|
ctx.globalThis = ctx;
|
|
vm.createContext(ctx);
|
|
// After context creation, make the sandbox global object also be `window`
|
|
// so `window.X = ...` in roles.js makes X a bare global the same file
|
|
// can reference. We do this by copying assignments back — simpler: install
|
|
// a Proxy. Cheapest practical fix: shadow common globals after load.
|
|
return ctx;
|
|
}
|
|
|
|
function loadInto(ctx, relPath) {
|
|
const src = fs.readFileSync(path.join(__dirname, relPath), 'utf8');
|
|
vm.runInContext(src, ctx, { filename: relPath });
|
|
// Mirror window.* back to sandbox globals so code that uses bare names
|
|
// (which in a browser are window.X) can still resolve them in vm. We do
|
|
// this AFTER each file load so window.getTileUrl, window.TILE_DARK, etc.
|
|
// become bare refs `getTileUrl` / `TILE_DARK` for callers in the same
|
|
// sandbox.
|
|
for (const k of Object.keys(ctx.window)) {
|
|
if (!(k in ctx)) ctx[k] = ctx.window[k];
|
|
}
|
|
}
|
|
|
|
function extractApplyTilesHelper() {
|
|
const nodesSrc = fs.readFileSync(path.join(__dirname, 'public', 'nodes.js'), 'utf8');
|
|
// Match the function _applyTilesToNodeMap(map) { ... } block. Brace-count.
|
|
const startMatch = nodesSrc.match(/function\s+_applyTilesToNodeMap\s*\(map\)\s*\{/);
|
|
if (!startMatch) throw new Error('_applyTilesToNodeMap definition not found in public/nodes.js');
|
|
const start = startMatch.index;
|
|
let depth = 0, i = start;
|
|
for (; i < nodesSrc.length; i++) {
|
|
const ch = nodesSrc[i];
|
|
if (ch === '{') depth++;
|
|
else if (ch === '}') {
|
|
depth--;
|
|
if (depth === 0) { i++; break; }
|
|
}
|
|
}
|
|
return nodesSrc.slice(start, i);
|
|
}
|
|
|
|
console.log('── #1470 node-detail inset-map tile-provider routing ──');
|
|
|
|
test('roles.js + providers: getActiveTileProvider returns null in light mode', () => {
|
|
const ctx = makeSandbox({ theme: 'light', prefersDark: false });
|
|
loadInto(ctx, 'public/map-tile-providers.js');
|
|
loadInto(ctx, 'public/roles.js');
|
|
const p = ctx.window.getActiveTileProvider();
|
|
assert.strictEqual(p, null, 'expected null in light mode, got ' + JSON.stringify(p));
|
|
});
|
|
|
|
test('roles.js + providers: getActiveTileProvider returns selected provider in dark mode', () => {
|
|
const ctx = makeSandbox({ theme: 'dark' });
|
|
loadInto(ctx, 'public/map-tile-providers.js');
|
|
loadInto(ctx, 'public/roles.js');
|
|
ctx.window.MC_setDarkTileProvider('carto-voyager-dark');
|
|
const p = ctx.window.getActiveTileProvider();
|
|
assert.ok(p, 'provider returned in dark mode');
|
|
// #1533 made provider urls lazy functions so the Carto base can be resolved
|
|
// at call time (_getCartoBase); they were plain strings under #1430.
|
|
assert.ok(typeof p.url === 'function', 'provider url is a resolver function — got ' + typeof p.url);
|
|
assert.ok(/voyager/.test(p.url()),
|
|
'provider url resolves to voyager — got ' + JSON.stringify(p.url()));
|
|
assert.ok(typeof p.invertFilter === 'string' && /invert\(/.test(p.invertFilter),
|
|
'carto-voyager-dark invertFilter present — got ' + JSON.stringify(p.invertFilter));
|
|
});
|
|
|
|
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('carto-voyager-dark');
|
|
const url = ctx.window.getTileUrl();
|
|
assert.ok(/voyager/.test(url), 'getTileUrl returns voyager URL — got ' + url);
|
|
});
|
|
|
|
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('carto-voyager-dark');
|
|
|
|
const mock = makeLeafletMock();
|
|
ctx.L = mock.L;
|
|
ctx._fakeMap = mock.fakeMap;
|
|
vm.runInContext(extractApplyTilesHelper(), ctx, { filename: 'nodes.js#_applyTilesToNodeMap' });
|
|
vm.runInContext('_applyTilesToNodeMap(_fakeMap);', ctx);
|
|
|
|
assert.ok(mock.calls.length >= 1, 'tileLayer was called — got ' + mock.calls.length + ' calls');
|
|
const firstCall = mock.calls[0];
|
|
assert.ok(/voyager/.test(firstCall.url),
|
|
'first tileLayer URL is voyager — got ' + firstCall.url);
|
|
assert.ok(/invert\(/.test(mock.tilePane.style.filter),
|
|
'tile pane filter set to invert(...) — got ' + JSON.stringify(mock.tilePane.style.filter));
|
|
});
|
|
|
|
test('_applyTilesToNodeMap: dark + carto-dark (non-inverted) → no invert filter applied', () => {
|
|
const ctx = makeSandbox({ theme: 'dark' });
|
|
loadInto(ctx, 'public/map-tile-providers.js');
|
|
loadInto(ctx, 'public/roles.js');
|
|
ctx.window.MC_setDarkTileProvider('carto-dark');
|
|
|
|
const mock = makeLeafletMock();
|
|
ctx.L = mock.L;
|
|
ctx._fakeMap = mock.fakeMap;
|
|
vm.runInContext(extractApplyTilesHelper(), ctx, { filename: 'nodes.js#_applyTilesToNodeMap' });
|
|
vm.runInContext('_applyTilesToNodeMap(_fakeMap);', ctx);
|
|
|
|
assert.ok(mock.calls.length >= 1, 'tileLayer was called');
|
|
assert.ok(/cartocdn|basemaps\.cartocdn|dark_all/.test(mock.calls[0].url),
|
|
'carto-dark URL — got ' + mock.calls[0].url);
|
|
assert.strictEqual(mock.tilePane.style.filter, '',
|
|
'no invert filter for carto-dark — got ' + JSON.stringify(mock.tilePane.style.filter));
|
|
});
|
|
|
|
test('_applyTilesToNodeMap: light mode → uses TILE_LIGHT (carto light_all), no invert', () => {
|
|
const ctx = makeSandbox({ theme: 'light', prefersDark: false });
|
|
loadInto(ctx, 'public/map-tile-providers.js');
|
|
loadInto(ctx, 'public/roles.js');
|
|
|
|
const mock = makeLeafletMock();
|
|
ctx.L = mock.L;
|
|
ctx._fakeMap = mock.fakeMap;
|
|
vm.runInContext(extractApplyTilesHelper(), ctx, { filename: 'nodes.js#_applyTilesToNodeMap' });
|
|
vm.runInContext('_applyTilesToNodeMap(_fakeMap);', ctx);
|
|
|
|
assert.ok(mock.calls.length >= 1, 'tileLayer called');
|
|
assert.ok(/light_all|openstreetmap/.test(mock.calls[0].url),
|
|
'light mode uses light tile URL — got ' + mock.calls[0].url);
|
|
assert.strictEqual(mock.tilePane.style.filter, '', 'no invert filter in light mode');
|
|
});
|
|
|
|
console.log(`\n#1470 node-detail tile helper: ${passed} passed, ${failed} failed`);
|
|
process.exit(failed === 0 ? 0 : 1);
|