Compare commits

...

2 Commits

Author SHA1 Message Date
CoreScope Bot db85e59fe2 fix(nav): hide .nav-stats at ≤1100px to match JS overflow assumption (#1343)
Restores the .nav-stats { display: none } rule inside the existing
@media (min-width: 768px) and (max-width: 1100px) block so that the
JS contract documented in applyNavPriority (public/app.js:1146) —
'in the 768-1100px band the CSS already hides .nav-stats' — holds.

Without this rule, at 900<viewport≤1100 the .nav-stats badge claims
inline width on .nav-left (which has overflow:hidden + flex-shrink:1
on .top-nav), silently clipping the 5 high-priority links out of view.

Also removes the now-redundant .nav-stats { display: none } from the
@media (max-width: 900px) block — the 1100px rule covers that range
and the duplicate misled readers (and the issue reporter) into
thinking 900px was the hide breakpoint.

Green: test-nav-stats-1343-e2e.js passes at 800/960/1080/1200px.
Existing nav-priority tests (1055/1102/1139/1311) still pass.
2026-05-24 04:36:12 +00:00
CoreScope Bot 92165c13e5 test(nav): regression test for #1343 nav-stats hide-band
RED: removes the .nav-stats { display: none } from the
@media (min-width: 768px) and (max-width: 1100px) block to demonstrate
the test catches the JS/CSS contract mismatch.

Test asserts at 800/960/1080/1200px viewports on /#/observers:
- all 5 high-priority links visible (clientWidth>0 + inside viewport)
- nav-stats hidden at 800/960/1080, visible at 1200

Without the 1100 hide rule, at 960/1080 the nav-stats steals enough
inline width that the high-pri link cluster either clips or never
renders inline (the applyNavPriority comment in app.js documents the
assumption: 'in the 768-1100px band the CSS already hides .nav-stats').
2026-05-24 04:36:11 +00:00
3 changed files with 110 additions and 1 deletions
+1
View File
@@ -251,6 +251,7 @@ jobs:
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-fluid-1055-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-priority-1102-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-priority-1311-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-stats-1343-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-more-floor-1139-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-bottom-nav-1061-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-gestures-1062-e2e.js 2>&1 | tee -a e2e-output.txt
+5 -1
View File
@@ -1633,8 +1633,12 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; }
/* === Responsive — Tablet (≤900px) === */
@media (max-width: 900px) {
/* nav-stats hidden in the (min-width:768) and (max-width:1100) band
below — see #1343. Keeping the rule here too is harmless but
misleading: it implies 900px is the breakpoint when in reality
the JS applyNavPriority assumes (and the 1100px block enforces)
the hide-band extends up to 1100px. */
.panel-right { width: 320px; min-width: 320px; }
.nav-stats { display: none; }
.brand-logo { height: 32px; width: 112px; }
.nav-link { padding: 14px 8px; font-size: 13px; }
.map-controls { width: 180px; font-size: 12px; }
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env node
/* Issue #1343 nav-stats hide-band must match JS overflow assumption.
*
* applyNavPriority in public/app.js assumes that at viewport <=1100px
* the CSS hides .nav-stats so the 5 high-priority links + "More ▾"
* actually fit on screen. If the hide band is narrower than 1100px,
* the high-priority links silently clip out of view in the gap.
*
* Cases:
* - 800x800 on /#/observers high-priority links visible, nav-stats hidden
* - 960x800 on /#/observers high-priority links visible, nav-stats hidden
* - 1080x800 on /#/observers high-priority links visible, nav-stats hidden
* - 1200x800 on /#/observers high-priority links visible, nav-stats RE-APPEARS
*
* A link is "visible" iff: clientWidth > 0 AND its bounding rect is
* fully inside the viewport horizontally (left>=0, right<=innerWidth).
*/
'use strict';
const assert = require('assert');
const { chromium } = require('playwright');
const BASE = process.env.BASE_URL || 'http://localhost:13581';
const HIGH_PRIORITY_HREFS = ['#/home', '#/packets', '#/map', '#/live', '#/nodes'];
const CASES = [
{ w: 800, h: 800, navStatsHidden: true, label: '800px — narrow desktop' },
{ w: 960, h: 800, navStatsHidden: true, label: '960px — operator-reported' },
{ w: 1080, h: 800, navStatsHidden: true, label: '1080px — narrow desktop' },
{ w: 1200, h: 800, navStatsHidden: false, label: '1200px — wide desktop' },
];
async function main() {
let browser;
let failures = 0;
try {
browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROMIUM_PATH || undefined,
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
});
for (const c of CASES) {
const ctx = await browser.newContext({ viewport: { width: c.w, height: c.h } });
const page = await ctx.newPage();
await page.goto(`${BASE}/#/observers`, { waitUntil: 'domcontentloaded', timeout: 15000 });
// Wait for nav to be rendered (top-nav appears as part of SPA shell)
await page.waitForSelector('.top-nav .nav-links', { timeout: 10000 });
// Allow nav-priority pass + font ready callback to settle
await page.waitForTimeout(400);
const result = await page.evaluate((hrefs) => {
const navStats = document.querySelector('.nav-stats');
const navStatsW = navStats ? navStats.clientWidth : 0;
const innerW = window.innerWidth;
const links = hrefs.map((href) => {
const a = document.querySelector(`.nav-links a[href="${href}"]`);
if (!a) return { href, present: false, w: 0, left: null, right: null };
const r = a.getBoundingClientRect();
return {
href,
present: true,
w: a.clientWidth,
left: r.left,
right: r.right,
inView: r.left >= 0 && r.right <= innerW && a.clientWidth > 0,
};
});
return { navStatsW, innerW, links };
}, HIGH_PRIORITY_HREFS);
const navStatsOk = c.navStatsHidden
? result.navStatsW === 0
: result.navStatsW > 0;
const allLinksVisible = result.links.every((l) => l.present && l.inView);
const status = navStatsOk && allLinksVisible ? 'PASS' : 'FAIL';
if (status === 'FAIL') failures++;
console.log(`[${status}] ${c.label} — innerW=${result.innerW} navStatsW=${result.navStatsW}`);
for (const l of result.links) {
console.log(` ${l.href}: w=${l.w} left=${l.left} right=${l.right} inView=${l.inView}`);
}
// Hard assertion so CI failure carries an explicit error trace
try {
assert.strictEqual(navStatsOk, true,
`${c.label}: expected nav-stats ${c.navStatsHidden ? 'hidden (clientWidth=0)' : 'visible (clientWidth>0)'}, got clientWidth=${result.navStatsW}`);
assert.strictEqual(allLinksVisible, true,
`${c.label}: expected all 5 high-priority links visible in viewport, got ${result.links.filter(l => !l.inView).map(l => l.href).join(',')} clipped`);
} catch (err) {
console.error(` ASSERT: ${err.message}`);
}
await ctx.close();
}
} finally {
if (browser) await browser.close();
}
// Final assertion — fail the process loudly with a stack
assert.strictEqual(failures, 0, `${failures} viewport case(s) failed`);
console.log('\nAll viewport cases passed');
}
main().catch((e) => {
console.error(e);
process.exit(1);
});