mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-04 08:51:19 +00:00
9d1f5d2395
Red commit: a200704d5e27e47c0b29a4745bf1a1772a8876fe (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>
196 lines
8.7 KiB
JavaScript
196 lines
8.7 KiB
JavaScript
#!/usr/bin/env node
|
||
/* Issue #1109 (post-#1174 conversion) — Long-tail routes reachable on phones.
|
||
*
|
||
* Original contract: tapping the hamburger surfaces the long-tail routes
|
||
* (Tools/Lab/Perf/Analytics/Observers/Nodes) that don't fit in the primary
|
||
* nav. Origin used a CSS-clip-prone dropdown.
|
||
*
|
||
* #1174 replaced the hamburger-at-narrow-widths path with a 6th "More" tab
|
||
* in the bottom-nav that opens a bottom-anchored sheet listing the same
|
||
* long-tail routes. The hamburger is HIDDEN at ≤768px (its job at narrow
|
||
* widths is now done by the More tab).
|
||
*
|
||
* This test asserts the converted contract:
|
||
* 1. At iPhone-13 viewport (390×844, mobile UA), #hamburger is NOT visible.
|
||
* 2. The More tab IS visible and toggles the sheet.
|
||
* 3. Tap More → sheet visible (pixel-level: elementFromPoint inside sheet,
|
||
* bounding rect non-zero, top above the bottom-nav).
|
||
* 4. Tap a long-tail route inside the sheet → URL hash updates AND
|
||
* sheet closes.
|
||
* 5. Tap More again → sheet re-opens (toggle, not push).
|
||
*/
|
||
'use strict';
|
||
|
||
const { chromium } = require('playwright');
|
||
|
||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||
const VIEWPORT = { width: 390, height: 844 }; // iPhone 13 dimensions
|
||
|
||
function fail(msg) {
|
||
console.error(`test-issue-1109-hamburger-dropdown-visible-e2e.js: FAIL — ${msg}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
async function main() {
|
||
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 (process.env.CHROMIUM_REQUIRE === '1') {
|
||
console.error(`test-issue-1109-hamburger-dropdown-visible-e2e.js: FAIL — Chromium required but unavailable: ${err.message}`);
|
||
process.exit(1);
|
||
}
|
||
console.log(`test-issue-1109-hamburger-dropdown-visible-e2e.js: SKIP (Chromium unavailable: ${err.message.split('\n')[0]})`);
|
||
process.exit(0);
|
||
}
|
||
|
||
try {
|
||
const ctx = await browser.newContext({
|
||
viewport: VIEWPORT,
|
||
hasTouch: true,
|
||
isMobile: true,
|
||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
||
});
|
||
const page = await ctx.newPage();
|
||
page.setDefaultTimeout(15000);
|
||
|
||
await page.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded' });
|
||
await page.waitForSelector('[data-bottom-nav-tab="more"]');
|
||
|
||
// 1. #hamburger hidden at ≤768px.
|
||
const hamburgerState = await page.evaluate(() => {
|
||
const h = document.getElementById('hamburger');
|
||
if (!h) return { present: false };
|
||
const cs = getComputedStyle(h);
|
||
const r = h.getBoundingClientRect();
|
||
return {
|
||
present: true,
|
||
display: cs.display,
|
||
visibility: cs.visibility,
|
||
width: r.width, height: r.height,
|
||
hidden: cs.display === 'none' || cs.visibility === 'hidden' || (r.width === 0 && r.height === 0),
|
||
};
|
||
});
|
||
if (hamburgerState.present && !hamburgerState.hidden) {
|
||
fail(`#hamburger should be hidden at ≤768px (replaced by More tab); got display=${hamburgerState.display}, visibility=${hamburgerState.visibility}, size=${hamburgerState.width}x${hamburgerState.height}`);
|
||
}
|
||
|
||
// 2. More tab visible.
|
||
const moreState = await page.evaluate(() => {
|
||
const el = document.querySelector('[data-bottom-nav-tab="more"]');
|
||
if (!el) return { present: false };
|
||
const cs = getComputedStyle(el);
|
||
const r = el.getBoundingClientRect();
|
||
return {
|
||
present: true,
|
||
visible: cs.display !== 'none' && r.width > 0 && r.height > 0,
|
||
ariaExpanded: el.getAttribute('aria-expanded'),
|
||
ariaControls: el.getAttribute('aria-controls'),
|
||
};
|
||
});
|
||
if (!moreState.present) fail('[data-bottom-nav-tab="more"] missing');
|
||
if (!moreState.visible) fail('More tab not visible at 390×844');
|
||
if (moreState.ariaExpanded !== 'false') fail(`More tab aria-expanded should be 'false' before tap, got ${moreState.ariaExpanded}`);
|
||
|
||
// 3. Tap More → sheet visible (pixel-level).
|
||
await page.tap('[data-bottom-nav-tab="more"]');
|
||
await page.waitForSelector('[data-bottom-nav-sheet]', { timeout: 3000 });
|
||
const probe = await page.evaluate(() => {
|
||
const sheet = document.querySelector('[data-bottom-nav-sheet]');
|
||
const cs = sheet ? getComputedStyle(sheet) : null;
|
||
const r = sheet ? sheet.getBoundingClientRect() : null;
|
||
const moreTab = document.querySelector('[data-bottom-nav-tab="more"]');
|
||
// Probe a point inside the sheet's rect.
|
||
let hitInside = false;
|
||
if (sheet && r && r.width > 0 && r.height > 0) {
|
||
const x = Math.floor(r.left + r.width / 2);
|
||
const y = Math.floor(r.top + r.height / 2);
|
||
const hit = document.elementFromPoint(x, y);
|
||
hitInside = !!(hit && sheet.contains(hit));
|
||
}
|
||
const items = sheet ? Array.from(sheet.querySelectorAll('[data-bottom-nav-more-route]')).map(e => e.getAttribute('data-bottom-nav-more-route')) : [];
|
||
return {
|
||
rect: r,
|
||
display: cs ? cs.display : null,
|
||
visibility: cs ? cs.visibility : null,
|
||
role: sheet ? sheet.getAttribute('role') : null,
|
||
hitInside,
|
||
items,
|
||
moreExpanded: moreTab ? moreTab.getAttribute('aria-expanded') : null,
|
||
};
|
||
});
|
||
if (!probe.rect || probe.rect.width === 0 || probe.rect.height === 0) {
|
||
fail(`sheet has zero area: ${JSON.stringify(probe.rect)}`);
|
||
}
|
||
if (probe.display === 'none' || probe.visibility === 'hidden') {
|
||
fail(`sheet hidden: display=${probe.display}, visibility=${probe.visibility}`);
|
||
}
|
||
if (!probe.hitInside) {
|
||
fail(`pixel-level visibility check failed: elementFromPoint inside sheet rect did not hit a sheet descendant. rect=${JSON.stringify(probe.rect)}`);
|
||
}
|
||
if (probe.role !== 'menu') fail(`sheet role should be 'menu', got ${probe.role}`);
|
||
if (probe.items.length < 6) fail(`sheet should list ≥6 long-tail routes, got ${probe.items.length}: ${probe.items.join(',')}`);
|
||
if (probe.moreExpanded !== 'true') fail(`More tab aria-expanded should be 'true' while sheet open, got ${probe.moreExpanded}`);
|
||
|
||
// 4. Tap a long-tail route → hash changes, sheet closes.
|
||
const firstRoute = probe.items[0];
|
||
await page.tap(`[data-bottom-nav-more-route="${firstRoute}"]`);
|
||
await page.waitForFunction((r) => location.hash === `#/${r}`, firstRoute, { timeout: 3000 });
|
||
await page.waitForFunction(() => {
|
||
const s = document.querySelector('[data-bottom-nav-sheet]');
|
||
if (!s) return true;
|
||
const cs = getComputedStyle(s);
|
||
return cs.display === 'none' || cs.visibility === 'hidden';
|
||
}, null, { timeout: 3000 }).catch(() => {});
|
||
const afterTap = await page.evaluate(() => {
|
||
const s = document.querySelector('[data-bottom-nav-sheet]');
|
||
if (!s) return { sheetClosed: true, hash: location.hash };
|
||
const cs = getComputedStyle(s);
|
||
const r = s.getBoundingClientRect();
|
||
return {
|
||
sheetClosed: cs.display === 'none' || cs.visibility === 'hidden' || (r.width === 0 && r.height === 0),
|
||
hash: location.hash,
|
||
};
|
||
});
|
||
if (afterTap.hash !== `#/${firstRoute}`) fail(`hash did not change to #/${firstRoute}, got ${afterTap.hash}`);
|
||
if (!afterTap.sheetClosed) fail('sheet did not close after route tap');
|
||
|
||
// 5. Tap More again → sheet reopens (toggle).
|
||
await page.tap('[data-bottom-nav-tab="more"]');
|
||
await page.waitForFunction(() => {
|
||
const s = document.querySelector('[data-bottom-nav-sheet]');
|
||
if (!s) return false;
|
||
const cs = getComputedStyle(s);
|
||
return cs.display !== 'none' && cs.visibility !== 'hidden';
|
||
}, null, { timeout: 3000 });
|
||
// Now tap More AGAIN to confirm toggle (close).
|
||
await page.tap('[data-bottom-nav-tab="more"]');
|
||
await page.waitForFunction(() => {
|
||
const s = document.querySelector('[data-bottom-nav-sheet]');
|
||
if (!s) return true;
|
||
const cs = getComputedStyle(s);
|
||
return cs.display === 'none' || cs.visibility === 'hidden';
|
||
}, null, { timeout: 3000 }).catch(() => {});
|
||
const afterToggle = await page.evaluate(() => {
|
||
const s = document.querySelector('[data-bottom-nav-sheet]');
|
||
if (!s) return { closed: true };
|
||
const cs = getComputedStyle(s);
|
||
return { closed: cs.display === 'none' || cs.visibility === 'hidden' };
|
||
});
|
||
if (!afterToggle.closed) fail('sheet did not close on second More tap (toggle behavior expected)');
|
||
|
||
console.log('test-issue-1109-hamburger-dropdown-visible-e2e.js: PASS');
|
||
} finally {
|
||
await browser.close();
|
||
}
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error(`test-issue-1109-hamburger-dropdown-visible-e2e.js: FAIL — ${err.stack || err.message}`);
|
||
process.exit(1);
|
||
});
|