appium-webdriveragent 16.1.0 → 16.1.2

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [16.1.2](https://github.com/appium/WebDriverAgent/compare/v16.1.1...v16.1.2) (2026-08-03)
2
+
3
+ ### Bug Fixes
4
+
5
+ * Optimize class chain lookup ([#1194](https://github.com/appium/WebDriverAgent/issues/1194)) ([47cbbb5](https://github.com/appium/WebDriverAgent/commit/47cbbb597495ac78c4290c27c2ef0ee349004406))
6
+
7
+ ## [16.1.1](https://github.com/appium/WebDriverAgent/compare/v16.1.0...v16.1.1) (2026-08-01)
8
+
9
+ ### Miscellaneous Chores
10
+
11
+ * Drop esmock ([#1195](https://github.com/appium/WebDriverAgent/issues/1195)) ([987b06b](https://github.com/appium/WebDriverAgent/commit/987b06ba11c0084b9d6b921d7ecf86e8234c110a))
12
+
1
13
  ## [16.1.0](https://github.com/appium/WebDriverAgent/compare/v16.0.3...v16.1.0) (2026-07-30)
2
14
 
3
15
  ### Features
@@ -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
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.1.0</string>
18
+ <string>16.1.2</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.1.0</string>
22
+ <string>16.1.2</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -48,6 +48,40 @@
48
48
  button.selected = !button.selected;
49
49
  }
50
50
 
51
+ - (IBAction)goToDeepHierarchy:(id)sender
52
+ {
53
+ // Plain UIViews with fixed frames only - no Auto Layout constraints and no
54
+ // specialized subclasses (e.g. UITextView) that carry their own layout/text
55
+ // engines, which can make a deep nested chain pathologically expensive to
56
+ // lay out. This page exists purely as a fixture for exercising element
57
+ // lookups (e.g. class chain locators) against a deep accessibility tree.
58
+ UIViewController *deepHierarchyViewController = [UIViewController new];
59
+ deepHierarchyViewController.view.backgroundColor = UIColor.whiteColor;
60
+ deepHierarchyViewController.view.accessibilityIdentifier = @"DeepHierarchyPage";
61
+
62
+ NSInteger depth = 70;
63
+ // A plain UILabel sibling, not part of the nested chain below, so the
64
+ // fixture stays recognizable to a human glancing at the simulator instead
65
+ // of showing a blank white screen.
66
+ UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 60, CGRectGetWidth(UIScreen.mainScreen.bounds) - 40, 60)];
67
+ titleLabel.text = [NSString stringWithFormat:@"Deep Hierarchy\n%ld nested elements", (long)depth];
68
+ titleLabel.numberOfLines = 2;
69
+ titleLabel.textAlignment = NSTextAlignmentCenter;
70
+ titleLabel.font = [UIFont systemFontOfSize:20];
71
+ [deepHierarchyViewController.view addSubview:titleLabel];
72
+
73
+ UIView *parent = deepHierarchyViewController.view;
74
+ for (NSInteger i = 0; i < depth; i++) {
75
+ UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
76
+ view.accessibilityIdentifier = [NSString stringWithFormat:@"view_%ld", (long)i];
77
+ view.accessibilityLabel = [NSString stringWithFormat:@"View %ld", (long)i];
78
+ [parent addSubview:view];
79
+ parent = view;
80
+ }
81
+
82
+ [self.navigationController pushViewController:deepHierarchyViewController animated:NO];
83
+ }
84
+
51
85
  - (void)viewDidLayoutSubviews
52
86
  {
53
87
  [super viewDidLayoutSubviews];
@@ -74,6 +74,16 @@
74
74
  <segue destination="XaE-eF-eIt" kind="show" id="q3u-B9-6R6"/>
75
75
  </connections>
76
76
  </button>
77
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dHi-01-Btn">
78
+ <rect key="frame" x="150" y="380" width="114" height="30"/>
79
+ <constraints>
80
+ <constraint firstAttribute="height" constant="30" id="dHi-02-Hgt"/>
81
+ </constraints>
82
+ <state key="normal" title="DeepHierarchy"/>
83
+ <connections>
84
+ <action selector="goToDeepHierarchy:" destination="BYZ-38-t0r" eventType="touchUpInside" id="dHi-03-Act"/>
85
+ </connections>
86
+ </button>
77
87
  </subviews>
78
88
  <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
79
89
  <accessibility key="accessibilityConfiguration" label="MainView"/>
@@ -90,6 +100,8 @@
90
100
  <constraint firstItem="uiD-R4-b34" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="nFx-Xr-rGC"/>
91
101
  <constraint firstItem="S56-6U-3gG" firstAttribute="top" secondItem="M2N-Yn-ytb" secondAttribute="bottom" constant="8" id="rMW-LW-ejO"/>
92
102
  <constraint firstItem="M2N-Yn-ytb" firstAttribute="top" secondItem="YgP-SF-TkS" secondAttribute="bottom" constant="8" id="tMN-gl-smg"/>
103
+ <constraint firstItem="dHi-01-Btn" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="dHi-04-Cnt"/>
104
+ <constraint firstItem="dHi-01-Btn" firstAttribute="top" secondItem="1Ht-AF-MGW" secondAttribute="bottom" constant="8" id="dHi-05-Top"/>
93
105
  </constraints>
94
106
  </view>
95
107
  <navigationItem key="navigationItem" id="dmu-Fe-aoT"/>
@@ -14,6 +14,13 @@ extern NSString *const FBShowAlertForceTouchButtonName;
14
14
  extern NSString *const FBTouchesCountLabelIdentifier;
15
15
  extern NSString *const FBTapsCountLabelIdentifier;
16
16
 
17
+ /**
18
+ Labels of the buttons on the integration app's main page, in their on-screen
19
+ (top to bottom) order. Update this in one place - WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard -
20
+ and here, rather than duplicating the list/count across individual tests.
21
+ */
22
+ extern NSArray<NSString *> *const FBMainViewButtonLabels;
23
+
17
24
  /**
18
25
  XCTestCase helper class used for integration tests
19
26
  */
@@ -62,6 +69,15 @@ extern NSString *const FBTapsCountLabelIdentifier;
62
69
  */
63
70
  - (void)goToScrollPageWithCells:(BOOL)showCells;
64
71
 
72
+ /**
73
+ Navigates integration app to a page containing a 70-level-deep chain of
74
+ nested elements (otherElements[@"view_0"]...otherElements[@"view_69"]),
75
+ each nested directly inside the previous one. Intended as a fixture for
76
+ performance testing of element lookups (e.g. class chain locators) against
77
+ a deep accessibility tree.
78
+ */
79
+ - (void)goToDeepHierarchyPage;
80
+
65
81
  /**
66
82
  Verifies no alerts are present on the page.
67
83
  If an alert exists then it is going to be dismissed.
@@ -26,6 +26,14 @@ NSString *const FBShowSheetAlertButtonName = @"Create Sheet Alert";
26
26
  NSString *const FBShowAlertForceTouchButtonName = @"Create Alert (Force Touch)";
27
27
  NSString *const FBTouchesCountLabelIdentifier = @"numberOfTouchesLabel";
28
28
  NSString *const FBTapsCountLabelIdentifier = @"numberOfTapsLabel";
29
+ NSArray<NSString *> *const FBMainViewButtonLabels = @[
30
+ @"Alerts",
31
+ @"Deadlock app",
32
+ @"Attributes",
33
+ @"Scrolling",
34
+ @"Touch",
35
+ @"DeepHierarchy",
36
+ ];
29
37
 
30
38
  @interface FBIntegrationTestCase ()
31
39
  @property (nonatomic, strong) XCUIApplication *testedApplication;
@@ -129,6 +137,17 @@ NSString *const FBTapsCountLabelIdentifier = @"numberOfTapsLabel";
129
137
  FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts[@"3"].fb_isVisible);
130
138
  }
131
139
 
140
+ - (void)goToDeepHierarchyPage
141
+ {
142
+ [self.testedApplication.buttons[@"DeepHierarchy"] tap];
143
+ [self.testedApplication fb_waitUntilStable];
144
+ // Not fb_isVisible: that runs a native visibility computation which is
145
+ // pathologically slow to resolve for a view nested 70 levels deep at a
146
+ // 1x1 frame. Existence in the accessibility tree is all this fixture
147
+ // actually needs to confirm navigation succeeded.
148
+ FBAssertWaitTillBecomesTrue(self.testedApplication.otherElements[@"view_0"].exists);
149
+ }
150
+
132
151
  - (void)clearAlert
133
152
  {
134
153
  [self.testedApplication fb_waitUntilStable];
@@ -130,7 +130,7 @@
130
130
  - (void)testFindMatchesInElement
131
131
  {
132
132
  NSArray<id<FBXCElementSnapshot>> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication forQuery:@"//XCUIElementTypeButton"];
133
- XCTAssertEqual([matchingSnapshots count], 5);
133
+ XCTAssertEqual([matchingSnapshots count], FBMainViewButtonLabels.count);
134
134
  for (id<FBXCElementSnapshot> element in matchingSnapshots) {
135
135
  XCTAssertTrue([[FBXCElementSnapshotWrapper ensureWrapped:element].wdType isEqualToString:@"XCUIElementTypeButton"]);
136
136
  }
@@ -174,7 +174,7 @@
174
174
  - (void)testFindMatchesInElementWithDotNotation
175
175
  {
176
176
  NSArray<id<FBXCElementSnapshot>> *matchingSnapshots = [FBXPath matchesWithRootElement:self.testedApplication forQuery:@".//XCUIElementTypeButton"];
177
- XCTAssertEqual([matchingSnapshots count], 5);
177
+ XCTAssertEqual([matchingSnapshots count], FBMainViewButtonLabels.count);
178
178
  for (id<FBXCElementSnapshot> element in matchingSnapshots) {
179
179
  XCTAssertTrue([[FBXCElementSnapshotWrapper ensureWrapped:element].wdType isEqualToString:@"XCUIElementTypeButton"]);
180
180
  }
@@ -232,7 +232,7 @@
232
232
  - (void)testFindMultipleMatchesWithMatchesFunction
233
233
  {
234
234
  [self assertXPathQuery:@"//XCUIElementTypeButton[matches(@label, '.*')]"
235
- findsButtonLabels:@[@"Alerts", @"Deadlock app", @"Attributes", @"Scrolling", @"Touch"]];
235
+ findsButtonLabels:FBMainViewButtonLabels];
236
236
  }
237
237
 
238
238
  - (void)testInvalidXPathExtensionFunctionViaElementLookup
@@ -36,13 +36,7 @@
36
36
 
37
37
  - (void)testDescendantsMatchingType
38
38
  {
39
- NSSet<NSString *> *expectedLabels = [NSSet setWithArray:@[
40
- @"Alerts",
41
- @"Attributes",
42
- @"Scrolling",
43
- @"Deadlock app",
44
- @"Touch",
45
- ]];
39
+ NSSet<NSString *> *expectedLabels = [NSSet setWithArray:FBMainViewButtonLabels];
46
40
  NSArray<id<FBXCElementSnapshot>> *matchingSnapshots = [[FBXCElementSnapshotWrapper ensureWrapped:
47
41
  [self.testedView fb_customSnapshot]]
48
42
  fb_descendantsMatchingType:XCUIElementTypeButton];
@@ -41,13 +41,7 @@
41
41
 
42
42
  - (void)testDescendantsWithClassName
43
43
  {
44
- NSSet<NSString *> *expectedLabels = [NSSet setWithArray:@[
45
- @"Alerts",
46
- @"Attributes",
47
- @"Scrolling",
48
- @"Deadlock app",
49
- @"Touch",
50
- ]];
44
+ NSSet<NSString *> *expectedLabels = [NSSet setWithArray:FBMainViewButtonLabels];
51
45
  NSArray<XCUIElement *> *matchingSnapshots = [self.testedView fb_descendantsMatchingClassName:@"XCUIElementTypeButton"
52
46
  shouldReturnAfterFirstMatch:NO];
53
47
  XCTAssertEqual(matchingSnapshots.count, expectedLabels.count);
@@ -273,7 +267,7 @@
273
267
  NSString *queryString =@"XCUIElementTypeWindow/XCUIElementTypeOther/**/XCUIElementTypeButton";
274
268
  matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString
275
269
  shouldReturnAfterFirstMatch:NO];
276
- XCTAssertEqual(matchingSnapshots.count, 5); // /XCUIElementTypeButton
270
+ XCTAssertEqual(matchingSnapshots.count, FBMainViewButtonLabels.count); // /XCUIElementTypeButton
277
271
  for (XCUIElement *matchingSnapshot in matchingSnapshots) {
278
272
  XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton);
279
273
  }
@@ -292,7 +286,7 @@
292
286
  matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString
293
287
  shouldReturnAfterFirstMatch:NO];
