Skip to content

Commit 61e30e3

Browse files
fix: Optimize alert operations
1 parent e6ce542 commit 61e30e3

3 files changed

Lines changed: 154 additions & 9 deletions

File tree

WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
#import <XCTest/XCTest.h>
1010

11+
#import "FBXCElementSnapshot.h"
12+
1113
NS_ASSUME_NONNULL_BEGIN
1214

1315
@interface XCUIApplication (FBAlert)
@@ -22,6 +24,21 @@ extern NSString *const FB_SAFARI_APP_NAME;
2224
*/
2325
- (nullable XCUIElement *)fb_alertElement;
2426

27+
/**
28+
Retrieve the snapshot of the currently displayed alert, if any, using a
29+
single upfront application snapshot and purely in-memory tree traversal for
30+
all subsequent type/candidate checks. This avoids the multiple discrete
31+
accessibility round trips that fb_alertElement's XCUIElementQuery-based
32+
lookup performs, which is significantly more expensive while the
33+
application's main thread is busy (e.g. blocked showing a JS alert in
34+
Safari). Intended for read-only detection/text-extraction use cases
35+
(repeatedly polled while waiting for an atom to complete); callers that need
36+
to interact with (tap) the alert should still use fb_alertElement.
37+
38+
@return Alert snapshot instance, or nil if no alert is present
39+
*/
40+
- (nullable id<FBXCElementSnapshot>)fb_alertSnapshot;
41+
2542
/**
2643
Retrieve an alert element hosted by the iOS 18+ limited access permission prompt
2744
process. See https://github.com/appium/appium/issues/20591

WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,113 @@ + (nullable XCUIElement *)fb_limitedAccessPromptAlertElement
3333
return promptApp.fb_alertElement;
3434
}
3535

36+
+ (nullable id<FBXCElementSnapshot>)fb_findSafariAlertSnapshotInScrollView:(id<FBXCElementSnapshot>)scrollViewSnapshot
37+
{
38+
CGRect appFrame = scrollViewSnapshot.frame;
39+
40+
__block id<FBXCElementSnapshot> webView = nil;
41+
[scrollViewSnapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
42+
if (nil == webView && nil != descendant.identifier && [descendant.identifier isEqualToString:@"WebView"]) {
43+
webView = descendant;
44+
}
45+
}];
46+
if (nil == webView) {
47+
return nil;
48+
}
49+
50+
// Find the first XCUIElementTypeOther which is the grandchild of the web view
51+
// and is horizontally aligned to the center of the screen, and contains one
52+
// to two buttons and at least one text view.
53+
__block id<FBXCElementSnapshot> candidate = nil;
54+
[webView enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
55+
if (nil != candidate || descendant.elementType != XCUIElementTypeOther) {
56+
return;
57+
}
58+
CGRect curFrame = descendant.frame;
59+
if (CGRectEqualToRect(appFrame, curFrame)
60+
|| curFrame.origin.x <= 0
61+
|| curFrame.size.width >= appFrame.size.width) {
62+
return;
63+
}
64+
CGFloat possibleCenterX = (appFrame.size.width - curFrame.size.width) / 2;
65+
if (fabs(possibleCenterX - curFrame.origin.x) >= MAX_CENTER_DELTA) {
66+
return;
67+
}
68+
69+
__block NSUInteger buttonsCount = 0;
70+
__block NSUInteger textViewsCount = 0;
71+
[descendant enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> innerDescendant) {
72+
XCUIElementType curType = innerDescendant.elementType;
73+
if (curType == XCUIElementTypeButton) {
74+
buttonsCount++;
75+
} else if (curType == XCUIElementTypeTextView) {
76+
textViewsCount++;
77+
}
78+
}];
79+
if (buttonsCount >= 1 && buttonsCount <= 2 && textViewsCount > 0) {
80+
candidate = descendant;
81+
}
82+
}];
83+
return candidate;
84+
}
85+
86+
+ (nullable id<FBXCElementSnapshot>)fb_findAlertSnapshotInApplicationSnapshot:(id<FBXCElementSnapshot>)appSnapshot
87+
{
88+
__block id<FBXCElementSnapshot> found = nil;
89+
[appSnapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
90+
if (nil != found) {
91+
return;
92+
}
93+
XCUIElementType curType = descendant.elementType;
94+
if (curType == XCUIElementTypeAlert || curType == XCUIElementTypeSheet || curType == XCUIElementTypeScrollView) {
95+
found = descendant;
96+
}
97+
}];
98+
if (nil == found) {
99+
return nil;
100+
}
101+
102+
if (found.elementType == XCUIElementTypeAlert) {
103+
return found;
104+
}
105+
106+
if (found.elementType == XCUIElementTypeSheet) {
107+
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) {
108+
return found;
109+
}
110+
111+
// In case of iPad we want to check if sheet isn't contained by popover.
112+
// In that case we ignore it.
113+
id<FBXCElementSnapshot> ancestor = found.parent;
114+
while (nil != ancestor) {
115+
if (nil != ancestor.identifier && [ancestor.identifier isEqualToString:@"PopoverDismissRegion"]) {
116+
return nil;
117+
}
118+
ancestor = ancestor.parent;
119+
}
120+
return found;
121+
}
122+
123+
if (found.elementType == XCUIElementTypeScrollView) {
124+
id<FBXCElementSnapshot> app = [[FBXCElementSnapshotWrapper ensureWrapped:found] fb_parentMatchingType:XCUIElementTypeApplication];
125+
if (nil != app && [app.label isEqualToString:FB_SAFARI_APP_NAME]) {
126+
// Check alert presence in Safari web view
127+
return [self fb_findSafariAlertSnapshotInScrollView:found];
128+
}
129+
}
130+
131+
return nil;
132+
}
133+
134+
- (nullable id<FBXCElementSnapshot>)fb_alertSnapshot
135+
{
136+
id<FBXCElementSnapshot> appSnapshot = self.fb_cachedSnapshot ?: [self fb_customSnapshot];
137+
if (nil == appSnapshot) {
138+
return nil;
139+
}
140+
return [self.class fb_findAlertSnapshotInApplicationSnapshot:appSnapshot];
141+
}
142+
36143
- (nullable XCUIElement *)fb_alertElementFromSafariWithScrollView:(XCUIElement *)scrollView
37144
viewSnapshot:(id<FBXCElementSnapshot>)viewSnapshot
38145
{

WebDriverAgentLib/FBAlert.m

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
@interface FBAlert ()
2525
@property (nonatomic, strong) XCUIApplication *application;
2626
@property (nonatomic, strong, nullable) XCUIElement *element;
27+
@property (nonatomic, strong, nullable) id<FBXCElementSnapshot> detectionSnapshot;
28+
@property (nonatomic, assign) BOOL didResolveDetectionSnapshot;
2729
@end
2830

2931
@implementation FBAlert
@@ -46,16 +48,35 @@ + (instancetype)alertWithElement:(XCUIElement *)element
4648
- (BOOL)isPresent
4749
{
4850
@try {
49-
if (nil == self.alertElement) {
50-
return NO;
51-
}
52-
[self.alertElement fb_customSnapshot];
53-
return YES;
51+
return nil != self.detectionSnapshot;
5452
} @catch (NSException *) {
5553
return NO;
5654
}
5755
}
5856

57+
// Read-only detection path (isPresent/text/buttonLabels): resolves a single
58+
// snapshot via fb_alertSnapshot, which takes one upfront application
59+
// snapshot and does all type/candidate matching via in-memory tree
60+
// traversal, instead of the multiple discrete accessibility round trips
61+
// that alertElement's XCUIElementQuery-based resolution performs. This
62+
// matters a lot while the target app's main thread is busy (e.g. blocked
63+
// showing a JS alert in Safari), where each such round trip can cost ~5s.
64+
// Callers that need to interact with (tap) the alert must use alertElement
65+
// instead, since a snapshot cannot be tapped.
66+
- (nullable id<FBXCElementSnapshot>)detectionSnapshot
67+
{
68+
if (!self.didResolveDetectionSnapshot) {
69+
XCUIApplication *systemApp = XCUIApplication.fb_systemApplication;
70+
if ([systemApp fb_isSameAppAs:self.application]) {
71+
self->_detectionSnapshot = systemApp.fb_alertSnapshot;
72+
} else {
73+
self->_detectionSnapshot = systemApp.fb_alertSnapshot ?: self.application.fb_alertSnapshot;
74+
}
75+
self.didResolveDetectionSnapshot = YES;
76+
}
77+
return self->_detectionSnapshot;
78+
}
79+
5980
- (BOOL)notPresentWithError:(NSError **)error
6081
{
6182
return [[[FBErrorBuilder builder]
@@ -76,12 +97,12 @@ + (BOOL)isSafariWebAlertWithSnapshot:(id<FBXCElementSnapshot>)snapshot
7697

7798
- (NSString *)text
7899
{
79-
if (!self.isPresent) {
100+
id<FBXCElementSnapshot> snapshot = self.detectionSnapshot;
101+
if (nil == snapshot) {
80102
return nil;
81103
}
82104

83105
NSMutableArray<NSString *> *resultText = [NSMutableArray array];
84-
id<FBXCElementSnapshot> snapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
85106
BOOL isSafariAlert = [self.class isSafariWebAlertWithSnapshot:snapshot];
86107
[snapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
87108
XCUIElementType elementType = descendant.elementType;
@@ -139,12 +160,12 @@ - (BOOL)typeText:(NSString *)text error:(NSError **)error
139160

140161
- (NSArray *)buttonLabels
141162
{
142-
if (!self.isPresent) {
163+
id<FBXCElementSnapshot> alertSnapshot = self.detectionSnapshot;
164+
if (nil == alertSnapshot) {
143165
return nil;
144166
}
145167

146168
NSMutableArray<NSString *> *labels = [NSMutableArray array];
147-
id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
148169
[alertSnapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
149170
if (descendant.elementType != XCUIElementTypeButton) {
150171
return;

0 commit comments

Comments
 (0)