Files
meshcore-analyzer/test-live-mql-leak-1180-e2e.js
T
Kpa-clawbot 16c48e73b3 fix(live): compact header + pinned controls with narrow-viewport collapse (#1178, #1179) (#1180)
Red commit: 61fcc8c19b96543f1b4bbd6fd2ce54e6265d5e38 (CI run: pending —
see Checks tab on this PR)

Fixes #1178
Fixes #1179

## Summary
Live page layout polish — both issues touch `public/live.css` + a
small `public/live.js` slice, so they ship as one PR per AGENTS rule
34.

### #1178 — Header compactness + narrow-viewport collapse
- `.live-header` total height ≤ 40px at desktop widths (smaller
  padding, gap, title font, and pill sizing; `max-height: 40px` as a
  belt-and-suspenders gate).
- Body wrapped in `.live-header-body` so it can collapse cleanly.
- New 32×32 toggle button `[data-live-header-toggle]`, hidden at
  wide viewports, visible at `≤768px`.

### #1179 — Controls pinned bottom-right + narrow-viewport collapse
- New `.live-controls` cluster around the toggles list and audio
  controls, `position: fixed; right: 12px;` and
`bottom: calc(78px + var(--bottom-nav-height, 56px) +
env(safe-area-inset-bottom, 0px))`.
- That bottom calc reserves space for the VCR bar **and** the bottom
  nav (#1061, currently in PR #1174). When the bottom-nav exposes
  `--bottom-nav-height` the cluster tracks it; otherwise the 56px
  fallback keeps it clear regardless of merge order.
- `z-index: 1000` keeps it above map markers but below modals.
- New 32×32 toggle button `[data-live-controls-toggle]`, hidden at
  wide viewports, visible at `≤768px`.

### Breakpoint + selectors
- Narrow = `max-width: 768px` (matches #1061 bottom-nav activation).
- Stable selectors for E2E: `[data-live-header-toggle]`,
  `[data-live-header-body]`, `[data-live-controls-toggle]`,
  `[data-live-controls-body]`. No DOM-order dependence.

### Bottom-nav coexistence
The expanded narrow-viewport controls panel uses
`max-height: 50vh; overflow-y: auto` on its toggles list, and the
cluster's `bottom` reservation guarantees the panel's bottom edge
sits above the (possibly absent) bottom-nav region. The E2E test
asserts exactly this with `expandedRect.bottom + 8 < innerHeight −
navH`,
defaulting `navH` to 56 if `.bottom-nav` is not in the DOM yet.

### Theming
All new colors via existing CSS tokens (`--surface-1`, `--text`,
`--text-muted`, `--border`, `--accent`). check-css-vars passes.

### TDD
- Red commit: `61fcc8c` — assertions only (no impl), wired into
  `.github/workflows/deploy.yml` Playwright matrix.
- Green commit: `7d591be` — DOM split + CSS + collapse JS.
- E2E assertion added: `test-live-layout-1178-1179-e2e.js:55`
  (desktop header height) through `:170` (narrow controls
  bottom-nav coexistence).

### Local verification
```
./corescope-server -port 13581 -db test-fixtures/e2e-fixture.db &
CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 \
  node test-live-layout-1178-1179-e2e.js
# → 8/8 passed
```

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-08 18:50:30 -07:00

79 lines
3.1 KiB
JavaScript

/**
* E2E regression for #1180 review must-fix:
* MediaQueryList 'change' listener leak in wireLiveCollapseToggles().
*
* SPA navigates to /#/live, then bounces /#/explore ↔ /#/live N times.
* Each /#/live mount re-runs the wiring IIFE; without a guard, every
* mount calls narrowMql.addEventListener('change', applyForViewport)
* against a process-global MediaQueryList instance, so listeners
* accumulate without bound.
*
* live.js exposes a debug seam: window.__liveMQLBindCount is incremented
* exactly when the MQL listener is registered. After 5 round-trips it
* MUST be ≤ 1.
*
* Run: BASE_URL=http://localhost:13581 node test-live-mql-leak-1180-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 gotoHash(page, hash) {
await page.evaluate((h) => { window.location.hash = h; }, hash);
// Allow router to run
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=== #1180 MQL listener leak E2E against ${BASE} ===`);
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
page.setDefaultTimeout(8000);
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
await step('initial /#/live load registers MQL listener at most once', async () => {
await page.goto(BASE + '/#/live', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#liveHeader, .live-header', { timeout: 8000 });
await page.waitForTimeout(300);
const count = await page.evaluate(() => window.__liveMQLBindCount);
assert(typeof count === 'number',
'window.__liveMQLBindCount missing — debug seam not exposed by live.js');
assert(count <= 1, `expected MQL bind count ≤ 1 after first mount, got ${count}`);
});
await step('5 SPA round-trips do NOT accumulate MQL listeners', async () => {
for (let i = 0; i < 5; i++) {
await gotoHash(page, '#/packets');
await page.waitForTimeout(80);
await gotoHash(page, '#/live');
await page.waitForSelector('#liveHeader, .live-header', { timeout: 8000 });
await page.waitForTimeout(120);
}
const count = await page.evaluate(() => window.__liveMQLBindCount);
assert(typeof count === 'number',
'window.__liveMQLBindCount missing after navigations');
assert(count <= 1,
`MQL listener leak: bind count after 5 round-trips = ${count}, expected ≤ 1`);
});
await ctx.close();
await browser.close();
console.log(`\n=== Results: passed ${passed} failed ${failed} ===`);
process.exit(failed > 0 ? 1 : 0);
})().catch(e => { console.error(e); process.exit(1); });