mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-10 01:21:45 +00:00
Compare commits
4 Commits
v3.8.0
...
fix/issue-1402
| Author | SHA1 | Date | |
|---|---|---|---|
| 0878d91e97 | |||
| 2a6a5b578e | |||
| 6ec08acb9a | |||
| 99313ea2a8 |
@@ -271,6 +271,7 @@ jobs:
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-gestures-1062-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-gestures-1185-scroll-discriminator-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-gesture-hints-1065-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1402-gesture-hints-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-touch-gestures-coverage-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
|
||||
|
||||
+39
-6
@@ -8,9 +8,21 @@
|
||||
* - aria-live=polite, role=status, no focus stealing, pointer-events:none.
|
||||
* - prefers-reduced-motion: animation-name: none (style.css handles via media query).
|
||||
* - Singleton + cleanup: module-scoped guard; SPA re-mount must not re-show dismissed.
|
||||
* - Pull-to-refresh hint only when .pull-to-reconnect element exists in DOM.
|
||||
* - Edge-drawer hint only at viewport > 768px (where edge-swipe drawer applies).
|
||||
* - Row-swipe hint only on table pages: /#/packets, /#/nodes, etc.
|
||||
* - #1402 fixes:
|
||||
* - Bug 1: tab-swipe race with bottom-nav init — schedule on initial load
|
||||
* AND on 'load' event (later than DOMContentLoaded) so [data-bottom-nav]
|
||||
* has been built by bottom-nav.js. Also schedule on any hashchange.
|
||||
* - Bug 2: edge-drawer is a MOBILE feature (per #1064/#1184). Condition
|
||||
* flipped from innerWidth > 768 to innerWidth < 768.
|
||||
* - Bug 3: pull-refresh no longer gated on `.pull-to-reconnect` (which
|
||||
* only renders on WS-disconnect per #1068). Use touch-viewport probe.
|
||||
* - Bug 4: row-swipe route filter widened to cover other tables with
|
||||
* swipable rows (channels, observers — verified to render tr/data rows).
|
||||
* - Bug 5 (confirmed via operator console trace): the schedule path was
|
||||
* only re-firing on hashchange because the initial `init()` race with
|
||||
* bottom-nav.js left the relevance checks failing — the 800ms timer
|
||||
* fired before [data-bottom-nav] was injected. Now a second schedule
|
||||
* runs on window 'load' (after all assets settle) as a safety net.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
@@ -38,7 +50,11 @@
|
||||
relevant: function () {
|
||||
if (onLiveRoute()) return false; // #1244
|
||||
var h = location.hash || '';
|
||||
return /^#\/(packets|nodes)/.test(h);
|
||||
// #1402 Bug 4: widen to other tables with swipable rows.
|
||||
// channels (.ch-item / .ch-row data-hash), observers (#obsTable tr) —
|
||||
// verified via grep before adding. /perf and /analytics omitted: no
|
||||
// swipable rows confirmed there.
|
||||
return /^#\/(packets|nodes|channels|observers)/.test(h);
|
||||
},
|
||||
position: 'bottom',
|
||||
},
|
||||
@@ -56,7 +72,10 @@
|
||||
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]');
|
||||
// #1402 Bug 2: edge-swipe drawer (#1064/#1184) is a MOBILE feature.
|
||||
// Original condition (> 768) was inverted — hint only fired on desktop
|
||||
// where the drawer doesn't apply.
|
||||
return window.innerWidth < 768 && !!document.querySelector('.nav-drawer, [data-nav-drawer]');
|
||||
},
|
||||
position: 'top-left',
|
||||
},
|
||||
@@ -65,7 +84,11 @@
|
||||
text: 'Tip: pull down to refresh the connection.',
|
||||
relevant: function () {
|
||||
if (onLiveRoute()) return false; // #1244
|
||||
return !!document.querySelector('.pull-to-reconnect');
|
||||
// #1402 Bug 3: was gated on `.pull-to-reconnect` which only renders
|
||||
// on WS-disconnect (#1068). First-visit healthy-connection operators
|
||||
// never saw the hint. Decoupled: any touch viewport gets the hint.
|
||||
var mm = window.matchMedia && window.matchMedia('(pointer: coarse)');
|
||||
return !!(mm && mm.matches);
|
||||
},
|
||||
position: 'top',
|
||||
},
|
||||
@@ -192,6 +215,16 @@
|
||||
if (!_routeChangeBound) {
|
||||
_routeChangeBound = true;
|
||||
window.addEventListener('hashchange', onRouteChange);
|
||||
// #1402 Bug 5: schedule path was only firing reliably on hashchange.
|
||||
// The initial scheduleHints() call below races bottom-nav.js (which
|
||||
// injects [data-bottom-nav] from its own DOMContentLoaded init), so
|
||||
// the 800ms tab-swipe relevance check returned false on first visit.
|
||||
// Re-schedule on 'load' (after all sync init has completed) as a
|
||||
// safety net. scheduleHints() is idempotent (clears prior timer),
|
||||
// so this is a no-op when the first schedule already rendered.
|
||||
if (document.readyState !== 'complete') {
|
||||
window.addEventListener('load', scheduleHints, { once: true });
|
||||
}
|
||||
}
|
||||
scheduleHints();
|
||||
}
|
||||
|
||||
@@ -208,8 +208,11 @@ async function main() {
|
||||
|
||||
await ctx.close();
|
||||
|
||||
// ── (e) at 1024x800, edge-swipe hint visible on first visit ──
|
||||
const ctx2 = await browser.newContext({ viewport: { width: 1024, height: 800 } });
|
||||
// ── (e) edge-drawer hint visible on first visit at narrow viewport ──
|
||||
// #1402 Bug 2: edge-swipe drawer (#1064/#1184) is a MOBILE feature; original
|
||||
// code/test had the condition inverted (innerWidth > 768). Corrected: assert
|
||||
// edge-drawer at vw=393 (mobile), NOT at desktop.
|
||||
const ctx2 = await browser.newContext({ viewport: { width: 393, height: 800 }, hasTouch: true });
|
||||
const page2 = await ctx2.newPage();
|
||||
await page2.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
||||
await page2.evaluate((keys) => Object.values(keys).forEach((k) => localStorage.removeItem(k)), KEYS);
|
||||
@@ -217,9 +220,9 @@ async function main() {
|
||||
await page2.waitForTimeout(HINT_SETTLE_MS);
|
||||
const edgeHint = await hintVisible(page2, 'edge-drawer');
|
||||
if (edgeHint.present && edgeHint.visible) {
|
||||
pass('(e) edge-drawer hint visible at 1024x800');
|
||||
pass('(e) edge-drawer hint visible at 393x800 (mobile — corrected per #1402)');
|
||||
} else {
|
||||
fail(`(e) edge-drawer hint NOT visible at 1024x800 — state=${JSON.stringify(edgeHint)}`);
|
||||
fail(`(e) edge-drawer hint NOT visible at 393x800 — state=${JSON.stringify(edgeHint)}`);
|
||||
}
|
||||
await ctx2.close();
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env node
|
||||
/* Issue #1402 — Gesture-hint regressions on iPhone-class mobile.
|
||||
*
|
||||
* Per issue body, vw=393, /#/home, console probe at deploy:
|
||||
* bottomNav: true, navDrawer: true, pullEl: false, storedKeys: []
|
||||
*
|
||||
* Asserts (gates the 4 fixes):
|
||||
* (1) vw=393 /#/home → tab-swipe hint renders within 1500ms (Bug 1)
|
||||
* (2) vw=393 /#/home → edge-drawer hint renders within 1500ms (Bug 2 — currently
|
||||
* inverted: code says innerWidth > 768)
|
||||
* (3) vw=393 /#/home → pull-refresh hint renders within 1500ms (Bug 3 — currently
|
||||
* requires .pull-to-reconnect in DOM, which only exists on WS-disconnect)
|
||||
* (4) vw=393 /#/channels and /#/observers → row-swipe hint renders (Bug 4 — currently
|
||||
* scoped to /packets|/nodes only)
|
||||
* (5) vw=1024 /#/home → edge-drawer hint does NOT render (mobile-only per fix)
|
||||
* (6) auto-fade does NOT mark seen for tab-swipe; explicit dismiss DOES
|
||||
* (regression guard on the dismissal flow under the new render conditions)
|
||||
* (7) FIRST-LOAD path: vw=393 /#/home, fresh page (no hashchange fired), hints render.
|
||||
* Bug confirmed via operator console trace: hints_in_dom=0 on initial load
|
||||
* but hints_appended_in_2s=[row-swipe,tab-swipe] after a hashchange.
|
||||
* Asserts the schedule path runs without needing a hashchange.
|
||||
* (8) HASHCHANGE path: after first load, navigate to a different route — hints
|
||||
* relevant for the new route render. Validates _routeChangeBound still works.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
const HINT_SETTLE_MS = 1700; // SHOW_DELAY_MS (800) + margin
|
||||
|
||||
const KEYS = {
|
||||
rowSwipe: 'meshcore-gesture-hints-row-swipe',
|
||||
tabSwipe: 'meshcore-gesture-hints-tab-swipe',
|
||||
edgeDrawer: 'meshcore-gesture-hints-edge-drawer',
|
||||
pullRefresh: 'meshcore-gesture-hints-pull-refresh',
|
||||
};
|
||||
|
||||
async function clearAllHintFlags(page) {
|
||||
await page.evaluate((keys) => {
|
||||
Object.values(keys).forEach((k) => localStorage.removeItem(k));
|
||||
}, KEYS);
|
||||
}
|
||||
|
||||
async function hintVisible(page, hintId) {
|
||||
return page.evaluate((id) => {
|
||||
const el = document.querySelector('[data-gesture-hint="' + id + '"]');
|
||||
if (!el) return { present: false };
|
||||
const cs = getComputedStyle(el);
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
present: true,
|
||||
visible: cs.display !== 'none' && cs.visibility !== 'hidden' && parseFloat(cs.opacity || '1') > 0.01 && r.width > 0 && r.height > 0,
|
||||
};
|
||||
}, hintId);
|
||||
}
|
||||
|
||||
async function freshContext(browser, viewport, hasTouch) {
|
||||
return browser.newContext({ viewport, hasTouch: !!hasTouch });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const requireChromium = process.env.CHROMIUM_REQUIRE === '1';
|
||||
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 (requireChromium) {
|
||||
console.error(`test-issue-1402-gesture-hints-e2e.js: FAIL — Chromium required but unavailable: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`test-issue-1402-gesture-hints-e2e.js: SKIP (Chromium unavailable: ${err.message.split('\n')[0]})`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let failures = 0, passes = 0;
|
||||
const fail = (m) => { failures++; console.error(' FAIL: ' + m); };
|
||||
const pass = (m) => { passes++; console.log(' PASS: ' + m); };
|
||||
const assert = (cond, msg) => { if (cond) pass(msg); else fail(msg); };
|
||||
void assert; // exported via fail/pass helpers; named for preflight grep clarity
|
||||
|
||||
// ── Mobile (vw=393, hasTouch) — operator's actual device class ──
|
||||
const mobileCtx = await freshContext(browser, { width: 393, height: 852 }, true);
|
||||
const mPage = await mobileCtx.newPage();
|
||||
mPage.setDefaultTimeout(15000);
|
||||
mPage.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
||||
|
||||
// First-visit /#/home setup.
|
||||
await mPage.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded' });
|
||||
await clearAllHintFlags(mPage);
|
||||
await mPage.reload({ waitUntil: 'domcontentloaded' });
|
||||
await mPage.waitForTimeout(HINT_SETTLE_MS);
|
||||
|
||||
// Sanity probe — mirrors the operator's console probe.
|
||||
const probe = await mPage.evaluate(() => ({
|
||||
vw: window.innerWidth,
|
||||
bottomNav: !!document.querySelector('[data-bottom-nav]'),
|
||||
navDrawer: !!document.querySelector('.nav-drawer, [data-nav-drawer]'),
|
||||
pullEl: !!document.querySelector('.pull-to-reconnect'),
|
||||
pointerCoarse: window.matchMedia && window.matchMedia('(pointer: coarse)').matches,
|
||||
}));
|
||||
console.log(' PROBE (mobile /#/home): ' + JSON.stringify(probe));
|
||||
|
||||
// ── (1) Bug 1: tab-swipe at /#/home, vw=393 ──
|
||||
const tabSwipe = await hintVisible(mPage, 'tab-swipe');
|
||||
if (tabSwipe.present && tabSwipe.visible) {
|
||||
pass('(1) tab-swipe hint visible at vw=393 /#/home within 1500ms (Bug 1)');
|
||||
} else {
|
||||
fail(`(1) tab-swipe hint NOT visible at vw=393 /#/home — state=${JSON.stringify(tabSwipe)} probe=${JSON.stringify(probe)}`);
|
||||
}
|
||||
|
||||
// ── (2) Bug 2: edge-drawer at /#/home, vw=393 ──
|
||||
const edgeMobile = await hintVisible(mPage, 'edge-drawer');
|
||||
if (edgeMobile.present && edgeMobile.visible) {
|
||||
pass('(2) edge-drawer hint visible at vw=393 /#/home (Bug 2 — was inverted to desktop-only)');
|
||||
} else {
|
||||
fail(`(2) edge-drawer hint NOT visible at vw=393 /#/home — state=${JSON.stringify(edgeMobile)}`);
|
||||
}
|
||||
|
||||
// ── (3) Bug 3: pull-refresh at /#/home, vw=393 (touch viewport) ──
|
||||
const pullRefresh = await hintVisible(mPage, 'pull-refresh');
|
||||
if (pullRefresh.present && pullRefresh.visible) {
|
||||
pass('(3) pull-refresh hint visible at vw=393 /#/home (Bug 3 — was gated on WS-disconnect element)');
|
||||
} else {
|
||||
fail(`(3) pull-refresh hint NOT visible at vw=393 /#/home — state=${JSON.stringify(pullRefresh)}`);
|
||||
}
|
||||
|
||||
await mobileCtx.close();
|
||||
|
||||
// ── (4) Bug 4: row-swipe on /#/channels and /#/observers ──
|
||||
for (const route of ['/#/channels', '/#/observers']) {
|
||||
const ctx = await freshContext(browser, { width: 393, height: 852 }, true);
|
||||
const p = await ctx.newPage();
|
||||
p.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
||||
await p.goto(`${BASE}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
await clearAllHintFlags(p);
|
||||
await p.reload({ waitUntil: 'domcontentloaded' });
|
||||
await p.waitForTimeout(HINT_SETTLE_MS);
|
||||
const rs = await hintVisible(p, 'row-swipe');
|
||||
if (rs.present && rs.visible) {
|
||||
pass(`(4) row-swipe hint visible at vw=393 ${route} (Bug 4 — route scope widened)`);
|
||||
} else {
|
||||
fail(`(4) row-swipe hint NOT visible at vw=393 ${route} — state=${JSON.stringify(rs)}`);
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
// ── (5) Desktop: edge-drawer hint must NOT render at vw=1024 (mobile-only) ──
|
||||
const dCtx = await freshContext(browser, { width: 1024, height: 800 }, false);
|
||||
const dPage = await dCtx.newPage();
|
||||
await dPage.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded' });
|
||||
await clearAllHintFlags(dPage);
|
||||
await dPage.reload({ waitUntil: 'domcontentloaded' });
|
||||
await dPage.waitForTimeout(HINT_SETTLE_MS);
|
||||
const edgeDesktop = await hintVisible(dPage, 'edge-drawer');
|
||||
if (!edgeDesktop.present || !edgeDesktop.visible) {
|
||||
pass('(5) edge-drawer hint NOT visible at vw=1024 /#/home (mobile-only per Bug 2 fix)');
|
||||
} else {
|
||||
fail(`(5) edge-drawer hint SHOULD NOT render at vw=1024 but did — state=${JSON.stringify(edgeDesktop)}`);
|
||||
}
|
||||
await dCtx.close();
|
||||
|
||||
// ── (6) tab-swipe explicit-dismiss sets seen flag ──
|
||||
const dismissCtx = await freshContext(browser, { width: 393, height: 852 }, true);
|
||||
const dpPage = await dismissCtx.newPage();
|
||||
await dpPage.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded' });
|
||||
await clearAllHintFlags(dpPage);
|
||||
await dpPage.reload({ waitUntil: 'domcontentloaded' });
|
||||
await dpPage.waitForTimeout(HINT_SETTLE_MS);
|
||||
const clicked = await dpPage.evaluate(() => {
|
||||
const el = document.querySelector('[data-gesture-hint="tab-swipe"]');
|
||||
if (!el) return false;
|
||||
const btn = el.querySelector('[data-gesture-hint-dismiss]');
|
||||
if (!btn) return false;
|
||||
btn.click();
|
||||
return true;
|
||||
});
|
||||
await dpPage.waitForTimeout(300);
|
||||
const flagAfter = await dpPage.evaluate((k) => localStorage.getItem(k), KEYS.tabSwipe);
|
||||
if (clicked && flagAfter === 'seen') {
|
||||
pass('(6) tab-swipe explicit dismiss sets localStorage seen flag');
|
||||
} else {
|
||||
fail(`(6) tab-swipe dismiss did not record seen — clicked=${clicked} flag=${flagAfter}`);
|
||||
}
|
||||
await dismissCtx.close();
|
||||
|
||||
// ── (7) FIRST-LOAD path: fresh page, no hashchange — hints must render ──
|
||||
// Operator console trace showed hints_in_dom=0 on initial paint and only
|
||||
// hashchange triggered the schedule path. Asserts schedule fires without nav.
|
||||
const flCtx = await freshContext(browser, { width: 393, height: 852 }, true);
|
||||
const flPage = await flCtx.newPage();
|
||||
flPage.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
||||
// Pre-clear flags via prelude script BEFORE any navigation so the very-first
|
||||
// page-load is clean. (Reloading would still be a "first load" technically,
|
||||
// but this exercises the genuinely-cold path with no prior hashchange.)
|
||||
await flPage.addInitScript((keys) => {
|
||||
try { Object.values(keys).forEach((k) => localStorage.removeItem(k)); } catch (_) {}
|
||||
}, KEYS);
|
||||
await flPage.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded' });
|
||||
await flPage.waitForTimeout(HINT_SETTLE_MS);
|
||||
const flHints = await flPage.evaluate(() =>
|
||||
Array.from(document.querySelectorAll('[data-gesture-hint]')).map((e) => e.getAttribute('data-gesture-hint'))
|
||||
);
|
||||
if (flHints.includes('tab-swipe')) {
|
||||
pass(`(7) FIRST-LOAD: tab-swipe hint rendered without prior hashchange (hints=${JSON.stringify(flHints)})`);
|
||||
} else {
|
||||
fail(`(7) FIRST-LOAD: no tab-swipe hint on initial paint (hints=${JSON.stringify(flHints)})`);
|
||||
}
|
||||
await flCtx.close();
|
||||
|
||||
// ── (8) HASHCHANGE path: after first load, navigating still triggers hints ──
|
||||
const hcCtx = await freshContext(browser, { width: 393, height: 852 }, true);
|
||||
const hcPage = await hcCtx.newPage();
|
||||
hcPage.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
||||
await hcPage.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded' });
|
||||
await clearAllHintFlags(hcPage);
|
||||
// Mark home-relevant hints as seen so we can prove navigation to a NEW route
|
||||
// surfaces NEW hints (row-swipe on packets) — proving the hashchange path is alive.
|
||||
await hcPage.evaluate((keys) => {
|
||||
localStorage.setItem(keys.tabSwipe, 'seen');
|
||||
localStorage.setItem(keys.edgeDrawer, 'seen');
|
||||
localStorage.setItem(keys.pullRefresh, 'seen');
|
||||
}, KEYS);
|
||||
await hcPage.waitForTimeout(300);
|
||||
await hcPage.evaluate(() => { location.hash = '#/packets'; });
|
||||
await hcPage.waitForTimeout(HINT_SETTLE_MS);
|
||||
const rowAfterNav = await hintVisible(hcPage, 'row-swipe');
|
||||
if (rowAfterNav.present && rowAfterNav.visible) {
|
||||
pass('(8) HASHCHANGE: row-swipe hint rendered after nav from /#/home to /#/packets');
|
||||
} else {
|
||||
fail(`(8) HASHCHANGE: row-swipe not rendered after hashchange — state=${JSON.stringify(rowAfterNav)}`);
|
||||
}
|
||||
await hcCtx.close();
|
||||
|
||||
await browser.close();
|
||||
console.log(`\ntest-issue-1402-gesture-hints-e2e.js: ${passes} passed, ${failures} failed`);
|
||||
process.exit(failures > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((err) => { console.error('test-issue-1402-gesture-hints-e2e.js: FAIL —', err); process.exit(1); });
|
||||
Reference in New Issue
Block a user