Skip to content

fix: Optimize alert operations - #1193

Merged
mykola-mokhnach merged 15 commits into
masterfrom
alert-text
Aug 3, 2026
Merged

fix: Optimize alert operations#1193
mykola-mokhnach merged 15 commits into
masterfrom
alert-text

Conversation

@mykola-mokhnach

@mykola-mokhnach mykola-mokhnach commented Jul 31, 2026

Copy link
Copy Markdown

Summary

Reworks FBAlert's alert detection/interaction pipeline to minimize accessibility round trips and to use typed exceptions instead of BOOL + NSError**, consistent with how the rest of the routing layer already reports errors.

Started from a narrow bug — detectionSnapshot re-scanning the whole app even when an alert was already constructed from a known parent element — and widened after finding the constructor causing that bug (alertWithElement:) had no real reason to exist, plus several other correctness/performance issues in the surrounding code.

Changes

Detection: one snapshot, walked in-memory

  • XCUIApplication+FBAlert: dropped the XCUIElementQuery-based fb_alertElement/fb_alertElementFromSafariWithScrollView: (each round trip could cost seconds while the target app's main thread is busy, e.g. a blocked JS alert in Safari). fb_alertSnapshot now takes one application snapshot and walks it entirely via enumerateDescendantsUsingBlock: — no further AX round trips.
  • Fixed a real bug in that walk: fb_findAlertSnapshotInApplicationSnapshot: was picking whichever of {Alert, Sheet, ScrollView} it hit first in tree order, with no type priority — a Sheet or ScrollView elsewhere in the tree (e.g. springboard's own UI) could permanently shadow the real Alert, since only one candidate was ever kept. Now it collects candidates by type in a single pass and applies real priority: an Alert wins outright; otherwise the first Sheet that passes the iPad popover-containment check; otherwise the first ScrollView that resolves to a genuine Safari web alert.

Interaction: one detection snapshot + at most one targeted element resolution

  • alertSnapshotFromApplication: is the sole detection path (tries systemApp, then self.application if different, then the iOS 18+ limited-access-prompt app).
  • elementForSnapshot:inApplication: turns an already-found snapshot into a live, tappable element via a single targeted fb_uid-predicate query instead of re-walking the tree with an attribute/type query. (fb_stableInstanceWithUid: couldn't be reused for this — it short-circuits and returns self whenever called on an XCUIApplication instance, so the underlying uid-predicate query is replicated directly.)
  • isPresent/text/buttonLabels need no element resolution at all now. accept/dismiss/clickAlertButton:/typeText: resolve only the specific button/field they're about to tap, not the whole alert container — except when a custom class-chain selector is configured, which still needs a live container to query against.
  • No caching anywhere: every call re-resolves against the live UI. Every resolution path is hardened against FBStaleElementException (the UI can change mid-resolution), folding it into a plain "not present" outcome instead of letting it propagate uncontrolled.

iOS < 18 compatibility fix (found via CI)

CI failed only on the Min-Xcode jobs (iOS 17.5), consistently and reproducibly, with a large cascade of unrelated test failures following FBAlertTests. Root cause: the snapshot-based button lookup (buttonSnapshotsInAlertSnapshot:) is bounded by FBConfiguration.snapshotMaxDepth, measured from the application root. The previous XCUIElementQuery-based code resolved the alert element live first and queried buttons from that element, which gets its own fresh depth budget. On iOS 17.5, some system alert button hierarchies apparently sit deep enough from the app root to exceed that budget (while the alert's own type/text near the top of its subtree is still reachable), so accept/dismiss silently found no button, left the alert on screen, and broke every subsequent test in the run.

Fix: accept/dismiss/clickAlertButton:'s default (non-custom-selector) button lookup now branches on @available(iOS 18.0, *) — the snapshot-only fast path on 18+, and on <18 it resolves the live alert element first and queries buttons from it (restoring the old depth-budget-reset behavior) via a new fb_buttonInAlertSnapshot:inApplication:preferLast:/matchingLabel: helper.

API: exceptions instead of BOOL + NSError**

  • accept, dismiss, clickAlertButton:, clickElementMatchingClassChain:, and typeText: are now void and @throw one of three new typed exceptions (FBAlertNotPresentException, FBAlertActionFailedException, FBAlertSetTextFailedException), wired into FBExceptionHandler alongside the existing exception types (FBStaleElementException, FBTimeoutException, etc.) so they map to the right HTTP status automatically.
  • FBAlertViewCommands shrank accordingly — no more manual NSError plumbing or redundant isPresent pre-checks duplicating the presence check the action methods already do internally.
  • Removed alertWithElement: — its only two callers (FBAlertsMonitor, FBPasteboard) were pre-resolving the interactive element themselves before wrapping it, defeating the point of the fast detection path. Both now use alertWithApplication: + isPresent/accept, resolving the interactive element lazily only if something actually needs tapping.
  • alertElement is now fully private (lives only in FBAlert's own class extension) since nothing outside FBAlert.m needs it anymore.
  • Added clickElementMatchingClassChain: (public) to replace FBSession's direct use of alert.alertElement for the autoClickAlertSelector feature.
  • XCUIApplication+FBAlert gained fb_limitedAccessPromptApplication (cheap .state-gated app lookup) so callers can avoid alert resolution entirely when that prompt process isn't foregrounded.

Tests

  • Updated FBAlertTests, FBSafariAlertTests, and FBIntegrationTestCase.clearAlert for the new void-throwing signatures (XCTAssertNoThrow/XCTAssertThrows instead of checking a returned BOOL/NSError).
  • Removed testAlertElement, which exercised the now-private alertElement accessor directly with no public equivalent; other tests already cover the underlying resolution via accept/dismiss/text/buttonLabels.

Test plan

  • WebDriverAgentLib and IntegrationTests_1 build cleanly.
  • Manually found and fixed the Alert/Sheet/ScrollView priority bug above.
  • Diagnosed the CI-only failure on Min-Xcode (iOS 17.5) jobs (iphone_int_test_1_Min_Xcode, ipad_int_test_1_Min_Xcode) against a clean master baseline run, root-caused it to the snapshot-depth issue above, and fixed it with a version-gated fallback.
  • Rerun CI to confirm the Min-Xcode jobs are green.

@mykola-mokhnach
mykola-mokhnach requested a review from Dan-Maor July 31, 2026 19:31
@mykola-mokhnach

Copy link
Copy Markdown
Author

@Dan-Maor Could you please help testing this PR?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Reworks FBAlert alert detection and interaction to reduce accessibility round trips by using snapshot-based traversal, and updates alert APIs/callers to use typed exceptions (instead of BOOL + NSError**) so routing can map failures to appropriate HTTP statuses.

Changes:

  • Replaces query-based alert discovery with a single application snapshot walk (including improved Alert/Sheet/ScrollView prioritization and Safari web-alert handling).
  • Updates alert interaction APIs (accept/dismiss/clickAlertButton:/typeText:) to void methods that throw typed exceptions; wires them into FBExceptionHandler.
  • Updates routing, monitors, pasteboard handling, and integration tests to the new throwing API.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m Updates Safari alert test to use XCTAssertNoThrow([alert accept]).
WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m Updates clearAlert to call throwing dismiss and ignore “no alert” via exception.
WebDriverAgentTests/IntegrationTests/FBAlertTests.m Updates alert tests for throwing alert APIs and removes direct alertElement test.
WebDriverAgentLib/Utilities/FBPasteboard.m Switches pasteboard alert handling to use FBAlert alertWithApplication: + throwing accept.
WebDriverAgentLib/Utilities/FBAlertsMonitor.m Updates monitoring to construct FBAlert from applications and use isPresent.
WebDriverAgentLib/Routing/FBSession.m Routes auto-click and default alert actions through new FBAlert APIs and exception reasons.
WebDriverAgentLib/Routing/FBExceptions.m Adds new alert exception name constants.
WebDriverAgentLib/Routing/FBExceptions.h Declares new alert exception name constants and documents their meaning.
WebDriverAgentLib/Routing/FBExceptionHandler.m Maps new alert exception types to no such alert, invalid element state, and unsupported operation.
WebDriverAgentLib/FBAlert.m Implements snapshot-based detection + UID-based element resolution; converts actions to throw typed exceptions; adds iOS < 18 button-lookup fallback.
WebDriverAgentLib/FBAlert.h Updates the public alert API to throwing void methods and documents behavior.
WebDriverAgentLib/Commands/FBAlertViewCommands.m Simplifies alert routes by relying on exception mapping instead of manual NSError plumbing.
WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m Implements in-memory snapshot traversal for alert detection and adds limited-access-prompt application lookup.
WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h Updates category API to return alert snapshots and expose limited-access-prompt application helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m
Comment thread WebDriverAgentLib/Utilities/FBPasteboard.m
mykola-mokhnach and others added 2 commits August 1, 2026 07:41
Some system alerts nest buttons deep enough to exceed
FBConfiguration.snapshotMaxDepth from the application root, causing the
in-memory snapshot walk to miss them regardless of iOS version. Try the
cheap snapshot walk first, then fall back to resolving the live alert
element and querying it directly, which gets a fresh depth budget.

Also address Copilot review comments on PR #1193: the @catch blocks in
FBIntegrationTestCase and FBPasteboard were swallowing all exceptions
instead of only the expected "no alert open" case, which could mask real
dismiss/accept failures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI showed the "always try the snapshot walk first" change regressed
iphone/ipad Min_Xcode (iOS 17.5): FBAlertTests.testNotificationAlert
started failing deterministically (4/4 retries) on both device types,
while the prior commit with @available(iOS 18.0, *) gating passed cleanly.

Root cause: on iOS < 18, sibling buttons in a system alert can sit at
different snapshot depths, so a depth-truncated snapshot walk doesn't
reliably come back empty - it can return a partial, wrong button set
instead. That silently picks the wrong button to tap, which is unsafe.
Restore the iOS 18.0 gate so iOS < 18 always resolves the live element
and queries buttons from it (the previously verified-safe path), while
keeping the "fall back to a live query if the snapshot walk finds
nothing" safety net for iOS 18+, where it's known safe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Dan-Maor

Dan-Maor commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

I’ll run some tests tomorrow, don’t have access to my devices today.

@Dan-Maor

Dan-Maor commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

I've tested the changes on an iPhone 17 Pro Max running the current iOS 27 beta and on an iPhone 12 Pro running iOS 26.6, and performance with this change appears to be slower.

Using a simple test application with a deep-nested view which launches an alert asynchronously during launch, getting the alert text is 200ms slower on average compared to the current master (~750ms on master, so 26% slower), and dismissing the alert is 300ms slower (~1250ms on master, so 32% slower), tested directly against WDA.

@mykola-mokhnach I was more focused on verifying the performance first so I haven't reviewed the code yet, would you still like me to go through it?

@mykola-mokhnach

Copy link
Copy Markdown
Author

@Dan-Maor Thanks for the concrete numbers — that was very useful, and you were right, this was a real regression, not noise.

Root cause: the version you tested was resolving the alert by taking a full snapshot of the whole application tree and walking it in memory. That's fine on a shallow app, but its cost scales with total app size/depth, which is exactly what your deep-nested test app would hit hardest.

I've replaced that with a single matchingPredicate: query for {Alert, Sheet, ScrollView} types, whose cost is proportional to the number of alert-shaped elements rather than app size — algorithmically the same shape master uses for detection, while still fixing a real Alert/Sheet/ScrollView priority bug that existed in the walk-based version. On top of that, the detection and interaction paths now share snapshots instead of each re-fetching one, and each FBAlert instance resolves the alert at most once and reuses that result for the rest of its calls.

Locally this brings text/dismiss back in line with the numbers before this PR touched detection. Would you be able to re-run your benchmark against the latest commit on this branch? And yes, please do go ahead and review the code now — appreciate you holding off until the performance question was settled.

mykola-mokhnach and others added 2 commits August 3, 2026 08:48
buttonSnapshotsInSnapshot's in-memory tree-order walk doesn't always
match a live XCUIElementQuery's ordering. Confirmed via manual testing
against a real iOS 17.5 simulator: on the system location-permission
alert, that mismatch put the non-dismissing "Precise: On/Off" toggle
first among the "buttons", so accept/dismiss silently tapped it instead
of a real action button and left the alert stuck open - which then
cascaded into every subsequent test in the run (matches the CI failure
pattern on ipad/iphone_int_test_1_Min_Xcode).

Not reproduced on iOS 18+, so accept/dismiss keep the cheaper snapshot
walk there and only fall back to a live descendantsMatchingType: query
below iOS 18, mirroring how master's pre-rework code resolved alert
buttons.

Verified on freshly erased iOS 17.5 and iOS 27 simulators: FBAlertTests
passes 11/11 on both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Dan-Maor

Dan-Maor commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Performance looks more promising now.

The same scenario of getting the alert text as I've tested previously is now performed between 470ms and 680ms compared to ~750ms on master - a definite improvement, however the fluctuations are a bit odd.

Alert dismiss durations are hovering around the same area as master now, so I think we're good on that front. Going to review the code now.

Comment thread WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h Outdated
Comment thread WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m
Comment thread WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m Outdated
- Move fb_elementForSnapshot:underElement: out of XCUIApplication+FBAlert
  into XCUIApplication+FBHelpers, since it's a generic snapshot-to-live-
  element resolver with no alert-specific logic.
- In that method, filter by the snapshot's own elementType instead of
  XCUIElementTypeAny, letting the query narrow down before the uid
  predicate is applied (~150ms/~12% faster on a dismiss action per
  Dan-Maor's measurement).
- Add an explicit nil-guard for scrollViewSnapshot at the top of
  fb_findSafariAlertSnapshotInScrollView: - ObjC's nil-messaging already
  made this safe in practice, but the guard documents the invariant
  instead of leaving it implicit.

Verified: FBAlertTests (11/11) on a freshly erased iOS 27 simulator, and
the real appium-xcuitest-driver safari-alerts e2e suite (5/5, 1 correctly
skipped) against WDA rebuilt from this source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@Dan-Maor Dan-Maor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice 👍

@mykola-mokhnach
mykola-mokhnach merged commit 2bc1c40 into master Aug 3, 2026
45 checks passed
@mykola-mokhnach
mykola-mokhnach deleted the alert-text branch August 3, 2026 15:08
github-actions Bot pushed a commit that referenced this pull request Aug 3, 2026
## [16.1.3](v16.1.2...v16.1.3) (2026-08-03)

### Bug Fixes

* Optimize alert operations ([#1193](#1193)) ([2bc1c40](2bc1c40))
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 16.1.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants