appium-webdriveragent 16.13.3 → 16.13.4

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,9 @@
1
+ ## [16.13.4](https://github.com/appium/WebDriverAgent/compare/v16.13.3...v16.13.4) (2026-09-30)
2
+
3
+ ### Performance Improvements
4
+
5
+ * use native predicates for accessibility id lookups ([#1286](https://github.com/appium/WebDriverAgent/issues/1286)) ([2ceb940](https://github.com/appium/WebDriverAgent/commit/2ceb940939c2033cb37b846962a966d42bb0ddab))
6
+
1
7
  ## [16.13.3](https://github.com/appium/WebDriverAgent/compare/v16.13.2...v16.13.3) (2026-09-30)
2
8
 
3
9
  ### Bug Fixes
@@ -120,14 +120,27 @@
120
120
  - (NSArray<XCUIElement *> *)fb_descendantsMatchingIdentifier:(NSString *)accessibilityId
121
121
  shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
122
122
  {
123
- NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot,
124
- NSDictionary<NSString *,id> * _Nullable bindings) {
125
- @autoreleasepool {
126
- return [[FBXCElementSnapshotWrapper wdNameWithSnapshot:snapshot] isEqualToString:accessibilityId];
123
+ // Standard snapshot attributes let XCTest evaluate a structured predicate
124
+ // without WDA's per-snapshot block callback, while preserving wdName semantics.
125
+ // Local benchmarks showed lower end-to-end lookup latency; see measurements
126
+ // and regression checks in https://github.com/appium/WebDriverAgent/pull/1286.
127
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:
128
+ @"(identifier != nil AND identifier != '' AND identifier == %@) OR "
129
+ "((identifier == nil OR identifier == '') AND label != nil AND label != '' AND label == %@)",
130
+ accessibilityId, accessibilityId];
131
+ XCUIElementQuery *query = [[self.fb_query descendantsMatchingType:XCUIElementTypeAny]
132
+ matchingPredicate:predicate];
133
+ NSMutableArray<XCUIElement *> *result = [NSMutableArray arrayWithArray:
134
+ [self.class fb_extractMatchingElementsFromQuery:query
135
+ shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]];
136
+ id<FBXCElementSnapshot> cachedSnapshot = [self fb_cachedSnapshotWithQuery:query];
137
+ if ([predicate evaluateWithObject:cachedSnapshot]) {
138
+ if (shouldReturnAfterFirstMatch || result.count == 0) {
139
+ return @[self];
127
140
  }
128
- }];
129
- return [self fb_descendantsMatchingPredicate:predicate
130
- shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
141
+ [result insertObject:self atIndex:0];
142
+ }
143
+ return result.copy;
131
144
  }
132
145
 
133
146
  @end
@@ -15,11 +15,11 @@
15
15
  <key>CFBundlePackageType</key>
16
16
  <string>FMWK</string>
17
17
  <key>CFBundleShortVersionString</key>
18
- <string>16.13.3</string>
18
+ <string>16.13.4</string>
19
19
  <key>CFBundleSignature</key>
20
20
  <string>????</string>
21
21
  <key>CFBundleVersion</key>
22
- <string>16.13.3</string>
22
+ <string>16.13.4</string>
23
23
  <key>NSPrincipalClass</key>
24
24
  <string/>
25
25
  </dict>
@@ -10,7 +10,40 @@
10
10
 
11
11
  @implementation SceneDelegate
12
12
 
13
+ - (void)showIdentifierLookupFixture
14
+ {
15
+ UIViewController *controller = [UIViewController new];
16
+ controller.view.backgroundColor = UIColor.whiteColor;
17
+ controller.view.accessibilityIdentifier = @"IdentifierLookupFixture";
18
+ NSArray *identifiers = @[@"something", @"different", @"", NSNull.null, @"something", @"", NSNull.null, @"quote'\"é", NSNull.null];
19
+ NSArray<NSString *> *labels = @[@"other label", @"something", @"something", @"something", @"something", @"", @"", @"quoted", @"日本語"];
20
+ NSMutableArray<UILabel *> *labelViews = [NSMutableArray array];
21
+ for (NSUInteger i = 0; i < identifiers.count; i++) {
22
+ UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(20, 90 + i * 40, 300, 35)];
23
+ label.text = [NSString stringWithFormat:@"Fixture %lu", (unsigned long)i];
24
+ label.accessibilityIdentifier = identifiers[i] == NSNull.null ? nil : identifiers[i];
25
+ label.accessibilityLabel = labels[i];
26
+ label.isAccessibilityElement = YES;
27
+ [labelViews addObject:label];
28
+ [controller.view addSubview:label];
29
+ }
30
+ UIButton *mutate = [UIButton buttonWithType:UIButtonTypeSystem];
31
+ mutate.frame = CGRectMake(20, 470, 220, 40);
32
+ [mutate setTitle:@"Change identifier" forState:UIControlStateNormal];
33
+ mutate.accessibilityIdentifier = @"mutateIdentifier";
34
+ [mutate addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
35
+ labelViews[0].accessibilityIdentifier = @"changed";
36
+ }] forControlEvents:UIControlEventTouchUpInside];
37
+ [controller.view addSubview:mutate];
38
+ self.window.rootViewController = controller;
39
+ [self.window makeKeyAndVisible];
40
+ }
41
+
13
42
  - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
