diff --git a/.github/workflows/_e2e-ios.yml b/.github/workflows/_e2e-ios.yml index 9a96882187..d6f5d97f1b 100644 --- a/.github/workflows/_e2e-ios.yml +++ b/.github/workflows/_e2e-ios.yml @@ -178,12 +178,15 @@ jobs: # flows' way. Never fails the leg. - name: Warm up (approve the deep-link confirmation once) env: + # Which simulator the fixture server opens deep links on (open-link-ios.js). + SIM_UDID: ${{ steps.sim.outputs.udid }} MAESTRO_DRIVER_STARTUP_TIMEOUT: '300000' run: | - maestro test -e APP_ID=${{ env.APP_ID }} e2e/warmup/ios-approve-open-link.yaml || echo "warm-up flow did not pass (non-fatal)" + maestro test -e APP_ID=${{ env.APP_ID }} -e SIM_UDID="$SIM_UDID" e2e/warmup/ios-approve-open-link.yaml || echo "warm-up flow did not pass (non-fatal)" - name: Run flows env: + SIM_UDID: ${{ steps.sim.outputs.udid }} # Passed through the environment so a tags value cannot break out of the # command line via `${{ }}` text substitution. TAGS: ${{ inputs.tags }} @@ -196,7 +199,7 @@ jobs: run: | set -euo pipefail # shellcheck disable=SC2086 # $TAGS is a list of flags and must word-split - maestro test -e APP_ID=${{ env.APP_ID }} $TAGS \ + maestro test -e APP_ID=${{ env.APP_ID }} -e SIM_UDID="$SIM_UDID" $TAGS \ --format junit --output e2e-report.xml \ --debug-output "$PWD/maestro-debug" --flatten-debug-output \ e2e/flows/ @@ -226,6 +229,19 @@ jobs: node scripts/e2e/junit-summary.mjs e2e-report.xml \ "ios · RN ${{ inputs.rn-version }} · iOS ${{ inputs.ios-runtime }}" + # The warm-up's logs land in ~/.maestro/tests. Copied into the workspace because + # upload-artifact roots the archive at the common ancestor of all paths: a $HOME path + # buried e2e-report.xml under work///, and the report job, which expects + # it at the artifact root, scored every iOS leg `missing-report`. + - name: Collect Maestro home logs + if: always() + run: | + if [ -d "$HOME/.maestro/tests" ]; then + rm -rf maestro-tests + cp -R "$HOME/.maestro/tests" maestro-tests + fi + + # Every path here must stay inside the workspace (see the step above). - name: Upload artifacts if: always() uses: actions/upload-artifact@v7 @@ -234,7 +250,7 @@ jobs: path: | e2e-report.xml maestro-debug/** + maestro-tests/** post-mortem/** device.log - ~/.maestro/tests/** if-no-files-found: ignore diff --git a/e2e/CONTEXT.md b/e2e/CONTEXT.md index c859d053ad..5a04f0dd61 100644 --- a/e2e/CONTEXT.md +++ b/e2e/CONTEXT.md @@ -41,13 +41,27 @@ How to run the suite and how to add a flow: [`README.md`](README.md). The CI mat - **Zero retries.** A flow is never re-run to turn it green — a retry hides exactly the race conditions this suite exists to catch. A flow that proves unstable gets quarantined (`tags: [flaky]`) with an issue, and drops out of the gate until it is fixed. The single - exception is transport, not behaviour: `e2e/shared/open-scenario.yaml` retries the - `openLink` command itself, because `simctl openurl` on hosted macOS runners has timed - out before the app received anything. What the app then shows is never retried. + exception is transport, not behaviour: on iOS the deep link is opened by the fixture + server (`POST /__open-link`, `e2e/fixtures/open-link.mjs`), which retries `simctl + openurl`, because on hosted macOS runners that command has timed out + (NSPOSIXErrorDomain 60) while the link still arrived seconds later. It cannot be done in + the flow: Maestro's `retry` and `optional` only catch `MaestroException`, and this + failure is an `IllegalStateException`, so a `retry` around `openLink` never ran a second + attempt. A link delivered twice is harmless (the screen is keyed on the scenario). What + the app then shows is never retried. - **iOS: the first deep link on a fresh simulator can raise "Open in app?", and the link after that confirmation never reaches JS.** The iOS leg runs `e2e/warmup/ios-approve-open-link.yaml` once before the suite to take that confirmation out of the real flows' way; the shared subflow still handles it defensively. +- **iOS: an unanswered "Open in app?" prompt outlives the app and hides it.** The prompt + belongs to SpringBoard: it survives `stopApp`, `clearState` and `launchApp`, and while it + is up the app is missing from the hierarchy, so `e2e-host-ready` never becomes visible. + On a slow `macos-15` runner the prompt came up only after the warm-up had stopped + waiting for it (no prompt 20 s after `openLink`), and that leg lost all 10 flows. + `launch-app.yaml` therefore taps Open on a prompt that is already up after launch and + relaunches. Open rather than Cancel, so the approval sticks. With no prompt the check + takes ~7 s per launch (Maestro's fixed lookup time for a `when: visible` that is false; + 7.5 s measured in CI), which did not show up in leg durations against runner variance. - v7 API only in the test app: `useVideoPlayer` + `VideoView` + `useEvent`, `player.seekTo()`. No `