mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-25 23:33:35 +00:00
test: add coverage for scopeCellHtml and the column-prefs backfill
Per bot review on PR #1852 (comment 5011643190): two changes in the recent delta had zero test coverage, which the repo's TDD policy (AGENTS.md § Test-First Development) requires for both net-new UI helpers and bug fixes on existing UI. - scopeCellHtml (public/app.js): unit tests for all three states (non-transport dash, resolved region, unresolved "unknown") plus an XSS-escaping check. - Column-prefs backfill (public/packets.js): extracted the inline reconciliation logic into a standalone reconcileVisibleCols(), exposed via _packetsTestAPI like the rest of packets.js's pure helpers, and unit tested directly instead of only through the DOM. Writing the test surfaced a real bug in the original backfill: it couldn't distinguish "column didn't exist yet" from "user explicitly unchecked this column" — both look identical as a missing key in the saved array, so any existing column a user had hidden (e.g. Observer) would get silently resurrected on next page load. Fixed by persisting a second "known columns" baseline (packets-visible-cols-known) alongside the visibility array: a missing key only gets backfilled as newly-visible if it's also absent from the known-columns baseline: a pre-existing key missing from the saved array is a deliberate user choice and stays hidden. Falls back to the pre-#1852 column list for visitors whose saved prefs predate persisting the baseline at all. Also registers scopeCellHtml in .eslintrc.json's cross-file globals list (same treatment as transportBadge) — packets.js referencing it was tripping no-undef since app.js and packets.js are separate non-module scripts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
557d14e50f
commit
696aecc10d
@@ -266,6 +266,7 @@
|
||||
"require": "readonly",
|
||||
"routeLayer": "readonly",
|
||||
"routeTypeName": "readonly",
|
||||
"scopeCellHtml": "readonly",
|
||||
"setupPullToReconnect": "readonly",
|
||||
"syncBadgeColors": "readonly",
|
||||
"timeAgo": "readonly",
|
||||
|
||||
+31
-11
@@ -729,6 +729,31 @@
|
||||
let regionMap = {};
|
||||
const TYPE_NAMES = SHORT_BY_ID;
|
||||
function typeName(t) { return TYPE_NAMES[t] ?? `Type ${t}`; }
|
||||
|
||||
// Column keys that existed before the Scope column (#1852) — the "known"
|
||||
// baseline for a saved array from before packets-visible-cols-known was
|
||||
// ever persisted. A key missing from `saved` is only auto-shown if it's
|
||||
// ALSO missing from `known`; a pre-existing key missing from `saved`
|
||||
// means the user explicitly hid it, and must stay hidden.
|
||||
const LEGACY_COL_KEYS = ['region', 'time', 'hash', 'size', 'type', 'observer', 'path', 'rpt', 'details'];
|
||||
|
||||
// Reconcile a saved column-visibility array (#71) against the current
|
||||
// COL_DEFS, so a since-added column (e.g. "scope") gets shown by default
|
||||
// for returning visitors instead of staying hidden forever — while a
|
||||
// column the user explicitly unchecked (present in `known` but absent
|
||||
// from `saved`) stays hidden. `known` is the column-key set from the last
|
||||
// time prefs were saved; falls back to LEGACY_COL_KEYS when absent
|
||||
// (a visitor whose saved prefs predate persisting it at all).
|
||||
function reconcileVisibleCols(saved, known, colDefs, defaultHidden) {
|
||||
if (!saved) return colDefs.map(c => c.key).filter(k => !defaultHidden.includes(k));
|
||||
const knownSet = new Set(known && known.length ? known : LEGACY_COL_KEYS);
|
||||
const result = saved.slice();
|
||||
for (const c of colDefs) {
|
||||
if (knownSet.has(c.key)) continue;
|
||||
if (!result.includes(c.key) && !defaultHidden.includes(c.key)) result.push(c.key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const isMobile = window.innerWidth <= 1024;
|
||||
const PACKET_LIMIT = isMobile ? 1000 : 50000;
|
||||
let savedTimeWindowMin = Number(localStorage.getItem('meshcore-time-window'));
|
||||
@@ -2021,19 +2046,12 @@
|
||||
// badge (#1188) renders on mobile. Without observer in scope the user
|
||||
// can't see who heard the packet at all.
|
||||
const defaultHidden = isNarrow ? ['region', 'hash', 'path', 'rpt', 'size', 'scope'] : ['region'];
|
||||
let visibleCols;
|
||||
let savedCols, knownCols;
|
||||
try {
|
||||
visibleCols = JSON.parse(localStorage.getItem('packets-visible-cols'));
|
||||
savedCols = JSON.parse(localStorage.getItem('packets-visible-cols'));
|
||||
knownCols = JSON.parse(localStorage.getItem('packets-visible-cols-known'));
|
||||
} catch {}
|
||||
if (!visibleCols) {
|
||||
visibleCols = COL_DEFS.map(c => c.key).filter(k => !defaultHidden.includes(k));
|
||||
} else {
|
||||
// A saved preference predates a since-added column (e.g. "scope") —
|
||||
// default new columns to visible instead of silently hiding them.
|
||||
for (const c of COL_DEFS) {
|
||||
if (!visibleCols.includes(c.key) && !defaultHidden.includes(c.key)) visibleCols.push(c.key);
|
||||
}
|
||||
}
|
||||
let visibleCols = reconcileVisibleCols(savedCols, knownCols, COL_DEFS, defaultHidden);
|
||||
const colMenu = document.getElementById('colToggleMenu');
|
||||
const pktTable = document.getElementById('pktTable');
|
||||
function applyColVisibility() {
|
||||
@@ -2041,6 +2059,7 @@
|
||||
pktTable.classList.toggle('hide-col-' + c.key, !visibleCols.includes(c.key));
|
||||
});
|
||||
localStorage.setItem('packets-visible-cols', JSON.stringify(visibleCols));
|
||||
localStorage.setItem('packets-visible-cols-known', JSON.stringify(COL_DEFS.map(c => c.key)));
|
||||
}
|
||||
colMenu.innerHTML = COL_DEFS.map(c =>
|
||||
`<label><input type="checkbox" data-col="${c.key}" ${visibleCols.includes(c.key) ? 'checked' : ''}> ${c.label}</label>`
|
||||
@@ -3887,6 +3906,7 @@
|
||||
window._packetsTestAPI = {
|
||||
typeName,
|
||||
obsName,
|
||||
reconcileVisibleCols,
|
||||
getDetailPreview,
|
||||
sortGroupChildren,
|
||||
getPathHopCount,
|
||||
|
||||
@@ -1958,6 +1958,27 @@ console.log('\n=== app.js: isTransportRoute + transportBadge ===');
|
||||
assert.ok(html.includes('TRANSPORT_FLOOD'), 'should contain route type name in title');
|
||||
});
|
||||
test('transportBadge(1) returns empty string', () => assert.strictEqual(transportBadge(1), ''));
|
||||
|
||||
const scopeCellHtml = ctx.scopeCellHtml;
|
||||
test('scopeCellHtml non-transport route returns em-dash', () => {
|
||||
assert.strictEqual(scopeCellHtml(1, 'dk'), '—');
|
||||
assert.strictEqual(scopeCellHtml(2, ''), '—');
|
||||
});
|
||||
test('scopeCellHtml transport route with resolved scope shows the region name', () => {
|
||||
const html = scopeCellHtml(0, 'dk-oj');
|
||||
assert.ok(html.includes('badge-scope'), 'should contain badge-scope class');
|
||||
assert.ok(!html.includes('badge-scope-unknown'), 'resolved scope should not use the unknown modifier');
|
||||
assert.ok(html.includes('dk-oj'), 'should contain the region name');
|
||||
});
|
||||
test('scopeCellHtml transport route with no match shows muted unknown', () => {
|
||||
const html = scopeCellHtml(3, '');
|
||||
assert.ok(html.includes('badge-scope-unknown'), 'should contain the unknown modifier class');
|
||||
assert.ok(html.includes('unknown'), 'should contain the word unknown');
|
||||
});
|
||||
test('scopeCellHtml escapes region names', () => {
|
||||
const html = scopeCellHtml(0, '<img src=x onerror=alert(1)>');
|
||||
assert.ok(!html.includes('<img'), 'should not contain raw injected markup');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== ANALYTICS.JS: Channel Sort =====
|
||||
|
||||
@@ -1019,6 +1019,58 @@ console.log('\n=== packets.js: page registration ===');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n=== packets.js: reconcileVisibleCols (column-prefs backfill) ===');
|
||||
{
|
||||
const ctx = loadPacketsSandbox();
|
||||
const api = ctx._packetsTestAPI;
|
||||
const COL_DEFS = [
|
||||
{ key: 'region' }, { key: 'time' }, { key: 'hash' }, { key: 'size' },
|
||||
{ key: 'type' }, { key: 'scope' }, { key: 'observer' }, { key: 'path' },
|
||||
{ key: 'rpt' }, { key: 'details' },
|
||||
];
|
||||
const defaultHidden = ['region'];
|
||||
|
||||
test('reconcileVisibleCols is exported', () => {
|
||||
assert(typeof api.reconcileVisibleCols === 'function');
|
||||
});
|
||||
|
||||
test('no saved prefs: falls back to all columns minus defaultHidden', () => {
|
||||
const result = api.reconcileVisibleCols(null, null, COL_DEFS, defaultHidden);
|
||||
assert.deepStrictEqual(result, COL_DEFS.map(c => c.key).filter(k => k !== 'region'));
|
||||
});
|
||||
|
||||
test('legacy saved prefs (no known-cols baseline yet) get the scope column backfilled', () => {
|
||||
// Reproduces a real saved localStorage value from before the Scope
|
||||
// column (#1852) existed, and before packets-visible-cols-known was
|
||||
// ever persisted — must not stay permanently hidden.
|
||||
const legacy = ['time', 'hash', 'size', 'type', 'observer', 'path', 'rpt', 'details'];
|
||||
const result = api.reconcileVisibleCols(legacy, null, COL_DEFS, defaultHidden);
|
||||
assert.ok(result.includes('scope'), 'scope column should be backfilled into a legacy saved array');
|
||||
});
|
||||
|
||||
test('a column the user explicitly hid (present in known, absent from saved) stays hidden', () => {
|
||||
const known = ['region', 'time', 'hash', 'size', 'type', 'scope', 'observer', 'path', 'rpt', 'details'];
|
||||
const saved = ['time', 'hash', 'size', 'type', 'scope', 'path', 'rpt', 'details']; // observer unchecked by user
|
||||
const result = api.reconcileVisibleCols(saved, known, COL_DEFS, defaultHidden);
|
||||
assert.ok(!result.includes('observer'), 'user-hidden column must not be resurrected');
|
||||
});
|
||||
|
||||
test('a genuinely new column (absent from known) gets backfilled even with a known-cols baseline', () => {
|
||||
const known = ['region', 'time', 'hash', 'size', 'type', 'observer', 'path', 'rpt', 'details']; // no scope yet
|
||||
const saved = ['time', 'hash', 'size', 'type', 'observer', 'path', 'rpt', 'details'];
|
||||
const result = api.reconcileVisibleCols(saved, known, COL_DEFS, defaultHidden);
|
||||
assert.ok(result.includes('scope'), 'a column absent from the known baseline should be backfilled');
|
||||
});
|
||||
|
||||
test('a column in defaultHidden is not force-added even if missing from saved and known', () => {
|
||||
const legacy = ['time', 'hash', 'type', 'observer', 'path', 'rpt', 'details']; // no size, no scope
|
||||
const narrowDefaultHidden = ['region', 'size', 'scope'];
|
||||
const result = api.reconcileVisibleCols(legacy, null, COL_DEFS, narrowDefaultHidden);
|
||||
assert.ok(!result.includes('scope'), 'columns in defaultHidden should not be backfilled');
|
||||
assert.ok(!result.includes('size'), 'columns in defaultHidden should not be backfilled');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n=== packets.js: _invalidateRowCounts / _refreshRowCountsIfDirty (#410) ===');
|
||||
{
|
||||
const ctx = loadPacketsSandbox();
|
||||
|
||||
Reference in New Issue
Block a user