diff --git a/public/packet-path-map.js b/public/packet-path-map.js index 952ed72c5..370039173 100644 --- a/public/packet-path-map.js +++ b/public/packet-path-map.js @@ -94,12 +94,18 @@ // markers stay circleMarker dots throughout (a role-specific shape // would clash with the color/dash coding already carrying primary, // approx, and observer meaning). + // Phosphor sprite icons, not emoji (#1648 lint gate); same glyphs as + // observer-detail.js's directNeighborRoleIcon. + function phIcon(name) { + return ' '; + } + function roleIcon(role) { switch (role) { - case 'repeater': return '📡 '; - case 'room': return '🏠 '; - case 'client': return '📱 '; - case 'sensor': return '🌡️ '; + case 'repeater': return phIcon('broadcast'); + case 'room': return phIcon('house-line'); + case 'client': return phIcon('radio'); + case 'sensor': return phIcon('thermometer'); default: return ''; } } @@ -412,7 +418,7 @@ radius: 11, color: cssVar('--status-green'), weight: 3, fillOpacity: 0, opacity: 0.9, }) .addTo(map) - .bindTooltip('🏁 First to hear it: ' + escapeHtml(firstPoint.name) + ' (' + data.first.hops + ' hop' + (data.first.hops === 1 ? '' : 's') + (firstPoint.approx ? ', approx. position' : '') + ')', { className: 'packet-path-tooltip' }); + .bindTooltip(phIcon('flag') + 'First to hear it: ' + escapeHtml(firstPoint.name) + ' (' + data.first.hops + ' hop' + (data.first.hops === 1 ? '' : 's') + (firstPoint.approx ? ', approx. position' : '') + ')', { className: 'packet-path-tooltip' }); } try { map.fitBounds(bounds, { padding: [30, 30] }); } catch (e) { /* single point */ } diff --git a/test-issue-1485-live-anim-z.js b/test-issue-1485-live-anim-z.js index 794a6c495..2d3a496ff 100644 --- a/test-issue-1485-live-anim-z.js +++ b/test-issue-1485-live-anim-z.js @@ -79,15 +79,26 @@ console.log('\n=== #1485 live anim z-order C: per-shape inheritance ==='); // At least one polyline/circleMarker addTo(animLayer) call exists — we're // trusting Leaflet pane inheritance from the LayerGroup parent. Sanity -// check that the four animLayer / pathsLayer addTo sites still exist +// check that the animLayer / pathsLayer addTo sites still exist // (regression detector if someone moves circles to the default pane). +// animLayer hosts 2 since #1521 (ghost markers + matrix char markers): +// the pulseNode() rings moved off animLayer into the canvas engine, +// whose pane is checked below. const animAddTo = (liveSrc.match(/\.addTo\(animLayer\)/g) || []).length; const pathsAddTo = (liveSrc.match(/\.addTo\(pathsLayer\)/g) || []).length; -assert(animAddTo >= 3, - 'animLayer still hosts >=3 .addTo() animation shapes (got ' + animAddTo + ')'); +assert(animAddTo >= 2, + 'animLayer still hosts >=2 .addTo() animation shapes (got ' + animAddTo + ')'); assert(pathsAddTo >= 3, 'pathsLayer still hosts >=3 .addTo() trail shapes (got ' + pathsAddTo + ')'); +// The canvas engine (in-flight packets + pulse rings since #1521) draws +// on 'animationsPane', which must also sit above markerPane (600). +const canvasZ = liveSrc.match( + /getPane\(\s*['"]animationsPane['"]\s*\)\.style\.zIndex\s*=\s*['"]?(\d+)/ +); +assert(!!canvasZ && parseInt(canvasZ[1], 10) > 600, + 'animationsPane (canvas pulses) z-index > markerPane 600 (got ' + (canvasZ ? canvasZ[1] : 'none') + ')'); + console.log('\n=== Summary ==='); console.log(' Passed: ' + passed); console.log(' Failed: ' + failed); diff --git a/test-live-anims.js b/test-live-anims.js index 31b4f4e81..671addb78 100644 --- a/test-live-anims.js +++ b/test-live-anims.js @@ -117,16 +117,24 @@ test2('drawAnimatedLine renderFades() has null guard', () => { assert.ok(fadeBody.includes('!pathsLayer'), 'renderFades() missing pathsLayer null guard'); }); -test2('pulseNode animatePulse() has null guard', () => { - const pulseStart = src2.indexOf('function animatePulse(now)'); +// #1521 replaced the per-node animatePulse()/ghostPulse() rAF loops with +// the activePulses/activeGhosts queues drained by renderAnimations(); the +// guards now live at pulseNode() entry and in renderAnimations(). +test2('pulseNode() has null guard', () => { + const pulseStart = src2.indexOf('function pulseNode('); + assert.ok(pulseStart >= 0, 'pulseNode() not found'); const pulseBody = src2.substring(pulseStart, pulseStart + 200); - assert.ok(pulseBody.includes('!animLayer'), 'animatePulse() missing animLayer null guard'); + assert.ok(pulseBody.includes('!animLayer || !nodesLayer'), 'pulseNode() missing animLayer/nodesLayer null guard'); }); -test2('ghostPulse has null guard', () => { - const ghostStart = src2.indexOf('function ghostPulse(now)'); - const ghostBody = src2.substring(ghostStart, ghostStart + 200); - assert.ok(ghostBody.includes('!animLayer'), 'ghostPulse() missing animLayer null guard'); +test2('renderAnimations() has null guards for canvas and ghost expiry', () => { + const renderStart = src2.indexOf('function renderAnimations(now)'); + assert.ok(renderStart >= 0, 'renderAnimations() not found'); + const renderEnd = src2.indexOf('// Render Pulses', renderStart); + assert.ok(renderEnd > renderStart, 'renderAnimations() pulse section not found'); + const renderBody = src2.substring(renderStart, renderEnd); + assert.ok(renderBody.includes('if (!animCtx) return;'), 'renderAnimations() missing animCtx null guard'); + assert.ok(renderBody.includes('animLayer && animLayer.hasLayer(g.marker)'), 'renderAnimations() ghost expiry missing animLayer null guard'); }); console.log(`\n${p2} passed, ${f2} failed\n`); diff --git a/test-naive-banner-tone.js b/test-naive-banner-tone.js index 6beb16b9a..e28fd2509 100644 --- a/test-naive-banner-tone.js +++ b/test-naive-banner-tone.js @@ -13,7 +13,7 @@ function test(name, fn) { // Load observer-detail.js in a VM sandbox const src = fs.readFileSync(__dirname + '/public/observer-detail.js', 'utf8'); const window = {}; -const ctx = vm.createContext({ window, document: { getElementById: () => null, querySelector: () => null, querySelectorAll: () => [], createElement: () => ({ style: {}, classList: { add(){}, remove(){} }, appendChild(){}, setAttribute(){} }), createDocumentFragment: () => ({ appendChild(){} }) }, console, setTimeout, setInterval, clearInterval, fetch: () => Promise.resolve({ ok: true, json: () => Promise.resolve({}) }), location: { hash: '' }, history: { replaceState() {} }, Chart: function() { return { destroy(){}, update(){} }; }, registerPage() {}, L: { map(){ return { setView(){ return this; }, on(){ return this; }, remove(){} }; }, tileLayer(){ return { addTo(){} }; } } }); +const ctx = vm.createContext({ window, document: { addEventListener() {}, getElementById: () => null, querySelector: () => null, querySelectorAll: () => [], createElement: () => ({ style: {}, classList: { add(){}, remove(){} }, appendChild(){}, setAttribute(){} }), createDocumentFragment: () => ({ appendChild(){} }) }, console, setTimeout, setInterval, clearInterval, fetch: () => Promise.resolve({ ok: true, json: () => Promise.resolve({}) }), location: { hash: '' }, history: { replaceState() {} }, Chart: function() { return { destroy(){}, update(){} }; }, registerPage() {}, L: { map(){ return { setView(){ return this; }, on(){ return this; }, remove(){} }; }, tileLayer(){ return { addTo(){} }; } } }); vm.runInContext(src, ctx); const render = ctx.window.ObserverDetailNaiveBanner.render; diff --git a/test-observers-headings.js b/test-observers-headings.js index f980c0025..3d4bf2080 100644 --- a/test-observers-headings.js +++ b/test-observers-headings.js @@ -59,7 +59,7 @@ test('expected headings present and ordered', () => { while ((m = re.exec(thead)) !== null) labels.push(m[1].trim()); const expected = ['Status', 'Name', 'Region', 'Last Status', 'Last Packet', 'Packet Health', 'Total Packets', 'Packets/Hour', 'Clock Offset', 'Uptime', - 'Firmware', 'Client']; + 'Firmware', 'Client', 'Neighbors']; assert.deepStrictEqual(labels, expected, `Headings out of sync.\nGot: ${JSON.stringify(labels)}\nExpected: ${JSON.stringify(expected)}`); }); diff --git a/test-packet-path-map.js b/test-packet-path-map.js index 7920e9cb6..f5643dc54 100644 --- a/test-packet-path-map.js +++ b/test-packet-path-map.js @@ -1042,8 +1042,8 @@ function makeSandbox(apiImpl) { }; await ctx.window.PacketPathMap.open('deadbeef'); - assert.ok(tooltips.some((t) => t.includes('📡') && t.includes('RepeaterA')), 'expected a repeater icon on RepeaterA, got: ' + JSON.stringify(tooltips)); - assert.ok(tooltips.some((t) => t.includes('🏠') && t.includes('RoomObserver')), 'expected a room icon on RoomObserver, got: ' + JSON.stringify(tooltips)); + assert.ok(tooltips.some((t) => t.includes('#ph-broadcast') && t.includes('RepeaterA')), 'expected a repeater icon on RepeaterA, got: ' + JSON.stringify(tooltips)); + assert.ok(tooltips.some((t) => t.includes('#ph-house-line') && t.includes('RoomObserver')), 'expected a room icon on RoomObserver, got: ' + JSON.stringify(tooltips)); passed++; console.log(' ✅ nodes with a known role get a role icon in their tooltip'); } catch (e) { failed++; console.log(' ❌ nodes with a known role get a role icon in their tooltip: ' + e.message); }