Skip to content

Play a tap again only after a cancelled contact, and look twice before doing it - #206

Merged
arun279 merged 2 commits into
feat/expo-nativefrom
fix/retap-settle
Oct 23, 2026
Merged

arun279 merged 2 commits into
feat/expo-nativefrom
fix/retap-settle

Conversation

@arun279

@arun279 arun279 commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

What happened

#204's run 39893036064 failed ui-screenshots-ios-dark (job 120217387461) on Element not found: Text matching regex: Profile. Lane health read tap proxy checked 10 taps, matched playback for 10, saw 1 cancels, tapped again 1. It was the first time the generalized re-tap fired in CI, and it double-fired.

  • The re-tap: tap at 187, 502: tapped again, … backboardd: touch down without previous range event: 2 under [... "profile-to-settings" "Settings" ...], ignoring changes to frame. testmanagerd logged service canceled at 21.263, and backboardd set the touch down at 21.533, 270 ms later.
  • The first touch landed:
    • backboardd logged contact 2 presence: touching at 21.533 and presence: none at 21.542.
    • The app dispatched both UIEvents to its window at 21.540 and 21.552, then logged send gesture actions at 21.557.
    • The proxy looked at the hierarchy from 21.62 to 22.04, before the push showed, found the row unchanged, and tapped again at 22.08.
    • Settings was pushed twice. The failure's screenshot shows a back button reading "Settings", so lib/back.yaml's tap on "Profile" found nothing.
  • It is not the first late touch to reach the app. The earlier TV shows switch case, run 39740669724 and the switch-late fixture, logged the same lift (presence: none at 07:48:25.184) and the same two UIEvents, and that switch did not flip.

Design, replacing the one introduced in #178

The proxy plays a tap again only when the simulator log proves the tap delivered no press and nothing on screen changed. Two backboardd signatures appear in a tap's playback window:

Signature What the app gets Re-tap?
A cancelled contact: canceling paths contacts: [… touching …]. The digitizer went away while the contact was still down, with no lift before the cancel. The touch, cancelled. No press fires. This is #173's Connect case. Yes, if the elements under the point are unchanged
A late touch: touch down without previous range event after testmanagerd's service canceled The whole touch, late. Whether the control acts is unknown: the Settings row acted, the TV shows switch did not No, never
  • Cancelled contact: a re-tap still needs the elements under the point to be unchanged on two looks: right after the tap, and 2 s later. A cancelled contact can still change what it touched; the switch in run 39722599952 toggled on one. The second look is defence in depth for a change that takes over a second to show on a slow host. It reads the hierarchy only after a cancel and costs 2 s on the rare tap it re-plays. The test below shows it holding back a re-tap.
  • Late touch: it is still recorded (cancel: true, with the reason the touch may still have reached the app: …) and still named by maestro-failure.mjs when a flow fails. A late-touch flake therefore fails the lane with its cause named instead of being resent. The proxy no longer reads the hierarchy for one.

The repository's docs never listed late touches as a re-tap trigger. The only description of them was the comment in scripts/simulator-touch.mjs, which claimed the control does not act. That comment now says what the two cases show.

Tests

  • Late touches never get a second tap: the TV shows switch (switch-late.txt) and Profile's Settings row (settings-late.txt, this run's playback lines) return no re-tap with the late-touch reason, even with the hierarchy unchanged. The tab bar's late touch (tab-late.txt) is the same. Through the proxy, Settings' late touch passes once with no extra hierarchy read. maestro-failure.test.ts still names a late touch.
  • Cancelled contacts get one: Connect (connect-cancelled.txt), Sign out, the Profile sheet's Done and an unchanged tab bar tab are re-tapped.
  • Changes still block a re-tap: the switch that toggled despite its cancel is not re-tapped, and neither is a tab that became selected. A cancel whose switch change shows only on the second look is not re-tapped either.

Mutation check:

Break Caught by
Late touches re-tapped again (the decision uses every drop signature) both late-touch tests
The proxy reads the hierarchy for a late touch the proxy's late-touch test
Cancels never re-tapped the Sign out, Done and Connect tests
No second look the second-look test and the Done proxy test

Footprint

No product code changes. CI and tooling: scripts/simulator-touch.mjs and scripts/tap-proxy.mjs, about 40 lines added. Tests: one fixture plus the cases above.

pnpm check passes on Node 22.23.4.

…ed twice

In #204's run 39893036064 the tap proxy re-tapped Profile's Settings row,
and Settings opened twice: the second copy's back button read "Settings",
so the flow's tap on "Profile" failed. The simulator had logged the first
touch as late (touch down without previous range event, after the
touchscreen was cancelled), but the app received it at 03:15:21.55 and
acted on it. The proxy's hierarchy read at 21.62 to 22.04 still showed the
Settings row unchanged, because the push had not shown yet, so it tapped
again at 22.08.

The proxy now compares a second hierarchy read 2 s after the first look
and taps again only if both are unchanged; a cancel it re-taps costs 2 s
more. The run's playback lines and the row before, just after and 2 s
after the tap are a new tap proxy test.
…ouch

Both late touches on record reached the app: run 39740669724's TV shows
switch did not flip, and run 39893036064's Settings row opened Settings,
which the re-tap then opened a second time. A touch backboardd sets down
after testmanagerd cancelled the touchscreen therefore leaves the outcome
unknown, and resending on an unknown outcome is what the re-tap must never
do. The proxy now plays a tap again only when backboardd cancelled a
contact that was still down ("canceling paths ... touching"), which
delivers no press. A late touch is still recorded and still named by the
failure classifier, so a flow it breaks fails with its cause, but it no
longer reads the hierarchy or earns a second tap.

The second look stays for cancels: a cancelled contact can still change
what it touched, and the change can take over a second to show.
@arun279 arun279 changed the title Look again before re-tapping, so a late touch that landed is not played twice Play a tap again only after a cancelled contact, and look twice before doing it Oct 23, 2026
@github-actions

Copy link
Copy Markdown

Pull request footprint

measurement base to head
Product code lines in core/src, native/src, native/app, and native/modules +0
Test lines in test, packages/*/{test,__tests__,e2e}, and .maestro +84
CI and tooling lines in .github and scripts +39
Product comment lines identified by a comment prefix +0

User-delivered artifact sizes

measurement base head delta
Expo iOS JavaScript bundle, raw file 5.45 MB 5.45 MB 0 B
Expo Android JavaScript bundle, raw file 5.48 MB 5.48 MB 0 B
Firebase tester APK, arm64-v8a and all densities 41.16 MB 41.16 MB 0 B
Play download estimate, XXXHDPI arm64-v8a English Android 15 19.83 MB 19.83 MB 0 B

Most complex product function

measurement base head
Cyclomatic complexity, oxlint complexity 10 10
Cognitive complexity, Biome noExcessiveCognitiveComplexity 15 15

@arun279
arun279 merged commit cc7c8f5 into feat/expo-native Oct 23, 2026
24 checks passed
@arun279
arun279 deleted the fix/retap-settle branch October 23, 2026 04:09
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