294
288
  }
295
- XCTAssertEqual(matchingSnapshots.count, 5); // /XCUIElementTypeButton
289
+ XCTAssertEqual(matchingSnapshots.count, FBMainViewButtonLabels.count); // /XCUIElementTypeButton
296
290
  for (XCUIElement *matchingSnapshot in matchingSnapshots) {
297
291
  XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton);
298
292
  }
@@ -376,7 +370,7 @@
376
370
 
377
371
  XCTAssertEqual(matchingSnapshots.count, 1);
378
372
  XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton);
379
- XCTAssertTrue([matchingSnapshots.lastObject.label isEqualToString:@"Touch"]);
373
+ XCTAssertEqualObjects(matchingSnapshots.lastObject.label, FBMainViewButtonLabels.lastObject);
380
374
 
381
375
  matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton[-10]"
382
376
  shouldReturnAfterFirstMatch:YES];
@@ -470,3 +464,75 @@
470
464
  }
471
465
 
472
466
  @end
467
+
468
+ @interface XCUIElementFBFindTests_DeepHierarchyPage : FBIntegrationTestCase
469
+ @end
470
+ @implementation XCUIElementFBFindTests_DeepHierarchyPage
471
+
472
+ - (void)setUp
473
+ {
474
+ [super setUp];
475
+ static dispatch_once_t onceToken;
476
+ dispatch_once(&onceToken, ^{
477
+ [self launchApplication];
478
+ [self goToDeepHierarchyPage];
479
+ });
480
+ }
481
+
482
+ // No intermediate segment has a position here (only the final one does), so
483
+ // this exercises the query-based class chain strategy - the fast path for
484
+ // the common case. See fb_hasIntermediatePosition: in XCUIElement+FBClassChain.m.
485
+ - (void)testClassChainWithoutIntermediatePositionOnDeepHierarchy
486
+ {
487
+ NSString *query = @"**/XCUIElementTypeOther[`label BEGINSWITH \"View 19\"`]/XCUIElementTypeOther[1]";
488
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingClassChain:query
489
+ shouldReturnAfterFirstMatch:NO];
490
+ XCTAssertEqual(matches.count, 1);
491
+ XCTAssertEqualObjects(matches.firstObject.label, @"View 20");
492
+ }
493
+
494
+ // The first segment here has an explicit position and is not the last
495
+ // segment in the chain, so this exercises the snapshot-walk class chain
496
+ // strategy - the one that avoids paying an extra accessibility round trip
497
+ // per intermediate indexed segment.
498
+ - (void)testClassChainWithIntermediatePositionOnDeepHierarchy
499
+ {
500
+ NSString *query = @"**/XCUIElementTypeOther[`label == \"View 10\"`][1]/**/XCUIElementTypeOther[`label BEGINSWITH \"View 19\"`]";
501
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingClassChain:query
502
+ shouldReturnAfterFirstMatch:NO];
503
+ XCTAssertEqual(matches.count, 1);
504
+ XCTAssertEqualObjects(matches.firstObject.label, @"View 19");
505
+ }
506
+
507
+ // Exercises the snapshot-walk strategy with several intermediate positions
508
+ // (the shape it exists for), as opposed to testPerformanceOfClassChainLookupOnDeepHierarchy
509
+ // above, which only has a position on the final segment and stays on the
510
+ // query-based strategy. Kept within the default snapshotMaxDepth (50)
511
+ // combined with the app's own chrome depth above this fixture's root -
512
+ // going deeper hit a native kAXErrorIllegalArgument even after raising
513
+ // snapshotMaxDepth, which looks like a platform-level limitation
514
+ // independent of this lookup, not something to route around here.
515
+ - (void)testPerformanceOfMultiCheckpointClassChainLookupOnDeepHierarchy
516
+ {
517
+ NSString *query = @"**/XCUIElementTypeOther[`label == \"View 5\"`][1]/**/XCUIElementTypeOther[`label == \"View 15\"`][1]/**/XCUIElementTypeOther[`label == \"View 25\"`][1]/**/XCUIElementTypeOther[`label BEGINSWITH \"View 30\"`]";
518
+ [self measureBlock:^{
519
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingClassChain:query
520
+ shouldReturnAfterFirstMatch:NO];
521
+ XCTAssertEqual(matches.count, 1);
522
+ }];
523
+ }
524
+
525
+ // Not a strict pass/fail assertion (timings vary across machines/CI) - this
526
+ // records a measurement baseline so future regressions on this lookup show
527
+ // up in Xcode's test reports.
528
+ - (void)testPerformanceOfClassChainLookupOnDeepHierarchy
529
+ {
530
+ NSString *query = @"**/XCUIElementTypeOther[`label BEGINSWITH \"View 19\"`]/XCUIElementTypeOther[1]";
531
+ [self measureBlock:^{
532
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingClassChain:query
533
+ shouldReturnAfterFirstMatch:NO];
534
+ XCTAssertEqual(matches.count, 1);
535
+ }];
536
+ }
537
+
538
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-webdriveragent",
3
- "version": "16.1.0",
3
+ "version": "16.1.2",
4
4
  "description": "Package bundling WebDriverAgent",
