Files
meshcore-analyzer/test-issue-1206-resize-observer-leak-e2e.js
T
Kpa-clawbot ab34d9fb65 fix(#1206): keep VCR bar from occluding the live packet feed (#1213)
Red commit: `bcfc74de` (CI:
https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2Fissue-1206)

Fixes #1206.

## Problem
On Live Map the VCR (timeline/playback) bar overlays the bottom of the
viewport. Bottom-pinned overlays — the live packet feed, the legend, any
corner panel — used hard-coded `bottom: 58–88px` offsets that are
smaller than the real bar height (two-row mobile layout +
`env(safe-area-inset-bottom)` push it to ~80px and beyond). The last N
packet-feed rows slid under the bar and became unreadable / unclickable.

## Fix
Publish the bar's measured height as a CSS variable on the live page
and bind every bottom-anchored overlay to it.

- `public/live.js` — new `initVCRHeightTracker()` runs after init; uses
  `ResizeObserver` + `resize` / `visualViewport.resize` to keep
  `--vcr-bar-height` on `.live-page` in sync with `#vcrBar`.
- `public/live.css` — `.live-feed`, `.feed-show-btn`, and the
  `.live-overlay[data-position="bl"|"br"]` corner slots now use
  `bottom: calc(var(--vcr-bar-height, 58px) + 10px)`. The feed's
  `max-height` is also capped against `100dvh - top - vcr - margin`
  so its scroll container can never extend past the bar.
- Stale per-breakpoint overrides (the `@supports(env(safe-area-inset))`
  hard-coded `78px + safe-area` for feed/legend) are removed in favor
  of the single tracked variable.

## TDD
- Red commit `bcfc74de` adds `test-issue-1206-vcr-overlap-e2e.js`:
  asserts `#liveFeed.getBoundingClientRect().bottom <= #vcrBar.top`
  (and same for the last row) at desktop 1280x800 and mid 720x800.
  Verified locally that reverting the green commit makes the feed-bottom
  assertions fail (feed bottom 742px > VCR top 721px) — see PR body for
  exact numbers from the local run.
- Green commit `1ad17e7f` makes all 5 assertions pass.

## Browser verified
Local Go server with `test-fixtures/e2e-fixture.db`, headless Chromium
via the new E2E test — all 5 assertions green.

## E2E assertion added
`test-issue-1206-vcr-overlap-e2e.js:84` (bottom-row vs VCR-top) plus
container check at `:74`.

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: clawbot <bot@corescope.local>
2026-05-16 05:55:21 +00:00

124 lines
4.8 KiB
JavaScript

/**
* E2E regression for #1206 review must-fix (kent-beck #2):
* ResizeObserver leak in initVCRHeightTracker().
*
* SPA navigates to /#/live, then bounces /#/nodes ↔ /#/live ≥ 3 times.
* Each /#/live mount re-runs initVCRHeightTracker(); without the cleanup
* tear-down (or with a future regression that orphans cleanup) each visit
* would accumulate another ResizeObserver against #vcrBar.
*
* We can't read live ResizeObserver instances directly — wrap the
* constructor + .disconnect() via addInitScript so we can count
* outstanding (constructed but not disconnected) observers and assert it
* does NOT grow with each /live mount.
*
* Run: BASE_URL=http://localhost:13581 node test-issue-1206-resize-observer-leak-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(' \u2713 ' + name); }
catch (e) { failed++; console.error(' \u2717 ' + name + ': ' + e.message); }
}
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
async function gotoHash(page, hash) {
await page.evaluate((h) => { window.location.hash = h; }, hash);
await page.waitForTimeout(150);
}
(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=== #1206 ResizeObserver leak E2E against ' + BASE + ' ===');
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
// Install ResizeObserver wrapper BEFORE any page script runs.
await ctx.addInitScript(() => {
var RealRO = window.ResizeObserver;
if (typeof RealRO !== 'function') {
window.__roOutstanding = 0;
window.__roConstructed = 0;
return;
}
window.__roConstructed = 0;
window.__roOutstanding = 0;
function WrappedRO(cb) {
var inst = new RealRO(cb);
window.__roConstructed++;
window.__roOutstanding++;
var realDisconnect = inst.disconnect.bind(inst);
var disconnected = false;
inst.disconnect = function() {
if (!disconnected) {
disconnected = true;
window.__roOutstanding--;
}
return realDisconnect();
};
return inst;
}
WrappedRO.prototype = RealRO.prototype;
window.ResizeObserver = WrappedRO;
});
const page = await ctx.newPage();
page.setDefaultTimeout(8000);
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
await step('initial /#/live mount constructs at most 1 VCR ResizeObserver', async () => {
await page.goto(BASE + '/#/live', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#vcrBar', { timeout: 8000 });
await page.waitForTimeout(300);
// Baseline snapshot — record outstanding right after first /live mount.
const snap = await page.evaluate(() => ({
outstanding: window.__roOutstanding,
constructed: window.__roConstructed,
}));
assert(typeof snap.outstanding === 'number',
'ResizeObserver wrapper not installed (snap=' + JSON.stringify(snap) + ')');
// Stash the first-mount baseline on window for the next step.
await page.evaluate((b) => { window.__roBaseline = b; }, snap);
});
await step('3 SPA round-trips /live<->/nodes do NOT grow outstanding observer count', async () => {
for (let i = 0; i < 3; i++) {
await gotoHash(page, '#/nodes');
await page.waitForTimeout(150);
await gotoHash(page, '#/live');
await page.waitForSelector('#vcrBar', { timeout: 8000 });
await page.waitForTimeout(200);
}
const after = await page.evaluate(() => ({
outstanding: window.__roOutstanding,
constructed: window.__roConstructed,
baseline: window.__roBaseline,
}));
// The VCR tracker MUST clean its observer on destroy(). After N
// remounts the outstanding count for VCR-tracking observers must not
// exceed the baseline. We can't isolate which observers are
// ours, so we use the delta: 4 mounts * leak-of-1 = 3 extra
// outstanding observers, which is the failure mode this test gates.
var delta = after.outstanding - after.baseline.outstanding;
assert(delta <= 0,
'ResizeObserver leak: outstanding count grew by ' + delta +
' across 3 SPA round-trips (baseline=' + after.baseline.outstanding +
', after=' + after.outstanding + ', constructed=' + after.constructed +
'). Expected delta <= 0.');
});
await ctx.close();
await browser.close();
console.log('\n#1206 ResizeObserver leak: ' + passed + ' passed, ' + failed + ' failed');
process.exit(failed ? 1 : 0);
})().catch((e) => { console.error(e); process.exit(1); });