mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-27 05:27:55 +00:00
Merge branch 'areas-meshguide-sync'
This commit is contained in:
+130
-16
@@ -37,6 +37,17 @@
|
||||
return '+' + m + 'm ' + s + 's';
|
||||
}
|
||||
|
||||
// Plain "Xs"/"Xm Ys" duration, no leading "+" or "first to arrive" --
|
||||
// for footer stats standing on their own (unlike formatElapsed's
|
||||
// tooltip use, these aren't continuing a "this station arrived..."
|
||||
// sentence).
|
||||
function formatDuration(seconds) {
|
||||
if (seconds < 60) return seconds.toFixed(1) + 's';
|
||||
var m = Math.floor(seconds / 60);
|
||||
var s = Math.round(seconds % 60);
|
||||
return m + 'm ' + s + 's';
|
||||
}
|
||||
|
||||
// How much bigger/fuzzier an approximate marker's ring should be than
|
||||
// a normal marker, given how many positioned neighbors fed the
|
||||
// estimate (more = tighter) and how much they disagreed (a wide
|
||||
@@ -131,7 +142,8 @@
|
||||
'<h3 style="margin:0 0 4px;padding-right:24px">Relay Path</h3>' +
|
||||
'<p class="text-muted" style="margin:0 0 8px;font-size:12px">How far and how wide this packet spread. Click a marker to open that node\'s detail page.</p>' +
|
||||
'<div style="display:flex;flex-wrap:wrap;gap:10px 14px;align-items:center;margin:0 0 10px;font-size:11px;color:var(--text-muted)">' +
|
||||
'<span style="display:inline-flex;align-items:center;gap:4px"><span style="display:inline-block;width:14px;height:2px;background:var(--accent)"></span>farthest-traveled route</span>' +
|
||||
'<span style="display:inline-flex;align-items:center;gap:4px"><span style="display:inline-block;width:14px;height:2px;background:var(--accent)"></span><span id="packetPathPrimaryLegendLabel">farthest-traveled route</span></span>' +
|
||||
'<span id="packetPathDeepestLegendItem" style="display:none;align-items:center;gap:4px"><span style="display:inline-block;width:14px;height:2px;background:var(--status-purple)"></span>deepest (most hops) route</span>' +
|
||||
'<span style="display:inline-flex;align-items:center;gap:4px"><span style="display:inline-block;width:14px;height:2px;background:var(--text-muted)"></span>other station</span>' +
|
||||
'<span style="display:inline-flex;align-items:center;gap:4px"><span style="display:inline-block;width:9px;height:9px;border:2px dashed var(--text-muted);border-radius:50%;box-sizing:border-box"></span>approximate position</span>' +
|
||||
'<span style="display:inline-flex;align-items:center;gap:4px"><span style="display:inline-block;width:9px;height:9px;border:2px solid var(--status-green);border-radius:50%;box-sizing:border-box"></span>first to hear it</span>' +
|
||||
@@ -157,11 +169,53 @@
|
||||
}
|
||||
|
||||
var branches = data.branches || [];
|
||||
var plotted = branches.map(function (b, i) {
|
||||
var plotted = branches.map(function (b) {
|
||||
var built = chainForBranch(b);
|
||||
return { branch: b, chain: built.chain, missing: built.missing, primary: i === 0 };
|
||||
return { branch: b, chain: built.chain, missing: built.missing, highlightRole: null };
|
||||
}).filter(function (p) { return p.chain.length > 0; });
|
||||
|
||||
// Two different questions, not always the same branch: which station
|
||||
// did the packet reach at the greatest real-world DISTANCE
|
||||
// (distanceFromFirstKm), and which took the most HOPS to reach it (a
|
||||
// dense area can take many short hops; a couple of long-range links
|
||||
// can cover more real distance in fewer -- caught on a real packet:
|
||||
// 7 hops but 117km vs. 6 hops but 124km). Both get their own
|
||||
// highlight when they're different branches; when they coincide (the
|
||||
// common case) it's shown once, combined, exactly as before.
|
||||
var hasDistanceData = plotted.some(function (p) { return typeof p.branch.distanceFromFirstKm === 'number'; });
|
||||
var deepestIdx = 0;
|
||||
var deepestHopsSeen = -1;
|
||||
plotted.forEach(function (p, i) {
|
||||
if (p.branch.hops > deepestHopsSeen) {
|
||||
deepestHopsSeen = p.branch.hops;
|
||||
deepestIdx = i;
|
||||
}
|
||||
});
|
||||
var farthestIdx = -1;
|
||||
if (hasDistanceData) {
|
||||
var farthestKm = -1;
|
||||
plotted.forEach(function (p, i) {
|
||||
var d = p.branch.distanceFromFirstKm;
|
||||
if (typeof d === 'number' && d > farthestKm) {
|
||||
farthestKm = d;
|
||||
farthestIdx = i;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 'deepestFallback' (no distance data anywhere) reuses the farthest
|
||||
// slot's accent color -- there's only one highlighted concept in
|
||||
// that case, same single-highlight look as before this branch/role
|
||||
// split existed. 'deepest' (purple) only appears when it's actually
|
||||
// a DIFFERENT branch from farthest -- the genuinely new information.
|
||||
if (!hasDistanceData) {
|
||||
if (plotted[deepestIdx]) plotted[deepestIdx].highlightRole = 'deepestFallback';
|
||||
} else if (farthestIdx === deepestIdx) {
|
||||
if (plotted[deepestIdx]) plotted[deepestIdx].highlightRole = 'both';
|
||||
} else {
|
||||
if (plotted[farthestIdx]) plotted[farthestIdx].highlightRole = 'farthest';
|
||||
if (plotted[deepestIdx]) plotted[deepestIdx].highlightRole = 'deepest';
|
||||
}
|
||||
|
||||
if (plotted.length === 0) {
|
||||
if (statusEl) {
|
||||
if (branches.length === 0) {
|
||||
@@ -193,8 +247,16 @@
|
||||
|
||||
var outline = cssVar('--surface-0');
|
||||
var accent = cssVar('--accent');
|
||||
var deepestColor = cssVar('--status-purple');
|
||||
var observerColor = cssVar('--status-yellow');
|
||||
var muted = cssVar('--text-muted');
|
||||
// 'both'/'farthest'/'deepestFallback' all read as the accent color;
|
||||
// only a genuinely-distinct 'deepest' branch gets the second color.
|
||||
function colorForRole(role) {
|
||||
if (role === 'deepest') return deepestColor;
|
||||
if (role) return accent;
|
||||
return muted;
|
||||
}
|
||||
|
||||
// Shade every touched area's configured boundary as a faint
|
||||
// background layer -- ties the "touched: X, Y" footer text to actual
|
||||
@@ -238,11 +300,13 @@
|
||||
// appearances (#1... a packet heard by 12 stations through one shared
|
||||
// repeater was showing "11 approximate" for what was really 1 node).
|
||||
var approxSeen = {};
|
||||
// Draw secondary branches first so the primary (deepest) one ends up on top.
|
||||
var ordered = plotted.slice().sort(function (a, b) { return (a.primary ? 1 : 0) - (b.primary ? 1 : 0); });
|
||||
// Draw non-highlighted branches first so the highlighted one(s) --
|
||||
// farthest and/or deepest -- end up drawn on top.
|
||||
var ordered = plotted.slice().sort(function (a, b) { return (a.highlightRole ? 1 : 0) - (b.highlightRole ? 1 : 0); });
|
||||
ordered.forEach(function (p) {
|
||||
missingTotal += p.missing;
|
||||
var lineColor = p.primary ? accent : muted;
|
||||
var isHighlighted = !!p.highlightRole;
|
||||
var lineColor = colorForRole(p.highlightRole);
|
||||
var line = [];
|
||||
p.chain.forEach(function (pt) {
|
||||
if (pt.approx) {
|
||||
@@ -255,7 +319,17 @@
|
||||
bounds.push([pt.lat, pt.lon]);
|
||||
line.push([pt.lat, pt.lon]);
|
||||
var color = pt.isObserver ? observerColor : lineColor;
|
||||
var radius = p.primary ? (pt.isObserver ? 7 : 6) : (pt.isObserver ? 5 : 4);
|
||||
var radius = isHighlighted ? (pt.isObserver ? 7 : 6) : (pt.isObserver ? 5 : 4);
|
||||
// Observer dots always FILL yellow (the established "this is a
|
||||
// hearing station" language, kept regardless of role) -- but for
|
||||
// an observer that's also the branch's only point (a direct/
|
||||
// 0-hop reception has no relay hops and thus no polyline
|
||||
// either), fill color alone can't show whether it's the
|
||||
// farthest, deepest, or neither. The STROKE carries the role
|
||||
// color instead when highlighted, so a farthest/deepest observer
|
||||
// still reads as accent/purple-ringed, not just "a yellow dot"
|
||||
// indistinguishable from every other observer.
|
||||
var strokeColor = isHighlighted ? lineColor : outline;
|
||||
var markerOpts = pt.approx
|
||||
// Approximate (borrowed-from-neighbor) position: larger,
|
||||
// thick-dashed ring with a faint fill -- a plain hollow outline
|
||||
@@ -265,10 +339,10 @@
|
||||
// more agreeing neighbors = tighter, more solid; one neighbor
|
||||
// or a wide spread among several = bigger, fainter.
|
||||
? {
|
||||
radius: radius + approxRadiusBonus(pt.approxNeighborCount, pt.approxSpreadKm), color: color, weight: 3,
|
||||
radius: radius + approxRadiusBonus(pt.approxNeighborCount, pt.approxSpreadKm), color: isHighlighted ? lineColor : color, weight: 3,
|
||||
fillColor: color, fillOpacity: approxFillOpacity(pt.approxNeighborCount), dashArray: '5,4',
|
||||
}
|
||||
: { radius: radius, color: outline, weight: p.primary ? 2 : 1, fillColor: color, fillOpacity: p.primary ? 1 : 0.8 };
|
||||
: { radius: radius, color: strokeColor, weight: isHighlighted ? 2 : 1, fillColor: color, fillOpacity: isHighlighted ? 1 : 0.8 };
|
||||
var approxNote = '';
|
||||
if (pt.approx) {
|
||||
approxNote = ', approx. position';
|
||||
@@ -286,11 +360,11 @@
|
||||
window.location.hash = '#/nodes/' + encodeURIComponent(pt.publicKey);
|
||||
});
|
||||
}
|
||||
markerEntries.push({ layer: marker, primary: p.primary, approx: !!pt.approx });
|
||||
markerEntries.push({ layer: marker, primary: isHighlighted, approx: !!pt.approx });
|
||||
});
|
||||
if (line.length > 1) {
|
||||
var polyline = L.polyline(line, { color: lineColor, weight: p.primary ? 2.5 : 1.5, opacity: p.primary ? 0.85 : 0.5 }).addTo(map);
|
||||
polylineEntries.push({ layer: polyline, primary: p.primary });
|
||||
var polyline = L.polyline(line, { color: lineColor, weight: isHighlighted ? 2.5 : 1.5, opacity: isHighlighted ? 0.85 : 0.5 }).addTo(map);
|
||||
polylineEntries.push({ layer: polyline, primary: isHighlighted });
|
||||
}
|
||||
});
|
||||
// The earliest-arriving observation, drawn last so its landmark ring
|
||||
@@ -315,17 +389,40 @@
|
||||
setTimeout(function () { map.invalidateSize(); }, 120);
|
||||
activeMap = map;
|
||||
|
||||
// Label the highlight(s) honestly, matching the role split above:
|
||||
// no distance data at all -> one accent-colored "deepest (most
|
||||
// hops)" route (old single-highlight look); farthest and deepest
|
||||
// are the same branch -> one accent-colored route, combined label;
|
||||
// genuinely different branches -> two legend entries, accent
|
||||
// "farthest-traveled" plus a second purple "deepest (most hops)".
|
||||
var primaryLabel;
|
||||
var showDeepestLegendItem = false;
|
||||
if (!hasDistanceData) {
|
||||
primaryLabel = 'deepest (most hops)';
|
||||
} else if (farthestIdx === deepestIdx) {
|
||||
primaryLabel = 'farthest-traveled & deepest';
|
||||
} else {
|
||||
primaryLabel = 'farthest-traveled';
|
||||
showDeepestLegendItem = true;
|
||||
}
|
||||
var primaryLegendLabel = document.getElementById('packetPathPrimaryLegendLabel');
|
||||
if (primaryLegendLabel) primaryLegendLabel.textContent = primaryLabel + ' route';
|
||||
var deepestLegendItem = document.getElementById('packetPathDeepestLegendItem');
|
||||
if (deepestLegendItem && showDeepestLegendItem) deepestLegendItem.style.display = 'inline-flex';
|
||||
|
||||
// Build whichever filter checkboxes are actually relevant for this
|
||||
// packet -- a single-branch packet gets no declutter toggle, one
|
||||
// with no approximate positions gets no approx-only toggle, etc.
|
||||
var controlsEl = document.getElementById('packetPathControls');
|
||||
var controlsHtml = '';
|
||||
var hiddenCount = plotted.length - 1;
|
||||
if (plotted.length > 1) {
|
||||
var highlightedCount = plotted.filter(function (p) { return !!p.highlightRole; }).length;
|
||||
var hiddenCount = plotted.length - highlightedCount;
|
||||
var toggleRouteLabel = showDeepestLegendItem ? 'farthest-traveled and deepest routes' : (primaryLabel + ' route');
|
||||
if (plotted.length > highlightedCount) {
|
||||
controlsHtml +=
|
||||
'<label style="display:flex;align-items:center;gap:6px;font-size:12px;margin:0 0 8px;cursor:pointer;color:var(--text-muted)">' +
|
||||
'<input type="checkbox" id="packetPathPrimaryOnly">' +
|
||||
'Show only the farthest-traveled route (' + hiddenCount + ' other station' + (hiddenCount === 1 ? '' : 's') + ' hidden when checked)' +
|
||||
'Show only the ' + toggleRouteLabel + ' (' + hiddenCount + ' other station' + (hiddenCount === 1 ? '' : 's') + ' hidden when checked)' +
|
||||
'</label>';
|
||||
}
|
||||
if (approxTotal > 0) {
|
||||
@@ -381,10 +478,27 @@
|
||||
}
|
||||
|
||||
var deepestHops = branches[0].hops;
|
||||
var deepestBranch = plotted[deepestIdx] && plotted[deepestIdx].branch;
|
||||
var deepestSeconds = deepestBranch && typeof deepestBranch.secondsAfterFirst === 'number' ? deepestBranch.secondsAfterFirst : null;
|
||||
var statusParts = [
|
||||
plotted.length + ' of ' + branches.length + ' station' + (branches.length === 1 ? '' : 's') + ' shown',
|
||||
'deepest reached ' + deepestHops + ' hop' + (deepestHops === 1 ? '' : 's'),
|
||||
'deepest reached ' + deepestHops + ' hop' + (deepestHops === 1 ? '' : 's') + (deepestSeconds != null ? ' (' + formatDuration(deepestSeconds) + ')' : ''),
|
||||
];
|
||||
if (hasDistanceData) {
|
||||
var farthestBranch = plotted[farthestIdx] && plotted[farthestIdx].branch;
|
||||
var farthestSeconds = farthestBranch && typeof farthestBranch.secondsAfterFirst === 'number' ? farthestBranch.secondsAfterFirst : null;
|
||||
statusParts.push('farthest reached ' + farthestKm.toFixed(1) + 'km' + (farthestSeconds != null ? ' (' + formatDuration(farthestSeconds) + ')' : ''));
|
||||
}
|
||||
// How long the whole flood took to finish reaching every station it
|
||||
// ever reached -- the largest secondsAfterFirst across ALL branches,
|
||||
// not just the farthest/deepest ones (a station that's neither can
|
||||
// still be the last to hear it).
|
||||
var maxSpreadSeconds = null;
|
||||
plotted.forEach(function (p) {
|
||||
var s = p.branch.secondsAfterFirst;
|
||||
if (typeof s === 'number' && (maxSpreadSeconds === null || s > maxSpreadSeconds)) maxSpreadSeconds = s;
|
||||
});
|
||||
if (maxSpreadSeconds != null && maxSpreadSeconds > 0) statusParts.push('fully spread in ' + formatDuration(maxSpreadSeconds));
|
||||
if (firstPoint) statusParts.push('entered near ' + firstPoint.name);
|
||||
if (approxTotal > 0) statusParts.push(approxTotal + ' approximate (estimated from neighbors)');
|
||||
if (missingTotal > 0) statusParts.push(missingTotal + ' hop' + (missingTotal === 1 ? '' : 's') + ' without a known position (not shown)');
|
||||
|
||||
+181
-2
@@ -51,8 +51,8 @@ test('draws every branch, not just the deepest one', () => {
|
||||
assert.ok(/branches\.map/.test(src), 'should iterate all branches from the response');
|
||||
});
|
||||
|
||||
test('draws the deepest branch on top of the others (primary drawn last)', () => {
|
||||
assert.ok(/a\.primary \? 1 : 0/.test(src) || /primary.*sort/.test(src), 'should reorder so the primary branch paints last');
|
||||
test('draws highlighted branch(es) on top of the others', () => {
|
||||
assert.ok(/a\.highlightRole \? 1 : 0/.test(src), 'should reorder so highlighted (farthest/deepest) branches paint last');
|
||||
});
|
||||
|
||||
test('handles Escape key and click-outside to close, matching other CoreScope modals', () => {
|
||||
@@ -687,6 +687,185 @@ function makeSandbox(apiImpl) {
|
||||
} catch (e) { failed++; console.log(' ❌ distanceFromFirstKm renders as a "N km away" label in the tooltip: ' + e.message); }
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
try {
|
||||
// More hops does not mean more geographic distance, and they're not
|
||||
// always the same branch -- both deserve their own highlight. Here
|
||||
// the SHALLOWER branch (2 hops) travels much farther (200km) than
|
||||
// the DEEPER one (5 hops, only 50km): a dense-mesh short-hop chain
|
||||
// vs. a couple of long-range links. Both get weight-2 (highlighted)
|
||||
// styling, but in DIFFERENT colors -- farthest keeps the accent
|
||||
// color, deepest gets the second (purple) color -- and the legend
|
||||
// shows both.
|
||||
const ctx = makeSandbox(() => Promise.resolve({
|
||||
hash: 'deadbeef',
|
||||
branches: [
|
||||
{ hops: 5, points: [], observer: { name: 'DeepButClose', lat: 56.0, lon: 10.0 }, distanceFromFirstKm: 50 },
|
||||
{ hops: 2, points: [], observer: { name: 'ShallowButFar', lat: 57.0, lon: 11.0 }, distanceFromFirstKm: 200 },
|
||||
// A third, fully-secondary branch -- neither farthest nor
|
||||
// deepest -- so there's something left for the declutter
|
||||
// toggle to actually hide (with only the two highlighted
|
||||
// branches, hiddenCount would be 0 and no toggle appears at
|
||||
// all, same as the existing single-branch case).
|
||||
{ hops: 3, points: [], observer: { name: 'PlainSecondary', lat: 56.5, lon: 10.5 }, distanceFromFirstKm: 80 },
|
||||
],
|
||||
}));
|
||||
|
||||
const markerCalls = [];
|
||||
ctx.L = {
|
||||
map: () => ({ setView() { return this; }, fitBounds() {}, invalidateSize() {}, remove() {} }),
|
||||
tileLayer: () => ({ addTo() { return this; } }),
|
||||
circleMarker: (latlng, opts) => {
|
||||
const entry = { opts, tooltip: null };
|
||||
markerCalls.push(entry);
|
||||
return { addTo() { return this; }, bindTooltip(t) { entry.tooltip = t; return this; }, on() { return this; } };
|
||||
},
|
||||
polyline: () => ({ addTo() { return this; } }),
|
||||
};
|
||||
|
||||
await ctx.window.PacketPathMap.open('deadbeef');
|
||||
const deepMarker = markerCalls.find((m) => m.tooltip && m.tooltip.includes('DeepButClose'));
|
||||
const farMarker = markerCalls.find((m) => m.tooltip && m.tooltip.includes('ShallowButFar'));
|
||||
assert.ok(deepMarker && farMarker, 'expected markers for both observers, got: ' + JSON.stringify(markerCalls.map((m) => m.tooltip)));
|
||||
assert.strictEqual(farMarker.opts.weight, 2, 'ShallowButFar (200km, actually farthest) should get highlighted styling (weight 2), got ' + farMarker.opts.weight);
|
||||
assert.strictEqual(deepMarker.opts.weight, 2, 'DeepButClose (5 hops, most hops) should ALSO get highlighted styling (weight 2) via its own "deepest" role, got ' + deepMarker.opts.weight);
|
||||
// Both are observer-only (no relay hop) points, so both FILL
|
||||
// yellow (the constant "this is an observer" color) -- the role
|
||||
// distinction shows up in the ring (stroke) color instead.
|
||||
assert.notStrictEqual(farMarker.opts.color, deepMarker.opts.color, 'farthest and deepest are different branches here, so their marker rings must use different colors, got matching color ' + farMarker.opts.color);
|
||||
|
||||
const legendLabel = ctx.document.getElementById('packetPathPrimaryLegendLabel');
|
||||
assert.ok(legendLabel && legendLabel.textContent.includes('farthest-traveled') && !legendLabel.textContent.includes('deepest'), 'primary legend slot should say just "farthest-traveled" (deepest gets its own slot) when they diverge, got: ' + (legendLabel && legendLabel.textContent));
|
||||
const deepestLegendItem = ctx.document.getElementById('packetPathDeepestLegendItem');
|
||||
assert.strictEqual(deepestLegendItem.style.display, 'inline-flex', 'the second "deepest" legend swatch should be shown when farthest and deepest are different branches');
|
||||
const controlsEl = ctx.document.getElementById('packetPathControls');
|
||||
assert.ok(controlsEl.innerHTML.includes('farthest-traveled and deepest routes'), 'declutter checkbox should mention both routes when they diverge, got: ' + controlsEl.innerHTML);
|
||||
passed++;
|
||||
console.log(' ✅ farthest and deepest get their own distinct highlight (color + legend entry) when they are different branches');
|
||||
} catch (e) { failed++; console.log(' ❌ farthest and deepest get their own distinct highlight (color + legend entry) when they are different branches: ' + e.message); }
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
try {
|
||||
// The common case: the deepest branch IS also the farthest one.
|
||||
// Must show a SINGLE combined highlight (not two), still only the
|
||||
// accent color, with a combined label -- not a second purple
|
||||
// "deepest" swatch for a branch that's already shown.
|
||||
const ctx = makeSandbox(() => Promise.resolve({
|
||||
hash: 'deadbeef',
|
||||
branches: [
|
||||
{ hops: 5, points: [], observer: { name: 'DeepAndFar', lat: 57.0, lon: 11.0 }, distanceFromFirstKm: 200 },
|
||||
{ hops: 2, points: [], observer: { name: 'ShallowAndClose', lat: 56.0, lon: 10.0 }, distanceFromFirstKm: 20 },
|
||||
],
|
||||
}));
|
||||
ctx.L = {
|
||||
map: () => ({ setView() { return this; }, fitBounds() {}, invalidateSize() {}, remove() {} }),
|
||||
tileLayer: () => ({ addTo() { return this; } }),
|
||||
circleMarker: () => ({ addTo() { return this; }, bindTooltip() { return this; }, on() { return this; } }),
|
||||
polyline: () => ({ addTo() { return this; } }),
|
||||
};
|
||||
|
||||
await ctx.window.PacketPathMap.open('deadbeef');
|
||||
const legendLabel = ctx.document.getElementById('packetPathPrimaryLegendLabel');
|
||||
assert.ok(legendLabel && legendLabel.textContent.includes('farthest-traveled & deepest'), 'legend should combine both facts into one label for the same branch, got: ' + (legendLabel && legendLabel.textContent));
|
||||
const deepestLegendItem = ctx.document.getElementById('packetPathDeepestLegendItem');
|
||||
assert.notStrictEqual(deepestLegendItem.style.display, 'inline-flex', 'the second "deepest" legend swatch must stay hidden when it is the same branch as farthest, got display=' + deepestLegendItem.style.display);
|
||||
passed++;
|
||||
console.log(' ✅ shows one combined highlight (not two) when the deepest branch is also the farthest one');
|
||||
} catch (e) { failed++; console.log(' ❌ shows one combined highlight (not two) when the deepest branch is also the farthest one: ' + e.message); }
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
try {
|
||||
// No branch has distanceFromFirstKm at all (e.g. sparse GPS
|
||||
// coverage) -- falls back to the old hops-based pick, and the
|
||||
// legend/checkbox wording must say so honestly rather than still
|
||||
// claiming "farthest-traveled" for a branch nobody measured.
|
||||
const ctx = makeSandbox(() => Promise.resolve({
|
||||
hash: 'deadbeef',
|
||||
branches: [
|
||||
{ hops: 3, points: [], observer: { name: 'ObsA', lat: 56.0, lon: 10.0 } },
|
||||
{ hops: 1, points: [], observer: { name: 'ObsB', lat: 56.1, lon: 10.1 } },
|
||||
],
|
||||
}));
|
||||
ctx.L = {
|
||||
map: () => ({ setView() { return this; }, fitBounds() {}, invalidateSize() {}, remove() {} }),
|
||||
tileLayer: () => ({ addTo() { return this; } }),
|
||||
circleMarker: () => ({ addTo() { return this; }, bindTooltip() { return this; }, on() { return this; } }),
|
||||
polyline: () => ({ addTo() { return this; } }),
|
||||
};
|
||||
|
||||
await ctx.window.PacketPathMap.open('deadbeef');
|
||||
const legendLabel = ctx.document.getElementById('packetPathPrimaryLegendLabel');
|
||||
assert.ok(legendLabel && legendLabel.textContent.includes('deepest (most hops)'), 'legend should honestly say "deepest (most hops)" when no branch has distance data, got: ' + (legendLabel && legendLabel.textContent));
|
||||
assert.ok(!legendLabel.textContent.includes('farthest'), 'legend must not claim "farthest" when nothing was actually measured by distance, got: ' + legendLabel.textContent);
|
||||
const controlsEl = ctx.document.getElementById('packetPathControls');
|
||||
assert.ok(controlsEl.innerHTML.includes('deepest (most hops)'), 'declutter checkbox label should also use the honest wording, got: ' + controlsEl.innerHTML);
|
||||
passed++;
|
||||
console.log(' ✅ falls back to hop-based selection with honest "deepest (most hops)" wording when no branch has distance data');
|
||||
} catch (e) { failed++; console.log(' ❌ falls back to hop-based selection with honest "deepest (most hops)" wording when no branch has distance data: ' + e.message); }
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
try {
|
||||
// Status line should report how long it took to reach the deepest
|
||||
// and farthest stations, plus the overall spread duration (the
|
||||
// largest secondsAfterFirst across ALL branches, not just those
|
||||
// two -- a station that's neither can still be the last to hear
|
||||
// it, e.g. a middling branch stuck behind a slow relay).
|
||||
const ctx = makeSandbox(() => Promise.resolve({
|
||||
hash: 'deadbeef',
|
||||
branches: [
|
||||
{ hops: 5, points: [], observer: { name: 'DeepButClose', lat: 56.0, lon: 10.0 }, distanceFromFirstKm: 50, secondsAfterFirst: 4.1 },
|
||||
{ hops: 2, points: [], observer: { name: 'ShallowButFar', lat: 57.0, lon: 11.0 }, distanceFromFirstKm: 200, secondsAfterFirst: 3.2 },
|
||||
{ hops: 3, points: [], observer: { name: 'SlowestOfAll', lat: 56.5, lon: 10.5 }, distanceFromFirstKm: 80, secondsAfterFirst: 9.7 },
|
||||
],
|
||||
}));
|
||||
ctx.L = {
|
||||
map: () => ({ setView() { return this; }, fitBounds() {}, invalidateSize() {}, remove() {} }),
|
||||
tileLayer: () => ({ addTo() { return this; } }),
|
||||
circleMarker: () => ({ addTo() { return this; }, bindTooltip() { return this; }, on() { return this; } }),
|
||||
polyline: () => ({ addTo() { return this; } }),
|
||||
};
|
||||
|
||||
await ctx.window.PacketPathMap.open('deadbeef');
|
||||
const status = ctx.document.getElementById('packetPathStatus');
|
||||
assert.ok(status.textContent.includes('deepest reached 5 hops (4.1s)'), 'status should show the deepest branch\'s own elapsed time, got: ' + status.textContent);
|
||||
assert.ok(status.textContent.includes('farthest reached 200.0km (3.2s)'), 'status should show the farthest branch\'s distance and its own elapsed time, got: ' + status.textContent);
|
||||
assert.ok(status.textContent.includes('fully spread in 9.7s'), 'status should report the LARGEST elapsed time across all branches (SlowestOfAll, neither deepest nor farthest), not just the deepest/farthest ones, got: ' + status.textContent);
|
||||
passed++;
|
||||
console.log(' ✅ status line reports deepest/farthest elapsed time plus overall spread duration (max across all branches)');
|
||||
} catch (e) { failed++; console.log(' ❌ status line reports deepest/farthest elapsed time plus overall spread duration (max across all branches): ' + e.message); }
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
try {
|
||||
// No branch has secondsAfterFirst at all -- must omit all three
|
||||
// timing additions cleanly rather than showing "(NaNs)" or a
|
||||
// spurious "fully spread in 0.0s".
|
||||
const ctx = makeSandbox(() => Promise.resolve({
|
||||
hash: 'deadbeef',
|
||||
branches: [
|
||||
{ hops: 3, points: [], observer: { name: 'ObsA', lat: 56.0, lon: 10.0 }, distanceFromFirstKm: 40 },
|
||||
],
|
||||
}));
|
||||
ctx.L = {
|
||||
map: () => ({ setView() { return this; }, fitBounds() {}, invalidateSize() {}, remove() {} }),
|
||||
tileLayer: () => ({ addTo() { return this; } }),
|
||||
circleMarker: () => ({ addTo() { return this; }, bindTooltip() { return this; }, on() { return this; } }),
|
||||
polyline: () => ({ addTo() { return this; } }),
|
||||
};
|
||||
|
||||
await ctx.window.PacketPathMap.open('deadbeef');
|
||||
const status = ctx.document.getElementById('packetPathStatus');
|
||||
assert.ok(status.textContent.includes('deepest reached 3 hops') && !status.textContent.includes('deepest reached 3 hops ('), 'deepest line should have no "(Xs)" suffix when secondsAfterFirst is unknown, got: ' + status.textContent);
|
||||
assert.ok(status.textContent.includes('farthest reached 40.0km') && !status.textContent.includes('farthest reached 40.0km ('), 'farthest line should have no "(Xs)" suffix when secondsAfterFirst is unknown, got: ' + status.textContent);
|
||||
assert.ok(!status.textContent.includes('fully spread'), 'should not claim a spread duration when no branch has timing data, got: ' + status.textContent);
|
||||
passed++;
|
||||
console.log(' ✅ omits timing suffixes and the spread-duration stat when no branch has secondsAfterFirst');
|
||||
} catch (e) { failed++; console.log(' ❌ omits timing suffixes and the spread-duration stat when no branch has secondsAfterFirst: ' + e.message); }
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
try {
|
||||
// A single-neighbor approx point should render with a bigger,
|
||||
|
||||
Reference in New Issue
Block a user