mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-21 02:21:09 +00:00
RED: 862d7c82 — E2E asserting (A) leaflet-map width == viewport on
mobile and (B) sticky panel header. CI URL: see Checks tab.
Fixes #1236.
## Sub-issue A — Map Controls panel scroll affordance
**Root cause:** `.map-controls` already had `max-height` + `overflow-y:
auto`, but the `<h3>` title was static — once the panel scrolled, the
title scrolled away with it and users lost the affordance that they were
inside a scroll container. No visual cue, no anchor.
**Fix:** make `.map-controls h3` `position: sticky` at the top of the
scroll container (pulled flush to the panel edges with negative margins
so it covers the corner radius cleanly), with the panel `--card-bg`
background and a `--border` bottom rule. Added `scrollbar-gutter:
stable` so the scroll indicator is consistently present.
## Sub-issue B — Map canvas offset left with right gutter
**Root cause:** `.map-side-pane` (Path Inspector) is `flex: 0 0 32px`
inside the flexbox `#map-wrap`. At every viewport width that 32px is
consumed before the leaflet canvas gets sized, leaving an unused band on
the right. Desktop has room for it; mobile (375px viewport) does not —
and Path Inspector hex-prefix entry is impractical on a phone anyway.
**Fix:** `display: none` on `.map-side-pane` at `≤640px`. Leaflet canvas
now fills 100% of the viewport.
## Verification
- E2E `test-issue-1236-map-mobile-e2e.js` covers both at 375x800 +
desktop guard at 1280x800. RED commit (`862d7c82`) failed 2/3 mobile
assertions; GREEN commit (`85efcba7`) passes 3/3.
- Map canvas width at 375x800: **343px → 375px**.
- Existing channels mobile E2E (#1224) still passes.
- Desktop (1280px): panel stays `position: absolute`, Path Inspector
pane still present.
All colors via CSS variables. No JS changes.
---------
Co-authored-by: OpenClaw Bot <bot@openclaw.local>
This commit is contained in:
co-authored by
OpenClaw Bot
parent
2e28aa3e04
commit
4ea1bf8ebc
@@ -259,6 +259,7 @@ jobs:
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1204-live-panel-structure-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1206-vcr-overlap-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1206-resize-observer-leak-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-drawer-1064-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
|
||||
|
||||
+24
-1
@@ -1068,8 +1068,24 @@ body.scroll-locked { overflow: hidden; }
|
||||
max-width: calc(100vw - 24px);
|
||||
font-size: 13px;
|
||||
max-height: calc(100% - 24px); overflow-y: auto;
|
||||
/* #1236: keep the scrollbar gutter reserved so the scroll affordance is
|
||||
visible even when content briefly fits — users get a consistent visual
|
||||
cue that the panel is scrollable. */
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
/* #1236: sticky panel title so the scroll affordance is always visible at the
|
||||
top of the scroll container when content overflows on small viewports. */
|
||||
.map-controls h3 {
|
||||
font-size: 15px;
|
||||
margin: -14px -16px 10px -16px;
|
||||
padding: 10px 16px;
|
||||
position: sticky;
|
||||
top: -14px;
|
||||
background: var(--card-bg);
|
||||
z-index: 2;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.map-controls h3 { font-size: 15px; margin-bottom: 10px; }
|
||||
.mc-section { margin-bottom: 10px; }
|
||||
.mc-label { font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .3px; color: var(--text-muted); margin-bottom: 4px; }
|
||||
.mc-section label { display: block; padding: 2px 0; cursor: pointer; }
|
||||
@@ -3203,6 +3219,13 @@ th.sort-active { color: var(--accent, #60a5fa); }
|
||||
.map-side-pane .pane-toggle { cursor: pointer; padding: 8px; font-size: 14px; text-align: center; }
|
||||
.map-side-pane .pane-content { display: none; }
|
||||
.map-side-pane.expanded .pane-content { display: block; }
|
||||
/* #1236: on mobile the path-inspector side pane was eating 32px of horizontal
|
||||
width (flex:0 0 32px) inside #map-wrap, leaving an unused gutter on the
|
||||
right of the leaflet canvas. Path Inspector is a desktop-only convenience;
|
||||
hide the pane on narrow viewports so the map fills 100% of the viewport. */
|
||||
@media (max-width: 640px) {
|
||||
.map-side-pane { display: none; }
|
||||
}
|
||||
|
||||
/* Tools landing page */
|
||||
.tools-landing { padding: 24px; max-width: 600px; }
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* E2E (#1236): Map page mobile layout.
|
||||
*
|
||||
* At 375x800 viewport the /#/map page must:
|
||||
* - Collapsed map controls: leaflet map canvas width must equal the viewport
|
||||
* width (within 1px tolerance). No silent gutter on the right.
|
||||
* - Expanded map controls: the panel must either fit within viewport OR
|
||||
* have a sticky element at the top of the scroll container AND
|
||||
* `overflow-y: auto` so the scroll affordance is real.
|
||||
*
|
||||
* Desktop guard (≥768px): map controls panel layout must remain absolute
|
||||
* (position: absolute), not stretched full width.
|
||||
*
|
||||
* Run: BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js
|
||||
*/
|
||||
'use strict';
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
|
||||
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 run() {
|
||||
const launchOpts = { args: ['--no-sandbox'] };
|
||||
if (process.env.CHROMIUM_PATH) launchOpts.executablePath = process.env.CHROMIUM_PATH;
|
||||
const browser = await chromium.launch(launchOpts);
|
||||
|
||||
// === Mobile: 375x800 ===
|
||||
const ctx = await browser.newContext({ viewport: { width: 375, height: 800 } });
|
||||
const page = await ctx.newPage();
|
||||
|
||||
await page.goto(BASE + '/#/map', { waitUntil: 'load', timeout: 60000 });
|
||||
await page.waitForSelector('#leaflet-map', { timeout: 10000 });
|
||||
await page.waitForSelector('#mapControls', { state: 'attached', timeout: 10000 });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await step('mobile collapsed: leaflet-map width fills viewport (no right gutter)', async () => {
|
||||
// Ensure controls panel is collapsed (default on mobile per map.js)
|
||||
const data = await page.evaluate(() => {
|
||||
const panel = document.getElementById('mapControls');
|
||||
const btn = document.getElementById('mapControlsToggle');
|
||||
if (panel && !panel.classList.contains('collapsed')) btn && btn.click();
|
||||
const lm = document.getElementById('leaflet-map');
|
||||
return {
|
||||
mapW: lm ? Math.round(lm.getBoundingClientRect().width) : null,
|
||||
mapLeft: lm ? Math.round(lm.getBoundingClientRect().left) : null,
|
||||
vw: window.innerWidth,
|
||||
};
|
||||
});
|
||||
assert(data.mapW !== null, 'leaflet-map not found');
|
||||
// Map must start at left edge and span to right edge (allow 1px rounding).
|
||||
assert(data.mapLeft <= 1,
|
||||
'leaflet-map must start at viewport left, got left=' + data.mapLeft);
|
||||
assert(data.mapW >= data.vw - 1,
|
||||
'leaflet-map width must equal viewport (' + data.vw + 'px), got ' + data.mapW + 'px');
|
||||
});
|
||||
|
||||
await step('mobile expanded: panel has sticky header AND overflow-y auto', async () => {
|
||||
const data = await page.evaluate(() => {
|
||||
const panel = document.getElementById('mapControls');
|
||||
const btn = document.getElementById('mapControlsToggle');
|
||||
if (panel.classList.contains('collapsed')) btn && btn.click();
|
||||
const cs = getComputedStyle(panel);
|
||||
const h3 = panel.querySelector('h3');
|
||||
const hcs = h3 ? getComputedStyle(h3) : null;
|
||||
return {
|
||||
overflowY: cs.overflowY,
|
||||
h3Position: hcs ? hcs.position : null,
|
||||
scrollGutter: cs.scrollbarGutter || '',
|
||||
scrollH: panel.scrollHeight,
|
||||
clientH: panel.clientHeight,
|
||||
isScrollable: panel.scrollHeight > panel.clientHeight + 1,
|
||||
};
|
||||
});
|
||||
// Require explicit sticky header + scrollable overflow regardless of
|
||||
// whether content currently overflows (so future content additions are
|
||||
// covered too).
|
||||
assert(data.h3Position === 'sticky',
|
||||
'panel h3 must be position:sticky (scroll affordance), got ' + data.h3Position);
|
||||
assert(data.overflowY === 'auto' || data.overflowY === 'scroll',
|
||||
'panel overflow-y must be auto/scroll, got ' + data.overflowY);
|
||||
});
|
||||
|
||||
await ctx.close();
|
||||
|
||||
// === Desktop: 1280x800 — guard against regression ===
|
||||
const ctx2 = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||
const p2 = await ctx2.newPage();
|
||||
await p2.goto(BASE + '/#/map', { waitUntil: 'load', timeout: 60000 });
|
||||
await p2.waitForSelector('#mapControls', { state: 'attached', timeout: 10000 });
|
||||
await p2.waitForTimeout(300);
|
||||
|
||||
await step('desktop (1280px): map controls panel position is absolute', async () => {
|
||||
const data = await p2.evaluate(() => {
|
||||
const panel = document.getElementById('mapControls');
|
||||
const cs = getComputedStyle(panel);
|
||||
const rect = panel.getBoundingClientRect();
|
||||
return {
|
||||
position: cs.position,
|
||||
width: Math.round(rect.width),
|
||||
vw: window.innerWidth,
|
||||
};
|
||||
});
|
||||
assert(data.position === 'absolute',
|
||||
'desktop panel must be position:absolute, got ' + data.position);
|
||||
// Should be modest width (not full viewport)
|
||||
assert(data.width < data.vw * 0.5,
|
||||
'desktop panel must be <50% viewport width, got ' + data.width + '/' + data.vw);
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
console.log('\n' + passed + '/' + (passed + failed) + ' tests passed' +
|
||||
(failed ? ', ' + failed + ' failed' : ''));
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
run().catch(err => { console.error('Fatal:', err); process.exit(1); });
|
||||
Reference in New Issue
Block a user