diff --git a/public/index.html b/public/index.html index cd8df9f..a64f36a 100644 --- a/public/index.html +++ b/public/index.html @@ -22,9 +22,9 @@ - - - + + + @@ -85,30 +85,30 @@
- - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/live.js b/public/live.js index 2be005b..5086160 100644 --- a/public/live.js +++ b/public/live.js @@ -1850,9 +1850,11 @@ function nextHop() { if (hopIndex >= hopPositions.length) { activeAnims = Math.max(0, activeAnims - 1); - document.getElementById('liveAnimCount').textContent = activeAnims; + const countEl = document.getElementById('liveAnimCount'); + if (countEl) countEl.textContent = activeAnims; return; } + if (!animLayer) return; // Audio hook: notify per-hop callback if (onHop) try { onHop(hopIndex, hopPositions.length, hopPositions[hopIndex]); } catch (e) {} const hp = hopPositions[hopIndex]; @@ -1865,11 +1867,11 @@ }).addTo(animLayer); let pulseUp = true; const pulseTimer = setInterval(() => { - if (!animLayer.hasLayer(ghost)) { clearInterval(pulseTimer); return; } + if (!animLayer || !animLayer.hasLayer(ghost)) { clearInterval(pulseTimer); return; } ghost.setStyle({ fillOpacity: pulseUp ? 0.6 : 0.25, opacity: pulseUp ? 0.7 : 0.4 }); pulseUp = !pulseUp; }, 600); - setTimeout(() => { clearInterval(pulseTimer); if (animLayer.hasLayer(ghost)) animLayer.removeLayer(ghost); }, 3000); + setTimeout(() => { clearInterval(pulseTimer); if (animLayer && animLayer.hasLayer(ghost)) animLayer.removeLayer(ghost); }, 3000); } } else { pulseNode(hp.key, hp.pos, typeName); diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index 5c72224..b0153a6 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -2788,6 +2788,34 @@ console.log('\n=== packets.js: savedTimeWindowMin defaults ==='); }); } +// ===== live.js: nextHop null guards ===== +console.log('\n=== live.js: nextHop null guards ==='); +{ + const liveSource = fs.readFileSync('public/live.js', 'utf8'); + + test('nextHop guards animLayer null before use', () => { + assert.ok(liveSource.includes('if (!animLayer) return;'), + 'nextHop must return early when animLayer is null (post-destroy)'); + }); + + test('nextHop setInterval guards animLayer null', () => { + assert.ok(liveSource.includes('if (!animLayer || !animLayer.hasLayer(ghost))'), + 'setInterval in nextHop must guard animLayer null'); + }); + + test('nextHop setTimeout guards animLayer null', () => { + assert.ok(liveSource.includes('if (animLayer && animLayer.hasLayer(ghost)) animLayer.removeLayer(ghost)'), + 'setTimeout in nextHop must guard animLayer null'); + }); + + test('nextHop guards liveAnimCount element null', () => { + assert.ok(liveSource.includes('const countEl = document.getElementById(\'liveAnimCount\')'), + 'nextHop must null-check liveAnimCount element'); + assert.ok(liveSource.includes('if (countEl) countEl.textContent = activeAnims'), + 'nextHop must conditionally update liveAnimCount'); + }); +} + // ===== SUMMARY ===== Promise.allSettled(pendingTests).then(() => { console.log(`\n${'═'.repeat(40)}`);