mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-29 10:01:38 +00:00
Red commit: 03546923b4 (CI run: pending —
see Checks)
E2E assertion added: test-issue-1630-reach-mobile-e2e.js:97
## Summary
Adds narrow-viewport CSS to `public/node-reach.css` so the
`/nodes/{pubkey}/reach` page no longer overflows phone-class viewports.
Fixes #1630
## Approach (red → green)
1. **RED** (`03546923`): added `test-issue-1630-reach-mobile-e2e.js`
asserting at 393×800 and 360×740 that:
- `#nqMap` computed height ≤ 320px
- `.nq-table` scrollWidth ≤ clientWidth (no inner h-scroll)
- ≤ 4 visible TH columns (low-signal collapsed)
Desktop guard at 1440×900: map height stays ~420px and all 6 columns
remain visible — proves no desktop regression.
Wired into `.github/workflows/deploy.yml` Playwright job so CI is the
source of truth.
2. **GREEN**: added `@media (max-width: 480px)` block in
`public/node-reach.css` that shrinks `.nq-map` to 280px, hides the
`distance (km)` column, and stacks `we hear` / `they hear us` into a
single compact column.
## Out of scope (intentionally not touched)
- Backend `cmd/server/node_reach.go` (tracked in #1631 / #1629).
- Reach page re-theming.
- Per-column user toggles.
## Local verification
Screenshots at the three target viewports (393×800, 360×740, 1440×900)
attached below.
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
This commit is contained in:
@@ -427,6 +427,7 @@ jobs:
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channels-ws-batch-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channels-ws-race-1498-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1487-byop-modal-layout-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1630-reach-mobile-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
|
||||
# #1616: slide-over focus-restore flake-gate. Runs the slide-over
|
||||
# E2E 20 consecutive times against the SAME backend instance so
|
||||
|
||||
@@ -53,3 +53,22 @@
|
||||
.nq-map { width:var(--nq-print-width) !important; height:300px; }
|
||||
.nq-table { font-size:10px; }
|
||||
}
|
||||
|
||||
/* #1630 — narrow-viewport (phone) layout.
|
||||
At ≤480px the 420px map dominates and the 6-col link table h-scrolls.
|
||||
Shrink the map to 280px so stats land above the fold, and collapse the
|
||||
two lowest-signal columns (`they hear us` and `distance (km)`) — the
|
||||
`we hear` value plus the colour-coded `bottleneck` tier preserve the
|
||||
asymmetry signal. Desktop layout (≥481px) is untouched. */
|
||||
@media (max-width: 480px) {
|
||||
.nq-map { height:280px; }
|
||||
/* col 4 = `they hear us`, col 6 = `distance (km)` (see node-reach.js
|
||||
thead/tbody emit order). Hide on phones to fit a 360–393px viewport
|
||||
without horizontal scroll. */
|
||||
.nq-table th:nth-child(4),
|
||||
.nq-table td:nth-child(4),
|
||||
.nq-table th:nth-child(6),
|
||||
.nq-table td:nth-child(6) { display:none; }
|
||||
/* Tighter cell padding so the remaining four columns breathe. */
|
||||
.nq-table th, .nq-table td { padding:4px 6px; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* E2E (#1630): Reach page mobile layout.
|
||||
*
|
||||
* On narrow viewports (393×800 and 360×740 — common iPhone/Android sizes)
|
||||
* the /#/nodes/{pubkey}/reach page must:
|
||||
* - Shrink the map height (#nqMap) to ≤ 320px so stats/table are visible
|
||||
* above the fold instead of being pushed below a 420px-tall map.
|
||||
* - Lay the 6-column link table out so its scrollWidth ≤ clientWidth (no
|
||||
* horizontal scroll inside the table).
|
||||
*
|
||||
* Desktop guard (≥768px / 1440×900): map height must remain the original
|
||||
* 420px and the table must still render 6 visible columns — no regression.
|
||||
*
|
||||
* Run: BASE_URL=http://localhost:13581 node test-issue-1630-reach-mobile-e2e.js
|
||||
*/
|
||||
'use strict';
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
const SUBPIXEL_TOL = 1; // browsers round subpixels; tolerate 1px noise.
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function step(name, fn) {
|
||||
try { await fn(); passed++; console.log(' \u2713 ' + name); }
|
||||
catch (e) { failed++; console.error(' \u2717 ' + name + ': ' + e.message); }
|
||||
}
|
||||
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
||||
|
||||
async function pickRepeaterWithReach(page) {
|
||||
// Find a repeater whose 30-day reach has >= 2 links (table renders rows).
|
||||
const list = await page.request.get(BASE + '/api/nodes?role=repeater&limit=80');
|
||||
if (!list.ok()) throw new Error('GET /api/nodes failed: ' + list.status());
|
||||
const nodes = (await list.json()).nodes || [];
|
||||
for (const n of nodes) {
|
||||
const r = await page.request.get(BASE + '/api/nodes/' + n.public_key + '/reach?days=30');
|
||||
if (!r.ok()) continue;
|
||||
const j = await r.json();
|
||||
if (Array.isArray(j.links) && j.links.length >= 2) return n.public_key;
|
||||
}
|
||||
throw new Error('no repeater with reach links found in fixture');
|
||||
}
|
||||
|
||||
async function loadReachPage(page, pubkey) {
|
||||
await page.goto(BASE + '/#/nodes/' + pubkey + '/reach');
|
||||
await page.waitForSelector('.nq-head', { timeout: 20000 });
|
||||
// The fixture only has reach data within a 30-day window, but DEFAULT_DAYS
|
||||
// is 7. Click the 30d button so the table actually renders rows.
|
||||
const btn30 = await page.$('button[data-days="30"]');
|
||||
if (btn30) {
|
||||
await btn30.click();
|
||||
}
|
||||
// Wait for the row body to appear and have at least one row.
|
||||
await page.waitForFunction(() => {
|
||||
const tb = document.getElementById('nqRows');
|
||||
return tb && tb.children.length > 0;
|
||||
}, { timeout: 15000 });
|
||||
// Let leaflet paint (map height is set by CSS, not waiting on tiles).
|
||||
await page.waitForSelector('#nqMap', { timeout: 5000 });
|
||||
}
|
||||
|
||||
async function measure(page) {
|
||||
return page.evaluate(() => {
|
||||
const m = document.getElementById('nqMap');
|
||||
const t = document.querySelector('.nq-table');
|
||||
return {
|
||||
mapH: m ? Math.round(m.getBoundingClientRect().height) : -1,
|
||||
tableSw: t ? t.scrollWidth : -1,
|
||||
tableCw: t ? t.clientWidth : -1,
|
||||
htmlSw: document.documentElement.scrollWidth,
|
||||
htmlCw: document.documentElement.clientWidth,
|
||||
visibleThCount: t ? [...t.querySelectorAll('thead th')].filter(th => {
|
||||
const cs = getComputedStyle(th);
|
||||
return cs.display !== 'none' && cs.visibility !== 'hidden';
|
||||
}).length : 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const launchOpts = { args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'] };
|
||||
if (process.env.CHROMIUM_PATH) launchOpts.executablePath = process.env.CHROMIUM_PATH;
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch(launchOpts);
|
||||
} catch (e) {
|
||||
if (process.env.CHROMIUM_REQUIRE === '1') {
|
||||
console.error('test-issue-1630: Chromium required but unavailable: ' + e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('test-issue-1630 SKIP (no chromium): ' + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const page = await browser.newPage();
|
||||
const pubkey = await pickRepeaterWithReach(page);
|
||||
console.log(' using pubkey ' + pubkey.slice(0, 12) + '...');
|
||||
|
||||
// --- 393×800 phone viewport (iPhone 14 / Pixel-class) ---
|
||||
await page.setViewportSize({ width: 393, height: 800 });
|
||||
await loadReachPage(page, pubkey);
|
||||
const m393 = await measure(page);
|
||||
await step('393×800: map height ≤ 320px (currently ' + m393.mapH + ')', () => {
|
||||
assert(m393.mapH > 0 && m393.mapH <= 320,
|
||||
'map height ' + m393.mapH + 'px exceeds 320px cap on narrow viewport');
|
||||
});
|
||||
await step('393×800: link table fits without horizontal scroll', () => {
|
||||
assert(m393.tableSw - m393.tableCw <= SUBPIXEL_TOL,
|
||||
'table scrollWidth ' + m393.tableSw + ' exceeds clientWidth ' + m393.tableCw);
|
||||
});
|
||||
await step('393×800: low-signal columns collapsed (≤4 visible TH)', () => {
|
||||
assert(m393.visibleThCount > 0 && m393.visibleThCount <= 4,
|
||||
'narrow viewport visible TH count ' + m393.visibleThCount +
|
||||
' — distance and/or we_hear/they_hear must be hidden/stacked');
|
||||
});
|
||||
|
||||
// --- 360×740 (small Android) ---
|
||||
await page.setViewportSize({ width: 360, height: 740 });
|
||||
// Re-trigger paint by clicking 30d again (no-op if already 30) — ensures
|
||||
// measurements reflect the new viewport.
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('resize')));
|
||||
await page.waitForTimeout(200);
|
||||
const m360 = await measure(page);
|
||||
await step('360×740: map height ≤ 320px (currently ' + m360.mapH + ')', () => {
|
||||
assert(m360.mapH > 0 && m360.mapH <= 320,
|
||||
'map height ' + m360.mapH + 'px exceeds 320px cap on narrow viewport');
|
||||
});
|
||||
await step('360×740: link table fits without horizontal scroll', () => {
|
||||
assert(m360.tableSw - m360.tableCw <= SUBPIXEL_TOL,
|
||||
'table scrollWidth ' + m360.tableSw + ' exceeds clientWidth ' + m360.tableCw);
|
||||
});
|
||||
|
||||
// --- 1440×900 desktop guard ---
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('resize')));
|
||||
await page.waitForTimeout(200);
|
||||
const mDesk = await measure(page);
|
||||
await step('1440×900: desktop map height unchanged (~420px)', () => {
|
||||
assert(mDesk.mapH >= 400 && mDesk.mapH <= 440,
|
||||
'desktop map height ' + mDesk.mapH + 'px regressed from 420px baseline');
|
||||
});
|
||||
await step('1440×900: desktop shows all 6 columns', () => {
|
||||
assert(mDesk.visibleThCount === 6,
|
||||
'desktop visible TH count ' + mDesk.visibleThCount + ' != 6');
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
console.log(passed + ' passed, ' + failed + ' failed');
|
||||
if (failed > 0) process.exit(1);
|
||||
}
|
||||
|
||||
run().catch(e => { console.error(e); process.exit(1); });
|
||||
Reference in New Issue
Block a user