Files
meshcore-analyzer/test-issue-1234-live-chrome-pass2-e2e.js
T
Kpa-clawbot aba20b3eda fix(#1234): Live mobile chrome pass 2 — single-row header, hide top-nav, VCR overflow (#1238)
## Summary

Live page mobile chrome-reduction pass 2. Three coordinated trims at
≤640px:

1. **`.live-header` → single row, ≤44px.** Drop the MESH LIVE text label
and the chart-icon (📊) header toggle. Promote `.live-stats-row` to a
direct child of `.live-header` so beacon + pkts + nodes + active + rate
+ gear all sit on one row. The (now empty) `.live-header-body` collapses
to `display:none`. `.live-controls-toggle` shrinks to 36×36 to fit the
strip.
2. **Top app navbar hidden on `/live`.** `body:has(.live-page) .top-nav
{ display:none }` — scoped via `:has()` so other routes are unaffected.
The `.live-page` height reclaims the freed 52px.
3. **VCR scope row: >6h collapsed into `More ▾`.** `12h` and `24h` get
`.vcr-scope-btn--overflow`; the new `.vcr-scope-more-wrap` dropdown is
desktop-hidden, mobile-shown. Dropdown items proxy `.click()` to the
underlying scope buttons — single source of truth, existing handler
unchanged.

## TDD

- **RED** (`b975c828`): `test-issue-1234-live-chrome-pass2-e2e.js` — one
E2E asserting all three acceptance items at 375×800 + desktop sanity at
1280×800. Wired into `deploy.yml`. Fails on master (no More button,
navbar visible, MESH LIVE label visible).
- **GREEN** (`1e529e63`): CSS + JS implementation. Updates
`test-live-layout-1178-1179-e2e.js` and
`test-issue-1204-live-panel-structure-e2e.js` in-place to match the new
single-row contract (chart toggle gone, MESH LIVE label gone on mobile,
gear shrunk to 36×36).

## Verification (local)

- New E2E: 7/7 
- `test-issue-1178-1179`: 10/10 
- `test-issue-1204`: 10/10 
- `test-issue-1205`: 18/18 
- `test-issue-1206`: 7/7 
- `test-live-mql-leak-1180`: 2/2 
- `#1220` empty-chrome guard (in `test-e2e-playwright.js`): header =
38px collapsed 

Desktop (1280×800) layout unchanged — top-nav visible, all 4 VCR scopes
inline, header behavior identical.

Fixes #1234.

---------

Co-authored-by: corescope-bot <bot@corescope.local>
2026-05-16 20:09:24 +00:00

160 lines
7.4 KiB
JavaScript

/**
* E2E for #1234 — Live page mobile chrome-reduction pass 2.
*
* At 375x800 the Live page must:
* (1) render `.live-header` as a single row, height ≤44px
* (2) hide the top `.top-nav` (display:none) on /live route at ≤640px
* (3) collapse VCR scope buttons >6h into one overflow `More` dropdown;
* the inline button count (excluding the dropdown menu) must be ≤3
* (currently 1h + 6h + More on mobile).
*
* Desktop (≥768px) sanity: top-nav visible, all 4 scope buttons visible
* (More button hidden).
*
* Run: BASE_URL=http://localhost:13581 node test-issue-1234-live-chrome-pass2-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('#liveHeader, .live-header', { timeout: 8000 });
await page.waitForTimeout(500);
}
(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=== #1234 Live mobile chrome pass 2 E2E against ${BASE} ===`);
// ── Mobile 375x800 ──────────────────────────────────────────────────────
{
const ctx = await browser.newContext({ viewport: { width: 375, height: 800 } });
const page = await ctx.newPage();
page.setDefaultTimeout(8000);
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
await step('[375x800] navigate to /live', async () => { await gotoLive(page); });
// (1) Single-row header, height ≤44px.
await step('[375x800] .live-header height ≤44px (single row, no MESH LIVE label, no chart toggle)', async () => {
const r = await page.evaluate(() => {
const h = document.getElementById('liveHeader');
const r = h.getBoundingClientRect();
const title = document.querySelector('.live-title');
const titleVisible = title && getComputedStyle(title).display !== 'none' &&
title.getBoundingClientRect().height > 0;
const chartBtn = document.getElementById('liveHeaderToggle');
const chartVisible = chartBtn && getComputedStyle(chartBtn).display !== 'none' &&
chartBtn.getBoundingClientRect().height > 0;
return { height: r.height, titleVisible, chartVisible };
});
assert(r.height <= 44, `live-header height must be ≤44px (got ${r.height}px)`);
assert(!r.titleVisible, 'MESH LIVE title label must not be visible at 375px');
assert(!r.chartVisible, 'chart-icon header toggle (📊) must not be visible at 375px');
});
// (2) Top nav hidden on /live route at mobile.
await step('[375x800] top-nav hidden on /live route', async () => {
const r = await page.evaluate(() => {
const nav = document.querySelector('.top-nav');
if (!nav) return { found: false };
const cs = getComputedStyle(nav);
const rect = nav.getBoundingClientRect();
return { found: true, display: cs.display, height: rect.height };
});
assert(r.found, '.top-nav element missing');
assert(r.display === 'none',
`.top-nav must be display:none on /live at ≤640px (got display=${r.display}, height=${r.height})`);
});
// (3) VCR scope buttons: >6h collapsed into overflow.
await step('[375x800] VCR scope row: ≤3 inline buttons (1h, 6h, More); 12h/24h hidden inline', async () => {
const r = await page.evaluate(() => {
const container = document.querySelector('.vcr-scope-btns');
if (!container) return { found: false };
function vis(el) {
if (!el) return false;
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden') return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
const scopeBtns = Array.from(container.querySelectorAll('.vcr-scope-btn[data-scope]'));
const more = container.querySelector('.vcr-scope-more, [data-vcr-scope-more]');
const inlineScopes = scopeBtns.filter(vis).map(b => b.dataset.scope);
return {
found: true,
inlineScopes,
moreVisible: vis(more),
totalInline: inlineScopes.length + (vis(more) ? 1 : 0),
};
});
assert(r.found, '.vcr-scope-btns container missing');
assert(r.moreVisible, 'More overflow button must be visible at 375px');
assert(r.totalInline <= 3,
`inline VCR scope row must have ≤3 buttons at 375px (got ${r.totalInline}: ${JSON.stringify(r.inlineScopes)} + more=${r.moreVisible})`);
// 12h and 24h must NOT be inline (they live in the More dropdown).
assert(!r.inlineScopes.includes('43200000'),
`12h scope button must not be inline at 375px (got inline: ${JSON.stringify(r.inlineScopes)})`);
assert(!r.inlineScopes.includes('86400000'),
`24h scope button must not be inline at 375px (got inline: ${JSON.stringify(r.inlineScopes)})`);
});
await ctx.close();
}
// ── Desktop 1280x800 sanity ─────────────────────────────────────────────
{
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
const page = await ctx.newPage();
page.setDefaultTimeout(8000);
await step('[1280x800] navigate to /live', async () => { await gotoLive(page); });
await step('[1280x800] top-nav visible (desktop unaffected)', async () => {
const r = await page.evaluate(() => {
const nav = document.querySelector('.top-nav');
const cs = nav && getComputedStyle(nav);
return { display: cs && cs.display, height: nav && nav.getBoundingClientRect().height };
});
assert(r.display !== 'none', `.top-nav must remain visible on desktop (got display=${r.display})`);
assert(r.height >= 40, `.top-nav must have nonzero height on desktop (got ${r.height})`);
});
await step('[1280x800] all 4 VCR scopes visible inline on desktop', async () => {
const r = await page.evaluate(() => {
const btns = Array.from(document.querySelectorAll('.vcr-scope-btns .vcr-scope-btn[data-scope]'));
function vis(el) {
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden') return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
return {
visibleScopes: btns.filter(vis).map(b => b.dataset.scope),
};
});
assert(r.visibleScopes.length === 4,
`desktop must show 4 inline scope buttons (got ${r.visibleScopes.length}: ${JSON.stringify(r.visibleScopes)})`);
});
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); });