mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-18 04:25:11 +00:00
9d1f5d2395
Red commit: a200704d5e (CI URL added once
Actions resolves the run)
Fixes #1061
## What
Bottom navigation at ≤768px with 5 tabs in spec order: Home, Packets,
Live, Map, Channels. Top-nav suppressed at the same breakpoint — no
duplicate nav UX.
## Files
- NEW `public/bottom-nav.js` — renders 5 tabs, syncs `.active` on
`hashchange`, reuses the existing in-app hash router (`<a
href="#/...">`). Stable selector `[data-bottom-nav-tab="<route>"]`.
Container `[data-bottom-nav]`.
- NEW `public/bottom-nav.css` — styles. Tokens reused: `--nav-bg`,
`--nav-text`, `--nav-text-muted`, `--nav-active-bg`, `--accent`,
`--border` (all global → resolve in BOTH light and dark themes).
- `public/index.html` — one `<link>` for the CSS, one `<script>` after
`app.js`. The `<nav>` is appended by JS as a sibling of `<main
id="app">` at DOMContentLoaded.
- `test-bottom-nav-1061-e2e.js` + `.github/workflows/deploy.yml` —
Playwright wiring.
## Decisions
- **Breakpoint:** `@media (max-width: 768px)`. No `@container` rules
exist anywhere in `style.css` today — media query is consistent.
- **Top-nav suppression:** `display:none` at ≤768px. Simpler than a
hamburger collapse; long-tail routes (Tools/Lab/Perf) remain reachable
by URL; "More"-tab/hamburger fallback deferred per issue body.
- **Active indicator:** `var(--nav-active-bg)` + 2px accent top-border.
No moving pill.
- **Safe-area:** `padding-bottom: env(safe-area-inset-bottom)` on nav +
reciprocal `body` reservation. `viewport-fit=cover` already in place.
- **Reduced motion:** `prefers-reduced-motion: reduce` disables the
transition.
## TDD
- Red: `a200704` — assertions fail (no bottom-nav).
- Green: `53851a1` — component + styles.
E2E assertion added: `test-bottom-nav-1061-e2e.js:71` (case (a) —
bottom-nav visible at 360x800).
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: openclaw-bot <bot@openclaw>
160 lines
7.3 KiB
JavaScript
160 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/* Issue #1055 — Nav fluid Priority+ adaptation at all widths.
|
||
*
|
||
* Asserts the top-nav never overflows the viewport at common widths:
|
||
* the right edge of `.nav-right` MUST be ≤ document.documentElement.clientWidth.
|
||
*
|
||
* Pre-fix behavior: the Priority+ collapse rule was scoped to
|
||
* `(min-width: 768px) and (max-width: 1279px)`, so at 1280/1440/1920 the
|
||
* full link strip + nav-stats + nav-right buttons could push past the
|
||
* viewport's right edge (no collapse happened above 1279px).
|
||
*
|
||
* Post-fix: Priority+ collapses at all widths >=768px when needed.
|
||
*
|
||
* Run against a CoreScope server (defaults to localhost:13581 with the
|
||
* E2E fixture DB, matching the playwright job in .github/workflows/deploy.yml).
|
||
*
|
||
* CI gating: when CHROMIUM_REQUIRE=1 (set by the GH Actions workflow) a
|
||
* missing/broken Chromium is a HARD FAIL — no soft-skip. Locally the
|
||
* test is allowed to skip so devs without Playwright browsers installed
|
||
* can still run other tests.
|
||
*/
|
||
'use strict';
|
||
|
||
const { chromium } = require('playwright');
|
||
|
||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||
// Common widths the nav must stay clean at. 1280/1440 are the historic
|
||
// failure window: the Priority+ rule used to stop at 1279px but the full
|
||
// link strip + nav-right buttons don't fit on one row until ~1600px+.
|
||
// #1061: bottom-nav activates at max-width:768px and hides the top-nav.
|
||
// This test asserts top-nav layout stability — start at 769 to stay above
|
||
// that breakpoint. Below 768 the top nav is intentionally display:none.
|
||
const VIEWPORTS = [769, 1024, 1280, 1440, 1920];
|
||
// Routes asserted at every viewport. The pre-#1097 version only checked
|
||
// /#/home, but the bug reproduces on every top-level page since they
|
||
// all share the same .top-nav. Cover the four primary routes.
|
||
const ROUTES = ['/#/home', '/#/packets', '/#/nodes', '/#/map'];
|
||
const HEIGHT = 900;
|
||
// Whitespace tolerance (px) for the overflow/overlap assertions.
|
||
// Browsers occasionally hand back layout coordinates with sub-pixel
|
||
// rounding noise (≈0.1–0.4px) even when the box model is clean. We
|
||
// allow up to 0.5px so the test doesn't false-fail on rounding while
|
||
// still catching real overlaps (the bug this guards against was
|
||
// ~20px). Tighter than 0.5 caused intermittent CI flakes; looser
|
||
// would risk masking 1px regressions.
|
||
const SUBPIXEL_TOL = 0.5;
|
||
|
||
async function main() {
|
||
const requireChromium = process.env.CHROMIUM_REQUIRE === '1' ||
|
||
process.env.NAV_FLUID_REQUIRE === '1';
|
||
let browser;
|
||
try {
|
||
browser = await chromium.launch({
|
||
headless: true,
|
||
executablePath: process.env.CHROMIUM_PATH || undefined,
|
||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||
});
|
||
} catch (err) {
|
||
if (requireChromium) {
|
||
console.error(`test-nav-fluid-1055-e2e.js: FAIL — Chromium required (CHROMIUM_REQUIRE=1) but unavailable: ${err.message}`);
|
||
process.exit(1);
|
||
}
|
||
console.log(`test-nav-fluid-1055-e2e.js: SKIP (Chromium unavailable: ${err.message.split('\n')[0]})`);
|
||
process.exit(0);
|
||
}
|
||
|
||
let failures = 0;
|
||
let passes = 0;
|
||
const context = await browser.newContext();
|
||
const page = await context.newPage();
|
||
page.setDefaultTimeout(15000);
|
||
|
||
for (const route of ROUTES) {
|
||
for (const w of VIEWPORTS) {
|
||
await page.setViewportSize({ width: w, height: HEIGHT });
|
||
await page.goto(`${BASE}${route}`, { waitUntil: 'domcontentloaded' });
|
||
await page.waitForSelector('.top-nav .nav-right');
|
||
// Wait for fonts (which affect text measurement) AND for the nav
|
||
// layout to settle: the .nav-right bounding box must hold steady
|
||
// for two consecutive animation frames at the same coordinates.
|
||
// This replaces a magic 150ms sleep with a deterministic gate
|
||
// that asserts what we actually care about (layout has stopped
|
||
// moving) before measuring.
|
||
await page.evaluate(() => document.fonts && document.fonts.ready ? document.fonts.ready : null);
|
||
await page.waitForFunction(() => {
|
||
const el = document.querySelector('.top-nav .nav-right');
|
||
if (!el) return false;
|
||
const r1 = el.getBoundingClientRect();
|
||
return new Promise((resolve) => {
|
||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||
const r2 = el.getBoundingClientRect();
|
||
resolve(r1.right === r2.right && r1.left === r2.left && r1.top === r2.top);
|
||
}));
|
||
});
|
||
}, null, { timeout: 5000 });
|
||
|
||
const data = await page.evaluate(() => {
|
||
const navRight = document.querySelector('.top-nav .nav-right');
|
||
const navLeft = document.querySelector('.top-nav .nav-left');
|
||
const topNav = document.querySelector('.top-nav');
|
||
const more = document.querySelector('.nav-more-wrap');
|
||
const moreCs = more ? getComputedStyle(more) : null;
|
||
const links = Array.from(document.querySelectorAll('.nav-links .nav-link'));
|
||
const visible = links.filter(a => getComputedStyle(a).display !== 'none');
|
||
const lastVisible = visible[visible.length - 1] || null;
|
||
return {
|
||
clientW: document.documentElement.clientWidth,
|
||
navScroll: topNav.scrollWidth,
|
||
navClient: topNav.clientWidth,
|
||
navRight: navRight.getBoundingClientRect().right,
|
||
navRightL: navRight.getBoundingClientRect().left,
|
||
navLeftR: navLeft.getBoundingClientRect().right,
|
||
lastLinkR: lastVisible ? lastVisible.getBoundingClientRect().right : -1,
|
||
moreVisible: moreCs ? moreCs.display !== 'none' : false,
|
||
visibleLinks: visible.length,
|
||
totalLinks: links.length,
|
||
};
|
||
});
|
||
|
||
const tag = `${route} @ ${w}px`;
|
||
const reasons = [];
|
||
// 1. .nav-right must not extend past the viewport's right edge.
|
||
if (data.navRight > data.clientW + SUBPIXEL_TOL) {
|
||
reasons.push(`nav-right.right=${data.navRight.toFixed(1)} > clientWidth=${data.clientW} ` +
|
||
`(excess ${(data.navRight - data.clientW).toFixed(1)}px)`);
|
||
}
|
||
// 2. The visible link strip must not overlap .nav-right (parent overflow:hidden
|
||
// masks this visually but it still hides the rightmost links — the actual bug).
|
||
if (data.lastLinkR > data.navRightL + SUBPIXEL_TOL) {
|
||
reasons.push(`last visible link right=${data.lastLinkR.toFixed(1)} > nav-right.left=${data.navRightL.toFixed(1)} ` +
|
||
`(${(data.lastLinkR - data.navRightL).toFixed(1)}px overlap)`);
|
||
}
|
||
// 3. The nav row itself must not require horizontal scrolling.
|
||
if (data.navScroll > data.navClient + SUBPIXEL_TOL) {
|
||
reasons.push(`top-nav scrollWidth=${data.navScroll} > clientWidth=${data.navClient}`);
|
||
}
|
||
|
||
if (reasons.length === 0) {
|
||
passes++;
|
||
console.log(` ✅ ${tag}: clean (visible links ${data.visibleLinks}/${data.totalLinks}, more=${data.moreVisible})`);
|
||
} else {
|
||
failures++;
|
||
console.log(` ❌ ${tag}: ${reasons.join(' | ')} ` +
|
||
`(visible links ${data.visibleLinks}/${data.totalLinks}, more=${data.moreVisible})`);
|
||
}
|
||
}
|
||
}
|
||
|
||
await browser.close();
|
||
|
||
const total = ROUTES.length * VIEWPORTS.length;
|
||
console.log(`\ntest-nav-fluid-1055-e2e.js: ${failures === 0 ? 'OK' : 'FAIL'} — ${passes}/${total} passed`);
|
||
process.exit(failures === 0 ? 0 : 1);
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error('test-nav-fluid-1055-e2e.js: fatal', err);
|
||
process.exit(1);
|
||
});
|