mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 10:53:51 +00:00
Red commit: 58b307228e (CI run pending;
URL added after first workflow run posts).
Fixes #1244
## Sub-issue A — VCR controls still 2 rows on mobile
`public/live.css` mobile `@media (max-width:640px)` block had
`flex-wrap: wrap` plus `.vcr-timeline-container { width:100%; flex:none
}`, which guaranteed a 2-row layout (controls + LCD on row 1, scope
buttons + scrubber on row 2) — the exact bug #1234 was supposed to
eliminate.
Fix: switched `.vcr-bar` to `flex-wrap: nowrap`, gave
`.vcr-timeline-container` `flex: 1 1 0` so it absorbs leftover width,
and shrunk `.vcr-btn` / `.vcr-scope-btn` to a 32px touch target (still
WCAG 2.5.5 AA). Reorder on mobile: controls → scopes → timeline → LCD,
single row. `.vcr-mode` stays hidden on mobile as before (and `.vcr-lcd`
no longer needs `margin-left:auto` because the timeline pushes it right
via flex-grow).
## Sub-issue B — Orphan "Got it" hint pills hidden below the fold
`public/gesture-hints.js` row-swipe relevance included `/live`, and the
pills are bottom-anchored — so they rendered under the
absolute-positioned VCR bar + safe-area inset and were only findable by
scrolling.
Picked **option (a)** from the issue (simplest, matches user's report):
all four hints now early-return on `/#/live*`. Swipe-nav discoverability
doesn't apply on Live — map drag, VCR controls, and feed own the touch
surface.
## TDD
- RED `test-issue-1244-live-vcr-row-hints-e2e.js`: asserts at 375x800
(A) `.vcr-bar` children share a row (≤8px top spread OR
`flex-wrap:nowrap`), (B) zero `.gesture-hint` elements on `/live`.
Desktop sanity asserts LCD/controls still share a row.
- GREEN: the two source fixes.
E2E assertion added: `test-issue-1244-live-vcr-row-hints-e2e.js:67`
(single-row), `:101` (no hints). Wired into
`.github/workflows/deploy.yml` `e2e-test` job.
Browser verified: pending CI on Playwright fixture run (local Playwright
unavailable on this ARM host).
Desktop layout untouched — every mobile rule lives under `@media
(max-width:640px)`; existing #1221 + #1234 desktop assertions still
apply.
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
This commit is contained in:
1 parent
74685ac82f
commit
e395c471ed
4 files changed
+194
-16
No files matched your search
@@ -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-1234-live-chrome-pass2-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
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1244-live-vcr-row-hints-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
|
||||
|
||||
+15
-1
@@ -21,13 +21,24 @@
|
||||
window.__gestureHints1065Init = 1;
|
||||
|
||||
var NS = 'meshcore-gesture-hints-';
|
||||
// #1244: gesture hints are bottom-anchored pills. On /live they get
|
||||
// buried below the absolute-positioned VCR bar (+ safe-area inset),
|
||||
// appearing as orphan "Got it" litter visible only after scrolling.
|
||||
// Option (a) from #1244 — disable hints on /live entirely. Swipe-nav
|
||||
// discoverability doesn't apply on Live anyway (map drag, VCR
|
||||
// controls, and feed all own touch).
|
||||
function onLiveRoute() {
|
||||
var h = location.hash || '';
|
||||
return /^#\/live(\/|$|\?)/.test(h);
|
||||
}
|
||||
var HINTS = {
|
||||
'row-swipe': {
|
||||
key: NS + 'row-swipe',
|
||||
text: 'Tip: swipe a row left for quick actions.',
|
||||
relevant: function () {
|
||||
if (onLiveRoute()) return false; // #1244
|
||||
var h = location.hash || '';
|
||||
return /^#\/(packets|nodes|live)/.test(h);
|
||||
return /^#\/(packets|nodes)/.test(h);
|
||||
},
|
||||
position: 'bottom',
|
||||
},
|
||||
@@ -35,6 +46,7 @@
|
||||
key: NS + 'tab-swipe',
|
||||
text: 'Tip: swipe left or right to switch tabs.',
|
||||
relevant: function () {
|
||||
if (onLiveRoute()) return false; // #1244
|
||||
return !!document.querySelector('[data-bottom-nav]');
|
||||
},
|
||||
position: 'bottom',
|
||||
@@ -43,6 +55,7 @@
|
||||
key: NS + 'edge-drawer',
|
||||
text: 'Tip: swipe in from the left edge to open navigation.',
|
||||
relevant: function () {
|
||||
if (onLiveRoute()) return false; // #1244
|
||||
return window.innerWidth > 768 && !!document.querySelector('.nav-drawer, [data-nav-drawer]');
|
||||
},
|
||||
position: 'top-left',
|
||||
@@ -51,6 +64,7 @@
|
||||
key: NS + 'pull-refresh',
|
||||
text: 'Tip: pull down to refresh the connection.',
|
||||
relevant: function () {
|
||||
if (onLiveRoute()) return false; // #1244
|
||||
return !!document.querySelector('.pull-to-reconnect');
|
||||
},
|
||||
position: 'top',
|
||||
|
||||
+23
-15
@@ -1024,36 +1024,44 @@
|
||||
|
||||
/* Mobile VCR */
|
||||
@media (max-width: 640px) {
|
||||
/* Mobile VCR: two-row stacked layout */
|
||||
/* #1244: SINGLE-ROW layout. PR #1234 was meant to put all VCR controls
|
||||
* on one row but `flex-wrap: wrap` here + `width: 100%` on the timeline
|
||||
* forced a 2-row layout (controls/LCD on row 1, scope+scrubber on row 2).
|
||||
* Fix: `flex-wrap: nowrap`, the timeline gets `flex: 1 1 0` so it
|
||||
* absorbs leftover width, and scope buttons shrink (1/6/More) to fit. */
|
||||
.vcr-bar {
|
||||
padding: 4px 8px;
|
||||
padding-bottom: calc(4px + env(safe-area-inset-bottom, 20px));
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
overflow: visible;
|
||||
}
|
||||
/* #1221: LCD clock shares row with playback controls (not floating
|
||||
* bottom-right). Scope buttons wrap to row 2; timeline to row 3.
|
||||
* LCD is scaled ~70% of desktop so it fits next to the touch-target
|
||||
* buttons without clipping at 375px viewport. */
|
||||
.vcr-controls { order: 1; flex-shrink: 0; gap: 4px; }
|
||||
* bottom-right). LCD is scaled ~70% of desktop so it fits next to the
|
||||
* touch-target buttons without clipping at 375px viewport. */
|
||||
.vcr-controls { order: 1; flex-shrink: 0; gap: 2px; }
|
||||
.vcr-lcd {
|
||||
order: 2;
|
||||
margin-left: auto;
|
||||
min-width: 70px;
|
||||
order: 4;
|
||||
/* #1244: no auto margin in a no-wrap row — let the timeline absorb
|
||||
* free space via flex:1 instead. */
|
||||
margin-left: 0;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.vcr-lcd-canvas { width: 78px; height: 18px; }
|
||||
.vcr-lcd-mode { font-size: 0.55rem; letter-spacing: 1px; }
|
||||
.vcr-lcd-pkts { font-size: 0.5rem; letter-spacing: 1px; }
|
||||
.vcr-scope-btns { order: 3; flex-shrink: 0; gap: 1px; }
|
||||
.vcr-scope-btns { order: 2; flex-shrink: 0; gap: 1px; }
|
||||
.vcr-mode { display: none; }
|
||||
/* Timeline takes full width on its own row */
|
||||
.vcr-timeline-container { order: 4; width: 100%; flex: none; height: 20px; }
|
||||
/* #207 — 44px touch targets for VCR buttons */
|
||||
.vcr-btn { padding: 4px 8px; font-size: 0.75rem; min-height: 44px; min-width: 44px; }
|
||||
.vcr-scope-btn { font-size: 0.6rem; padding: 2px 6px; min-height: 44px; min-width: 44px; }
|
||||
/* #1244: timeline absorbs remaining width on the single row. */
|
||||
.vcr-timeline-container { order: 3; flex: 1 1 0; min-width: 40px; height: 28px; width: auto; }
|
||||
/* #207 — touch targets shrunk to fit single row at 375px. WCAG 2.5.5
|
||||
* Level AAA recommends 44px but Level AA accepts 24px; here we keep
|
||||
* 32px which still meets AA on mobile while letting all controls
|
||||
* occupy one row at the 375px breakpoint called out in #1244. */
|
||||
.vcr-btn { padding: 4px 6px; font-size: 0.7rem; min-height: 32px; min-width: 32px; }
|
||||
.vcr-scope-btn { font-size: 0.6rem; padding: 2px 5px; min-height: 32px; min-width: 0; }
|
||||
.vcr-prompt { order: 5; width: 100%; font-size: 0.7rem; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* E2E for #1244 — Live mobile (375x800):
|
||||
* A) VCR controls must lay out as a single row of children inside
|
||||
* `.vcr-bar` (no wrap) — all direct children share a common top
|
||||
* coordinate (within tolerance).
|
||||
* B) First-visit gesture hints (`.gesture-hint`, "Got it" pills from
|
||||
* PR #1186) must NOT be present on the /live route — they get
|
||||
* buried below the VCR bar + safe-area + (potential) bottom nav
|
||||
* and read as orphan litter. Fixed by disabling gesture hints
|
||||
* on /live entirely (option (a) in the issue).
|
||||
*
|
||||
* Desktop (1280x800) sanity: VCR still renders, no regression to the
|
||||
* existing single-row desktop layout.
|
||||
*
|
||||
* Run: BASE_URL=http://localhost:13581 node test-issue-1244-live-vcr-row-hints-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 gotoLive(page) {
|
||||
// Reset gesture-hint localStorage so the first-visit pills WOULD fire
|
||||
// (they're suppressed once "seen"). Otherwise the test is a no-op for
|
||||
// sub-issue B on a previously-visited fixture.
|
||||
await page.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate(() => {
|
||||
try {
|
||||
Object.keys(localStorage)
|
||||
.filter(k => k.indexOf('meshcore-gesture-hints-') === 0)
|
||||
.forEach(k => localStorage.removeItem(k));
|
||||
} catch (_e) {}
|
||||
});
|
||||
await page.goto(BASE + '/#/live', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('#vcrBar', { timeout: 10000 });
|
||||
// Wait past gesture-hints SHOW_DELAY_MS (800ms) so any hint that
|
||||
// _would_ render has had its chance to appear.
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||||
});
|
||||
|
||||
console.log(`\n=== #1244 Live mobile VCR single row + no orphan hints E2E against ${BASE} ===`);
|
||||
|
||||
// ── Mobile 375x800 ──────────────────────────────────────────────────────
|
||||
{
|
||||
const ctx = await browser.newContext({ viewport: { width: 375, height: 800 } });
|
||||
const page = await ctx.newPage();
|
||||
page.setDefaultTimeout(10000);
|
||||
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
||||
await step('[375x800] navigate to /live', async () => { await gotoLive(page); });
|
||||
|
||||
// (A) VCR-bar direct children share one row (no wrap).
|
||||
await step('[375x800] .vcr-bar direct children share top coordinate (single row, no wrap)', async () => {
|
||||
const r = await page.evaluate(() => {
|
||||
const bar = document.getElementById('vcrBar');
|
||||
if (!bar) return { found: false };
|
||||
const cs = getComputedStyle(bar);
|
||||
const kids = Array.from(bar.children).filter(el => {
|
||||
const k = getComputedStyle(el);
|
||||
if (k.display === 'none' || k.visibility === 'hidden') return false;
|
||||
if (el.id === 'panelPositionAnnounce') return false; // sr-only sibling
|
||||
if (el.classList && el.classList.contains('sr-only')) return false;
|
||||
if (el.id === 'vcrPrompt' && el.classList.contains('hidden')) return false;
|
||||
const rr = el.getBoundingClientRect();
|
||||
return rr.width > 0 && rr.height > 0;
|
||||
});
|
||||
const tops = kids.map(el => Math.round(el.getBoundingClientRect().top));
|
||||
const minTop = Math.min.apply(null, tops);
|
||||
const maxTop = Math.max.apply(null, tops);
|
||||
return {
|
||||
found: true,
|
||||
flexWrap: cs.flexWrap,
|
||||
flexDirection: cs.flexDirection,
|
||||
tops, minTop, maxTop,
|
||||
spread: maxTop - minTop,
|
||||
kidTags: kids.map(el => (el.tagName + (el.id ? '#' + el.id : '') + '.' +
|
||||
(typeof el.className === 'string' ? el.className : '')).trim()),
|
||||
};
|
||||
});
|
||||
assert(r.found, '#vcrBar element missing');
|
||||
// Either nowrap is set (canonical fix) OR all children genuinely share
|
||||
// the same top (≤8px spread for sub-pixel + line-height jitter).
|
||||
const sharedRow = r.spread <= 8;
|
||||
assert(r.flexWrap === 'nowrap' || sharedRow,
|
||||
'#vcr-bar must lay children out on a single row at 375x800 ' +
|
||||
'(flexWrap=' + r.flexWrap + ', topSpread=' + r.spread + 'px, ' +
|
||||
'tops=' + JSON.stringify(r.tops) + ', children=' + JSON.stringify(r.kidTags) + ')');
|
||||
});
|
||||
|
||||
// (B) No orphan gesture-hint pills on /live.
|
||||
await step('[375x800] no .gesture-hint pills on /live route (option (a) — disabled on Live)', async () => {
|
||||
const r = await page.evaluate(() => {
|
||||
const hints = Array.from(document.querySelectorAll('.gesture-hint, [data-gesture-hint]'));
|
||||
const vh = window.innerHeight;
|
||||
return {
|
||||
count: hints.length,
|
||||
buried: hints.filter(el => {
|
||||
const rr = el.getBoundingClientRect();
|
||||
return rr.height > 0 && rr.top > vh; // below the fold
|
||||
}).length,
|
||||
texts: hints.map(el => (el.textContent || '').trim().slice(0, 60)),
|
||||
};
|
||||
});
|
||||
assert(r.count === 0,
|
||||
'no .gesture-hint elements may exist on /live (found ' + r.count +
|
||||
', buried-below-fold=' + r.buried + ', texts=' + JSON.stringify(r.texts) + ')');
|
||||
});
|
||||
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
// ── Desktop 1280x800 sanity ─────────────────────────────────────────────
|
||||
{
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||
const page = await ctx.newPage();
|
||||
page.setDefaultTimeout(10000);
|
||||
await step('[1280x800] navigate to /live', async () => { await gotoLive(page); });
|
||||
|
||||
await step('[1280x800] VCR bar renders, controls + LCD still inline (no desktop regression)', async () => {
|
||||
const r = await page.evaluate(() => {
|
||||
const bar = document.getElementById('vcrBar');
|
||||
const ctrl = document.querySelector('.vcr-controls');
|
||||
const lcd = document.querySelector('.vcr-lcd');
|
||||
const cr = ctrl.getBoundingClientRect();
|
||||
const lr = lcd.getBoundingClientRect();
|
||||
// Same row check (vertical overlap).
|
||||
const sameRow = !(cr.bottom <= lr.top || cr.top >= lr.bottom);
|
||||
return { hasBar: !!bar, sameRow, ctrl: { top: cr.top, bottom: cr.bottom }, lcd: { top: lr.top, bottom: lr.bottom } };
|
||||
});
|
||||
assert(r.hasBar, '#vcrBar missing on desktop');
|
||||
assert(r.sameRow,
|
||||
'desktop regression — controls and LCD must share a row ' +
|
||||
'(ctrl=' + JSON.stringify(r.ctrl) + ', lcd=' + JSON.stringify(r.lcd) + ')');
|
||||
});
|
||||
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log(`\n=== Results: passed ${passed} failed ${failed} ===`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
Reference in new issue
Block a user