Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
104 changes: 104 additions & 0 deletions test-nav-stats-1343-e2e.js
Original file line number Diff line number Diff line change
@@ -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);
});