fix(app): unstick iOS image picker and land reusable Maestro primitives

On iOS, tapping "+" → "Add image" in the Composer left an invisible
backdrop trapping touches after the native photo picker dismissed. Root
cause: the dropdown menu's onSelect fired while the RN Modal was still
completing UIKit dismissal, so PHPicker presented over a dismissing
Modal that never cleanly released.

DropdownMenu now defers the selected item's onSelect until Modal
onDismiss (UIKit-level completion) plus a short buffer on iOS, then
invokes it. Same DropdownMenu instance — no per-call opt-in.

Along the way, the Maestro repro surfaced a controlled-TextInput
character-drop race: fast inputText on a controlled input drops
characters mid-type. Direct-host and pair-link inputs are now
ref-backed uncontrolled inputs with stable testIDs.

New reusable E2E primitives under packages/app/maestro/:
- flows/land-in-chat.yaml — clearState + direct connect to
  127.0.0.1:6767, lands in the composer. Compose on this for any
  composer-level fixture.
- image-picker-repro.yaml — regression for the zombie-backdrop bug.

docs/MOBILE_TESTING.md gains the patterns: reach-the-composer via
land-in-chat, uncontrolled inputs for Maestro-typed fields, and the
dropdown → native-presenter dismissal timing on iOS.

Existing relay-pairing and relay-then-direct fixtures updated to use
stable testIDs instead of ambiguous text / point-based taps.
This commit is contained in:
Mohamed Boudra
2026-04-17 14:07:15 +07:00
parent adf2f5921f
commit 9a8b01a738
11 changed files with 224 additions and 49 deletions
@@ -0,0 +1,40 @@
# NOTE: Maestro requires a config section for every flow file, even when used
# via `runFlow`. Keep this aligned with the dev build application id.
appId: sh.paseo
---
- launchApp:
clearState: true
- runFlow: launch.yaml
- extendedWaitUntil:
visible:
id: "welcome-screen"
timeout: 30000
- tapOn:
id: "welcome-direct-connection"
- extendedWaitUntil:
visible:
id: "add-host-modal"
timeout: 10000
- tapOn:
id: "direct-host-input"
- eraseText
- inputText: "127.0.0.1:6767"
- assertVisible:
id: "direct-host-input"
text: "127.0.0.1:6767"
- tapOn:
id: "direct-host-submit"
- extendedWaitUntil:
visible:
id: "message-input-root"
timeout: 60000
- assertVisible:
id: "message-input-attach-button"
@@ -0,0 +1,50 @@
appId: sh.paseo
---
- runFlow: flows/land-in-chat.yaml
- tapOn:
id: "message-input-attach-button"
- extendedWaitUntil:
visible:
id: "message-input-attachment-menu"
timeout: 10000
- tapOn:
id: "message-input-attachment-menu-item-image"
- runFlow:
when:
visible: "Allow Full Access"
commands:
- tapOn: "Allow Full Access"
- runFlow:
when:
visible: "Allow Access to All Photos"
commands:
- tapOn: "Allow Access to All Photos"
- extendedWaitUntil:
visible: "Cancel"
timeout: 30000
- tapOn: "Cancel"
- extendedWaitUntil:
visible:
id: "message-input-root"
timeout: 10000
# If the dropdown Modal left an invisible backdrop mounted behind the picker,
# this tap is swallowed and the attachment menu never reopens.
- tapOn:
id: "message-input-attach-button"
- extendedWaitUntil:
visible:
id: "message-input-attachment-menu"
timeout: 10000
- tapOn:
id: "message-input-attachment-menu-backdrop"
+9 -1
View File
@@ -8,7 +8,15 @@ appId: sh.paseo
- eraseText
- inputText: ${PAIRING_OFFER_URL}
- tapOn: "Pair"
# Prove the input received the URL intact. If this fails, iOS keyboard input
# dropped characters and the controlled input path needs a source fix.
- assertVisible:
id: "pair-link-input"
text: ${PAIRING_OFFER_URL}
- tapOn:
id: "pair-link-submit"
- extendedWaitUntil:
visible: "New agent"
+1 -1
View File
@@ -8,7 +8,7 @@ appId: sh.paseo
- eraseText
- inputText: ${PAIRING_OFFER_URL}
- tapOn:
point: "75%,36%"
id: "pair-link-submit"
- extendedWaitUntil:
visible: "New agent"