mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-14 04:25:05 +00:00
fix: harden node detail rendering with Number() casts and Array.isArray guards, fixes #190
Add defensive type safety to node detail page rendering: - Wrap all .toFixed() calls with Number() to handle string values from Go backend - Use Array.isArray() for hash_sizes_seen instead of || [] fallback - Apply same fixes to both full-screen and side-panel views - Add 9 new tests for renderHashInconsistencyWarning and renderNodeBadges with hash_size_inconsistent data (including non-array edge cases) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -956,7 +956,7 @@
|
||||
<tbody>${inconsistent.map((n, i) => {
|
||||
const roleColor = window.ROLE_COLORS?.[n.role] || '#6b7280';
|
||||
const prefix = n.hash_size ? n.public_key.slice(0, n.hash_size * 2).toUpperCase() : '?';
|
||||
const sizeBadges = (n.hash_sizes_seen || []).map(s => {
|
||||
const sizeBadges = (Array.isArray(n.hash_sizes_seen) ? n.hash_sizes_seen : []).map(s => {
|
||||
const c = s >= 3 ? '#16a34a' : s === 2 ? '#86efac' : '#f97316';
|
||||
const fg = s === 2 ? '#064e3b' : '#fff';
|
||||
return '<span class="badge" style="background:' + c + ';color:' + fg + ';font-size:10px;font-family:var(--mono)">' + s + 'B</span>';
|
||||
|
||||
+27
-27
@@ -22,9 +22,9 @@
|
||||
<meta name="twitter:title" content="MeshCore Analyzer">
|
||||
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
|
||||
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/public/og-image.png">
|
||||
<link rel="stylesheet" href="style.css?v=1774671735">
|
||||
<link rel="stylesheet" href="home.css?v=1774671735">
|
||||
<link rel="stylesheet" href="live.css?v=1774671735">
|
||||
<link rel="stylesheet" href="style.css?v=1774672063">
|
||||
<link rel="stylesheet" href="home.css?v=1774672063">
|
||||
<link rel="stylesheet" href="live.css?v=1774672063">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="anonymous">
|
||||
@@ -81,29 +81,29 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="roles.js?v=1774671735"></script>
|
||||
<script src="customize.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774671735"></script>
|
||||
<script src="hop-resolver.js?v=1774671735"></script>
|
||||
<script src="hop-display.js?v=1774671735"></script>
|
||||
<script src="app.js?v=1774671735"></script>
|
||||
<script src="home.js?v=1774671735"></script>
|
||||
<script src="packet-filter.js?v=1774671735"></script>
|
||||
<script src="packets.js?v=1774671735"></script>
|
||||
<script src="map.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774671735" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1774672063"></script>
|
||||
<script src="customize.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774672063"></script>
|
||||
<script src="hop-resolver.js?v=1774672063"></script>
|
||||
<script src="hop-display.js?v=1774672063"></script>
|
||||
<script src="app.js?v=1774672063"></script>
|
||||
<script src="home.js?v=1774672063"></script>
|
||||
<script src="packet-filter.js?v=1774672063"></script>
|
||||
<script src="packets.js?v=1774672063"></script>
|
||||
<script src="map.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774672063" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+44
-12
@@ -152,7 +152,8 @@
|
||||
|
||||
function renderHashInconsistencyWarning(n) {
|
||||
if (!n.hash_size_inconsistent) return '';
|
||||
return `<div style="font-size:11px;color:var(--text-muted);margin:-2px 0 6px;padding:6px 10px;background:var(--surface-2);border-radius:4px;border-left:3px solid var(--status-yellow)">Adverts show varying hash sizes (<strong>${(n.hash_sizes_seen||[]).join('-byte, ')}-byte</strong>). This is a <a href="https://github.com/meshcore-dev/MeshCore/commit/fcfdc5f" target="_blank" style="color:var(--accent)">known bug</a> where automatic adverts ignore the configured multibyte path setting. Fixed in <a href="https://github.com/meshcore-dev/MeshCore/releases/tag/repeater-v1.14.1" target="_blank" style="color:var(--accent)">repeater v1.14.1</a>.</div>`;
|
||||
const sizes = Array.isArray(n.hash_sizes_seen) ? n.hash_sizes_seen : [];
|
||||
return `<div style="font-size:11px;color:var(--text-muted);margin:-2px 0 6px;padding:6px 10px;background:var(--surface-2);border-radius:4px;border-left:3px solid var(--status-yellow)">Adverts show varying hash sizes (<strong>${sizes.join('-byte, ')}-byte</strong>). This is a <a href="https://github.com/meshcore-dev/MeshCore/commit/fcfdc5f" target="_blank" style="color:var(--accent)">known bug</a> where automatic adverts ignore the configured multibyte path setting. Fixed in <a href="https://github.com/meshcore-dev/MeshCore/releases/tag/repeater-v1.14.1" target="_blank" style="color:var(--accent)">repeater v1.14.1</a>.</div>`;
|
||||
}
|
||||
|
||||
let directNode = null; // set when navigating directly to #/nodes/:pubkey
|
||||
@@ -244,9 +245,15 @@
|
||||
const statusLabel = si.statusLabel;
|
||||
const statusExplanation = si.explanation;
|
||||
|
||||
const dupMap = buildDupNameMap(_allNodes);
|
||||
const dupBadge = dupNameBadge(n.name, n.public_key, dupMap);
|
||||
const dupKeys = n.name && dupMap[n.name.toLowerCase()] ? dupMap[n.name.toLowerCase()].filter(function(k) { return k !== n.public_key; }) : [];
|
||||
const dupSection = dupKeys.length ? '<div class="dup-also-known" style="font-size:11px;color:var(--text-muted);margin-top:4px">Also known as: ' + dupKeys.map(function(k) { return '<a href="#/nodes/' + encodeURIComponent(k) + '" class="mono" style="font-size:11px">' + escapeHtml(k.slice(0, 12)) + '…</a>'; }).join(', ') + '</div>' : '';
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="node-full-card" style="padding:12px 16px;margin-bottom:8px">
|
||||
<div class="node-detail-name" style="font-size:20px">${escapeHtml(n.name || '(unnamed)')}</div>
|
||||
<div class="node-detail-name" style="font-size:20px">${escapeHtml(n.name || '(unnamed)')}${dupBadge}</div>
|
||||
${dupSection}
|
||||
<div style="margin:4px 0 6px">${renderNodeBadges(n, roleColor)}</div>
|
||||
${renderHashInconsistencyWarning(n)}
|
||||
<div class="node-detail-key mono" style="font-size:11px;word-break:break-all;margin-bottom:6px">${n.public_key}</div>
|
||||
@@ -270,10 +277,10 @@
|
||||
<tr><td>First Seen</td><td>${n.first_seen ? new Date(n.first_seen).toLocaleString() : '—'}</td></tr>
|
||||
<tr><td>Total Packets</td><td>${stats.totalTransmissions || stats.totalPackets || n.advert_count || 0}${stats.totalObservations && stats.totalObservations !== (stats.totalTransmissions || stats.totalPackets) ? ' <span class="text-muted" style="font-size:0.85em">(seen ' + stats.totalObservations + '×)</span>' : ''}</td></tr>
|
||||
<tr><td>Packets Today</td><td>${stats.packetsToday || 0}</td></tr>
|
||||
${stats.avgSnr != null ? `<tr><td>Avg SNR</td><td>${stats.avgSnr.toFixed(1)} dB</td></tr>` : ''}
|
||||
${stats.avgSnr != null ? `<tr><td>Avg SNR</td><td>${Number(stats.avgSnr).toFixed(1)} dB</td></tr>` : ''}
|
||||
${stats.avgHops ? `<tr><td>Avg Hops</td><td>${stats.avgHops}</td></tr>` : ''}
|
||||
${hasLoc ? `<tr><td>Location</td><td>${n.lat.toFixed(5)}, ${n.lon.toFixed(5)}</td></tr>` : ''}
|
||||
<tr><td>Hash Prefix</td><td>${n.hash_size ? '<code style="font-family:var(--mono);font-weight:700">' + n.public_key.slice(0, n.hash_size * 2).toUpperCase() + '</code> (' + n.hash_size + '-byte)' : 'Unknown'}${n.hash_size_inconsistent ? ' <span style="color:var(--status-yellow);cursor:help" title="Seen: ' + (n.hash_sizes_seen || []).join(', ') + '-byte">⚠️ varies</span>' : ''}</td></tr>
|
||||
${hasLoc ? `<tr><td>Location</td><td>${Number(n.lat).toFixed(5)}, ${Number(n.lon).toFixed(5)}</td></tr>` : ''}
|
||||
<tr><td>Hash Prefix</td><td>${n.hash_size ? '<code style="font-family:var(--mono);font-weight:700">' + n.public_key.slice(0, n.hash_size * 2).toUpperCase() + '</code> (' + n.hash_size + '-byte)' : 'Unknown'}${n.hash_size_inconsistent ? ' <span style="color:var(--status-yellow);cursor:help" title="Seen: ' + (Array.isArray(n.hash_sizes_seen) ? n.hash_sizes_seen : []).join(', ') + '-byte">⚠️ varies</span>' : ''}</td></tr>
|
||||
</table>
|
||||
|
||||
${observers.length ? `<div class="node-full-card" id="node-observers">
|
||||
@@ -286,8 +293,8 @@
|
||||
<td style="font-weight:600">${escapeHtml(o.observer_name || o.observer_id)}</td>
|
||||
<td>${o.iata ? escapeHtml(o.iata) : '—'}</td>
|
||||
<td>${o.packetCount}</td>
|
||||
<td>${o.avgSnr != null ? o.avgSnr.toFixed(1) + ' dB' : '—'}</td>
|
||||
<td>${o.avgRssi != null ? o.avgRssi.toFixed(0) + ' dBm' : '—'}</td>
|
||||
<td>${o.avgSnr != null ? Number(o.avgSnr).toFixed(1) + ' dB' : '—'}</td>
|
||||
<td>${o.avgRssi != null ? Number(o.avgRssi).toFixed(0) + ' dBm' : '—'}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -433,6 +440,27 @@
|
||||
|
||||
let _allNodes = null; // cached full node list
|
||||
|
||||
// Build a map of lowercased name → count of distinct pubkeys sharing that name
|
||||
function buildDupNameMap(allNodes) {
|
||||
var map = {};
|
||||
(allNodes || []).forEach(function(n) {
|
||||
if (!n.name) return;
|
||||
var key = n.name.toLowerCase();
|
||||
if (!map[key]) map[key] = [];
|
||||
if (map[key].indexOf(n.public_key) === -1) map[key].push(n.public_key);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function dupNameBadge(name, pubkey, dupMap) {
|
||||
if (!name || !dupMap) return '';
|
||||
var keys = dupMap[name.toLowerCase()];
|
||||
if (!keys || keys.length <= 1) return '';
|
||||
var others = keys.filter(function(k) { return k !== pubkey; });
|
||||
var title = keys.length + ' nodes share this name (' + others.map(function(k) { return k.slice(0, 8) + '…'; }).join(', ') + ')';
|
||||
return ' <span class="dup-name-badge" title="' + escapeHtml(title) + '">(' + keys.length + ')</span>';
|
||||
}
|
||||
|
||||
async function loadNodes(refreshOnly) {
|
||||
try {
|
||||
// Fetch all nodes once, filter client-side
|
||||
@@ -637,6 +665,7 @@
|
||||
return aFav - bFav;
|
||||
});
|
||||
|
||||
const dupMap = buildDupNameMap(_allNodes);
|
||||
tbody.innerHTML = sorted.map(n => {
|
||||
const roleColor = ROLE_COLORS[n.role] || '#6b7280';
|
||||
const isClaimed = myKeys.has(n.public_key);
|
||||
@@ -644,7 +673,7 @@
|
||||
const status = getNodeStatus(n.role || 'companion', lastSeenTime ? new Date(lastSeenTime).getTime() : 0);
|
||||
const lastSeenClass = status === 'active' ? 'last-seen-active' : 'last-seen-stale';
|
||||
return `<tr data-key="${n.public_key}" data-action="select" data-value="${n.public_key}" tabindex="0" role="row" class="${selectedKey === n.public_key ? 'selected' : ''}${isClaimed ? ' claimed-row' : ''}">
|
||||
<td>${favStar(n.public_key, 'node-fav')}${isClaimed ? '<span class="claimed-badge" title="My Mesh">★</span> ' : ''}<strong>${n.name || '(unnamed)'}</strong></td>
|
||||
<td>${favStar(n.public_key, 'node-fav')}${isClaimed ? '<span class="claimed-badge" title="My Mesh">★</span> ' : ''}<strong>${n.name || '(unnamed)'}</strong>${dupNameBadge(n.name, n.public_key, dupMap)}</td>
|
||||
<td class="mono">${truncate(n.public_key, 16)}</td>
|
||||
<td><span class="badge" style="background:${roleColor}20;color:${roleColor}">${n.role}</span></td>
|
||||
<td class="${lastSeenClass}">${timeAgo(n.last_heard || n.last_seen)}</td>
|
||||
@@ -696,9 +725,12 @@
|
||||
const roleColor = si.roleColor;
|
||||
const totalPackets = stats.totalTransmissions || stats.totalPackets || n.advert_count || 0;
|
||||
|
||||
const dupMap = buildDupNameMap(_allNodes);
|
||||
const dupBadge = dupNameBadge(n.name, n.public_key, dupMap);
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="node-detail">
|
||||
<div class="node-detail-name">${escapeHtml(n.name || '(unnamed)')}</div>
|
||||
<div class="node-detail-name">${escapeHtml(n.name || '(unnamed)')}${dupBadge}</div>
|
||||
<div class="node-detail-role">${renderNodeBadges(n, roleColor)}
|
||||
<a href="#/nodes/${encodeURIComponent(n.public_key)}" class="btn-primary" style="display:inline-block;text-decoration:none;font-size:11px;padding:2px 8px;margin-left:8px">🔍 Details</a>
|
||||
<a href="#/nodes/${encodeURIComponent(n.public_key)}/analytics" class="btn-primary" style="display:inline-block;margin-left:4px;text-decoration:none;font-size:11px;padding:2px 8px">📊 Analytics</a>
|
||||
@@ -721,9 +753,9 @@
|
||||
<dt>First Seen</dt><dd>${n.first_seen ? new Date(n.first_seen).toLocaleString() : '—'}</dd>
|
||||
<dt>Total Packets</dt><dd>${totalPackets}</dd>
|
||||
<dt>Packets Today</dt><dd>${stats.packetsToday || 0}</dd>
|
||||
${stats.avgSnr != null ? `<dt>Avg SNR</dt><dd>${stats.avgSnr.toFixed(1)} dB</dd>` : ''}
|
||||
${stats.avgSnr != null ? `<dt>Avg SNR</dt><dd>${Number(stats.avgSnr).toFixed(1)} dB</dd>` : ''}
|
||||
${stats.avgHops ? `<dt>Avg Hops</dt><dd>${stats.avgHops}</dd>` : ''}
|
||||
${hasLoc ? `<dt>Location</dt><dd>${n.lat.toFixed(5)}, ${n.lon.toFixed(5)}</dd>` : ''}
|
||||
${hasLoc ? `<dt>Location</dt><dd>${Number(n.lat).toFixed(5)}, ${Number(n.lon).toFixed(5)}</dd>` : ''}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -733,7 +765,7 @@
|
||||
<div class="observer-list">
|
||||
${observers.map(o => `<div class="observer-row" style="display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid var(--border);font-size:12px">
|
||||
<span style="font-weight:600">${escapeHtml(o.observer_name || o.observer_id)}${o.iata ? ' <span class="badge" style="font-size:10px">' + escapeHtml(o.iata) + '</span>' : ''}</span>
|
||||
<span style="color:var(--text-muted)">${o.packetCount} pkts · ${o.avgSnr != null ? 'SNR ' + o.avgSnr.toFixed(1) + 'dB' : ''}${o.avgRssi != null ? ' · RSSI ' + o.avgRssi.toFixed(0) : ''}</span>
|
||||
<span style="color:var(--text-muted)">${o.packetCount} pkts · ${o.avgSnr != null ? 'SNR ' + Number(o.avgSnr).toFixed(1) + 'dB' : ''}${o.avgRssi != null ? ' · RSSI ' + Number(o.avgRssi).toFixed(0) : ''}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
@@ -206,6 +206,15 @@ console.log('\n=== nodes.js: getStatusTooltip / getStatusInfo (extracted) ===');
|
||||
).replace(
|
||||
/function sortNodes/,
|
||||
'window.__nodesExport.sortNodes = sortNodes; function sortNodes'
|
||||
).replace(
|
||||
/function buildDupNameMap/,
|
||||
'window.__nodesExport.buildDupNameMap = buildDupNameMap; function buildDupNameMap'
|
||||
).replace(
|
||||
/function renderHashInconsistencyWarning/,
|
||||
'window.__nodesExport.renderHashInconsistencyWarning = renderHashInconsistencyWarning; function renderHashInconsistencyWarning'
|
||||
).replace(
|
||||
/function dupNameBadge/,
|
||||
'window.__nodesExport.dupNameBadge = dupNameBadge; function dupNameBadge'
|
||||
);
|
||||
|
||||
// Provide required globals
|
||||
@@ -291,6 +300,128 @@ console.log('\n=== nodes.js: getStatusTooltip / getStatusInfo (extracted) ===');
|
||||
assert.ok(Array.isArray(result));
|
||||
});
|
||||
}
|
||||
|
||||
if (ex.buildDupNameMap) {
|
||||
const buildDupNameMap = ex.buildDupNameMap;
|
||||
test('buildDupNameMap returns empty for no nodes', () => {
|
||||
const m = buildDupNameMap([]);
|
||||
assert.strictEqual(Object.keys(m).length, 0);
|
||||
});
|
||||
test('buildDupNameMap groups nodes by lowercase name', () => {
|
||||
const m = buildDupNameMap([
|
||||
{ name: 'Alpha', public_key: 'key1' },
|
||||
{ name: 'alpha', public_key: 'key2' },
|
||||
{ name: 'Beta', public_key: 'key3' },
|
||||
]);
|
||||
assert.strictEqual(m['alpha'].length, 2);
|
||||
assert.ok(m['alpha'].includes('key1'));
|
||||
assert.ok(m['alpha'].includes('key2'));
|
||||
assert.strictEqual(m['beta'].length, 1);
|
||||
});
|
||||
test('buildDupNameMap ignores unnamed nodes', () => {
|
||||
const m = buildDupNameMap([
|
||||
{ name: '', public_key: 'key1' },
|
||||
{ name: null, public_key: 'key2' },
|
||||
{ name: 'Alpha', public_key: 'key3' },
|
||||
]);
|
||||
assert.strictEqual(Object.keys(m).length, 1);
|
||||
});
|
||||
test('buildDupNameMap deduplicates same pubkey', () => {
|
||||
const m = buildDupNameMap([
|
||||
{ name: 'Alpha', public_key: 'key1' },
|
||||
{ name: 'Alpha', public_key: 'key1' },
|
||||
]);
|
||||
assert.strictEqual(m['alpha'].length, 1);
|
||||
});
|
||||
}
|
||||
|
||||
if (ex.dupNameBadge) {
|
||||
const dupNameBadge = ex.dupNameBadge;
|
||||
test('dupNameBadge returns empty for unique name', () => {
|
||||
const m = { 'alpha': ['key1'] };
|
||||
assert.strictEqual(dupNameBadge('Alpha', 'key1', m), '');
|
||||
});
|
||||
test('dupNameBadge returns badge for duplicate names', () => {
|
||||
const m = { 'alpha': ['key1', 'key2'] };
|
||||
const html = dupNameBadge('Alpha', 'key1', m);
|
||||
assert.ok(html.includes('(2)'));
|
||||
assert.ok(html.includes('dup-name-badge'));
|
||||
});
|
||||
test('dupNameBadge returns empty for null name', () => {
|
||||
assert.strictEqual(dupNameBadge(null, 'key1', {}), '');
|
||||
});
|
||||
test('dupNameBadge returns empty for null map', () => {
|
||||
assert.strictEqual(dupNameBadge('Alpha', 'key1', null), '');
|
||||
});
|
||||
test('dupNameBadge shows count of 3 for three duplicates', () => {
|
||||
const m = { 'alpha': ['key1', 'key2', 'key3'] };
|
||||
const html = dupNameBadge('Alpha', 'key1', m);
|
||||
assert.ok(html.includes('(3)'));
|
||||
});
|
||||
}
|
||||
|
||||
// --- renderHashInconsistencyWarning tests (fixes #190) ---
|
||||
if (ex.renderHashInconsistencyWarning) {
|
||||
const warn = ex.renderHashInconsistencyWarning;
|
||||
test('renderHashInconsistencyWarning returns empty for consistent node', () => {
|
||||
assert.strictEqual(warn({ hash_size_inconsistent: false }), '');
|
||||
});
|
||||
test('renderHashInconsistencyWarning returns empty for undefined flag', () => {
|
||||
assert.strictEqual(warn({}), '');
|
||||
});
|
||||
test('renderHashInconsistencyWarning renders with valid array', () => {
|
||||
const html = warn({ hash_size_inconsistent: true, hash_sizes_seen: [1, 2] });
|
||||
assert.ok(html.includes('1-byte, 2-byte'));
|
||||
assert.ok(html.includes('varying hash sizes'));
|
||||
});
|
||||
test('renderHashInconsistencyWarning handles missing hash_sizes_seen', () => {
|
||||
const html = warn({ hash_size_inconsistent: true });
|
||||
assert.ok(html.includes('varying hash sizes'));
|
||||
// Should not crash — renders with empty sizes
|
||||
assert.ok(html.includes('-byte'));
|
||||
});
|
||||
test('renderHashInconsistencyWarning handles non-array hash_sizes_seen', () => {
|
||||
const html = warn({ hash_size_inconsistent: true, hash_sizes_seen: '[1, 2]' });
|
||||
assert.ok(html.includes('varying hash sizes'));
|
||||
// String should be treated as empty array (Array.isArray guard)
|
||||
assert.ok(html.includes('-byte'));
|
||||
});
|
||||
test('renderHashInconsistencyWarning handles null hash_sizes_seen', () => {
|
||||
const html = warn({ hash_size_inconsistent: true, hash_sizes_seen: null });
|
||||
assert.ok(html.includes('varying hash sizes'));
|
||||
});
|
||||
}
|
||||
|
||||
// --- renderNodeBadges with hash_size_inconsistent (fixes #190) ---
|
||||
if (ex.renderNodeBadges) {
|
||||
test('renderNodeBadges handles hash_size_inconsistent node', () => {
|
||||
const html = ex.renderNodeBadges({
|
||||
role: 'room', public_key: '9dc3e069d1b336c4af33167d3838147ca6449e12c1e1bdaa92fdfc0ecfdd98bc',
|
||||
hash_size: 2, hash_size_inconsistent: true, hash_sizes_seen: [1, 2],
|
||||
last_heard: new Date().toISOString()
|
||||
}, '#16a34a');
|
||||
assert.ok(html.includes('room'));
|
||||
assert.ok(html.includes('9DC3'));
|
||||
assert.ok(html.includes('variable hash size'));
|
||||
});
|
||||
test('renderNodeBadges handles null hash_size', () => {
|
||||
const html = ex.renderNodeBadges({
|
||||
role: 'room', public_key: 'abcdef1234567890',
|
||||
hash_size: null, hash_size_inconsistent: false,
|
||||
last_heard: new Date().toISOString()
|
||||
}, '#16a34a');
|
||||
assert.ok(html.includes('room'));
|
||||
assert.ok(!html.includes('variable hash size'));
|
||||
});
|
||||
test('renderNodeBadges handles string hash_sizes_seen gracefully', () => {
|
||||
const html = ex.renderNodeBadges({
|
||||
role: 'repeater', public_key: 'abcdef1234567890',
|
||||
hash_size: 2, hash_size_inconsistent: true, hash_sizes_seen: '[1, 2]',
|
||||
last_heard: new Date().toISOString()
|
||||
}, '#dc2626');
|
||||
assert.ok(html.includes('variable hash size'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== HOP-RESOLVER TESTS =====
|
||||
|
||||
Reference in New Issue
Block a user