appium-webdriveragent 16.1.1 → 16.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h +20 -8
  3. package/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +116 -60
  4. package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +15 -0
  5. package/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +16 -0
  6. package/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m +146 -3
  7. package/WebDriverAgentLib/Commands/FBAlertViewCommands.m +10 -36
  8. package/WebDriverAgentLib/FBAlert.h +43 -30
  9. package/WebDriverAgentLib/FBAlert.m +257 -91
  10. package/WebDriverAgentLib/Info.plist +2 -2
  11. package/WebDriverAgentLib/Routing/FBExceptionHandler.m +8 -1
  12. package/WebDriverAgentLib/Routing/FBExceptions.h +9 -0
  13. package/WebDriverAgentLib/Routing/FBExceptions.m +3 -0
  14. package/WebDriverAgentLib/Routing/FBSession.m +11 -13
  15. package/WebDriverAgentLib/Utilities/FBAlertsMonitor.m +11 -9
  16. package/WebDriverAgentLib/Utilities/FBPasteboard.m +8 -4
  17. package/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +34 -0
  18. package/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard +12 -0
  19. package/WebDriverAgentTests/IntegrationTests/FBAlertTests.m +32 -51
  20. package/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.h +16 -0
  21. package/WebDriverAgentTests/IntegrationTests/FBIntegrationTestCase.m +28 -1
  22. package/WebDriverAgentTests/IntegrationTests/FBSafariAlertTests.m +1 -1
  23. package/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m +3 -3
  24. package/WebDriverAgentTests/IntegrationTests/XCElementSnapshotHelperTests.m +1 -7
  25. package/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m +76 -10
  26. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [16.1.3](https://github.com/appium/WebDriverAgent/compare/v16.1.2...v16.1.3) (2026-08-03)
2
+
3
+ ### Bug Fixes
4
+
5
+ * Optimize alert operations ([#1193](https://github.com/appium/WebDriverAgent/issues/1193)) ([2bc1c40](https://github.com/appium/WebDriverAgent/commit/2bc1c40d20883a9b924b8a177be4d43ffd497901))
6
+
7
+ ## [16.1.2](https://github.com/appium/WebDriverAgent/compare/v16.1.1...v16.1.2) (2026-08-03)
8
+
9
+ ### Bug Fixes
10
+
11
+ * Optimize class chain lookup ([#1194](https://github.com/appium/WebDriverAgent/issues/1194)) ([47cbbb5](https://github.com/appium/WebDriverAgent/commit/47cbbb597495ac78c4290c27c2ef0ee349004406))
12
+
1
13
  ## [16.1.1](https://github.com/appium/WebDriverAgent/compare/v16.1.0...v16.1.1) (2026-08-01)
2
14
 
3
15
  ### Miscellaneous Chores
@@ -8,6 +8,8 @@
8
8
 
9
9
  #import <XCTest/XCTest.h>
10
10
 
11
+ #import "FBXCElementSnapshot.h"
12
+
11
13
  NS_ASSUME_NONNULL_BEGIN
12
14
 
13
15
  @interface XCUIApplication (FBAlert)
@@ -16,19 +18,29 @@ NS_ASSUME_NONNULL_BEGIN
16
18
  extern NSString *const FB_SAFARI_APP_NAME;
17
19
 
18
20
  /**
19
- Retrieve the current alert element
20
-
21
- @return Alert element instance
21
+ Retrieve the currently displayed alert element, if any, using a single
22
+ predicate-filtered query (Alert, Sheet, or ScrollView type) instead of
23
+ snapshotting and walking the whole application tree - the cost stays
24
+ proportional to the number of matching elements rather than the
25
+ size/depth of the whole app.
26
+
27
+ @param snapshotOut On return, set to the element's snapshot if resolving it
28
+ already required taking one as a side effect (the iPad sheet popover check,
29
+ or the Safari web-alert scan) - left untouched if no snapshot was taken, so
30
+ callers should seed it with nil beforehand. Pass NULL if not needed.
31
+ @return Alert element instance, or nil if no alert is present
22
32
  */
23
- - (nullable XCUIElement *)fb_alertElement;
33
+ - (nullable XCUIElement *)fb_alertElementWithSnapshot:(id<FBXCElementSnapshot> _Nullable * _Nullable)snapshotOut;
24
34
 
25
35
  /**
26
- Retrieve an alert element hosted by the iOS 18+ limited access permission prompt
27
- process. See https://github.com/appium/appium/issues/20591
36
+ Retrieve the application hosting the iOS 18+ limited access permission prompt,
37
+ cheaply gated on its running state so callers can avoid resolving its alert
38
+ snapshot when the prompt process isn't in the foreground.
39
+ See https://github.com/appium/appium/issues/20591
28
40
 
29
- @return Alert element instance if the prompt is present, otherwise nil
41
+ @return The prompt application if it is running in the foreground, otherwise nil
30
42
  */
31
- + (nullable XCUIElement *)fb_limitedAccessPromptAlertElement;
43
+ + (nullable XCUIApplication *)fb_limitedAccessPromptApplication;
32
44
 
33
45
  @end
34
46
 
@@ -8,7 +8,6 @@
8
8
 
9
9
  #import "XCUIApplication+FBAlert.h"
10
10
 
11
- #import "FBMacros.h"
12
11
  #import "FBXCElementSnapshotWrapper+Helpers.h"
13
12
  #import "FBXCodeCompatibility.h"
14
13
  #import "XCUIElement+FBUtilities.h"
@@ -24,95 +23,152 @@ static NSString *const FB_LIMITED_ACCESS_PROMPT_BUNDLE_ID = @"com.apple.Contacts
24
23
 
25
24
  @implementation XCUIApplication (FBAlert)
26
25
 
27
- + (nullable XCUIElement *)fb_limitedAccessPromptAlertElement
26
+ + (nullable XCUIApplication *)fb_limitedAccessPromptApplication
28
27
  {
29
28
  XCUIApplication *promptApp = [[XCUIApplication alloc] initWithBundleIdentifier:FB_LIMITED_ACCESS_PROMPT_BUNDLE_ID];
30
- if (promptApp.state < XCUIApplicationStateRunningForeground) {
31
- return nil;
32
- }
33
- return promptApp.fb_alertElement;
29
+ return promptApp.state < XCUIApplicationStateRunningForeground ? nil : promptApp;
34
30
  }
35
31
 
36
- - (nullable XCUIElement *)fb_alertElementFromSafariWithScrollView:(XCUIElement *)scrollView
37
- viewSnapshot:(id<FBXCElementSnapshot>)viewSnapshot
32
+ + (nullable id<FBXCElementSnapshot>)fb_findSafariAlertSnapshotInScrollView:(id<FBXCElementSnapshot>)scrollViewSnapshot
38
33
  {
39
- CGRect appFrame = viewSnapshot.frame;
40
- NSPredicate *dstViewMatchPredicate = [NSPredicate predicateWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot, NSDictionary *bindings) {
41
- CGRect curFrame = snapshot.frame;
42
- if (!CGRectEqualToRect(appFrame, curFrame)
43
- && curFrame.origin.x > 0 && curFrame.size.width < appFrame.size.width) {
44
- CGFloat possibleCenterX = (appFrame.size.width - curFrame.size.width) / 2;
45
- return fabs(possibleCenterX - curFrame.origin.x) < MAX_CENTER_DELTA;
34
+ if (nil == scrollViewSnapshot) {
35
+ return nil;
36
+ }
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;
46
44
  }
47
- return NO;
48
45
  }];
49
- NSPredicate *dstViewContainPredicate1 = [NSPredicate predicateWithFormat:@"elementType == %lu", XCUIElementTypeTextView];
50
- NSPredicate *dstViewContainPredicate2 = [NSPredicate predicateWithFormat:@"elementType == %lu", XCUIElementTypeButton];
51
- // Find the first XCUIElementTypeOther which is the grandchild of the web view
52
- // and is horizontally aligned to the center of the screen
53
- XCUIElement *candidate = [[[[[[scrollView descendantsMatchingType:XCUIElementTypeAny]
54
- matchingIdentifier:@"WebView"]
55
- descendantsMatchingType:XCUIElementTypeOther]
56
- matchingPredicate:dstViewMatchPredicate]
57
- containingPredicate:dstViewContainPredicate1]
58
- containingPredicate:dstViewContainPredicate2].allElementsBoundByIndex.firstObject;
59
-
60
- if (nil == candidate) {
46
+ if (nil == webView) {
61
47
  return nil;
62
48
  }
63
- // ...and contains one to two buttons
64
- // and conatins at least one text view
65
- __block NSUInteger buttonsCount = 0;
66
- __block NSUInteger textViewsCount = 0;
67
- id<FBXCElementSnapshot> snapshot = candidate.fb_cachedSnapshot ?: [candidate fb_customSnapshot];
68
- [snapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
69
- XCUIElementType curType = descendant.elementType;
70
- if (curType == XCUIElementTypeButton) {
71
- buttonsCount++;
72
- } else if (curType == XCUIElementTypeTextView) {
73
- textViewsCount++;
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;
74
81
  }
75
82
  }];
76
- return (buttonsCount >= 1 && buttonsCount <= 2 && textViewsCount > 0) ? candidate : nil;
83
+ return candidate;
77
84
  }
78
85
 
79
- - (XCUIElement *)fb_alertElement
86
+ // Resolving a query (e.g. allElementsBoundByIndex) is itself
87
+ // as expensive as taking a snapshot - it has to walk/resolve the matching
88
+ // subtree either way. So this issues exactly ONE such query, matching all
89
+ // three candidate types at once, instead of one query per type: querying
90
+ // Alert, then Sheet, then ScrollView separately would pay that cost up to
91
+ // three times over, which is worse than a single whole-app snapshot in the
92
+ // common case where no alert is present at all (the query comes back
93
+ // empty on the very first attempt). Priority is then resolved in memory
94
+ // over the (typically 0-1 element) result: an Alert always wins outright,
95
+ // a Sheet only loses to an Alert, and a ScrollView (the Safari web-alert
96
+ // case) is the last resort. Per-candidate ancestor/subtree checks (the
97
+ // iPad popover check, the Safari web-alert walk) only run for candidates
98
+ // that actually matched, not for every possible type.
99
+ - (nullable XCUIElement *)fb_alertElementWithSnapshot:(id<FBXCElementSnapshot> _Nullable * _Nullable)snapshotOut
80
100
  {
81
- NSPredicate *alertCollectorPredicate = [NSPredicate predicateWithFormat:@"elementType IN {%lu,%lu,%lu}",
82
- XCUIElementTypeAlert, XCUIElementTypeSheet, XCUIElementTypeScrollView];
83
- XCUIElement *alert = [[self descendantsMatchingType:XCUIElementTypeAny]
84
- matchingPredicate:alertCollectorPredicate].allElementsBoundByIndex.firstObject;
85
- if (nil == alert) {
101
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:@"elementType IN {%lu,%lu,%lu}",
102
+ XCUIElementTypeAlert, XCUIElementTypeSheet, XCUIElementTypeScrollView];
103
+ // allElementsBoundByAccessibilityElement resolves all matches in one
104
+ // round trip; allElementsBoundByIndex pays one extra round trip per
105
+ // match, costly while the target is JS-blocked inside alert() (~5s per
106
+ // hop).
107
+ NSArray<XCUIElement *> *candidates = [[self descendantsMatchingType:XCUIElementTypeAny]
108
+ matchingPredicate:predicate].allElementsBoundByAccessibilityElement;
109
+ if (0 == candidates.count) {
86
110
  return nil;
87
111
  }
88
- id<FBXCElementSnapshot> alertSnapshot = alert.fb_cachedSnapshot ?: [alert fb_customSnapshot];
89
112
 
90
- if (alertSnapshot.elementType == XCUIElementTypeAlert) {
91
- return alert;
113
+ NSMutableArray<XCUIElement *> *sheets = [NSMutableArray array];
114
+ NSMutableArray<XCUIElement *> *scrollViews = [NSMutableArray array];
115
+ for (XCUIElement *candidate in candidates) {
116
+ switch (candidate.elementType) {
117
+ case XCUIElementTypeAlert:
118
+ return candidate;
119
+ case XCUIElementTypeSheet:
120
+ [sheets addObject:candidate];
121
+ break;
122
+ case XCUIElementTypeScrollView:
123
+ [scrollViews addObject:candidate];
124
+ break;
125
+ default:
126
+ break;
127
+ }
92
128
  }
93
129
 
94
- if (alertSnapshot.elementType == XCUIElementTypeSheet) {
95
- if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) {
96
- return alert;
130
+ BOOL isPhone = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone;
131
+ for (XCUIElement *sheet in sheets) {
132
+ if (isPhone) {
133
+ return sheet;
97
134
  }
98
135
 
99
136
  // In case of iPad we want to check if sheet isn't contained by popover.
100
137
  // In that case we ignore it.
101
- id<FBXCElementSnapshot> ancestor = alertSnapshot.parent;
138
+ id<FBXCElementSnapshot> sheetSnapshot = sheet.lastSnapshot ?: sheet.fb_cachedSnapshot ?: [sheet fb_customSnapshot];
139
+ BOOL isInsidePopover = NO;
140
+ id<FBXCElementSnapshot> ancestor = sheetSnapshot.parent;
102
141
  while (nil != ancestor) {
103
142
  if (nil != ancestor.identifier && [ancestor.identifier isEqualToString:@"PopoverDismissRegion"]) {
104
- return nil;
143
+ isInsidePopover = YES;
144
+ break;
105
145
  }
106
146
  ancestor = ancestor.parent;
107
147
  }
108
- return alert;
148
+ if (!isInsidePopover) {
149
+ if (NULL != snapshotOut) {
150
+ *snapshotOut = sheetSnapshot;
151
+ }
152
+ return sheet;
153
+ }
109
154
  }
110
155
 
111
- if (alertSnapshot.elementType == XCUIElementTypeScrollView) {
112
- id<FBXCElementSnapshot> app = [[FBXCElementSnapshotWrapper ensureWrapped:alertSnapshot] fb_parentMatchingType:XCUIElementTypeApplication];
113
- if (nil != app && [app.label isEqualToString:FB_SAFARI_APP_NAME]) {
114
- // Check alert presence in Safari web view
115
- return [self fb_alertElementFromSafariWithScrollView:alert viewSnapshot:alertSnapshot];
156
+ for (XCUIElement *scrollView in scrollViews) {
157
+ id<FBXCElementSnapshot> scrollViewSnapshot = scrollView.lastSnapshot ?: scrollView.fb_cachedSnapshot ?: [scrollView fb_customSnapshot];
158
+ id<FBXCElementSnapshot> app = [[FBXCElementSnapshotWrapper ensureWrapped:scrollViewSnapshot] fb_parentMatchingType:XCUIElementTypeApplication];
159
+ if (nil == app || ![app.label isEqualToString:FB_SAFARI_APP_NAME]) {
160
+ continue;
161
+ }
162
+ // Check alert presence in Safari web view
163
+ id<FBXCElementSnapshot> safariAlertSnapshot = [self.class fb_findSafariAlertSnapshotInScrollView:scrollViewSnapshot];
164
+ if (nil != safariAlertSnapshot) {
165
+ // Not resolving safariAlertSnapshot to a live element here (another
166
+ // round trip) - scrollView is already live and is a valid ancestor
167
+ // for callers to resolve buttons/fields from later.
168
+ if (NULL != snapshotOut) {
169
+ *snapshotOut = safariAlertSnapshot;
170
+ }
171
+ return scrollView;
116
172
  }
117
173
  }
118
174
 
@@ -8,6 +8,8 @@
8
8
 
9
9
  #import <XCTest/XCTest.h>
10
10
 
11
+ #import "FBXCElementSnapshot.h"
12
+
11
13
  @class XCElementSnapshot;
12
14
  @protocol FBXCAccessibilityElement;
13
15
  @class FBXMLGenerationOptions;
@@ -166,6 +168,19 @@ NS_ASSUME_NONNULL_BEGIN
166
168
  */
167
169
  - (BOOL)fb_isSameAppAs:(nullable XCUIApplication *)otherApp;
168
170
 
171
+ /**
172
+ Resolves the live element that corresponds to an already-known snapshot
173
+ found somewhere under rootElement, by matching on its stable uid, instead
174
+ of re-running a fresh attribute/type-based query - a single targeted
175
+ accessibility round trip regardless of how deep the snapshot sits.
176
+
177
+ @param snapshot The snapshot to resolve a live element for
178
+ @param rootElement The element to scope the uid lookup query to
179
+ @return The live element matching the snapshot's uid, or nil if it could not be resolved
180
+ */
181
+ + (nullable XCUIElement *)fb_elementForSnapshot:(id<FBXCElementSnapshot>)snapshot
182
+ underElement:(XCUIElement *)rootElement;
183
+
169
184
  @end
170
185
 
171
186
  NS_ASSUME_NONNULL_END
@@ -33,6 +33,7 @@
33
33
  #import "XCUIElement.h"
34
34
  #import "XCUIElement+FBCaching.h"
35
35
  #import "XCUIElement+FBIsVisible.h"
36
+ #import "XCUIElement+FBUID.h"
36
37
  #import "XCUIElement+FBUtilities.h"
37
38
  #import "XCUIElement+FBWebDriverAttributes.h"
38
39
  #import "XCUIElementQuery.h"
@@ -653,4 +654,19 @@ NSDictionary<NSString *, NSString *> *customExclusionAttributesMap(void) {
653
654
  return self == otherApp || [self.bundleID isEqualToString:(NSString *)otherApp.bundleID];
654
655
  }
655
656
 
657
+ + (nullable XCUIElement *)fb_elementForSnapshot:(id<FBXCElementSnapshot>)snapshot
658
+ underElement:(XCUIElement *)rootElement
659
+ {
660
+ NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot];
661
+ if (nil == uid) {
662
+ return nil;
663
+ }
664
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@",
665
+ FBStringify(FBXCElementSnapshotWrapper, fb_uid), uid];
666
+ // Filtering by the snapshot's own type (instead of XCUIElementTypeAny) lets
667
+ // the query narrow down before the uid predicate is even applied.
668
+ return [[rootElement.fb_query descendantsMatchingType:snapshot.elementType]
669
+ matchingPredicate:predicate].allElementsBoundByIndex.firstObject;
670
+ }
671
+
656
672
  @end
@@ -11,6 +11,7 @@
11
11
  #import "FBClassChainQueryParser.h"
12
12
  #import "FBXCodeCompatibility.h"
13
13
  #import "FBExceptions.h"
14
+ #import "XCUIElement+FBUtilities.h"
14
15
 
15
16
  @implementation XCUIElement (FBClassChain)
16
17
 
@@ -22,7 +23,39 @@
22
23
  @throw [NSException exceptionWithName:FBClassChainQueryParseException reason:error.localizedDescription userInfo:error.userInfo];
23
24
  return nil;
24
25
  }
25
- NSMutableArray<FBClassChainItem *> *lookupChain = parsedChain.elements.mutableCopy;
26
+ // The snapshot-walk strategy below only pays off when an intermediate
27
+ // (non-final) segment carries an explicit position: that is the only
28
+ // shape where the query-based strategy has to resolve a live element
29
+ // mid-chain - and pay an accessibility round trip - just to keep building
30
+ // the next segment's query. Every other shape (including the common case
31
+ // of zero or one position, on the final segment only) already resolves in
32
+ // a single round trip with the query-based strategy, while the snapshot
33
+ // walk always pays for one full upfront subtree snapshot regardless of
34
+ // whether it is actually needed - a bad trade in deep/large trees. See
35
+ // https://github.com/appium/WebDriverAgent/pull/1194#issuecomment-5156633352
36
+ NSArray<FBClassChainItem *> *chainItems = parsedChain.elements;
37
+ return [self.class fb_hasIntermediatePosition:chainItems]
38
+ ? [self fb_snapshotDescendantsMatchingChainItems:chainItems shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]
39
+ : [self fb_queryDescendantsMatchingChainItems:chainItems shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
40
+ }
41
+
42
+ + (BOOL)fb_hasIntermediatePosition:(NSArray<FBClassChainItem *> *)chainItems
43
+ {
44
+ for (NSUInteger i = 0; i + 1 < chainItems.count; i++) {
45
+ if (nil != chainItems[i].position) {
46
+ return YES;
47
+ }
48
+ }
49
+ return NO;
50
+ }
51
+
52
+ #pragma mark - Query-based strategy
53
+ #pragma mark (single accessibility round trip; used whenever no intermediate
54
+ #pragma mark segment carries an explicit position)
55
+
56
+ - (NSArray<XCUIElement *> *)fb_queryDescendantsMatchingChainItems:(NSArray<FBClassChainItem *> *)chainItems shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
57
+ {
58
+ NSMutableArray<FBClassChainItem *> *lookupChain = chainItems.mutableCopy;
26
59
  FBClassChainItem *chainItem = lookupChain.firstObject;
27
60
  XCUIElement *currentRoot = self;
28
61
  XCUIElementQuery *query = [currentRoot fb_queryWithChainItem:chainItem query:nil];
@@ -30,8 +63,6 @@
30
63
  while (lookupChain.count > 0) {
31
64
  BOOL isRootChanged = NO;
32
65
  if (nil != chainItem.position) {
33
- // It is necessary to resolve the query if intermediate element index is not zero or one,
34
- // because predicates don't support search by indexes
35
66
  NSArray<XCUIElement *> *currentRootMatch = [self.class fb_matchingElementsWithItem:chainItem
36
67
  query:query
37
68
  shouldReturnAfterFirstMatch:nil];
@@ -95,4 +126,116 @@
95
126
  return @[];
96
127
  }
97
128
 
129
+ #pragma mark - Snapshot-based strategy
130
+ #pragma mark (single upfront snapshot walked in memory; used when an
131
+ #pragma mark intermediate segment has an explicit position, avoiding one
132
+ #pragma mark extra accessibility round trip per such segment)
133
+
134
+ - (NSArray<XCUIElement *> *)fb_snapshotDescendantsMatchingChainItems:(NSArray<FBClassChainItem *> *)chainItems shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
135
+ {
136
+ NSMutableArray<FBClassChainItem *> *lookupChain = chainItems.mutableCopy;
137
+ // Reuse an already-taken snapshot of `self` if one is available (e.g. the
138
+ // caller just resolved/inspected this same element) instead of always
139
+ // paying for a fresh one.
140
+ NSArray<id<FBXCElementSnapshot>> *currentRoots = @[self.lastSnapshot ?: [self fb_customSnapshot]];
141
+ FBClassChainItem *chainItem = lookupChain.firstObject;
142
+ NSArray<id<FBXCElementSnapshot>> *candidates = [self.class fb_snapshotsMatchingItem:chainItem inRoots:currentRoots];
143
+ [lookupChain removeObjectAtIndex:0];
144
+ while (lookupChain.count > 0) {
145
+ if (nil != chainItem.position) {
146
+ // An explicit position always narrows the match set down to a single
147
+ // element, which becomes the sole root for the rest of the chain, so
148
+ // it has to be resolved now instead of being folded into `candidates`
149
+ // like an unindexed segment would be
150
+ NSArray<id<FBXCElementSnapshot>> *currentRootMatch = [self.class fb_matchingSnapshotsWithItem:chainItem
151
+ candidates:candidates
152
+ shouldReturnAfterFirstMatch:nil];
153
+ if (0 == currentRootMatch.count) {
154
+ return @[];
155
+ }
156
+ currentRoots = @[currentRootMatch.firstObject];
157
+ } else {
158
+ currentRoots = candidates;
159
+ }
160
+ chainItem = lookupChain.firstObject;
161
+ candidates = [self.class fb_snapshotsMatchingItem:chainItem inRoots:currentRoots];
162
+ [lookupChain removeObjectAtIndex:0];
163
+ }
164
+ NSArray<id<FBXCElementSnapshot>> *matchedSnapshots = [self.class fb_matchingSnapshotsWithItem:chainItem
165
+ candidates:candidates
166
+ shouldReturnAfterFirstMatch:@(shouldReturnAfterFirstMatch)];
167
+ return [self fb_filterDescendantsWithSnapshots:matchedSnapshots onlyChildren:NO];
168
+ }
169
+
170
+ + (NSArray<id<FBXCElementSnapshot>> *)fb_snapshotsMatchingItem:(FBClassChainItem *)item inRoots:(NSArray<id<FBXCElementSnapshot>> *)roots
171
+ {
172
+ NSMutableArray<id<FBXCElementSnapshot>> *typeMatches = [NSMutableArray array];
173
+ for (id<FBXCElementSnapshot> root in roots) {
174
+ if (item.isDescendant) {
175
+ // descendantsByFilteringWithBlock: includes the receiver itself if it
176
+ // matches the filter, unlike XCUIElementQuery's descendantsMatchingType:,
177
+ // so the root has to be excluded explicitly here.
178
+ [typeMatches addObjectsFromArray:[root descendantsByFilteringWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot) {
179
+ return snapshot != root && (item.type == XCUIElementTypeAny || snapshot.elementType == item.type);
180
+ }]];
181
+ } else {
182
+ for (id<FBXCElementSnapshot> child in root.children) {
183
+ if (item.type == XCUIElementTypeAny || child.elementType == item.type) {
184
+ [typeMatches addObject:child];
185
+ }
186
+ }
187
+ }
188
+ }
189
+ if (roots.count > 1) {
190
+ // Overlapping roots (e.g. a previous segment matched both an ancestor
191
+ // and its own descendant) can otherwise yield the same snapshot twice,
192
+ // which would skew positional selection ([2], [-1], etc.) compared to
193
+ // the XCUIElementQuery-based matching this replaced, which always
194
+ // operated on a de-duplicated element set.
195
+ NSMutableArray<id<FBXCElementSnapshot>> *dedupedMatches = [NSMutableArray arrayWithCapacity:typeMatches.count];
196
+ NSHashTable<id<FBXCElementSnapshot>> *seenMatches = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];
197
+ for (id<FBXCElementSnapshot> match in typeMatches) {
198
+ if (![seenMatches containsObject:match]) {
199
+ [seenMatches addObject:match];
200
+ [dedupedMatches addObject:match];
201
+ }
202
+ }
203
+ typeMatches = dedupedMatches;
204
+ }
205
+ for (FBAbstractPredicateItem *predicateItem in item.predicates) {
206
+ if ([predicateItem isKindOfClass:FBSelfPredicateItem.class]) {
207
+ typeMatches = [[typeMatches filteredArrayUsingPredicate:predicateItem.value] mutableCopy];
208
+ } else if ([predicateItem isKindOfClass:FBDescendantPredicateItem.class]) {
209
+ NSMutableArray<id<FBXCElementSnapshot>> *containingMatches = [NSMutableArray array];
210
+ for (id<FBXCElementSnapshot> candidate in typeMatches) {
211
+ NSArray<id<FBXCElementSnapshot>> *matchingDescendants = [candidate descendantsByFilteringWithBlock:^BOOL(id<FBXCElementSnapshot> descendant) {
212
+ return descendant != candidate && [predicateItem.value evaluateWithObject:descendant];
213
+ }];
214
+ if (matchingDescendants.count > 0) {
215
+ [containingMatches addObject:candidate];
216
+ }
217
+ }
218
+ typeMatches = containingMatches;
219
+ }
220
+ }
221
+ return typeMatches.copy;
222
+ }
223
+
224
+ + (NSArray<id<FBXCElementSnapshot>> *)fb_matchingSnapshotsWithItem:(FBClassChainItem *)item candidates:(NSArray<id<FBXCElementSnapshot>> *)candidates shouldReturnAfterFirstMatch:(nullable NSNumber *)shouldReturnAfterFirstMatch
225
+ {
226
+ if (1 == item.position.integerValue || (0 == item.position.integerValue && shouldReturnAfterFirstMatch.boolValue)) {
227
+ id<FBXCElementSnapshot> result = candidates.firstObject;
228
+ return result ? @[result] : @[];
229
+ }
230
+ if (0 == item.position.integerValue) {
231
+ return candidates;
232
+ }
233
+ if (candidates.count >= (NSUInteger)ABS(item.position.integerValue)) {
234
+ return item.position.integerValue > 0
235
+ ? @[[candidates objectAtIndex:item.position.integerValue - 1]]
236
+ : @[[candidates objectAtIndex:candidates.count + item.position.integerValue]];
237
+ }
238
+ return @[];
239
+ }
240
+
98
241
  @end
@@ -54,19 +54,11 @@
54
54
  return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'value' parameter" traceback:nil]);
55
55
  }
56
56
  FBAlert *alert = [FBAlert alertWithApplication:session.activeApplication];
57
- if (!alert.isPresent) {
58
- return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
59
- traceback:nil]);
60
- }
61
57
  NSString *textToType = value;
62
58
  if ([value isKindOfClass:[NSArray class]]) {
63
59
  textToType = [value componentsJoinedByString:@""];
64
60
  }
65
- NSError *error;
66
- if (![alert typeText:textToType error:&error]) {
67
- return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:error.description
68
- traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
69
- }
61
+ [alert typeText:textToType];
70
62
  return FBResponseWithOK();
71
63
  }
72
64
 
@@ -75,20 +67,11 @@
75
67
  XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
76
68
  NSString *name = request.arguments[@"name"];
77
69
  FBAlert *alert = [FBAlert alertWithApplication:application];
78
- NSError *error;
79
70
 
80
- if (!alert.isPresent) {
81
- return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
82
- traceback:nil]);
83
- }
84
71
  if (name) {
85
- if (![alert clickAlertButton:name error:&error]) {
86
- return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
87
- traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
88
- }
89
- } else if (![alert acceptWithError:&error]) {
90
- return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
91
- traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
72
+ [alert clickAlertButton:name];
73
+ } else {
74
+ [alert accept];
92
75
  }
