mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-09 23:01:51 +00:00
Compare commits
2 Commits
v3.8.1
...
fix/issue-1205
| Author | SHA1 | Date | |
|---|---|---|---|
| e56bb2bfb5 | |||
| 4a417cc8cb |
@@ -253,6 +253,7 @@ jobs:
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1109-hamburger-dropdown-visible-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-live-layout-1178-1179-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-live-mql-leak-1180-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1205-live-controls-anchor-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-drawer-1064-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
|
||||
- name: Collect frontend coverage (parallel)
|
||||
|
||||
+17
-21
@@ -347,28 +347,22 @@
|
||||
.live-toggles label { display: flex; align-items: center; gap: 3px; cursor: pointer; white-space: nowrap; }
|
||||
.live-toggles input { margin: 0; }
|
||||
|
||||
/* ---- Live controls cluster (#1179) ----
|
||||
* Pinned to bottom-right, above the VCR bar and the global bottom-nav.
|
||||
* Reserves space for both env(safe-area-inset-bottom) and the bottom-nav
|
||||
* (#1061, currently in PR #1174). When the bottom-nav lands the layout
|
||||
* tracks its custom property (--bottom-nav-height); otherwise the
|
||||
* fallback (56px) keeps the cluster clear of the VCR bar / bottom-nav
|
||||
* region.
|
||||
/* ---- Live controls cluster (#1179, re-anchored #1205) ----
|
||||
* Nested INSIDE #liveLegend (.panel-content). No longer position:fixed —
|
||||
* flows as a normal block within the legend panel so the toggle row
|
||||
* cannot detach and float across the map.
|
||||
*/
|
||||
.live-controls {
|
||||
position: fixed;
|
||||
right: 12px;
|
||||
bottom: calc(78px + var(--bottom-nav-height, 56px) + env(safe-area-inset-bottom, 0px));
|
||||
z-index: 1000;
|
||||
background: color-mix(in srgb, var(--surface-1) 92%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255,255,255,0.04);
|
||||
max-width: min(620px, calc(100vw - 24px));
|
||||
position: static;
|
||||
background: transparent;
|
||||
padding: 0 0 8px 0;
|
||||
margin: 0 0 8px 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.live-controls-body {
|
||||
@@ -422,8 +416,10 @@
|
||||
@media (max-width: 640px) {
|
||||
.live-feed { display: none !important; }
|
||||
.feed-show-btn { display: none !important; }
|
||||
.live-legend { display: none !important; }
|
||||
.legend-toggle-btn { display: none !important; }
|
||||
/* #1205: legend now hosts the settings toggle row — keep visible on narrow
|
||||
viewports so toggles remain reachable. Users still get the explicit
|
||||
show/hide via #legendToggleBtn. */
|
||||
.live-legend { max-width: calc(100vw - 16px); max-height: 60vh; }
|
||||
.live-header {
|
||||
flex-wrap: wrap; gap: 6px; padding: 6px 10px;
|
||||
top: 56px; left: 8px; right: 8px; max-width: calc(100vw - 16px);
|
||||
|
||||
+39
-38
@@ -878,44 +878,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-overlay live-controls" id="liveControls">
|
||||
<div class="live-controls-body" data-live-controls-body id="liveControlsBody">
|
||||
<div class="live-toggles">
|
||||
<label><input type="checkbox" id="liveHeatToggle" checked aria-describedby="heatDesc"> Heat</label>
|
||||
<span id="heatDesc" class="sr-only">Overlay a density heat map on the mesh nodes</span>
|
||||
<label><input type="checkbox" id="liveGhostToggle" checked aria-describedby="ghostDesc"> Ghosts</label>
|
||||
<span id="ghostDesc" class="sr-only">Show interpolated ghost markers for unknown hops</span>
|
||||
<label><input type="checkbox" id="liveRealisticToggle" aria-describedby="realisticDesc"> Realistic</label>
|
||||
<span id="realisticDesc" class="sr-only">Buffer packets by hash and animate all paths simultaneously</span>
|
||||
<label><input type="checkbox" id="liveColorHashToggle" aria-describedby="colorHashDesc"> Color by hash</label>
|
||||
<span id="colorHashDesc" class="sr-only">Color flying-packet dots and contrails by packet hash for propagation tracing</span>
|
||||
<label><input type="checkbox" id="liveMatrixToggle" aria-describedby="matrixDesc"> Matrix</label>
|
||||
<span id="matrixDesc" class="sr-only">Animate packet hex bytes flowing along paths like the Matrix</span>
|
||||
<label><input type="checkbox" id="liveMatrixRainToggle" aria-describedby="rainDesc"> Rain</label>
|
||||
<span id="rainDesc" class="sr-only">Matrix rain overlay — packets fall as hex columns</span>
|
||||
<label><input type="checkbox" id="liveAudioToggle" aria-describedby="audioDesc"> 🎵 Audio</label>
|
||||
<span id="audioDesc" class="sr-only">Sonify packets — turn raw bytes into generative music</span>
|
||||
<label><input type="checkbox" id="liveFavoritesToggle" aria-describedby="favDesc"> ⭐ Favorites</label>
|
||||
<span id="favDesc" class="sr-only">Show only favorited and claimed nodes</span>
|
||||
<div class="live-node-filter-wrap" style="position:relative">
|
||||
<input type="text" id="liveNodeFilterInput" placeholder="Filter by node…" autocomplete="off" class="live-node-filter-input" role="combobox" aria-expanded="false" aria-owns="liveNodeFilterDropdown" aria-autocomplete="list" aria-activedescendant="">
|
||||
<div id="liveNodeFilterDropdown" class="live-node-filter-dropdown hidden" role="listbox"></div>
|
||||
<button id="liveNodeFilterClear" class="vcr-btn" title="Clear node filter" style="display:none">×</button>
|
||||
</div>
|
||||
<div id="liveNodeFilterCount" class="live-filter-count hidden"></div>
|
||||
<label id="liveGeoFilterLabel" style="display:none"><input type="checkbox" id="liveGeoFilterToggle"> Mesh live area</label>
|
||||
<div id="liveRegionFilter" class="region-filter-container live-region-filter-container" aria-label="Filter live packets by IATA region"></div>
|
||||
</div>
|
||||
<div class="audio-controls hidden" id="audioControls">
|
||||
<label class="audio-slider-label">Voice <select id="audioVoiceSelect" class="audio-voice-select"></select></label>
|
||||
<label class="audio-slider-label">BPM <input type="range" id="audioBpmSlider" min="40" max="300" value="120" class="audio-slider"><span id="audioBpmVal">120</span></label>
|
||||
<label class="audio-slider-label">Vol <input type="range" id="audioVolSlider" min="0" max="100" value="30" class="audio-slider"><span id="audioVolVal">30</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="live-controls-toggle" data-live-controls-toggle id="liveControlsToggle"
|
||||
aria-expanded="false" aria-controls="liveControlsBody"
|
||||
aria-label="Show live controls">⚙</button>
|
||||
</div>
|
||||
<div class="live-overlay live-feed" id="liveFeed">
|
||||
<div class="panel-header">
|
||||
<button class="panel-corner-btn" data-panel="liveFeed" title="Move panel to next corner" aria-label="Move panel to next corner">◫</button>
|
||||
@@ -938,6 +900,45 @@
|
||||
<button class="panel-corner-btn" data-panel="liveLegend" title="Move panel to next corner" aria-label="Move panel to next corner">◫</button>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<!-- #1205: settings toggle row re-anchored INSIDE the legend panel -->
|
||||
<div class="live-controls" id="liveControls">
|
||||
<div class="live-controls-body" data-live-controls-body id="liveControlsBody">
|
||||
<div class="live-toggles">
|
||||
<label><input type="checkbox" id="liveHeatToggle" checked aria-describedby="heatDesc"> Heat</label>
|
||||
<span id="heatDesc" class="sr-only">Overlay a density heat map on the mesh nodes</span>
|
||||
<label><input type="checkbox" id="liveGhostToggle" checked aria-describedby="ghostDesc"> Ghosts</label>
|
||||
<span id="ghostDesc" class="sr-only">Show interpolated ghost markers for unknown hops</span>
|
||||
<label><input type="checkbox" id="liveRealisticToggle" aria-describedby="realisticDesc"> Realistic</label>
|
||||
<span id="realisticDesc" class="sr-only">Buffer packets by hash and animate all paths simultaneously</span>
|
||||
<label><input type="checkbox" id="liveColorHashToggle" aria-describedby="colorHashDesc"> Color by hash</label>
|
||||
<span id="colorHashDesc" class="sr-only">Color flying-packet dots and contrails by packet hash for propagation tracing</span>
|
||||
<label><input type="checkbox" id="liveMatrixToggle" aria-describedby="matrixDesc"> Matrix</label>
|
||||
<span id="matrixDesc" class="sr-only">Animate packet hex bytes flowing along paths like the Matrix</span>
|
||||
<label><input type="checkbox" id="liveMatrixRainToggle" aria-describedby="rainDesc"> Rain</label>
|
||||
<span id="rainDesc" class="sr-only">Matrix rain overlay — packets fall as hex columns</span>
|
||||
<label><input type="checkbox" id="liveAudioToggle" aria-describedby="audioDesc"> 🎵 Audio</label>
|
||||
<span id="audioDesc" class="sr-only">Sonify packets — turn raw bytes into generative music</span>
|
||||
<label><input type="checkbox" id="liveFavoritesToggle" aria-describedby="favDesc"> ⭐ Favorites</label>
|
||||
<span id="favDesc" class="sr-only">Show only favorited and claimed nodes</span>
|
||||
<div class="live-node-filter-wrap" style="position:relative">
|
||||
<input type="text" id="liveNodeFilterInput" placeholder="Filter by node…" autocomplete="off" class="live-node-filter-input" role="combobox" aria-expanded="false" aria-owns="liveNodeFilterDropdown" aria-autocomplete="list" aria-activedescendant="">
|
||||
<div id="liveNodeFilterDropdown" class="live-node-filter-dropdown hidden" role="listbox"></div>
|
||||
<button id="liveNodeFilterClear" class="vcr-btn" title="Clear node filter" style="display:none">×</button>
|
||||
</div>
|
||||
<div id="liveNodeFilterCount" class="live-filter-count hidden"></div>
|
||||
<label id="liveGeoFilterLabel" style="display:none"><input type="checkbox" id="liveGeoFilterToggle"> Mesh live area</label>
|
||||
<div id="liveRegionFilter" class="region-filter-container live-region-filter-container" aria-label="Filter live packets by IATA region"></div>
|
||||
</div>
|
||||
<div class="audio-controls hidden" id="audioControls">
|
||||
<label class="audio-slider-label">Voice <select id="audioVoiceSelect" class="audio-voice-select"></select></label>
|
||||
<label class="audio-slider-label">BPM <input type="range" id="audioBpmSlider" min="40" max="300" value="120" class="audio-slider"><span id="audioBpmVal">120</span></label>
|
||||
<label class="audio-slider-label">Vol <input type="range" id="audioVolSlider" min="0" max="100" value="30" class="audio-slider"><span id="audioVolVal">30</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="live-controls-toggle" data-live-controls-toggle id="liveControlsToggle"
|
||||
aria-expanded="false" aria-controls="liveControlsBody"
|
||||
aria-label="Show live controls">⚙</button>
|
||||
</div>
|
||||
<h3 class="legend-title">PACKET TYPES</h3>
|
||||
<ul class="legend-list">
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.ADVERT}" aria-hidden="true"></span> Advert — Node advertisement</li>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* E2E regression for #1205:
|
||||
* Live Map settings toggle row (Heat / Ghosts / Realistic / Color by hash /
|
||||
* Matrix / Rain / …) must be DOM-anchored inside the legend / settings
|
||||
* panel container (#liveLegend), NOT a free-floating sibling of <body>
|
||||
* or a default-positioned .live-overlay parked elsewhere on the map.
|
||||
*
|
||||
* Acceptance criterion (from issue body):
|
||||
* "E2E DOM assertion: the toggle row's parent is the expected panel
|
||||
* container element (by class/id), not body or .map-overlay"
|
||||
*
|
||||
* Run: BASE_URL=http://localhost:13581 node test-issue-1205-live-controls-anchor-e2e.js
|
||||
*/
|
||||
'use strict';
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function step(name, fn) {
|
||||
try { await fn(); passed++; console.log(' ✓ ' + name); }
|
||||
catch (e) { failed++; console.error(' ✗ ' + name + ': ' + e.message); }
|
||||
}
|
||||
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
||||
|
||||
async function gotoLive(page) {
|
||||
await page.goto(BASE + '/#/live', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('#liveLegend, .live-legend', { timeout: 8000 });
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||||
});
|
||||
console.log(`\n=== #1205 live-controls DOM anchor E2E against ${BASE} ===`);
|
||||
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
await gotoLive(page);
|
||||
|
||||
await step('#liveControls exists', async () => {
|
||||
const present = await page.locator('#liveControls').count();
|
||||
assert(present === 1, 'expected exactly one #liveControls element');
|
||||
});
|
||||
|
||||
await step('#liveControls is a descendant of #liveLegend', async () => {
|
||||
const inside = await page.evaluate(() => {
|
||||
const ctrl = document.getElementById('liveControls');
|
||||
const legend = document.getElementById('liveLegend');
|
||||
if (!ctrl || !legend) return false;
|
||||
return legend.contains(ctrl);
|
||||
});
|
||||
assert(inside,
|
||||
'#liveControls must be a descendant of #liveLegend — got a free-floating overlay (issue #1205)');
|
||||
});
|
||||
|
||||
await step('#liveControls parent is the legend panel (not body / not .live-page)', async () => {
|
||||
const parentInfo = await page.evaluate(() => {
|
||||
const ctrl = document.getElementById('liveControls');
|
||||
if (!ctrl || !ctrl.parentElement) return { tag: null, id: null, cls: null };
|
||||
const p = ctrl.parentElement;
|
||||
return { tag: p.tagName, id: p.id, cls: p.className };
|
||||
});
|
||||
assert(parentInfo.tag !== 'BODY',
|
||||
`#liveControls parent is <body> — it has detached from the legend panel`);
|
||||
assert(!(parentInfo.cls || '').split(/\s+/).includes('live-page'),
|
||||
`#liveControls parent is .live-page (free-floating overlay), not anchored to legend`);
|
||||
// Parent must be the legend or one of its inner wrappers (panel-content).
|
||||
const ok = parentInfo.id === 'liveLegend' ||
|
||||
(parentInfo.cls || '').split(/\s+/).some(c => /panel-content|live-legend/.test(c));
|
||||
assert(ok,
|
||||
`#liveControls parent must be #liveLegend or its .panel-content — got id=${parentInfo.id} cls=${parentInfo.cls}`);
|
||||
});
|
||||
|
||||
await step('#liveControls is visually inside the legend bounding box', async () => {
|
||||
const fits = await page.evaluate(() => {
|
||||
const c = document.getElementById('liveControls').getBoundingClientRect();
|
||||
const l = document.getElementById('liveLegend').getBoundingClientRect();
|
||||
// 2px slack for borders/blur.
|
||||
return c.left >= l.left - 2 && c.right <= l.right + 2 &&
|
||||
c.top >= l.top - 2 && c.bottom <= l.bottom + 2;
|
||||
});
|
||||
assert(fits, '#liveControls bounding rect must lie inside #liveLegend bounding rect');
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
})().catch(e => { console.error(e); process.exit(2); });
|
||||
@@ -58,27 +58,33 @@ async function gotoLive(page) {
|
||||
assert(h <= 40, `expected ≤40px, got ${h}px`);
|
||||
});
|
||||
|
||||
// (b)
|
||||
await step('[1440x900] .live-controls fixed/absolute, right ≤ 24px, bottom > 0', async () => {
|
||||
// (b) #1205 supersedes the original "fixed bottom-right" contract:
|
||||
// the toggle row is now DOM-anchored INSIDE #liveLegend (the
|
||||
// right-edge legend panel) instead of being a free-floating
|
||||
// overlay. Assert containment + that the rect sits inside the
|
||||
// legend's bounding box at the right edge.
|
||||
await step('[1440x900] .live-controls anchored inside #liveLegend at right edge', async () => {
|
||||
const info = await page.evaluate(() => {
|
||||
const el = document.querySelector('.live-controls');
|
||||
if (!el) return null;
|
||||
const cs = getComputedStyle(el);
|
||||
const legend = document.getElementById('liveLegend');
|
||||
if (!el || !legend) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
const l = legend.getBoundingClientRect();
|
||||
return {
|
||||
position: cs.position,
|
||||
right: parseFloat(cs.right),
|
||||
bottom: parseFloat(cs.bottom),
|
||||
rectRight: r.right,
|
||||
containedInLegend: legend.contains(el),
|
||||
rectFitsLegend: r.left >= l.left - 2 && r.right <= l.right + 2 &&
|
||||
r.top >= l.top - 2 && r.bottom <= l.bottom + 2,
|
||||
legendRight: l.right,
|
||||
vw: window.innerWidth,
|
||||
};
|
||||
});
|
||||
assert(info, '.live-controls element not found');
|
||||
assert(info.position === 'fixed' || info.position === 'absolute',
|
||||
`.live-controls position must be fixed/absolute, got ${info.position}`);
|
||||
assert(info.right <= 24, `.live-controls right must be ≤24px, got ${info.right}px`);
|
||||
assert(info.bottom > 0,
|
||||
`.live-controls bottom must reserve space for safe-area/nav, got ${info.bottom}px`);
|
||||
assert(info, '.live-controls or #liveLegend not found');
|
||||
assert(info.containedInLegend,
|
||||
'.live-controls must be a DOM descendant of #liveLegend (#1205)');
|
||||
assert(info.rectFitsLegend,
|
||||
'.live-controls bounding rect must lie inside #liveLegend rect (#1205)');
|
||||
assert(info.vw - info.legendRight <= 24,
|
||||
`legend (and therefore controls) must be ≤24px from right edge, got ${info.vw - info.legendRight}px`);
|
||||
});
|
||||
|
||||
await ctx.close();
|
||||
|
||||
Reference in New Issue
Block a user