test(e2e): Phase 2 verte — S2/S5/S6/S7 débloqués, fixes #136 couverts, CI E2E fiabilisée - #138
test(e2e): Phase 2 verte — S2/S5/S6/S7 débloqués, fixes #136 couverts, CI E2E fiabilisée#138kherembourg wants to merge 34 commits into
Conversation
|
| Filename | Overview |
|---|---|
| purchasely/example/integration_test/tools/ci_run_e2e.sh | Rewritten to all-hard-gate model with portable run_with_timeout watchdog, --reporter expanded, ANR-clear preamble, and 4 new suites (inline_paywall, re_display, flow_dismiss, purchase_restore_android). Logic is sound. |
| purchasely/example/integration_test/tools/ci_run_e2e_ios.sh | iOS runner extended to 14 suites with same watchdog/hard-gate model; StoreKit Apple-bug exception logic (storekit_apple_sig vs storekit_other_failure) is correctly scoped to all-Apple-only failure runs. |
| purchasely/example/integration_test/tools/run_storekit_suite_ios.sh | Dart-marker-authoritative gate correctly requires both xcodebuild exit 0 AND S7-IOS-RESULT: PASS in the simulator unified log; log show fallback handles log stream buffering races. |
| purchasely/example/integration_test/helpers/e2e_start.dart | startWithRetry correctly gates retries on network-ish error substrings only; 180 s backstop timeout deliberately not classified as retryable to surface genuine hangs. |
| purchasely/example/integration_test/purchase_restore_ios_test.dart | S7-IOS-RESULT marker mechanism correctly uses _completedTests++ as last line of test body; manual run instructions in header still reference tap_purchase_ios.sh alongside xcodebuild, which silently fails now that RunnerIntegrationTests.m owns the CTA tap. |
| purchasely/example/ios/RunnerIntegrationTests/RunnerIntegrationTests.m | SKTestSession setup, CTA tap via XCUITest, and 420 s poll with XCTFail are correctly wired; disableDialogs eliminates SpringBoard confirmation sheet dependency. |
| purchasely/example/integration_test/interceptor_actions_ios_test.dart | S5/S6 correctly distinguish failed vs notHandled via AppLifecycleState transitions; load-proof assertion on presentation.type prevents silent fallback to bundled screen. |
| purchasely/example/integration_test/purchase_restore_android_test.dart | Honest-degradation suite: proves interceptor fires, then asserts restore fast-fails (≤20 s) without a real Play Store; all three outcome paths (resolved/timeout/PlatformException) are caught and asserted. |
| .github/workflows/e2e-android.yml | Flutter 3.44.0 pin fixes the AGP9/compileGroovy hang; step-level 50-min ceiling preserves log upload even on worst-case retry exhaustion. |
| .github/workflows/e2e-ios.yml | Mirrors Android workflow with 50-min step ceiling; branch trigger extended to feat/sdk-v6-migration to gate the current migration branch. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant CI as GitHub Actions
participant Script as ci_run_e2e_ios.sh
participant RWT as run_with_timeout
participant FT as flutter test
participant Driver as Driver script
participant Sim as iOS Simulator
CI->>Script: bash ci_run_e2e_ios.sh udid
loop Each suite (1-13)
Script->>Driver: "bash driver.sh udid &"
Script->>RWT: run_with_timeout flutter test
RWT->>FT: flutter test (set -m own PGID)
FT->>Sim: install + launch app
Driver->>Sim: idb tap / xcrun simctl launch
Sim-->>FT: Dart callbacks / events
FT-->>RWT: exit 0 or non-zero
alt timeout (600 s)
RWT->>FT: kill -TERM PGID then kill -KILL PGID
RWT-->>Script: exit 124
else pass
RWT-->>Script: exit 0
end
Script->>Driver: kill driver PID
end
Note over Script: Suite 14 StoreKit special path
Script->>RWT: run_with_timeout bash run_storekit_suite_ios.sh udid
RWT->>Script: xcodebuild test RunnerIntegrationTests
Script->>Sim: xcrun simctl spawn log stream flutter
Sim-->>Script: S7-IOS-RESULT PASS or FAIL
alt xcodebuild exit 0 AND PASS marker
Script-->>CI: exit 0
else Apple FB22237318 signature only
Script-->>CI: warning S7-iOS BLOCKED non-gating
else other failure
Script-->>CI: exit 1 gate
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant CI as GitHub Actions
participant Script as ci_run_e2e_ios.sh
participant RWT as run_with_timeout
participant FT as flutter test
participant Driver as Driver script
participant Sim as iOS Simulator
CI->>Script: bash ci_run_e2e_ios.sh udid
loop Each suite (1-13)
Script->>Driver: "bash driver.sh udid &"
Script->>RWT: run_with_timeout flutter test
RWT->>FT: flutter test (set -m own PGID)
FT->>Sim: install + launch app
Driver->>Sim: idb tap / xcrun simctl launch
Sim-->>FT: Dart callbacks / events
FT-->>RWT: exit 0 or non-zero
alt timeout (600 s)
RWT->>FT: kill -TERM PGID then kill -KILL PGID
RWT-->>Script: exit 124
else pass
RWT-->>Script: exit 0
end
Script->>Driver: kill driver PID
end
Note over Script: Suite 14 StoreKit special path
Script->>RWT: run_with_timeout bash run_storekit_suite_ios.sh udid
RWT->>Script: xcodebuild test RunnerIntegrationTests
Script->>Sim: xcrun simctl spawn log stream flutter
Sim-->>Script: S7-IOS-RESULT PASS or FAIL
alt xcodebuild exit 0 AND PASS marker
Script-->>CI: exit 0
else Apple FB22237318 signature only
Script-->>CI: warning S7-iOS BLOCKED non-gating
else other failure
Script-->>CI: exit 1 gate
end
Reviews (2): Last reviewed commit: "fix(e2e): make iOS review gates reliable" | Re-trigger Greptile
There was a problem hiding this comment.
Pull request overview
This PR expands and hardens the Flutter SDK v6 E2E test coverage (new Phase 2 suites for S2/S5/S6/S7 and regression guards for fixes from #136) while making CI E2E runs more reliable and diagnosable (portable per-attempt watchdog, retries, and stricter hard-gating).
Changes:
- Added new Android+iOS E2E suites for flow dismiss (S2), action interceptor outcomes (S5/S6), re-display source preservation (M2), modal swipe dismiss behavior (M1), and purchase/restore coverage (S7).
- Introduced shared
startWithRetry()to reduce CI flakes from transient network/TLS failures duringPurchasely.start(). - Reworked CI E2E runners/scripts to be hard-gated with per-attempt watchdogs and improved diagnostics; updated workflows (notably Flutter 3.44.0 for AGP9 compatibility on Android).
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| purchasely/example/ios/RunnerIntegrationTests/RunnerIntegrationTests.m | Adds a host UI test bundle to launch the app under SKTestSession for StoreKit E2E. |
| purchasely/example/ios/RunnerIntegrationTests/Info.plist | Adds Info.plist for the new RunnerIntegrationTests target. |
| purchasely/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme | Wires RunnerIntegrationTests into the scheme and attaches the StoreKit configuration reference. |
| purchasely/example/ios/Runner.xcodeproj/project.pbxproj | Adds the RunnerIntegrationTests target and includes Configuration.storekit as a resource. |
| purchasely/example/ios/Configuration.storekit | Defines local StoreKit products for S7 iOS purchase/restore testing. |
| purchasely/example/integration_test/helpers/e2e_start.dart | Adds startWithRetry() helper used by multiple suites. |
| purchasely/example/integration_test/user_attribute_listener_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/local_dismiss_handler_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/local_dismiss_handler_ios_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/interceptor_trigger_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/interceptor_trigger_ios_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/inline_paywall_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/inline_events_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/default_dismiss_handler_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/default_dismiss_handler_ios_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/default_dismiss_via_display_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/default_dismiss_via_display_ios_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/deeplink_cold_start_test.dart | Strengthens event assertions and switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/dart_android_bridge_test.dart | Switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/dart_ios_bridge_test.dart | Updates comments + switches SDK start to startWithRetry(). |
| purchasely/example/integration_test/re_display_test.dart | New Android suite guarding M2 (re-display preserves original source). |
| purchasely/example/integration_test/re_display_ios_test.dart | New iOS mirror suite guarding M2 (re-display preserves original source). |
| purchasely/example/integration_test/flow_dismiss_test.dart | New Android suite for S2 (flow display + dismiss). |
| purchasely/example/integration_test/flow_dismiss_ios_test.dart | New iOS mirror suite for S2 (flow display + dismiss). |
| purchasely/example/integration_test/modal_dismissible_ios_test.dart | New iOS suite guarding M1 and exercising swipe-dismiss for dismissible modals. |
| purchasely/example/integration_test/interceptor_actions_ios_test.dart | New iOS suite for S5/S6 (navigate action interceptor failed vs notHandled). |
| purchasely/example/integration_test/purchase_restore_ios_test.dart | New iOS suite for S7 (local StoreKit purchase + restore). |
| purchasely/example/integration_test/purchase_restore_android_test.dart | New Android “honest degradation” S7 suite (no Play Store). |
| purchasely/example/integration_test/tools/ci_run_e2e.sh | Makes all Android suites hard-gated; adds watchdog and improved diagnostics. |
| purchasely/example/integration_test/tools/ci_run_e2e_ios.sh | Makes all iOS suites hard-gated with StoreKit Apple-bug exception logic + watchdog. |
| purchasely/example/integration_test/tools/run_storekit_suite_ios.sh | New StoreKit iOS runner (xcodebuild + simulator log capture). |
| purchasely/example/integration_test/tools/tap_purchase.sh | Adds ANR-dialog recovery to reduce CI flakiness. |
| purchasely/example/integration_test/tools/press_back.sh | Adds ANR-dialog recovery to reduce CI flakiness. |
| purchasely/example/integration_test/tools/tap_content_desc.sh | New generalized Android UI driver for content-desc substring taps. |
| purchasely/example/integration_test/tools/flow_close_all.sh | New wrapper driver to tap action:close_all for flow dismiss on Android. |
| purchasely/example/integration_test/tools/re_display_driver.sh | New Android driver wrapper to close paywall twice for re-display suite. |
| purchasely/example/integration_test/tools/re_display_driver_ios.sh | New iOS driver wrapper to close paywall twice for re-display suite. |
| purchasely/example/integration_test/tools/tap_label_ios.sh | New generalized iOS idb driver to tap elements by AXLabel. |
| purchasely/example/integration_test/tools/swipe_dismiss_ios.sh | New iOS driver to send a fixed number of interactive swipe-down gestures. |
| purchasely/example/integration_test/tools/modal_dismissible_driver_ios.sh | New iOS driver wrapper to run swipe-dismiss twice for modal dismissible suite. |
| purchasely/example/integration_test/tools/interceptor_actions_driver_ios.sh | New iOS driver coordinating two “Login” taps with log-driven synchronization + re-foreground. |
| .github/workflows/e2e-android.yml | Updates Flutter version to 3.44.0 and adds step-level timeout. |
| .github/workflows/e2e-ios.yml | Adds step-level timeout to preserve diagnostics on failures/hangs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…start backstop timeout Three cheap Greptile P2 findings from PR #138: - swipe_dismiss_ios.sh: the no-frame fallback geometry (390x844, iPhone-SE proportions) undershoots the swipe endpoints by ~1% on an iPhone 15 Pro simulator (393x852), making dismissal less reliable. Bump to 390 852. - ci_run_e2e.sh (Android): add --reporter expanded to match the iOS runner (ci_run_e2e_ios.sh already has it), so a failing suite's log artifact shows per-test detail instead of a single "Some tests failed" line. - e2e_start.dart: add a 180s backstop `.timeout(...)` around each startWithRetry attempt so callers without their own Dart-level timeout (e.g. flow_dismiss_test.dart) can't hang for the full ~600s CI watchdog budget. A caller's own tighter timeout still wins. Left as the default TimeoutException (no "timed out" in its message) so it does NOT match _kNetworkNeedles and get silently retried like a network hiccup — a real hang should surface as a failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@greptileai review |
…ent assertion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…5/S6) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uites as hard gates Root fix: e2e-android.yml pinned flutter-version 3.24.x post-AGP9 (#130), breaking compileGroovy under Gradle 9 and surfacing only via flutter_tools' 12-minute per-test timeout, x3 retries, burning the whole 60-minute job on the first 1-2 suites. Bumped to 3.44.0 (e2e-ios.yml's 3.41.4 was already correct, left untouched). ci_run_e2e.sh / ci_run_e2e_ios.sh: - Portable per-attempt watchdog (run_with_timeout, no GNU coreutils dependency) wrapping every flutter test/xcodebuild invocation; two real bugs found and fixed via local testing (not just static review): an orphaned watchdog child holding a piped tee open (stalling every attempt for the full timeout), and an empty marker file defeating the 124 timeout-exit-code detection. - ::group::SUITE <name> attempt <n> / ::endgroup:: markers with exit code + duration on every attempt. - All suites promoted from best-effort/::warning:: to HARD gates (no || true, no continue-on-error) except one explicit, narrowly-scoped exception: the iOS StoreKit suite prints a loud "S7-iOS BLOCKED (Apple FB22237318)" marker and does not gate only when its failure matches that known, currently-open Apple/Xcode platform bug signature; any other failure of that suite gates normally. - Wired inline_paywall_test.dart (previously orphaned) plus the new re_display, flow_dismiss, purchase_restore_android/ios, modal_dismissible_ios and interceptor_actions_ios suites, adding 5 new driver-wrapper scripts (re_display_driver[_ios], modal_dismissible_driver_ios, flow_close_all, and a log-driven two-tap + re-foreground driver for interceptor_actions_ios). Local proof on emulator-5554: 2 suites passed end-to-end through the fixed script (user_attribute_listener, re_display — the latter also proving the new chained press_back.sh driver), plus a TIMEOUT=5 run showing 3 clean kill+retry cycles ending in a correctly gated failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…exception Fix 1: run_with_timeout backgrounds the suite command with job control (set -m) so it gets its own process group, then kills the group on timeout instead of just $cmd_pid — reaches gradle/dart/xcodebuild children that a single-PID kill orphaned. Portable: process-group creation via job control is core POSIX shell behavior, not a coreutils extra like setsid/timeout, so no per-runner branching is needed. Verified on the real emulator: 3 timed- out attempts leave zero leaked flutter/dart/gradle-client processes behind (only the pre-existing shared gradle daemon remains, as expected), and the normal/green path still completes correctly afterward. Fix 2: the StoreKit (FB22237318) non-gating exception now requires every failed attempt to match the Apple bug signature, not just any one of them — a mixed run (one attempt hits the known bug, another fails for a real regression) now gates instead of silently passing. Banner text states exactly how many attempts matched instead of a hardcoded "all 3 attempts". Fix 3: removed the newly-added || true instances (interceptor re-foreground, storekit cleanup) plus run_suite()'s pre-existing ones in both scripts, replaced with explicit non-fatal logging on failure. grep -c '|| true' on all three touched files is now 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…set -u Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ocus Task-9 CI run 29778283517: dumpsys window showed the IDENTICAL mCurrentFocus window IDs (ImmersiveModeConfirmation + "App Not Responding: com.google.android.apps.nexuslauncher") pinned across 5 different failing suites — one launcher ANR, under CI's resource-starved cold AVD, holds input focus for the rest of the job, so every driver BACK/tap lands on that dialog instead of the paywall. press_back.sh/tap_purchase.sh/ tap_content_desc.sh now force-stop the ANR'd package (never our own app) every poll iteration; ci_run_e2e.sh also pre-confirms the immersive-mode dialog to remove one contender. Bridge suite was never affected — it has no uiautomator driver at all, the T8/T9 comments only point at the separate interceptor/dismiss suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… P1) CI run 29778281515 was a confirmed false green: xcodebuild exited 0 while the Dart suite had actually failed `expect(capturedPayload, isA<PLYPurchasePayload>())` (interceptor never fired). RunnerIntegrationTests is a hostless XCTest bundle that only proves the app launched and exited/timed out, and run_storekit_suite_ios.sh's `exit $STATUS` never looked at the captured Dart log at all. purchase_restore_ios_test.dart now bumps a per-test completion counter as the last line of each test body and prints exactly one `S7-IOS-RESULT: PASS`/`FAIL (completed=N/M)` marker from tearDownAll. run_storekit_suite_ios.sh gates its exit code on BOTH xcodebuild exit 0 AND that PASS marker, hardens the log capture (longer drain + a `log show` fallback appended after the live stream is killed), and RunnerIntegrationTests.m now XCTFails if the app is still running after the 180s poll window instead of silently falling through to a pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…start backstop timeout Three cheap Greptile P2 findings from PR #138: - swipe_dismiss_ios.sh: the no-frame fallback geometry (390x844, iPhone-SE proportions) undershoots the swipe endpoints by ~1% on an iPhone 15 Pro simulator (393x852), making dismissal less reliable. Bump to 390 852. - ci_run_e2e.sh (Android): add --reporter expanded to match the iOS runner (ci_run_e2e_ios.sh already has it), so a failing suite's log artifact shows per-test detail instead of a single "Some tests failed" line. - e2e_start.dart: add a 180s backstop `.timeout(...)` around each startWithRetry attempt so callers without their own Dart-level timeout (e.g. flow_dismiss_test.dart) can't hang for the full ~600s CI watchdog budget. A caller's own tighter timeout still wins. Left as the default TimeoutException (no "timed out" in its message) so it does NOT match _kNetworkNeedles and get silently retried like a network hiccup — a real hang should surface as a failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CI autopsy showed the tap driver legitimately needs ~44s to find the CTA on a cold runner simulator while the interceptor-fire budget was 40s. Present 20s->60s, fire 40s->120s, outcome 90s->180s, host poll 180s->420s (each still bounded well under the 600s per-attempt watchdog). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…K path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2d1e807 to
951362d
Compare
Objectif
Rendre la Phase 2 E2E du SDK Flutter v6 verte et fiable : couverture des fixes Go/No-Go (PR #136), déblocage des scénarios S2/S5/S6/S7, et fiabilisation des jobs CI E2E (hang Android résolu à la racine).
Nouvelles suites (toutes en hard gate CI)
modal_dismissible_ios_test.dartmodal(dismissible:false)ignore le swipe + couverture du swipe interactif (dismissible:true)re_display_test.dart/re_display_ios_test.dartflow_dismiss_test.dart/flow_dismiss_ios_test.dartintegration_test_flow, flowintegration_test_v_1, preuve preload +PLYPresentation.flowId)interceptor_actions_ios_test.dartPLYInterceptResult.failedvs.notHandled, outcomes distincts (AppLifecycleState), ordre des callbacks assertépurchase_restore_ios_test.dart+Configuration.storekit+ hostRunnerIntegrationTests.purchased) + restore — voir « Limites »purchase_restore_android_test.dartDurcissement des suites existantes
helpers/e2e_start.dart: retry 3× + backoff sur erreurs réseau/TLS uniquement (flake TLS destart()en CI), adopté par les 22 suites.deeplink_cold_start_test.dart: assertion explicitePRESENTATION_OPENEDABSENT ; ordreDEEPLINK_OPENED/PRESENTATION_LOADEDvolontairement non contraint (divergence iOS/Android).inline_paywall_test.dart: suite orpheline désormais câblée en CI.CI (
e2e-android.yml,e2e-ios.yml,ci_run_e2e*.sh)flutter-version3.24.x → 3.44.0 (post-AGP 9/chore(android): validate Flutter 3.44 with AGP 9 #130,compileGroovyéchouait en 7 s maisintegration_testattendait son timeout de 12 min ×3 → job tué à 60 min).::group::SUITE <name> attempt <n>+ code retour + durée pour rendre tout futur hang diagnosticable.|| true/continue-on-errorajouté (et purge de ceux des scripts touchés).interceptor_actions).Preuves locales (sweep complet post-rebase)
fail=0(5 m 33, tentative 1 partout).storekit→ marqueur expliciteS7-iOS BLOCKED (Apple FB22237318)non-gating (comportement conçu) ;modal_dismissible(testdismissible:true) FAIL 3/3 local → arbitrage CI (sim iOS 26.5 local suspect, couche Flutter innocentée par revue de la sérialisation) ;interceptor_actions→ bug driver corrigé dans cette PR (ellipse Unicode +$SUITE_LOGnon bracé sousset -u).Limites connues (dites honnêtement)
.purchasedn'a jamais été observé sur la machine locale — bug Apple ouvert (SKTestSessionSKInternalErrorDomain Code=3, FB22237318, Xcode 26.5+, reproduit dans [Only on 26.3/26.4][in_app_purchase_storekit] iOS native tests failing with timeouts, SKInternalErrorDomain, and crashes flutter/flutter#184678). La suite est câblée et stricte (aucun échappatoire fake-green) ; le runner CI (Xcode plus ancien) peut ne pas être affecté.onPresentedréassigné sur handle post-preload ignoré ;PLYEventPropertiessans champs flow ; fallback_outcomeFromMapsur l'objet caché.Résultats CI sur le HEAD (
6cd504d)storekit-iosPASS tentative 1 : le Xcode du runner n'est pas affecté par FB22237318, S7 iOS est donc réellement prouvé en CI (.purchased+ restore).modal-dismissiblePASS (arbitrage : le FAIL local était la flakiness sim 26.5).dismiss-iosvert en tentative 2 (retry nominal).fail=0— après correction d'une cause CI-only : un dialogue ANR du launcher restait au premier plan de l'AVD froide et volait tous les inputs uiautomator (prouvé par IDs de fenêtres identiques inter-suites) ; les drivers force-stoppent désormais le package ANR à chaque poll.🤖 Generated with Claude Code