diff --git a/maestro/helpers/helpers.sh b/maestro/helpers/helpers.sh index 66d20946e..bcf4e6268 100644 --- a/maestro/helpers/helpers.sh +++ b/maestro/helpers/helpers.sh @@ -83,8 +83,17 @@ stop_recording() { # A wedged XCUITest driver escapes as UnknownFailure to main, so a flow-level `retry:` can never # catch it. Bound the wall clock here, then reset the device and retry once. +# +# Backstop only — the fatal-exception check in run_maestro catches the common hang within seconds. +# Must cover a cold driver bootstrap after a simulator reset (up to ~170s in CI on top of a ~35s +# flow, MAESTRO_DRIVER_STARTUP_TIMEOUT allows 240s), so it can't go much lower than this. +MAESTRO_FLOW_TIMEOUT="${MAESTRO_FLOW_TIMEOUT:-300}" -MAESTRO_FLOW_TIMEOUT="${MAESTRO_FLOW_TIMEOUT:-480}" +# Once main dies Maestro prints this and then idles (non-daemon threads keep the JVM alive) until +# the watchdog fires — in CI that was another ~4.5-6 min per hang. Nothing recovers after it. +MAESTRO_FATAL_PATTERN='Exception in thread "main"' +# Grace period after the fatal line so the stack trace reaches the log before we kill the JVM. +MAESTRO_FATAL_GRACE=10 # Outside maestro's 0-1 range and below the 128+N signal range. MAESTRO_INFRA_EXIT=90 @@ -125,10 +134,17 @@ run_maestro() { local waited=0 while [ "$waited" -lt "$MAESTRO_FLOW_TIMEOUT" ]; do kill -0 "$maestro_pid" 2>/dev/null || exit 0 + if [ -n "$log_file" ] && grep -qF "$MAESTRO_FATAL_PATTERN" "$log_file" 2>/dev/null; then + sleep "$MAESTRO_FATAL_GRACE" + kill -0 "$maestro_pid" 2>/dev/null || exit 0 + echo "::warning::maestro main thread died on $(basename "$flow") after ${waited}s — killing the lingering JVM (hung driver)" + break + fi sleep 5 waited=$((waited + 5)) done - echo "::warning::maestro exceeded ${MAESTRO_FLOW_TIMEOUT}s on $(basename "$flow") — killing it (hung driver)" + [ "$waited" -ge "$MAESTRO_FLOW_TIMEOUT" ] && + echo "::warning::maestro exceeded ${MAESTRO_FLOW_TIMEOUT}s on $(basename "$flow") — killing it (hung driver)" kill -TERM "$maestro_pid" 2>/dev/null || true sleep 10 kill -9 "$maestro_pid" 2>/dev/null || true diff --git a/maestro/helpers/prepare_ios.sh b/maestro/helpers/prepare_ios.sh index 56ed921c4..7540ab248 100644 --- a/maestro/helpers/prepare_ios.sh +++ b/maestro/helpers/prepare_ios.sh @@ -107,6 +107,25 @@ reduce_motion() { || echo "::warning::Could not set ReduceMotionReduceSlideTransitionsEnabled" } +# Every XCUITest hang in CI (`XCTPerformOnMainRunLoop work timed out`) came right after an +# inputText, i.e. with the software keyboard up. Keep the keyboard as plain as possible: no +# first-use "slide to type" overlay (a freshly booted/reset simulator shows it on the first +# keyboard), no prediction bar, no autocorrect/spellcheck popups. Same domain Appium writes. +configure_keyboard() { + echo "Configuring iOS Simulator keyboard..." + if [ -z "$SIMULATOR_DEVICE_ID" ]; then + echo "Error: SIMULATOR_DEVICE_ID not set" + return 1 + fi + xcrun simctl spawn "$SIMULATOR_DEVICE_ID" defaults write com.apple.keyboard.preferences DidShowContinuousPathIntroduction -bool true \ + || echo "::warning::Could not set keyboard preference DidShowContinuousPathIntroduction" + local key + for key in KeyboardPrediction KeyboardShowPredictionBar KeyboardAutocorrection KeyboardCheckSpelling KeyboardAutocapitalization KeyboardContinuousPathEnabled; do + xcrun simctl spawn "$SIMULATOR_DEVICE_ID" defaults write com.apple.keyboard.preferences "$key" -bool false \ + || echo "::warning::Could not set keyboard preference $key" + done +} + set_status_bar() { echo "Setting status bar on iOS Simulator..." if [ -z "$SIMULATOR_DEVICE_ID" ]; then @@ -147,6 +166,7 @@ report_slim_state() { start_simulator report_slim_state reduce_motion +configure_keyboard set_status_bar install_ios_app verify_installed_app \ No newline at end of file diff --git a/maestro/images/expected/android/image_url.png b/maestro/images/expected/android/image_url.png index 84137e764..c280ad840 100644 Binary files a/maestro/images/expected/android/image_url.png and b/maestro/images/expected/android/image_url.png differ diff --git a/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/Confirmation.yaml b/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/Confirmation.yaml index 7a9600401..a6c80b104 100644 --- a/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/Confirmation.yaml +++ b/packages/jsActions/nanoflow-actions-native/e2e/specs/maestro/Confirmation.yaml @@ -13,6 +13,13 @@ appId: "${APP_ID}" - tapOn: id: "container1" - inputText: "11" +# iOS: dismiss the keyboard first — every XCUITest hang in CI (viewHierarchy timing out) came with +# the keyboard up. Android-only hideKeyboard is a Back press, so it stays iOS-scoped. +- runFlow: + when: + platform: ios + commands: + - hideKeyboard - tapOn: "Show confirmation " - assertVisible: "Yolo" - assertVisible: "11" diff --git a/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_ios.yaml b/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_ios.yaml index 639f2af86..4ab4ffbfe 100644 --- a/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_ios.yaml +++ b/packages/pluggableWidgets/badge-native/e2e/specs/maestro/Badge_ios.yaml @@ -17,6 +17,9 @@ appId: "${APP_ID}" - inputText: text: "Maestro" - pressKey: Enter +# Dismiss the keyboard before reading the hierarchy — every XCUITest hang in CI (viewHierarchy +# timing out) came with the keyboard up. +- hideKeyboard - assertVisible: text: "Maestro" index: 1 diff --git a/packages/pluggableWidgets/gallery-text-filter-native/e2e/specs/maestro/Gallery_Text_Filter.yaml b/packages/pluggableWidgets/gallery-text-filter-native/e2e/specs/maestro/Gallery_Text_Filter.yaml index 94c3f334b..ab1f3ce38 100644 --- a/packages/pluggableWidgets/gallery-text-filter-native/e2e/specs/maestro/Gallery_Text_Filter.yaml +++ b/packages/pluggableWidgets/gallery-text-filter-native/e2e/specs/maestro/Gallery_Text_Filter.yaml @@ -21,6 +21,13 @@ appId: "${APP_ID}" - inputText: text: "Title 5" - pressKey: Enter +# iOS: dismiss the keyboard before reading the hierarchy — every XCUITest hang in CI (viewHierarchy +# timing out) came with the keyboard up. Android-only hideKeyboard is a Back press, so it stays iOS-scoped. +- runFlow: + when: + platform: ios + commands: + - hideKeyboard - assertVisible: text: "Title 5" index: 1 @@ -30,5 +37,10 @@ appId: "${APP_ID}" - inputText: text: "Title 100" - pressKey: Enter +- runFlow: + when: + platform: ios + commands: + - hideKeyboard - assertVisible: text: "No results found"