Skip to content

test(ios): repro test for setting caret on adjacent thought (#4173)#4538

Draft
ethan-james wants to merge 14 commits into
mainfrom
ethan-james-test-4173-ios-repro
Draft

test(ios): repro test for setting caret on adjacent thought (#4173)#4538
ethan-james wants to merge 14 commits into
mainfrom
ethan-james-test-4173-ios-repro

Conversation

@ethan-james

@ethan-james ethan-james commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

#4173

Goal

Determine whether #4173 — tapping an adjacent thought within ~1s of a prior tap fails to move the caret — can be reproduced by an automated iOS/wdio test, so it can be landed as a regression test.

Approach

Isolated spec src/e2e/iOS/__tests__/caretAdjacentTap.ts, pinned to iOS 18 (iPhone 16 Pro Max). It seeds a/b/c and taps a then a target with a controlled inter-tap gap, asserting the cursor moved to the target. Taps are dispatched as a single atomic performActions chain (finger-sized contact, native context) so sub-100ms gaps are reachable — sequential mobile: tap calls have a ~751ms floor.

Finding: NOT faithfully reproducible by synthetic touch

Vectors ruled out: zero-radius performActions (iOS 17); fat-taps ±pressure (iOS 18); native-context performActions; native mobile: tap gestures at ~751ms; single-chain two-tap 0–800ms. The app has no ~1s tap guard (Editable.tsx handleTapBehavior/onFocussetCursorOnThought), so #4173 is a browser/WebKit-level focus issue that synthetic touches don't drive faithfully.

Next

Instrument the tap→setCursor path and measure the real inter-tap timing/handler flow on a device where a human can reproduce the bug, to learn the true trigger window and which code path drops the second setCursor.

Draft — reproduction investigation, kept open.

Adds an iOS/wdio regression test that attempts to reproduce #4173, where
tapping an adjacent thought within ~1s of a previous tap fails to move the
cursor. Introduces a `doubleTap` touch helper that fires two real touch taps
with a deterministic sub-second interval, since a hand-driven repro cannot
reliably tap fast enough.

The bug does not reproduce in the local iOS Simulator via WebDriver touch
injection; this test is intended to run on a real BrowserStack device in CI to
determine whether the failure reproduces on real hardware.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Vercel preview: https://em-mnzb56gjm-cybersemics.vercel.app

ethan-james and others added 10 commits July 7, 2026 12:41
Adopts the touch techniques proven in #4407 to give the #4173 repro a real
chance of failing on hardware:

- doubleTap now dispatches from the NATIVE_APP context using a finger-sized
  contact area (width/height/pressure). A zero-radius webview-context tap does
  not trigger Safari's touch-adjustment / rapid-tap focus handling and cannot
  reproduce the bug. Both element centers are resolved in the webview context
  before switching, so the sub-second interval stays deterministic.
- Moves the #4173 test into its own spec (caretAdjacentTap.ts) pinned to a
  dedicated iOS 18 capability (iPhone 16 Pro Max), mirroring #4407's split. The
  retargeting behavior fires on iOS 18 but not the suite's default iOS 17, which
  is why the earlier iOS 17 run gave a false pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The iPhone 16 has no Force Touch hardware, so the `pressure: 0.9` field made
WebDriverAgent attempt a force-press and fail the whole tap with "This device
does not support force press interactions". Keep the finger-sized contact radius
(width/height), which is the part that drives Safari's touch-adjustment
retargeting; pressure is not needed for it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Parametrizes the adjacent-tap spec over a range of inter-tap intervals
(120/200/300/500/800ms) so a single BrowserStack session probes the whole
window. #4173 occurs "within 1 second" of the first tap and may only surface for
a subset of intervals near WebKit's double-tap detection threshold.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Switches the adjacent-tap repro from performActions HID synthesis to Appium's
`mobile: tap`, following the established showEditMenu.ts pattern: a native tap
issued from the webview context with getBoundingClientRect coordinates is
processed through WebKit's touch/gesture recognizers - the path #4173 depends
on. Sweeps the inter-tap interval and logs the measured gap. Removes the now
unused doubleTap performActions helper.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
mobile: tap without an element uses absolute screen coordinates, so the previous
getBoundingClientRect (webview CSS) coords landed above the thoughts in the
Safari chrome and the taps missed (cursor stayed on the post-paste thought c).
Switch to getElementRectByScreen, which adds the native Safari content offset -
the same screen-coordinate space that landed correctly with performActions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The previous run passed at every interval, but the logged a->b gap was ~3s even
at interval 0ms: getElementRectByScreen switches to the NATIVE_APP context and
back, and doing that for b *between* the two taps injected ~3s of latency -
pushing the second tap far outside #4173's ~1s window, so the bug could never
surface.

Prime the keyboard with an initial tap on a (which also settles the layout that
opening the keyboard shifts), then pre-resolve BOTH screen coordinates up-front,
then fire the measured rapid sequence (tap a, pause interval, tap b) with no
context switch in between. Now the a->b gap reflects only intervalMs plus a
single mobile:tap execution, actually exercising the sub-second window.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The pre-resolve variant hit sub-second gaps but every tap missed (cursor stuck on
c): the two back-to-back NATIVE_APP context switches used to resolve the screen
coordinates disturbed the webview/keyboard state, so the coordinates went stale.

The Safari webview container's screen offset is constant, so read it once via a
single native switch, then compute each editable's screen center from its fast
webview-context rect plus that cached offset. This drops the priming tap and the
per-tap context switches: a's and b's rects are read fresh (b's just before its
tap, so it reflects the keyboard-open layout) with no switch between the two taps,
keeping the a->b gap in the sub-second window while landing the taps accurately.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sequential mobile: tap calls have a ~751ms floor, above WebKit's double-tap
window (~300-350ms), so they could never exercise the sub-350ms regime. Dispatch
both taps in ONE atomic performActions chain whose in-chain pause is the exact
inter-tap gap, sweeping [0,100,200,300]ms - the only way to probe the double-tap
window at two distinct points (a then b).

Coordinates use the proven cached-Safari-offset screen centers; both are resolved
up front while the keyboard is still closed, which is correct because both taps
fire within the sub-second chain before the keyboard opens. Finger-sized contact
(no pressure) drives touch-adjustment like a physical tap; short hold keeps each
tap crisp for rapid-tap detection.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…repro

The single-chain two-tap reproduces #4173's symptom (cursor stuck on a) only at a
0ms gap and passes at >=100ms, which is inconsistent with the manually reported
"within a second" window. That raises the question of whether the 0ms failure is
a faithful repro or a synthetic-input artifact (two ultra-fast touches coalesced
/ the second dropped).

Add a discriminating control: at the SAME 0ms timing, tap a->c (non-adjacent),
which per the issue must still move the cursor. If a->b fails at 0ms but a->c
passes at 0ms, the bug is adjacency-specific (genuine #4173); if a->c also fails,
the second tap is merely being dropped (artifact). Also sweep the adjacent gap at
25/50/75/100ms to map where the trigger boundary actually is. Every case logs the
received cursor value for diagnosis regardless of pass/fail.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…porary]

Add [#4173]-tagged console.info logging with high-resolution performance.now()
timestamps across the full tap handling path in Editable.tsx:
- native touchend/click listeners (earliest per-tap signal)
- handleTapBehavior (long-press bail, hidden/multicursor branch, or the common
  no-op case where the cursor is set via onFocus instead)
- onFocus (suppressFocus return, long-press skip, -> setCursorOnThought)
- setCursorOnThought (the unchanged-cursor guard return vs. the setCursor dispatch)

Purpose: on a real iOS device where a human CAN reproduce #4173, inspect via
Safari Web Inspector (filter "[#4173]") to see which handler fires (or fails to
fire) on the second tap, whether the second setCursor is suppressed by the guard
or never dispatched at all, and the true inter-tap interval. This determines the
real trigger window and mechanism, which no synthetic-touch vector reproduced.

Temporary investigation scaffolding - remove before merge.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@ethan-james ethan-james self-assigned this Jul 7, 2026
ethan-james and others added 3 commits July 7, 2026 15:35
…n; skip synthetic 0ms artifact cases

Adds a faithful, handler-level regression test for #4173 in
src/components/__tests__/Editable.ts. It fires touchend on an adjacent
thought while the keyboard is open (the state left by a prior tap) and
asserts the cursor moves. This reproduces the bug's real mechanism: the
touchend hits handleTapBehavior's 'no-op (cursor set via onFocus)' branch
and, since iOS does not fire onFocus on a rapid re-tap, the cursor never
moves. Marked it.fails to document the unfixed bug while keeping the unit
suite green; remove .fails once #4173 is fixed.

A pure synthetic-touch e2e cannot reproduce #4173 because WDA taps force
focus onto the target, bypassing the real-finger rapid-tap focus
suppression. Skips the two 0ms cases in the iOS synthetic spec
(caretAdjacentTap.ts) that only reflected a synthetic-input coalescing
artifact, keeping the passing 25-100ms cases and documentation of the
boundary.

Removes the temporary [#4173] console.info instrumentation from
Editable.tsx now that the mechanism is pinned.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…fails

The project's global `it` is typed via jest-compatible types that do not
declare vitest's `.fails`, so `lint:tsc` (and every build-dependent CI job:
Lint, Puppeteer, BrowserStack, Vercel) failed with TS2339. Rewrite the
#4173 handler-level test as a running, green characterization test that
asserts the current buggy outcome (cursor stays on 'a') with an explicit
instruction to flip the expected value to 'b' when #4173 is fixed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The prior single assertion (touchend while keyboard open does not move the
cursor) only documented the branch structure: deferring to onFocus is by
design, so on its own it did not demonstrate a bug, and it could pass green
even if the touchend handler were detached.

Add a positive control that fires the SAME touchend on the adjacent thought
with the keyboard CLOSED and asserts the cursor DOES move to it. The two
cases now differ only in keyboard state, isolating #4173: touchend is proven
to drive the cursor-setting chain (control -> 'b'), and the keyboard-open
deferral is what suppresses it (bug -> stays 'a'). Extracts a shared
tapAdjacentThoughtOnTouchEnd helper.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

1 participant