Skip to content

fix(#1064): edge-swipe nav drawer (Option A, wide-only)#1184

Merged
Kpa-clawbot merged 8 commits intomasterfrom
fix/issue-1064-edge-swipe
May 10, 2026
Merged

fix(#1064): edge-swipe nav drawer (Option A, wide-only)#1184
Kpa-clawbot merged 8 commits intomasterfrom
fix/issue-1064-edge-swipe

Conversation

@Kpa-clawbot
Copy link
Copy Markdown
Owner

Red commit: a810b1f (CI run pending — draft PR)

Fixes #1064 (parent epic #1052).

What

Edge-swipe nav drawer (slide-over from left). Pointer-down within left 20px → drag-follows-finger animation → settles open/closed on velocity threshold. Drawer surfaces the same long-tail routes as the More sheet (PR #1174) as an alternate entry point at the EDGE.

Decisions

Option A — drawer wide-only (>768px). At ≤768px the bottom-nav has the More tab (PR #1174); a left-edge drawer there would compete with that UX. Wide viewports have no More tab, so the drawer replaces the top-nav hamburger as a faster keyboard-free entry. Bottom-nav coexistence at narrow widths: none — drawer is disabled.

Singleton + cleanup. Module-scoped guard + __navDrawerPointerBindCount debug seam — same pattern as #1180 fix. pointermove/pointerup are bound on document exactly once across SPA mounts.

body { touch-action: pan-y }. Vertical scroll preserved everywhere; the drawer claims horizontal swipes only inside our viewport. iOS browser-back left-edge gesture still works (it's the OS, not the page).

Accessibility. inert on the drawer when closed, removed when open. Focus trap (Tab cycles last↔first). Esc closes. Backdrop tap closes. prefers-reduced-motion: instant snap, no animation.

Tests (TDD)

Red commit pushes the E2E first; CI must FAIL on assertion.
E2E assertion added: test-nav-drawer-1064-e2e.js:1

CSS coordination with #1062

Additions are fenced (/* === Issue #1064 — Edge-swipe nav drawer === */ … /* === end #1064 === */) to minimize merge friction.

corescope-bot added 2 commits May 9, 2026 18:40
Adds test-nav-drawer-1064-e2e.js covering:
  (a) edge-swipe at x=10→200 opens drawer flush at left:0
  (b) drawer items mirror PR #1174 long-tail routes
  (c) tap drawer item navigates + closes
  (d) Esc closes
  (e) backdrop click closes
  (f) narrow viewport (≤768px) does NOT open drawer (Option A)
  (g) singleton: 5 SPA round-trips keep pointermove bind count ≤ 1
  (h) focus trap: Tab from last wraps to first

Wires test into deploy.yml Playwright matrix. CI must FAIL on
assertion: nav-drawer.js / CSS not implemented yet.
NEW public/nav-drawer.js
  - Pointer Events only (no touchstart/mousedown fallbacks)
  - Edge-swipe within left 20px starts drag-track
  - Drawer translateX follows finger; settle on velocity + position
  - Singleton: module-scoped wired guard, document listeners bound once
  - window.__navDrawerPointerBindCount debug seam (E2E asserts ≤ 1)
  - Focus trap (Tab cycles), Esc closes, backdrop tap closes
  - inert when closed, removed when open
  - prefers-reduced-motion: instant snap

NEW CSS section in public/style.css (fenced for #1062 parallel safety)
  - body { touch-action: pan-y } — preserves iOS back-swipe + vertical scroll
  - .nav-drawer + .nav-drawer-backdrop slide-over from left
  - z-index 1260/1250 (above bottom-nav 1200, below modal 9100)
  - @media (max-width: 768px) hides drawer (Option A)

public/index.html
  - Wires nav-drawer.js after bottom-nav.js

Closes the implementation half of #1064; CI E2E should now PASS.
@Kpa-clawbot Kpa-clawbot marked this pull request as ready for review May 9, 2026 18:53
The (e) backdrop-click test timed out at `page.click('[data-nav-drawer-backdrop]', { position: { x: 5, y: 5 } })`. Root cause: z-stack occlusion — the drawer (z-index 1260) sits on top of the backdrop (z-index 1250) over the left ~320px of the viewport. Position (5,5) within the backdrop element is OCCLUDED by the drawer, so Playwright's actionability check waits for a click target that never receives the event.

Production behavior is correct: backdrop has `pointer-events: auto` when open and a real click handler that calls close(). The test simply chose a coordinate that the drawer covers.

Fix: click at x=800,y=400 — viewport is 1024×800, drawer ≤360px wide, so the backdrop is the topmost element at that point and the click lands as intended.

No production code changed.
@Kpa-clawbot
Copy link
Copy Markdown
Owner Author

✅ Backdrop click fix pushed.

Root cause: z-stack occlusion (cause #3 from the brief). The backdrop has pointer-events: auto when open and a real click handler calling close() — production behavior is fine. But the test clicked at position: { x: 5, y: 5 } within the backdrop element, which is occluded by the drawer (drawer z-index 1260 sits over the backdrop z-index 1250 across the left ~320px). Playwright's actionability check timed out because the drawer captured the hit-test at that coordinate.

Fix: test-nav-drawer-1064-e2e.js:142 — click at x: 800, y: 400 instead. Viewport is 1024×800 and drawer ≤360px wide, so the backdrop is the topmost element at that point.

Commit: 21293a6
No production code changedpublic/nav-drawer.js and public/style.css unchanged.

Self-review findings (pr-polish):

1. nav-drawer.js — close() did not restore focus to the element that had
   focus before open(). Same regression class as #1168 (closing nav UI
   must return focus to its trigger so keyboard users don't get dumped
   at <body>). Capture activeElement on open(), restore on close() iff
   focus is still inside the drawer at close time.

2. test-nav-drawer-1064-e2e.js — backdrop click used hardcoded
   {x:800,y:400} which assumes the 1024x800 viewport. Brittle to any
   viewport change. Compute clickX from viewportSize() — clearly outside
   the drawer's max-width (360px) at any reasonable viewport.

3. test-nav-drawer-1064-e2e.js — added (d2) regression test that locks
   in the focus-restoration fix: park focus on a sentinel button, open
   drawer, confirm focus moves into drawer, close, assert focus came
   back to sentinel. Previously: the fix would silently regress.
Copy link
Copy Markdown
Owner Author

@Kpa-clawbot Kpa-clawbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review — PR #1184

REQUEST CHANGES — 1 must-fix

Reviewed gh pr diff only (no checkout). Scope: public/nav-drawer.js, public/style.css, public/index.html, test-nav-drawer-1064-e2e.js, .github/workflows/deploy.yml.


MUST-FIX 1 — close() applies inert before focus-restore, defeating both the restore and test (d2)

public/nav-drawer.js close():

drawerEl.classList.remove('is-open');
drawerEl.setAttribute('inert', '');           // ← (A) applied here
drawerEl.setAttribute('aria-hidden', 'true');
...
if (wasOpen && prevFocus && typeof prevFocus.focus === 'function') {
  try {
    if (drawerEl.contains(document.activeElement) && document.contains(prevFocus)) {
      prevFocus.focus({ preventScroll: true }); // ← (B) guard never passes
    }
  } catch (_e) {}
}

Per HTML spec, when inert becomes set on a subtree containing document.activeElement, browsers synchronously re-run the focusing steps and move focus to <body>. This is implemented natively in Chromium 102+, Firefox 112+, Safari 15.5+ (i.e. every browser the project targets — inert has no polyfill in this repo).

Consequence at line (B): after line (A) runs, document.activeElement === document.body. The guard drawerEl.contains(document.activeElement) becomes drawerEl.contains(document.body), which is false (<body> is the ancestor of the drawer, not a descendant). The restore branch never runs, prevFocus is set to null at the end, and focus stays on <body>.

This is the exact regression class the PR description (and test d2) explicitly claim to prevent (#1168 callback). E2E (d2) will fail in headless Chromium for the same reason — the assertion document.activeElement.id === '__nav_drawer_focus_sentinel' after close() will be false. (CI is currently still pending on Playwright at review time, so this hasn't been observed yet.)

Fix options (pick one):

  1. Restore focus before applying inert:
    drawerEl.classList.remove('is-open');
    // restore focus FIRST while activeElement is still inside drawer
    if (wasOpen && prevFocus && typeof prevFocus.focus === 'function'
        && drawerEl.contains(document.activeElement)
        && document.contains(prevFocus)) {
      try { prevFocus.focus({ preventScroll: true }); } catch (_e) {}
    }
    drawerEl.setAttribute('inert', '');
    drawerEl.setAttribute('aria-hidden', 'true');
  2. Or drop the drawerEl.contains(document.activeElement) half of the guard (since inert already pulled focus out — document.activeElement === document.body is the de-facto signal that the drawer "had" focus). Slightly less precise but works.

Option 1 is preferred — it preserves the original intent ("only restore if focus was actually inside the drawer when we closed it") without relying on the inert side-effect.


Out-of-scope (intentionally not flagged as must-fix)

  • Test (g) singleton coverage is weaker than advertised. Hash changes don't re-execute the IIFE, so the wired guard isn't actually exercised; the count is fixed at 1 from wireOnce() regardless. Verifying real re-bind resistance would require re-injecting the script. Not blocking — the code's wired guard is straightforward and correct, the test just doesn't tax it.
  • Drag-follow + velocity not unit-tested. Acknowledged in the brief, acceptable.
  • body { touch-action: pan-y } duplication risk with #1185. Identical declarations; CSS cascade is benign — fix at merge time of the second PR.
  • Z-index parity with #1174 sheet (both 1250). Confirmed safe: the drawer + backdrop are display: none !important at ≤768px via the @media (max-width: 768px) block in style.css, and #1174's sheet is narrow-only — no temporal overlap.
  • No setPointerCapture on drag. Document-level pointermove/up listeners cover the off-target release path, so capture is not strictly needed here.

Verified

  • Pointer Events lifecycle: down/move/up/cancel all wired once via wireOnce(); no per-mount rebind.
  • wired singleton guard: correct module-scoped state, addEventListener only called inside the if (wired) return gate.
  • Focus trap Tab + Shift+Tab: both branches present, early-return on zero focusables, single-focusable degenerates to no-op (correct).
  • inert toggling on open/close: applied at the right times (issue is solely the ordering vs focus-restore in close()).
  • @media (max-width: 768px) gate on drawer + backdrop: correct, matches Option A spec.
  • PR description: no deferred items beyond cross-PR touch-action coordination already noted.

Setting `inert` on the drawer synchronously moves document.activeElement
to <body>, so the previous `drawerEl.contains(document.activeElement)`
guard always saw <body>, returned false, and skipped the restore.

Reorder close() so we:
  1. Decide whether to restore (prevFocus exists, in DOM, not in drawer)
  2. Call prevFocus.focus() FIRST
  3. Then apply inert + aria-hidden + class changes

Also drop the brittle `drawerEl.contains(activeElement)` half of the
guard; the simpler invariant 'prevFocus is still a real element outside
the drawer' is what actually matters.

Surfaced by adversarial review on PR #1184.
@Kpa-clawbot
Copy link
Copy Markdown
Owner Author

✅ Adversarial finding addressed: focus-restore vs inert ordering

Defect: close() applied inert to the drawer before attempting focus-restore. Setting inert synchronously moves document.activeElement to <body>, so the subsequent drawerEl.contains(document.activeElement) guard always saw <body> → returned false → restore was silently skipped.

Fix: reordered close() so we (1) decide whether to restore, (2) call prevFocus.focus() FIRST, (3) then apply inert / aria-hidden / class changes. Also dropped the brittle drawerEl.contains(activeElement) half of the guard — the simpler invariant ("prevFocus exists, is in DOM, is not a descendant of the drawer") is what actually matters.

  • File: public/nav-drawer.js:193-221
  • Commit: 6242c0c
  • Pre-merge gate: pr-preflight/scripts/run-all.sh origin/master → clean ✅

E2E test (d2) already gates this behavior — it parks focus on a sentinel button, opens the drawer, closes it, and asserts document.activeElement.id === '__nav_drawer_focus_sentinel' after close. Under the prior ordering this assertion would only pass by accident; under the corrected ordering it's load-bearing.

Copy link
Copy Markdown
Owner Author

@Kpa-clawbot Kpa-clawbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mesh-Operator Review

REQUEST CHANGES — 3 must-fix

I deploy ~15 nodes across rooftops + solar sites, run a regional mesh of ~40 users, and check CoreScope from a laptop at home, a Surface tablet in the truck, and an iPad mini on the roof. Reviewing this drawer through that lens: the idea (long-tail routes one swipe away on touch hardware) is fine, but the implementation has three operator-visible failure modes that will bite within a week of shipping.


M1 (must-fix) — Mouse pointers can open the drawer; design says they shouldn't

The whole justification for Option A in the PR body is "touchscreen laptop or tablet." Desktop mouse users already have the top-nav with every route visible — the drawer is explicitly not for them.

But onPointerDown only filters out non-left mouse buttons:

if (e.pointerType === 'mouse' && e.button !== 0) return;

It does not filter out left-clicks. So at any wide viewport, a mouse-down within 20 px of the left edge plus any rightward drag past the threshold opens the drawer. Real-world triggers I will hit at my desk:

  • Grabbing the browser scrollbar gutter when the page has overflow on the left side of the content area (less common but happens with dev-tools docked right and the window snapped left).
  • Selecting text in a left-aligned table cell that starts at x≤20 (the Packets table left column sits right at the edge on a maximized 1920-wide window).
  • Click-dragging on a leaflet map near the left edge — mousedown at x=10, drag the map right to pan, drawer opens over the map.

The fix is one line: gate the handler to e.pointerType === 'touch' || e.pointerType === 'pen'. That matches the stated persona and removes the false-positive class entirely. If you still want a mouse affordance, that's a hamburger button — not an edge-drag.

M2 (must-fix) — iPad Safari back-swipe DOES collide with this drawer

The PR body asserts: "iOS browser back-swipe (system left-edge gesture) still works — it's at the OS layer, above the page." That's true. The unstated half is: on iPad Safari (which is overwhelmingly >768 px and therefore inside Option A's enabled range), both gestures fire on the same swipe. The OS does its back-nav AND the page sees pointerdown/pointermove. Net result on iPad — which is the single most common tablet I see operators carrying into the field — is one of:

  • History exists → Safari navigates back, drawer animates open behind the navigation, user lands on the previous page with a drawer visible. Confusing.
  • No history (fresh tab on /#/packets) → back-swipe is a no-op, drawer opens. Now every "I want to go back" gesture summons the menu.

Option A was supposed to avoid this collision by being wide-only. It doesn't, because iPad is wide. Either:

  • Detect iPad/iPadOS UA and disable the edge handler (cheap, narrow), or
  • Move the trigger off the absolute edge — start from x≥24 px, leaving a 24 px iOS-reserved zone — or
  • Document this as a known limitation in the PR body and the user-facing affordance (so I know to use the close-drawer or the top-nav instead of swiping back).

Pretending the collision doesn't exist is the wrong answer; iPad in the truck is half my mobile usage.

M3 (must-fix) — body { touch-action: pan-y } is global, but Option A is wide-only

body { touch-action: pan-y; }

This is set unconditionally — including at viewports where the drawer is display:none and the JS open() early-returns. So on phones, where this PR explicitly does nothing, you've still disabled browser-level horizontal panning on the entire body. That cascades into every child element that doesn't set its own touch-action. Things I rely on at narrow widths:

  • Horizontally-scrollable code blocks / wide tables (Packets, Stats) — operators read these on the phone while driving to a site.
  • The hop-display strip and any future horizontal-scroll telemetry component.
  • Any page that embeds an iframe or leaflet map without its own touch-action override (leaflet sets its own, so the map proper is fine — adjacent containers may not be).

Either scope the rule to where the drawer actually lives:

@media (min-width: 769px) {
  body { touch-action: pan-y; }
}

…or move it onto a drawer-owned wrapper element instead of body. As written, a wide-only feature is making a global mobile change with no mobile benefit — exactly the kind of cross-cutting side effect that breaks operators on the device they actually use in the field.


Out-of-scope (file as followups, not blocking this PR)

  • The hardcoded ROUTES array in nav-drawer.js duplicates MORE_ROUTES in bottom-nav.js with only a "⚠️ keep in sync" comment guarding drift. First time someone adds /messages to the More tab and forgets the drawer, my phone and my Surface will show different long-tail menus and I'll waste 30 seconds wondering which one is right. Single source of truth (export the array from one module) is the obvious fix — out-of-scope here, but file it before the next long-tail route lands.
  • DOM is built unconditionally even at ≤768 px where the drawer is hidden. Minor; skip unless someone is paying for the bytes.

What I do like

  • Singleton + bind-count debug seam (mirrors the #1180 fix). That's the right pattern; CI will catch handler leaks before they ship.
  • inert + focus restore + reduced-motion handling — keyboard / a11y is taken seriously, not bolted on.
  • Velocity threshold + position threshold for settle. That's how a drawer should feel.
  • E2E asserts the singleton invariant across 5 SPA round-trips. Good gate.

Fix M1/M2/M3 and this is a solid ship for the touch-tablet persona it's actually written for.

— mesh operator

OpenClaw Bot added 2 commits May 10, 2026 00:30
Add failing assertions for Mesh-Operator review on PR #1184:

- (a) updated: edge-swipe must start at x=30 (24px iOS reservation)
- (f) narrow viewport: same x=30 baseline
- (i) NEW: mouse.down at x=10 must NOT open drawer
  (pointerdown handler must reject pointerType='mouse')
- (j) NEW: touch swipe from x=10 must NOT open drawer
  (first 24px reserved for iOS Safari back-swipe)

These tests fail against current main:
- (a): EDGE_PX=20, so x=30 is rejected → drawer doesn't open → assert fails
- (i): mouse pointerdown is currently accepted → drawer opens → assert fails
- (j): EDGE_PX=20 covers x=10 → drawer opens → assert fails

Green commit follows.
Three fixes from Mesh-Operator review on PR #1184:

1. nav-drawer.js: pointerdown handler now filters on pointerType.
   Only 'touch' and 'pen' open the drawer; 'mouse' is rejected at the
   top of the handler (before any edge math). Stops a stray mouse-down
   at the left edge from hijacking clicks on left-side widgets.

2. nav-drawer.js: edge trigger zone narrowed from [0, 20] to [24, 44].
   First 24px reserved for iOS Safari's system back-swipe gesture.
   Drawer activates on the next 20px (24-44px from the left edge),
   eliminating the iPad double-fire collision. EDGE_PX renamed in
   intent (still the upper bound), EDGE_MIN_PX added (lower bound).

3. style.css: 'body { touch-action: pan-y }' scoped to
   @media (min-width: 769px). At narrow widths the drawer is
   display:none anyway, so the global rule did nothing useful and
   blocked horizontal panning gestures the future gesture system
   (#1185) might want.

Tests from RED commit (fd629d0) flip to green:
- (a) edge-swipe at x=30→220 opens drawer
- (i) mouse drag at x=10 does NOT open drawer
- (j) touch swipe at x=10 does NOT open drawer (inside iOS reservation)
- (f) narrow viewport: same x=30 baseline
@Kpa-clawbot
Copy link
Copy Markdown
Owner Author

Mesh-Operator review — all 3 must-fix addressed

Followed TDD: failing assertions landed first (RED fd629d0), production fix landed second (GREEN 29c4783).

✅ Item 1 — pointerdown filter on pointerType

  • File: public/nav-drawer.js:201 (top of onPointerDown)
  • Code: if (e.pointerType !== 'touch' && e.pointerType !== 'pen') return; — runs before any edge-zone math.
  • Test: test-nav-drawer-1064-e2e.js step (i) — drives mouse.down / mouse.move / mouse.up from x=10 and asserts the drawer stays closed.
  • Commit: 29c4783

✅ Item 2 — iPad iOS back-swipe collision (Option B, narrowed zone)

  • File: public/nav-drawer.js:79-80 (constants) + public/nav-drawer.js:217-220 (handler)
  • Change: trigger zone moved from [0, 20] to [24, 44]. First 24px reserved for iOS Safari's system back-swipe, next 20px is the drawer trigger.
  • Tests:
    • (j) touch swipe from x=10 (inside reservation) does NOT open drawer.
    • (a) updated: touch swipe from x=30 → x=220 opens drawer.
    • (f) narrow viewport baseline updated to x=30.
  • Commit: 29c4783

✅ Item 3 — scope body { touch-action: pan-y } to wide viewports

  • File: public/style.css:3379-3389
  • Change: wrapped the rule in @media (min-width: 769px) { ... }. At ≤768px the drawer is display:none, so the global rule did nothing useful and was blocking horizontal panning gestures the future fix(#1062): gesture system — swipe rows, tabs, slide-over dismiss #1185 gesture system might want to claim.
  • No test (no observable behavior in headless, as briefed).
  • Commit: 29c4783

CI

Preflight

bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master → all gates pass (PII, branch scope, red commit, CSS-var, LIKE-on-JSON, sync migration).

HEAD

29c4783fix(#1064): GREEN — mesh-op review (pointerType, iOS edge, CSS scope)

Copy link
Copy Markdown
Owner Author

@Kpa-clawbot Kpa-clawbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kent Beck Gate: NEEDS-WORK — 1 item

TDD compliance — PASS

Walked origin/master..origin/fix/issue-1064-edge-swipe. Three red→green pairs visible:

  1. Initial: a810b1f (RED) → c192367 (GREEN). RED CI = failure, with real assertion failures (✗ (a) edge-swipe ... [data-nav-drawer] not in DOM, ✗ (e) backdrop ... [data-nav-drawer-backdrop] missing, ✗ (g) ... debug seam not exposed). True RED on assertions, not build errors. ✅
  2. Backdrop coord 21293a6: pure test-only fix (z-stack occlusion in test, not prod bug). No production code touched — exemption stands. ✅
  3. Focus restore ecfa215 (fix + d2 regression test, CI green) → 6242c0c (inert-ordering hardening, no test added). The d2 test already covered the behavior; 6242c0c is a defensive cleanup of the invariant. Behavior remains covered by d2. Acceptable, but the d2 test is not strengthened to demonstrate the inert-ordering bug independently. ✅ (with note)
  4. Mesh-op review fd629d0 (RED, CI=cancelled — superseded by next push) → 29c4783 (GREEN, CI=success). Logic-level verification: fd629d0 adds assertions for EDGE_MIN_PX=24 and pointerType filter; c192367 had EDGE_PX=20 with no pointerType gate, so the new (a) and (j) assertions would have asserted-failed against it. Real RED. ✅

Test quality — Six Questions, 1 finding

Anti-tautology, test (i) — mouse-down at left edge does NOT open drawer:

await wide.mouse.move(10, 400);
await wide.mouse.down();
await wide.mouse.move(220, 400, { steps: 12 });
await wide.mouse.up();
// assert drawer NOT open

The implementation has TWO independent gates that reject this drag:

  • if (e.pointerType !== 'touch' && e.pointerType !== 'pen') return; (the gate the test claims to exercise)
  • if (x < EDGE_MIN_PX) return; where EDGE_MIN_PX = 24 (rejects x=10 regardless of pointerType)

Revert the pointerType filter and the test still passes, because x=10 is inside the iOS reservation zone and the edge-zone gate alone rejects the drag. Test (i) does not actually fail when its named guard is removed — that's a tautology by Kent Beck's "show me the test that fails when this change is reverted."

Fix: start the mouse drag at x=30 (inside the [24, 44] trigger zone), so the EDGE_MIN_PX gate would NOT reject it; only the pointerType filter can. Then removing the filter genuinely flips the test.

await wide.mouse.move(30, 400);   // inside trigger zone
await wide.mouse.down();
await wide.mouse.move(220, 400, { steps: 12 });
await wide.mouse.up();

Other Six Questions — PASS

  • Could a wrong impl pass d2? d2 asserts document.activeElement.id === '__nav_drawer_focus_sentinel' — exact identity, not "anything outside body." A wrong impl that focused <body> or any other element would fail. ✅
  • Edge case at x=24 (zone boundary): not directly tested, but x < EDGE_MIN_PX semantics make x=24 the first accepted pixel. (a) at x=30 covers the inside; (j) at x=10 covers the outside. Boundary x=24 untested but low-risk given strict inequality. Note, not blocker.
  • Test names: behavior-driven ((a) edge-swipe at x=30→220 opens drawer flush at left:0, (d2) close() restores focus to previously-focused element, (j) touch swipe from x=10 ... does NOT open drawer). ✅
  • (j) tautology check: uses pointerType: 'touch', x=10 inside reservation. Remove EDGE_MIN_PX gate → x=10 inside [0, EDGE_PX] → drawer opens → test fails. Genuine. ✅

Verdict

NEEDS-WORK — 1 item: re-aim test (i) at x=30 so it actually exercises the pointerType filter rather than the EDGE_MIN_PX zone. Single-line change. Once fixed, this PR is ready to merge.

— Kent Beck (via subagent)

…ointerType filter

x=10 falls inside EDGE_MIN_PX=24 (iOS back-swipe reservation), so the
edge-zone gate alone already rejected the drag — the pointerType filter
was untested. Move x=30 (inside the 24-44 trigger band) so only the
pointerType filter can reject. Also bumps end x to 240 so total horizontal
movement still exceeds the velocity/distance threshold.
Copy link
Copy Markdown
Owner Author

@Kpa-clawbot Kpa-clawbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kent Beck Gate (cycle 2): PASS

Cycle 1 flagged test (i) as a tautology: mouse.move(10, 400) started inside the iOS back-swipe reservation (x < EDGE_MIN_PX = 24), so the edge-zone gate at nav-drawer.js:247 already rejected the drag before the pointerType filter at :234 had anything to prove. Removing the filter would not have flipped the test → no behavior coverage.

7cc589e moves the start coordinate to x=30 (inside the [EDGE_MIN_PX=24, EDGE_PX=44] trigger band) and bumps the end to x=240 to keep total horizontal travel above the velocity/distance threshold. Now:

  • Edge-zone math accepts the drag (24 ≤ 30 ≤ 44).
  • The drag fails the pointerdown handler only because e.pointerType === 'mouse' returns at :234.
  • Delete that one-line filter and the test fails → assertion gates the production behavior, not a coincidental zone reject.

Six-question check

  1. Show me the test that fails when this change is reverted. ✅ Revert :234 (if (e.pointerType !== 'touch' && e.pointerType !== 'pen') return;) → mouse drag from x=30→240 satisfies trigger zone + distance/velocity → drawer opens → assertion !open fails.
  2. Smallest test that catches the bug. ✅ Single Playwright mouse-drag with start coord inside the validated trigger band; complement test (j) covers the touch+reservation axis at x=10.
  3. Could a wrong implementation pass? ✅ Not anymore — the only remaining gate between mouse-drag and drawer-open is the pointerType filter under test.
  4. Untested edges. Pen pointerType + edge-zone is not asserted, but spec says pen IS allowed; the rejection-only suite for mouse is what #1064 needs. Acceptable.
  5. Name describes behavior.(i) mouse-down at left edge does NOT open drawer (pointerType=mouse must be ignored).
  6. Setup vs. assertion ratio. ✅ Three mouse calls + close-and-wait setup; assertion is one boolean. Clean.

CI

HEAD 7cc589e10994cbcb492c53f7dff736d61ae44686: Go Build & Test ✅, Playwright E2E ✅, Docker ✅. mergeable: MERGEABLE.

Anti-tautology debt cleared. Merge-ready from the TDD axis.

@Kpa-clawbot Kpa-clawbot merged commit 9b98486 into master May 10, 2026
6 checks passed
@Kpa-clawbot Kpa-clawbot deleted the fix/issue-1064-edge-swipe branch May 10, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[#1052] Task 5: Edge-swipe nav drawer

1 participant