fix(#1402): gesture hints — edge-drawer mobile-only + row-swipe widening (re-fix) (#1586)

Partial fix for #1402

## Summary
Re-fix two of the four #1402 regressions on mobile after `#1452`
silently reverted the prior fix (`6ec08acb`). Two predicate flips in
`public/gesture-hints.js` + extended E2E coverage to prevent another
silent revert.

This PR is intentionally **scoped to Bug 2 and Bug 4 only**. Bug 1 and
Bug 3 were also dropped by `#1452` and are NOT restored here — `#1402`
remains open for the rest.

## Changes
- `public/gesture-hints.js` (edge-drawer): `window.innerWidth > 768` →
`window.innerWidth <= 768`. The edge-swipe drawer is the MOBILE layout's
nav per #1064/#1184; `nav-drawer.js` `NARROW_MAX=768` (inclusive —
narrow when width <= NARROW_MAX). Above 768 the sidebar is persistent,
no edge-swipe is needed.
- `public/gesture-hints.js` (row-swipe): widen route filter from
`/^#\/(packets|nodes)/` to `/^#\/(packets|nodes|channels|observers)/`.
Channels and observers also render swipable row tables.
- `public/gesture-hints.js`: expose read-only
`window.__gestureHintsDefs` test hook (frozen) for direct predicate
probes (avoids race with render path).
- `test-gesture-hints-1065-e2e.js`: add assertions (i)+(j) at vw=393 —
edge-drawer relevant on `/#/home`, row-swipe relevant on `/#/channels`;
(k) negative-direction gate at vw=1024 asserts `edge-drawer.relevant()
=== false` on desktop. Retarget (e) from 1024x800 → 393x800 to match the
corrected mobile-only gate.

## TDD
- Red commit: `1e7545d1` — test additions fail against current
production code (edge-drawer relevant returns false at vw=393, row-swipe
filter rejects /channels).
- Green commit: `6f844d5b` — predicate flips + route widening make both
assertions pass.
- Polish commit (round-1 fixes): boundary <= 768, doc-header refresh,
freeze the test hook, negative-direction gate (k), precondition
assertion on (i).

## Acceptance criteria from #1402
- [ ] Bug 1 (`window 'load'` rescheduler + `pointer: coarse` gate) —
dropped by #1452, NOT restored in this PR. Tracked in #1402.
- [x] Bug 2 (edge-drawer mobile-only) — fixed here.
- [ ] Bug 3 (pull-refresh touch-gate decoupling) — dropped by #1452, NOT
restored in this PR. Tracked in #1402.
- [x] Bug 4 (row-swipe widening → /channels + /observers) — fixed here.
- [x] E2E mutation gate: assertions (i)+(j)+(k) provably fail if either
predicate is reverted or re-broadened.

## Notes
- Silently reverted by #1452 — re-fix here, with regression gates so the
next reviewer of the next refactor will see the assertions fail rather
than the production behavior change unnoticed.

