Skip to content

fix(ios): read system dialogs from a throwing snapshot so the location prompt cannot kill the runner - #8

Merged
jochen-testingbot merged 1 commit into
tb-modsfrom
fix/ios-permission-helper-throwing-snapshot
Nov 13, 2026
Merged

jochen-testingbot merged 1 commit into
tb-modsfrom
fix/ios-permission-helper-throwing-snapshot

Conversation

@jochen-testingbot

Copy link
Copy Markdown
Collaborator

Problem

On a physical iPhone, the iOS location prompt with a map ("Precise: On", Allow Once / Allow While Using App / Don't Allow) made the XCTest runner exit on the next view-hierarchy request. Every later flow then failed its launchApp with Unable to set permissions … Connection reset (TestingBot session f3ffd402, FR8BOT).

SystemPermissionHelper.handleSystemPermissionAlertIfNeeded runs on every request and read SpringBoard's alerts with non-throwing XCUIElement APIs (.exists, .label, allElementsBoundByIndex, element(boundBy:)). The map part of the prompt is a remote view hosted by CoreLocationVanillaWhenInUseAuthPromptPlugin. When such an element cannot be resolved, these APIs record an XCTest failure instead of throwing, which ends testHttpServer. This is the same class of failure as #6.

Fix

  • ViewHierarchyHandler takes one throwing SpringBoard snapshot per request, or reuses the app hierarchy when SpringBoard is in front. The system dialog check and the status bars (fix(ios): read status bars from a throwing snapshot so a keyboard race cannot kill the runner #6) both use it.
  • New SystemDialogMatcher in MaestroDriverLib: pure functions over AXElement for permission dialogs and Apple Account alerts (fix(ios): dismiss Apple Account password alerts with Not Now #5).
    • Dialogs are matched on their title first, then their static texts, so a purpose string that mentions "Bluetooth" can't answer a location prompt.
    • Answer buttons are counted from the end of the dialog, so the duplicated "Precise: On" buttons can't shift the index.
  • The only live interaction left is the tap, made at the button's frame center through RunnerDaemonProxy. A failed snapshot is logged and skipped.
  • The interruption handler also reads from a throwing snapshot now, and taps the button by its label.
  • Dialog logging only fires when the dialog changes.
  • Simulator driver zips rebuilt.

Verification

  • MaestroDriverLib: 62 tests pass, including 12 new SystemDialogMatcherTests built on the prompt's real tree (duplicated map subtree, 3-button order).
  • ./gradlew :maestro-ios-driver:test :maestro-cli:test passes.
  • iPhone 17 simulator, iOS 26.3, with a small app that asks for location:
    • With location: unset, the flow taps the prompt itself.
    • With default permissions, the helper taps "Allow While Using App".
    • In both cases the later flows pass.
  • Physical iPhone 13, iOS 18.5 (192.168.3.202), customer's 6 FR8BOT flows, ios18 driver from Actions run 43401360652:
    • The helper logged Found alert 'Allow “FR8BOT” to use your location?' buttons: ["Precise: On", "Allow Once", "Allow Whi… and then Tapping 'Allow While Using App'.
    • One runner process for the whole run: no restart, no "Failed to resolve query", no "Connection reset".
    • The flows still fail on app-level assertions (view_pager, backButton ids on iOS). Those are in the customer's flows, not the runner.

Follow-ups (not in this PR)

  • fullScreenAlertHierarchy, keyboardHierarchy and findRecoveryElement still use .exists.
  • A launchApp for an app that isn't installed hangs for about 4.5 minutes and then fails the runner's test.

…n prompt cannot kill the runner

On a physical iPhone, the iOS location prompt with a map ("Precise: On", Allow Once /
Allow While Using App / Don't Allow) made the runner exit on the next view-hierarchy
request. Every later flow then failed its launchApp with "Unable to set permissions ...
Connection reset".

SystemPermissionHelper.handleSystemPermissionAlertIfNeeded runs on every request. It read
SpringBoard's alerts with non-throwing XCUIElement APIs (`.exists`, `.label`,
`allElementsBoundByIndex`, `element(boundBy:)`). These resolve each element again. The
map part of the prompt is a remote view hosted by
CoreLocationVanillaWhenInUseAuthPromptPlugin, and it appears twice in the tree. When such
an element cannot be resolved, these APIs record an XCTest failure instead of throwing,
which ends testHttpServer. Device logs show 8 matching queries into that process and then
the runner exiting, before the helper logged anything. The Apple Account check (#5) ran
first, on every request.

Now ViewHierarchyHandler takes one throwing SpringBoard snapshot per request. When
SpringBoard is in front, it reuses the app hierarchy. Both the dialog check and the status
bars (#6) use that snapshot. The matching logic moved to MaestroDriverLib as
SystemDialogMatcher: pure functions over AXElement, unit-tested with the prompt's tree.
Dialogs are matched on their title first, then their static texts. Answer buttons are
counted from the end of the dialog, so the "Precise: On" buttons cannot shift the index.
The only live interaction left is the tap, made at the button's frame center through
RunnerDaemonProxy. The interruption handler also reads from a throwing snapshot now, and
taps the button by its label.

Verified on an iPhone 17 / iOS 26.3 simulator with a small app that requests location.
With `location: unset`, the flow taps the prompt itself while hierarchy reads continue.
With default permissions, the helper taps "Allow While Using App" and the next flow
passes. The runner log has no "Failed to resolve query". The simulator prompt has no map,
so it does not reproduce the original crash; that still needs a physical device.

Also rebuilds the simulator driver zips.
@jochen-testingbot
jochen-testingbot merged commit 0dad2ab into tb-mods Nov 13, 2026
17 of 20 checks passed
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