From 5e9872c6054c8cb759cd196cc790bb098d4d954d Mon Sep 17 00:00:00 2001 From: openclaw-bot Date: Tue, 5 May 2026 19:13:39 +0000 Subject: [PATCH] test(nav): identity assertions + active-mirror + CI hookup (#1105 MINORs 7-9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1105 MINOR 7: at 1080/800px the test now asserts the visible set is EXACTLY the 5 high-priority hrefs (Home, Packets, Map, Live, Nodes). Previously only 'visibleCount >= 5' was checked — a buggy queue that hid Home and showed Lab would still pass. #1105 MINOR 9: new test case navigates to #/observers at 1080px (a route whose link overflows into the More menu) and asserts: - the inline #/observers link is in the overflow set, - the More-menu clone has .active, - #navMoreBtn has .active. This exercises rebuildMoreMenu's active-mirroring path, which depends on applyNavPriority running on hashchange after the route handler. #1105 MINOR 8: deploy.yml now runs test-nav-priority-1102-e2e.js with CHROMIUM_REQUIRE=1 in CI, so a Chromium provisioning regression fails the build instead of silently SKIPing. These are pure test-coverage tightenings; no production code change in this commit. Hardening commit (fa58cb6) already passed the original loose assertions and continues to pass the tighter ones. --- .github/workflows/deploy.yml | 1 + test-nav-priority-1102-e2e.js | 90 ++++++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a9df34eb..92b6deac 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -219,6 +219,7 @@ jobs: BASE_URL=http://localhost:13581 node test-channel-issue-1087-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-map-modal-fluid-e2e.js 2>&1 | tee -a e2e-output.txt 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 BASE_URL=http://localhost:13581 node test-channel-fluid-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-table-fluid-e2e.js 2>&1 | tee -a e2e-output.txt diff --git a/test-nav-priority-1102-e2e.js b/test-nav-priority-1102-e2e.js index 0c7ae3a0..64db40b3 100644 --- a/test-nav-priority-1102-e2e.js +++ b/test-nav-priority-1102-e2e.js @@ -15,6 +15,13 @@ * every link not currently visible inline. * - At 768px (just above hamburger threshold): 5 high-priority links * visible AND More menu non-empty. + * + * #1105 MINOR 7: at 1080/800px we now assert the visible set is *exactly* + * the 5 high-priority links (Home/Packets/Map/Live/Nodes). A buggy queue + * that hid Home and showed Lab would still pass the cardinality check. + * + * #1105 MINOR 9: also asserts that navigating to a route whose link + * lives in the More menu lights up #navMoreBtn with .active. */ 'use strict'; @@ -23,12 +30,14 @@ const { chromium } = require('playwright'); const BASE = process.env.BASE_URL || 'http://localhost:13581'; // [width, expected behavior] +// requireExactHighPri: when true, asserts the visible set matches HIGH_PRIORITY_HREFS exactly +const HIGH_PRIORITY_HREFS = ['#/home', '#/packets', '#/map', '#/live', '#/nodes']; const CASES = [ - // viewport, minVisible, moreVisible, label - { w: 2560, minVisible: 11, moreVisible: false, label: '2560px — all visible' }, - { w: 1920, minVisible: 9, moreVisible: null, label: '1920px — most visible' }, - { w: 1080, minVisible: 5, moreVisible: true, label: '1080px — collapsed' }, - { w: 800, minVisible: 5, moreVisible: true, label: '800px — collapsed' }, + // viewport, minVisible, moreVisible, requireExactHighPri, label + { w: 2560, minVisible: 11, moreVisible: false, requireExactHighPri: false, label: '2560px — all visible' }, + { w: 1920, minVisible: 9, moreVisible: null, requireExactHighPri: false, label: '1920px — most visible' }, + { w: 1080, minVisible: 5, moreVisible: true, requireExactHighPri: true, label: '1080px — collapsed' }, + { w: 800, minVisible: 5, moreVisible: true, requireExactHighPri: true, label: '800px — collapsed' }, ]; const HEIGHT = 900; @@ -106,6 +115,22 @@ async function main() { `(menu has ${data.moreMenuLinks.length}, expected ${data.hiddenInline.length})`); } } + // #1105 MINOR 7: identity, not just cardinality. The 5 visible links + // at the collapsed widths must be EXACTLY the high-priority set + // (Home/Packets/Map/Live/Nodes). A buggy queue that hid Home and + // showed Lab would still pass `visibleCount >= 5`. + if (c.requireExactHighPri) { + const missingHighPri = HIGH_PRIORITY_HREFS.filter(h => !data.visibleHrefs.includes(h)); + if (missingHighPri.length) { + reasons.push(`high-priority link(s) NOT visible inline: ${missingHighPri.join(', ')} ` + + `(visible=[${data.visibleHrefs.join(', ')}])`); + } + const extra = data.visibleHrefs.filter(h => !HIGH_PRIORITY_HREFS.includes(h)); + if (extra.length) { + reasons.push(`unexpected non-high-priority link(s) visible: ${extra.join(', ')} ` + + `(expected exactly [${HIGH_PRIORITY_HREFS.join(', ')}])`); + } + } const tag = c.label; if (reasons.length === 0) { @@ -118,8 +143,61 @@ async function main() { } } + // #1105 MINOR 9: when at a collapsed width, navigating to a route + // whose link overflows into the More menu must light up #navMoreBtn + // with .active. Verifies rebuildMoreMenu() correctly mirrors the + // active state from the inline (cloned) link to the More button on + // each hashchange (applyNavPriority is wired to hashchange and runs + // after the route handler's class toggles). + await page.setViewportSize({ width: 1080, height: HEIGHT }); + await page.goto(`${BASE}/#/observers`, { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('.top-nav .nav-links'); + await page.evaluate(() => document.fonts && document.fonts.ready ? document.fonts.ready : null); + // Wait for layout to settle. + 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); + })); + }); + }, null, { timeout: 5000 }); + // Give the hashchange-triggered applyNavPriority a frame to run. + await page.evaluate(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))); + + const activeMirror = await page.evaluate(() => { + const observersInline = document.querySelector('.nav-links .nav-link[href="#/observers"]'); + const inlineHidden = observersInline && observersInline.classList.contains('is-overflow'); + const moreBtn = document.getElementById('navMoreBtn'); + const moreBtnActive = moreBtn ? moreBtn.classList.contains('active') : false; + const moreMenuActiveHrefs = Array.from(document.querySelectorAll('#navMoreMenu .nav-link.active')) + .map(a => a.getAttribute('href')); + return { inlineHidden, moreBtnActive, moreMenuActiveHrefs }; + }); + + const mirrorReasons = []; + if (!activeMirror.inlineHidden) { + mirrorReasons.push('precondition: #/observers should be in the More menu at 1080px (not visible inline)'); + } + if (!activeMirror.moreBtnActive) { + mirrorReasons.push('navMoreBtn missing .active class while #/observers is the active route'); + } + if (!activeMirror.moreMenuActiveHrefs.includes('#/observers')) { + mirrorReasons.push(`More-menu clone of #/observers missing .active (active hrefs in menu: [${activeMirror.moreMenuActiveHrefs.join(', ')}])`); + } + if (mirrorReasons.length === 0) { + passes++; + console.log(` ✅ active-mirror @1080 #/observers: navMoreBtn.active=true, menu .active=#/observers`); + } else { + failures++; + console.log(` ❌ active-mirror @1080 #/observers: ${mirrorReasons.join(' | ')}`); + } + await browser.close(); - console.log(`\ntest-nav-priority-1102-e2e.js: ${failures === 0 ? 'OK' : 'FAIL'} — ${passes}/${CASES.length} passed`); + console.log(`\ntest-nav-priority-1102-e2e.js: ${failures === 0 ? 'OK' : 'FAIL'} — ${passes}/${CASES.length + 1} passed`); process.exit(failures === 0 ? 0 : 1); }