5
5
  "keywords": [
6
6
  "Appium",
@@ -54,7 +54,7 @@
54
54
  "format:check": "oxfmt -c oxfmt.config.mjs --check .",
55
55
  "prepare": "npm run build",
56
56
  "version": "npm run sync-wda-version",
57
- "test": "node --enable-source-maps --test --test-timeout=60000 \"./build/test/unit/**/*.spec.js\"",
57
+ "test": "node --enable-source-maps --experimental-test-module-mocks --test --test-timeout=60000 \"./build/test/unit/**/*.spec.js\"",
58
58
  "e2e-test": "node --enable-source-maps --test --test-force-exit --test-concurrency=1 --test-timeout=600000 \"./build/test/functional/**/*.spec.js\"",
59
59
  "bundle": "npm run bundle:ios && npm run bundle:tv",
60
60
  "bundle:ios": "TARGET=runner SDK=sim node ./Scripts/build-webdriveragent.mjs",
@@ -81,7 +81,6 @@
81
81
  "@types/node": "^26.0.0",
82
82
  "@types/sinon": "^22.0.0",
83
83
  "appium-xcode": "^7.0.0",
84
- "esmock": "^2.7.6",
85
84
  "node-simctl": "^9.0.0",
86
85
  "semver": "^7.3.7",
87
86
  "sinon": "^22.0.0"