93
76
  return FBResponseWithOK();
94
77
  }
@@ -98,20 +81,11 @@
98
81
  XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
99
82
  NSString *name = request.arguments[@"name"];
100
83
  FBAlert *alert = [FBAlert alertWithApplication:application];
101
- NSError *error;
102
-
103
- if (!alert.isPresent) {
104
- return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
105
- traceback:nil]);
106
- }
84
+
107
85
  if (name) {
108
- if (![alert clickAlertButton:name error:&error]) {
109
- return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
110
- traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
111
- }
112
- } else if (![alert dismissWithError:&error]) {
113
- return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
114
- traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
86
+ [alert clickAlertButton:name];
87
+ } else {
88
+ [alert dismiss];
115
89
  }
116
90
  return FBResponseWithOK();
117
91
  }
@@ -120,11 +94,11 @@
120
94
  FBSession *session = request.session;
121
95
  FBAlert *alert = [FBAlert alertWithApplication:session.activeApplication];
122
96
 
123
- if (!alert.isPresent) {
97
+ NSArray *labels = alert.buttonLabels;
98
+ if (!labels) {
124
99
  return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
125
100
  traceback:nil]);
126
101
  }
127
- NSArray *labels = alert.buttonLabels;
128
102
  return FBResponseWithObject(labels);
129
103
  }
130
104
  @end