Files
meshcore-analyzer/test-e2e-1267-mobile-vcr.js
T
Kpa-clawbot 78b666c248 fix(#1267): mobile VCR bar invisible — JS height clobbered bottom-nav reserve (#1269)
## Summary
Mobile-only regression: on the Live page at ≤768px viewports the VCR bar
was rendered behind the fixed bottom-nav and never visible to the user.
iOS Safari screenshot at 375x812 showed: top header strip, full-height
map, bottom-nav — **no VCR row at all**.

Fixes #1267.

## Root cause
`public/live.js` `initResizeHandler` (the existing JS height override)
was setting `page.style.height = window.innerHeight + 'px'`, which
clobbered the CSS rule that already subtracts `--bottom-nav-reserve`
from the live-page height. Because `.live-page` then spanned the full
viewport, the VCR bar (`position:absolute; bottom:0; z-index:1000`) was
painted underneath `.bottom-nav` (`position:fixed; z-index:1200`).

The VCR bar element WAS in the DOM, WAS `display: flex`, and HAD
`height: 53px` — it just sat at y=758..812 underneath the bottom-nav at
y=754..812. CSS-only checks for `display:none` would never catch this;
the test asserts the bar's bottom edge is at or above the bottom-nav's
top edge.

## Fix
One-liner in spirit: subtract the bottom-nav height before applying
`page.style.height`. The implementation measures the rendered
`.bottom-nav` (with a fallback to a hidden probe that resolves the
`--bottom-nav-reserve` token), so it survives safe-area inset and the
bottom-nav's 1px border.

```js
const reserve = /* measure .bottom-nav, fall back to --bottom-nav-reserve token */;
const h = Math.max(0, window.innerHeight - reserve);
```

Desktop is unchanged: `.bottom-nav` is `display: none`, the probe
resolves to 0, and `h === window.innerHeight` exactly as before.

## TDD
- **RED** (commit 1): `test-e2e-1267-mobile-vcr.js` — Playwright at
iPhone 375x812 asserts `.vcr-bar` has `display !== 'none'`, `visibility
!== 'hidden'`, `height > 0`, `top < viewport.height`, and (the key
check) `bottom <= bottom-nav.top`. Fails on `master` with: *"VCR bar
bottom 812 overlaps bottom-nav top 754"*.
- **GREEN** (commit 2): the fix above. Test passes: *"VCR bar bottom 754
≤ bottom-nav top 754"*.

## Verification
-  Mobile (375x812) repro reproduced against `master` (bar at
y=758..812, behind bottom-nav)
-  Mobile (375x812) E2E green after fix (bar at y=700..754, flush above
bottom-nav)
-  Desktop (1440x900) unaffected — bottom-nav hidden, page height =
viewport height as before, VCR bar at viewport bottom
-  #1234 (top-nav hidden on /live), #1246 (single-row VCR), #1206/#1213
(VCR/feed clearance) unchanged — none touched

## Files
- `public/live.js` — single function (`initResizeHandler`) modified
- `test-e2e-1267-mobile-vcr.js` — new mobile-viewport Playwright
regression test

Run: `BASE_URL=http://localhost:13581 node test-e2e-1267-mobile-vcr.js`

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-18 15:27:05 -07:00

113 lines
4.9 KiB
JavaScript

/**
* #1267 — VCR bar invisible on mobile /live (iOS Safari ~375x812).
*
* RED first: at a 375x812 mobile viewport, the `.vcr-bar` must be visible
* between the map and bottom-nav. Asserts measured height > 0, display !==
* 'none', visibility !== 'hidden', and that its top edge is within the
* viewport (not pushed below the visible area).
*
* Usage: BASE_URL=http://localhost:13581 node test-e2e-1267-mobile-vcr.js
*/
const { chromium, devices } = require('playwright');
const BASE = process.env.BASE_URL || 'http://localhost:13581';
(async () => {
const browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium',
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
});
const context = await browser.newContext({
viewport: { width: 375, height: 812 },
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
});
const page = await context.newPage();
page.setDefaultTimeout(15000);
let failed = false;
function fail(msg) { failed = true; console.log(' \u274c', msg); }
function pass(msg) { console.log(' \u2705', msg); }
console.log(`\n#1267 mobile VCR-bar visibility against ${BASE} (375x812)\n`);
await page.goto(`${BASE}/#/live`, { waitUntil: 'domcontentloaded' });
// Allow live page mount + initial render + VCR bar ResizeObserver publish.
await page.waitForSelector('.live-page', { timeout: 15000 });
await page.waitForSelector('#vcrBar', { timeout: 15000 });
// Wait until markers populate — #1267 only manifests after marker render.
// We poll for >=1 leaflet-marker-icon to appear in the DOM, then settle.
await page.waitForFunction(
() => document.querySelectorAll('#liveMap .leaflet-marker-icon, #liveMap .leaflet-marker-pane > *').length > 0,
null,
{ timeout: 15000 }
).catch(() => {});
await page.waitForTimeout(4000);
const info = await page.evaluate(() => {
const bar = document.getElementById('vcrBar');
if (!bar) return { missing: true };
const r = bar.getBoundingClientRect();
const cs = getComputedStyle(bar);
const page = document.querySelector('.live-page');
const pageR = page ? page.getBoundingClientRect() : null;
const bn = document.querySelector('.bottom-nav');
const bnR = bn ? bn.getBoundingClientRect() : null;
const bnCs = bn ? getComputedStyle(bn) : null;
const rootCs = getComputedStyle(document.documentElement);
return {
rect: { top: r.top, bottom: r.bottom, height: r.height, width: r.width, left: r.left, right: r.right },
display: cs.display,
visibility: cs.visibility,
opacity: cs.opacity,
position: cs.position,
zIndex: cs.zIndex,
viewportH: window.innerHeight,
viewportW: window.innerWidth,
pageRect: pageR ? { top: pageR.top, bottom: pageR.bottom, height: pageR.height } : null,
bottomNav: bnR ? { top: bnR.top, bottom: bnR.bottom, height: bnR.height, display: bnCs.display } : null,
bottomNavReserve: rootCs.getPropertyValue('--bottom-nav-reserve'),
vcrBarHeightVar: getComputedStyle(page || document.body).getPropertyValue('--vcr-bar-height'),
};
});
console.log('VCR bar measurement:', JSON.stringify(info, null, 2));
if (info.missing) {
fail('#vcrBar element not in DOM');
} else {
if (info.display === 'none') fail(`display:none on .vcr-bar`);
else pass(`display is ${info.display}`);
if (info.visibility === 'hidden') fail(`visibility:hidden on .vcr-bar`);
else pass(`visibility is ${info.visibility}`);
if (info.rect.height <= 0) fail(`getBoundingClientRect().height = ${info.rect.height} (expected > 0)`);
else pass(`height ${info.rect.height}px > 0`);
if (info.rect.top >= info.viewportH) fail(`bar top ${info.rect.top} >= viewport height ${info.viewportH} (pushed off-screen)`);
else pass(`bar top ${info.rect.top} < viewport height ${info.viewportH}`);
// #1267 root assertion: the VCR bar must not be occluded by the
// fixed bottom-nav (z=1200 > vcr-bar z=1000). The bar's bottom edge
// must sit AT OR ABOVE the bottom-nav's top edge.
if (info.bottomNav && info.bottomNav.display !== 'none') {
if (info.rect.bottom > info.bottomNav.top + 0.5) {
fail(`VCR bar bottom ${info.rect.bottom} overlaps bottom-nav top ${info.bottomNav.top} (hidden behind bottom-nav — #1267)`);
} else {
pass(`VCR bar bottom ${info.rect.bottom} ≤ bottom-nav top ${info.bottomNav.top}`);
}
}
// Sanity: the bar should occupy width across most of the viewport.
if (info.rect.width < info.viewportW * 0.5) fail(`bar width ${info.rect.width} < 50% of viewport ${info.viewportW}`);
else pass(`bar width ${info.rect.width} spans >50% viewport`);
}
await browser.close();
process.exit(failed ? 1 : 0);
})().catch(err => { console.error(err); process.exit(2); });