43
+ if ([NSProcessInfo.processInfo.arguments containsObject:@"--identifier-lookup-test"]) {
44
+ [self showIdentifierLookupFixture];
45
+ return;
46
+ }
14
47
  // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
15
48
  // If using a storyboard, the `window` property will automatically be set and attached to the scene.
16
49
  // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession:` instead).
@@ -9,6 +9,7 @@
9
9
  #import <XCTest/XCTest.h>
10
10
 
11
11
  #import "FBIntegrationTestCase.h"
12
+ #import "FBConfiguration.h"
12
13
  #import "FBElementUtils.h"
13
14
  #import "FBExceptions.h"
14
15
  #import "FBTestMacros.h"
@@ -558,3 +559,115 @@
558
559
  }
559
560
 
560
561
  @end
562
+
563
+
564
+ @interface XCUIElementFBFindTests_IdentifierSemantics : FBIntegrationTestCase
565
+ @end
566
+
567
+ @implementation XCUIElementFBFindTests_IdentifierSemantics
568
+
569
+ - (void)setUp
570
+ {
571
+ [super setUp];
572
+ self.testedApplication.launchArguments = @[@"--identifier-lookup-test"];
573
+ [self.testedApplication launch];
574
+ XCTAssertTrue([self.testedApplication.otherElements[@"IdentifierLookupFixture"] waitForExistenceWithTimeout:5]);
575
+ }
576
+
577
+ - (void)tearDown
578
+ {
579
+ [self.testedApplication terminate];
580
+ [super tearDown];
581
+ }
582
+
583
+ - (void)testIdentifierTakesPrecedenceOverLabel
584
+ {
585
+ NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingIdentifier:@"something"
586
+ shouldReturnAfterFirstMatch:NO];
587
+ XCTAssertEqual(matches.count, 4);
588
+ XCTAssertEqualObjects([matches valueForKey:@"label"], (@[@"other label", @"something", @"something", @"something"]));
589
+ XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"different" shouldReturnAfterFirstMatch:NO].count, 1);
590
+ // A matching label must not override a different, nonempty identifier.
591
+ XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"other label" shouldReturnAfterFirstMatch:NO].count, 0);
592
+ }
593
+
594
+ - (void)testEmptyIdentifierDoesNotMatchEmptyAccessibilityId
595
+ {
596
+ // The fixture has an empty identifier with a nonempty label at index 2,
597
+ // and an empty identifier with an empty label at index 5.
598
+ XCUIElement *labelFallback = [self.testedApplication.staticTexts elementBoundByIndex:2];
599
+ XCUIElement *unnamed = [self.testedApplication.staticTexts elementBoundByIndex:5];
600
+ XCTAssertEqualObjects(labelFallback.identifier, @"");
601
+ XCTAssertEqualObjects(labelFallback.label, @"something");
602
+ XCTAssertEqualObjects(unnamed.identifier, @"");
603
+ XCTAssertEqualObjects(unnamed.label, @"");
604
+
605
+ // Regression guard for "identifier != nil AND identifier != ''": the old
606
+ // wdName lookup used the nonempty label, or nil when both fields were empty.
607
+ // Thus an empty query matched neither element. Checking only identifier != nil
608
+ // would instead let identifier == @"" match both, changing existing behavior.
609
+ BOOL previousFirstMatch = FBConfiguration.sharedInstance.useFirstMatch;
610
+ @try {
611
+ for (NSNumber *useFirstMatch in @[@NO, @YES]) {
612
+ FBConfiguration.sharedInstance.useFirstMatch = useFirstMatch.boolValue;
613
+ for (NSNumber *firstMatch in @[@NO, @YES]) {
614
+ XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@""
615
+ shouldReturnAfterFirstMatch:firstMatch.boolValue].count, 0,
616
+ @"An empty accessibility ID must not match empty identifiers, even when they are non-nil");
617
+ }
618
+ }
619
+ } @finally {
620
+ FBConfiguration.sharedInstance.useFirstMatch = previousFirstMatch;
621
+ }
622
+ }
623
+
624
+ - (void)testMissingQuotedAndUnicodeIdentifiers
625
+ {
626
+ for (NSNumber *firstMatch in @[@NO, @YES]) {
627
+ XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"missing-target" shouldReturnAfterFirstMatch:firstMatch.boolValue].count, 0);
628
+ XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"quote'\"é" shouldReturnAfterFirstMatch:firstMatch.boolValue].count, 1);
629
+ XCTAssertEqual([self.testedApplication fb_descendantsMatchingIdentifier:@"日本語" shouldReturnAfterFirstMatch:firstMatch.boolValue].count, 1);
630
+ }
631
+ }
632
+
633
+ - (void)testFirstMatchAndParentScopeWithBothBindings
634
+ {
635
+ BOOL previousBinding = FBConfiguration.sharedInstance.boundElementsByIndex;
636
+ @try {
637
+ for (NSNumber *indexed in @[@NO, @YES]) {
638
+ FBConfiguration.sharedInstance.boundElementsByIndex = indexed.boolValue;
639
+ // Use the element returned by WDA lookup, as the element-scoped endpoint does.
640
+ XCUIElement *parent = [self.testedApplication fb_descendantsMatchingIdentifier:@"IdentifierLookupFixture"
641
+ shouldReturnAfterFirstMatch:YES].firstObject;
642
+ XCTAssertNotNil(parent);
643
+ // The HTTP endpoint resolves the parent when registering its element ID.
644
+ XCTAssertNotNil(parent.fb_uid);
645
+ NSArray<XCUIElement *> *matches = [parent fb_descendantsMatchingIdentifier:@"something" shouldReturnAfterFirstMatch:NO];
646
+ NSArray<XCUIElement *> *first = [parent fb_descendantsMatchingIdentifier:@"something" shouldReturnAfterFirstMatch:YES];
647
+ XCTAssertEqual(matches.count, 4);
648
+ XCTAssertEqual(first.count, 1);
649
+ XCTAssertEqualObjects(first.firstObject.fb_uid, matches.firstObject.fb_uid);
650
+ NSArray<XCUIElement *> *selfMatches = [parent fb_descendantsMatchingIdentifier:@"IdentifierLookupFixture" shouldReturnAfterFirstMatch:NO];
651
+ XCTAssertEqual(selfMatches.count, 1);
652
+ XCTAssertEqualObjects(selfMatches.firstObject.fb_uid, parent.fb_uid);
653
+ }
654
+ } @finally {
655
+ FBConfiguration.sharedInstance.boundElementsByIndex = previousBinding;
656
+ }
657
+ }
658
+
659
+ - (void)testIdentifierChangesAreObserved
660
+ {
661
+ NSArray<XCUIElement *> *before = [self.testedApplication fb_descendantsMatchingIdentifier:@"something" shouldReturnAfterFirstMatch:NO];
662
+ XCTAssertEqual(before.count, 4);
663
+ NSArray<NSString *> *beforeIds = [before valueForKey:@"fb_uid"];
664
+ [self.testedApplication.buttons[@"mutateIdentifier"] tap];
665
+ NSArray<XCUIElement *> *after = [self.testedApplication fb_descendantsMatchingIdentifier:@"something" shouldReturnAfterFirstMatch:NO];
666
+ NSArray<XCUIElement *> *changed = [self.testedApplication fb_descendantsMatchingIdentifier:@"changed" shouldReturnAfterFirstMatch:NO];
667
+ XCTAssertEqual(after.count, 3);
668
+ XCTAssertEqual(changed.count, 1);
669
+ XCTAssertEqualObjects([after valueForKey:@"fb_uid"], [beforeIds subarrayWithRange:NSMakeRange(1, 3)]);
670
+ XCTAssertEqualObjects(changed.firstObject.fb_uid, beforeIds.firstObject);
671
+ }
672
+
673
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-webdriveragent",
3
- "version": "16.13.3",
3
+ "version": "16.13.4",
4
4
  "description": "Package bundling WebDriverAgent",
5
5
  "keywords": [
6
6
  "Appium",