Fix regressions in gesture input (#4521 & #4536)#4711
Conversation
|
|
Arrange no longer performs a New Thought gesture and 20 keystroke-typed thoughts in a test that is itself about gestures. Seed the outline with the sanctioned paste helper and scroll with scrollTo, so setup cannot be broken by the input features the test is not covering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commands execute when the touch is released, but the test previously did all of its checking mid-drag and released the touch in cleanup, after the last assertion. A gesture that wrongly re-activates and fires a command on release was therefore invisible to it. Release inside the test body and assert that the exported outline is unchanged. The post-scroll tail of the touch now draws New Thought (rd) so that a wrongly re-activated gesture executes a command with a user-visible effect: pre-fix, an empty thought is created and the export comparison fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comparing the trace element's className couples the test to generated styling classes: any styling refactor fails the test with no change in behavior, and the failure message is two long, visually identical class strings. Assert what the user would see instead: the gesture menu does not appear while the touch is held, and (from the previous commit) no thought is created on release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two requestAnimationFrame waits named no condition. The first one is replaced by waiting for what it was actually for: the page has visibly scrolled after the upward move, which both asserts the desired scroll-zone behavior and guarantees the browser has taken over the touch. The second is removed: the assertions that follow no longer read render output mid-touch — the gesture menu check needs no settling (pre-fix the menu renders during the multi-segment moves; post-fix no state change is ever initiated), and the export comparison runs after release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ment it Helper files are named after what they export. Expand the doc comment to say why the navigator.webdriver spoof is justified (loading states cannot exist under test timing), what phase it belongs to (arrange), and why it needs no explicit cleanup (each test gets a fresh incognito context and page). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gesture menu check runs after the touch is released, so it can only catch a menu that is stuck open; the selection assertion is what detects the cybersemics#4521 regression. Verified against the pre-fix implementation on a local simulator: the hijacked handle drag collapses the selection to an empty string while the menu check still passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-commit red validation showed the pre-fix failure had become timing dependent: re-activation after responder termination always updates the gesture store (showing the trace), but whether the re-tracked sequence forms an executable command or raises the gesture menu varies from run to run. Assert the trace's computed visibility — the user-visible form of the one signal that fires on every re-activation — alongside the release-time export comparison that catches command execution when it occurs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
raineorshine
left a comment
There was a problem hiding this comment.
Thanks for the submission!
The iOS gesture test looks good to me.
I'm challenged a bit by the puppeteer test. Maybe finding a narrower way to stub the loading timing will make it okay. Still, there is a very complex relationship between the loading indicator, timing, and MultiGesture.
I thought that the pointerEvents: 'none' fix decoupled the MultiGesture from the EmptyThoughtspace component, but this test shows that gestures still have a real dependency on the LoadingEllipsis. These components are totally unrelated, so I wish this wasn't the case.
I don't have a clear solution to the coupling here, but wanted to articulate my thoughts.
There was a problem hiding this comment.
This seems overly broad. Exactly which loading states does this test depend on? Maybe there is a specific testFlag that can be used to add a delay or override an animation duration. Need more of a scalpel than a hammer here. By disabling navigating.webdriver completely it is affecting many other testing behaviors.
Forcing navigator.webdriver to false in a test feels wrong conceptually anyway. It's not like mocking a value or function with a dummy value for a test. It is making it the exact opposite of its intent. This could lead to some extremely confusing debugging scenarios.
Fixes #4521 & #4536
Summary
Fixes regressions introduced by #4038 where gestures could activate after a touch began in an abandoned region. Also includes regression tests for #4521, #4536 and the original #3887.
Root cause
A touch beginning near native text selection or in the scroll zone correctly set
abandon = true.However, if React Native Web terminated the responder while the physical touch remained active,
onPanResponderTerminatecalledreset(). This unconditionally clearedabandon.The responder could then be granted again during the same touch, allowing subsequent movement to activate the gesture menu or execute a command.
Fix
Remove
onPanResponderTerminatefromMultiGesture. The original #4038 behavior remains protected by itspointercancelfallback andonPanResponderTerminationRequestlogic.Regression tests
iOS:
keeps native text selection active without opening the gesture menu when dragging an end handle(#4521)Types a sentence, selects the first word by DOM range (
setSelection, the sanctioned driver backdoor — WebKit shows real selection handles for it), then drags the native end handle downward with a W3C touch action. Asserts the gesture menu did not appear and the selection extended (one two…) rather than collapsing.The menu check runs after the touch is released, so it can only catch a menu stuck open (the #3887 failure mode); the selection assertion is what detects this regression — pre-fix, the hijacked handle drag collapses the selection to
""while the menu check still passes. This is documented in the test.Red/green proof: verified on a local iOS Simulator (iPhone 16, iOS 26.5, Appium/XCUITest): fails pre-fix at the selection assertion (
Expected substring: "one two" / Received: "", consistent across 6 runs), passes on this branch. Note: the TDD workflow's iOS job on this PR reported validation but never actually ran the test — fork PRs receive no BrowserStack credentials and the config crash was miscounted as a red failure. That workflow defect is filed as #4744; the local simulator runs above stand in as the red-side evidence.Puppeteer:
does not activate a gesture that starts in the scroll zone(#4536)Seeds 30 thoughts with
paste, scrolls partway down, then performs a single touch that starts in the right-edge scroll zone: swipes up (and asserts the page actually scrolls — this wait also guarantees the browser has taken over the touch, the precondition for the bug), then draws New Thought (rd) with the rest of the touch, and releases inside the test body. Three assertions, each on user-visible state:[data-testid=gesture-trace]is0). Any wrongful re-activation updates the gesture store and shows the trace, even when the re-tracked swipe fragment doesn't form a command — this is the deterministic mid-touch signal.rdcreates a thought and this comparison fails with a readable outline diff.Red/green proof: red side verified deterministic — 3/3 pre-fix runs fail at the trace-visibility assertion (
expected '1' to be '0'); when the re-tracked fragment forms a command, the export comparison additionally shows the created thought. Green 3/3 on this branch. Every intermediate reshaping of this test was also matrix-checked red/green per commit.Puppeteer:
releases a gesture when its loading target unmounts(#3887)This one is a guard, not a regression test — it passes on the base branch too. The
onPanResponderTerminatehandler this PR removes was originally added for #3887 (gesture menu stuck when the touched element unmounts mid-gesture). This test holds a gesture on the loading indicator with production startup timing, waits for the indicator to unmount, releases, and asserts the menu is gone — proving the removal does not reintroduce #3887. The red side of this PR is carried by the two tests above.Test helper changes
src/e2e/puppeteer/helpers/gesture.ts— rewritten around a newstartGesture()API returning anActiveGesture(move,end). This allows one physical touch to be composed in phases — swipe, wait for an observed condition (e.g. real scrolling), swipe again, release — which the scroll-zone test requires and the old single-shotgesture()could not express.move()accepts per-callsegmentLength;startGestureacceptstarget/xStart/yStart. The existinggesture()remains as a thin wrapper, unchanged for existing tests.src/e2e/puppeteer/helpers/reloadWithProductionTiming.ts(new) — the app zeroes animation durations whennavigator.webdriveris present, which makes loading states too short-lived to interact with. This helper spoofsnavigator.webdriverand reloads so the [Mobile] Transparent layer is shown when user dismisses a stuck Gesture Menu with swipes up #3887 test can hold a gesture on the loading indicator. It is an arrange-phase backdoor; the spoof needs no cleanup because every test runs in a fresh incognito context. Justification and constraints are in its doc comment.src/e2e/iOS/helpers/getSelectionEndHandlePosition.ts(new) — computes the native screen coordinates of WebKit's selection end grab handle from the DOM range's client rect plus the native content / visual viewport origin, so the iOS test can grab the real handle. Throws descriptively if there is no selection.src/e2e/iOS/helpers/keyboard.ts—type()switched from WDIOsendKeysto W3C key actions (per-characterkeyDown/keyUp), so typed text goes through real key events while editing.src/e2e/browserEnvironment/helpers/getSelection.ts— addedtoString()to read the currently selected text (used by the iOS selection assertion).CI: TDD workflow
tdd.yml's base-branch (red) runs now check out changed e2e helper/support files from the PR head along with the test files. Test support is part of the test: without this, the base-branch run fails to compile against the newstartGestureAPI and the red phase proves nothing about behavior. (Broader hardening of the red-phase validation itself — it currently trusts exit codes — is tracked separately in #4744.)Demonstration videos
Recorded from the Puppeteer scroll-zone test session (emulated iPhone 15 Pro), same steps in both: swipe up from the right-edge scroll zone, then draw New Thought (
rd) with the same touch, then release.🔴 Before the fix — the swipe scrolls, then wrongly re-activates and creates a thought on release
scroll-zone-before-fix.mp4
What to watch for: the page scrolls (correct), then after the scroll steals the touch the gesture re-activates — and on finger-lift an empty thought appears under "thought 30".
🟢 After the fix — the same swipe only scrolls
scroll-zone-after-fix.mp4
The page scrolls and nothing else happens: no trace, no menu, no thought created on release.