## Preflight
All gates pass (PII, branch scope, red commit, CSS vars, XSS sinks,
etc.).

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: fix-1166-bot <bot@corescope.local>
This commit is contained in:
Kpa-clawbot
2026-06-04 16:41:32 -07:00
committed by GitHub
parent 7533b3b67b
commit 116efe4bd7
2 changed files with 101 additions and 12 deletions
+19 -5
View File
@@ -9,7 +9,8 @@
* - 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).
* - Edge-drawer hint only at viewport <= 768px (mobile layout, where the
* edge-swipe drawer is the nav UI; nav-drawer.js NARROW_MAX=768, inclusive).
* - Row-swipe hint only on table pages: /#/packets, /#/nodes, etc.
*/
(function () {
@@ -51,7 +52,7 @@
if (!hasTouchCapability()) return false;
if (onLiveRoute()) return false; // #1244
var h = location.hash || '';
return /^#\/(packets|nodes)/.test(h);
return /^#\/(packets|nodes|channels|observers)/.test(h);
},
position: 'bottom',
},
@@ -71,9 +72,12 @@
relevant: function () {
if (!hasTouchCapability()) return false;
if (onLiveRoute()) return false; // #1244
// nav-drawer.js: NARROW_MAX=768; edge-swipe drawer is the WIDE
// (>768) layout's nav UI. Below 768, the bottom-nav owns navigation.
return window.innerWidth > 768 && !!document.querySelector('.nav-drawer, [data-nav-drawer]');
// nav-drawer.js: NARROW_MAX=768; "narrow" is inclusive — narrow when
// width <= NARROW_MAX (nav-drawer treats width > NARROW_MAX as the
// non-narrow / sidebar layout). The edge-swipe drawer is the MOBILE
// (≤768) layout's nav UI per #1064/#1184. Above 768, the persistent
// sidebar is visible and no edge-swipe is needed.
return window.innerWidth <= 768 && !!document.querySelector('.nav-drawer, [data-nav-drawer]');
},
position: 'top-left',
},
@@ -220,6 +224,16 @@
init();
}
// #1402 test hook: expose hint definitions for E2E predicate probes.
// Read-only by convention; tests call .relevant() to verify routing/viewport gates.
window.__gestureHintsDefs = HINTS;
// M3: freeze the hint defs to prevent tests / page scripts from mutating
// production state via the test hook. Shallow-freeze HINTS + each def.
try {
Object.keys(HINTS).forEach(function (id) { Object.freeze(HINTS[id]); });
Object.freeze(HINTS);
} catch (_e) {}
window.GestureHints = {
show: show,
dismiss: dismiss,
+82 -7
View File
@@ -208,11 +208,11 @@ async function main() {
await ctx.close();
// ── (e) at 1024x800 with touch, edge-swipe hint visible on first visit ──
// #1065 follow-up: edge-swipe is a touch gesture; the hint must only
// appear when the viewport reports touch capability. Test context must
// pass hasTouch:true (real edge-swipe-on-tablet/touch-laptop scenario).
const ctx2 = await browser.newContext({ viewport: { width: 1024, height: 800 }, hasTouch: true });
// ── (e) at vw=393 with touch, edge-swipe hint visible on first visit ──
// #1065 follow-up: edge-swipe is a touch gesture. #1402 fix: edge-drawer
// is the MOBILE layout's nav UI (per #1064/#1184, nav-drawer.js NARROW_MAX=768);
// hint must appear at narrow viewports, not wide ones.
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);
@@ -220,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)');
} 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();
@@ -245,6 +245,81 @@ async function main() {
}
await ctx3.close();
// ── (i) #1402 regression — at vw=393 (mobile), edge-drawer hint IS relevant on /#/home ──
// Bug 2 in #1402: edge-drawer.relevant had window.innerWidth > 768 (inverted).
// nav-drawer.js NARROW_MAX=768; the edge-swipe drawer is the MOBILE feature
// per #1064/#1184. At vw=393 with a .nav-drawer in the DOM, the hint MUST
// be classified as relevant. Asserts the predicate directly so the test
// does not depend on the schedule/render path.
const ctx4 = await browser.newContext({ viewport: { width: 393, height: 800 }, hasTouch: true });
const page4 = await ctx4.newPage();
await page4.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded' });
await page4.evaluate((keys) => Object.values(keys).forEach((k) => localStorage.removeItem(k)), KEYS);
await page4.reload({ waitUntil: 'domcontentloaded' });
await page4.waitForTimeout(HINT_SETTLE_MS);
const probe1402 = await page4.evaluate(() => {
const hints = window.__gestureHintsDefs || null;
return {
hintsExposed: !!hints,
vw: window.innerWidth,
navDrawerInDom: !!document.querySelector('.nav-drawer, [data-nav-drawer]'),
edgeDrawerRelevant: hints && hints['edge-drawer'] ? !!hints['edge-drawer'].relevant() : null,
};
});
if (probe1402.hintsExposed && probe1402.navDrawerInDom && probe1402.edgeDrawerRelevant === true) {
pass(`(i) #1402 — edge-drawer relevant at vw=393 on /#/home (navDrawer=${probe1402.navDrawerInDom})`);
} else if (!probe1402.navDrawerInDom) {
fail(`(i) #1402 — precondition failed: .nav-drawer NOT in DOM at vw=393 — state=${JSON.stringify(probe1402)}`);
} else {
fail(`(i) #1402 — edge-drawer NOT relevant at vw=${probe1402.vw} — state=${JSON.stringify(probe1402)}`);
}
// ── (j) #1402 regression — at vw=393, row-swipe hint IS relevant on /#/channels ──
// Bug 4 in #1402: row-swipe filter was /^#\/(packets|nodes)/ — must widen to
// include channels and observers (both render swipable row tables).
await page4.goto(`${BASE}/#/channels`, { waitUntil: 'domcontentloaded' });
await page4.waitForTimeout(HINT_SETTLE_MS);
const probe1402b = await page4.evaluate(() => {
const hints = window.__gestureHintsDefs || null;
return {
hintsExposed: !!hints,
hash: location.hash,
rowSwipeRelevant: hints && hints['row-swipe'] ? !!hints['row-swipe'].relevant() : null,
};
});
if (probe1402b.hintsExposed && probe1402b.rowSwipeRelevant === true) {
pass(`(j) #1402 — row-swipe relevant at vw=393 on ${probe1402b.hash}`);
} else {
fail(`(j) #1402 — row-swipe NOT relevant on /#/channels — state=${JSON.stringify(probe1402b)}`);
}
await ctx4.close();
// ── (k) #1402 negative-direction regression gate — at vw=1024 (desktop),
// edge-drawer.relevant() MUST return false. This locks the predicate so
// it cannot be re-broadened to fire on desktop (the original #1402 Bug 2
// had the inequality inverted; this assertion guards against the reverse
// mistake — over-broadening — going forward).
const ctx5 = await browser.newContext({ viewport: { width: 1024, height: 800 }, hasTouch: true });
const page5 = await ctx5.newPage();
await page5.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded' });
await page5.evaluate((keys) => Object.values(keys).forEach((k) => localStorage.removeItem(k)), KEYS);
await page5.reload({ waitUntil: 'domcontentloaded' });
await page5.waitForTimeout(HINT_SETTLE_MS);
const probe1402c = await page5.evaluate(() => {
const hints = window.__gestureHintsDefs || null;
return {
hintsExposed: !!hints,
vw: window.innerWidth,
edgeDrawerRelevant: hints && hints['edge-drawer'] ? !!hints['edge-drawer'].relevant() : null,
};
});
if (probe1402c.hintsExposed && probe1402c.edgeDrawerRelevant === false) {
pass(`(k) #1402 negative gate — edge-drawer NOT relevant at vw=${probe1402c.vw} (desktop)`);
} else {
fail(`(k) #1402 negative gate FAILED — edge-drawer relevant at desktop width — state=${JSON.stringify(probe1402c)}`);
}
await ctx5.close();
await browser.close();
console.log(`\ntest-gesture-hints-1065-e2e.js: ${passes} passed, ${failures} failed`);
process.exit(failures > 0 ? 1 : 0);