1111#import " FBClassChainQueryParser.h"
1212#import " FBXCodeCompatibility.h"
1313#import " FBExceptions.h"
14+ #import " XCUIElement+FBUtilities.h"
1415
1516@implementation XCUIElement (FBClassChain)
1617
18+ // Walks a single upfront snapshot of `self` in memory to resolve every
19+ // non-indexed chain segment, instead of resolving an intermediate live
20+ // XCUIElement (and paying an accessibility round trip) just to obtain a
21+ // query root for the next segment. A live element is only ever resolved
22+ // once, for the final match(es), via a uid-predicate lookup.
1723- (NSArray <XCUIElement *> *)fb_descendantsMatchingClassChain : (NSString *)classChainQuery shouldReturnAfterFirstMatch : (BOOL )shouldReturnAfterFirstMatch
1824{
1925 NSError *error;
@@ -23,74 +29,85 @@ @implementation XCUIElement (FBClassChain)
2329 return nil ;
2430 }
2531 NSMutableArray <FBClassChainItem *> *lookupChain = parsedChain.elements .mutableCopy ;
32+ NSArray <id <FBXCElementSnapshot>> *currentRoots = @[[self fb_customSnapshot]] ;
2633 FBClassChainItem *chainItem = lookupChain.firstObject ;
27- XCUIElement *currentRoot = self;
28- XCUIElementQuery *query = [currentRoot fb_queryWithChainItem: chainItem query: nil ];
34+ NSArray <id <FBXCElementSnapshot>> *candidates = [self .class fb_snapshotsMatchingItem: chainItem inRoots: currentRoots];
2935 [lookupChain removeObjectAtIndex: 0 ];
3036 while (lookupChain.count > 0 ) {
31- BOOL isRootChanged = NO ;
3237 if (nil != chainItem.position ) {
33- // It is necessary to resolve the query if intermediate element index is not zero or one,
38+ // It is necessary to resolve the position if intermediate element index is not zero or one,
3439 // because predicates don't support search by indexes
35- NSArray <XCUIElement *> *currentRootMatch = [self .class fb_matchingElementsWithItem : chainItem
36- query: query
37- shouldReturnAfterFirstMatch: nil ];
40+ NSArray <id <FBXCElementSnapshot>> *currentRootMatch = [self .class fb_matchingSnapshotsWithItem : chainItem
41+ candidates: candidates
42+ shouldReturnAfterFirstMatch: nil ];
3843 if (0 == currentRootMatch.count ) {
3944 return @[];
4045 }
41- currentRoot = currentRootMatch.firstObject ;
42- isRootChanged = YES ;
46+ currentRoots = @[currentRootMatch.firstObject];
47+ } else {
48+ currentRoots = candidates;
4349 }
44- chainItem = [ lookupChain firstObject ] ;
45- query = [currentRoot fb_queryWithChainItem : chainItem query: isRootChanged ? nil : query ];
50+ chainItem = lookupChain. firstObject ;
51+ candidates = [self .class fb_snapshotsMatchingItem : chainItem inRoots: currentRoots ];
4652 [lookupChain removeObjectAtIndex: 0 ];
4753 }
48- return [self .class fb_matchingElementsWithItem: chainItem
49- query: query
50- shouldReturnAfterFirstMatch: @(shouldReturnAfterFirstMatch)];
54+ NSArray <id <FBXCElementSnapshot>> *matchedSnapshots = [self .class fb_matchingSnapshotsWithItem: chainItem
55+ candidates: candidates
56+ shouldReturnAfterFirstMatch: @(shouldReturnAfterFirstMatch)];
57+ return [self fb_filterDescendantsWithSnapshots: matchedSnapshots onlyChildren: NO ];
5158}
5259
53- - (XCUIElementQuery *)fb_queryWithChainItem : (FBClassChainItem *)item query : (nullable XCUIElementQuery *) query
60+ + ( NSArray <id<FBXCElementSnapshot>> *)fb_snapshotsMatchingItem : (FBClassChainItem *)item inRoots : ( NSArray <id<FBXCElementSnapshot>> *) roots
5461{
55- if (item. isDescendant ) {
56- if (query ) {
57- query = [query descendantsMatchingType: item.type];
58- } else {
59- query = [ self .fb_query descendantsMatchingType: item.type];
60- }
61- } else {
62- if (query) {
63- query = [query childrenMatchingType: item.type ];
62+ NSMutableArray < id <FBXCElementSnapshot>> *typeMatches = [ NSMutableArray array ];
63+ for ( id <FBXCElementSnapshot> root in roots ) {
64+ if ( item.isDescendant ) {
65+ // descendantsByFilteringWithBlock: includes the receiver itself if it
66+ // matches the filter, unlike XCUIElementQuery's descendantsMatchingType:,
67+ // so the root has to be excluded explicitly here.
68+ [typeMatches addObjectsFromArray: [root descendantsByFilteringWithBlock: ^ BOOL ( id <FBXCElementSnapshot> snapshot) {
69+ return snapshot != root && (item. type == XCUIElementTypeAny || snapshot. elementType == item. type );
70+ }] ];
6471 } else {
65- query = [self .fb_query childrenMatchingType: item.type];
72+ for (id <FBXCElementSnapshot> child in root.children ) {
73+ if (item.type == XCUIElementTypeAny || child.elementType == item.type ) {
74+ [typeMatches addObject: child];
75+ }
76+ }
6677 }
6778 }
68- if (item.predicates ) {
69- for (FBAbstractPredicateItem *predicate in item.predicates ) {
70- if ([predicate isKindOfClass: FBSelfPredicateItem.class]) {
71- query = [query matchingPredicate: predicate.value];
72- } else if ([predicate isKindOfClass: FBDescendantPredicateItem.class]) {
73- query = [query containingPredicate: predicate.value];
79+ for (FBAbstractPredicateItem *predicateItem in item.predicates ) {
80+ if ([predicateItem isKindOfClass: FBSelfPredicateItem.class]) {
81+ typeMatches = [[typeMatches filteredArrayUsingPredicate: predicateItem.value] mutableCopy ];
82+ } else if ([predicateItem isKindOfClass: FBDescendantPredicateItem.class]) {
83+ NSMutableArray <id <FBXCElementSnapshot>> *containingMatches = [NSMutableArray array ];
84+ for (id <FBXCElementSnapshot> candidate in typeMatches) {
85+ NSArray <id <FBXCElementSnapshot>> *matchingDescendants = [candidate descendantsByFilteringWithBlock: ^BOOL (id <FBXCElementSnapshot> descendant) {
86+ return descendant != candidate && [predicateItem.value evaluateWithObject: descendant];
87+ }];
88+ if (matchingDescendants.count > 0 ) {
89+ [containingMatches addObject: candidate];
90+ }
7491 }
92+ typeMatches = containingMatches;
7593 }
7694 }
77- return query ;
95+ return typeMatches. copy ;
7896}
7997
80- + (NSArray <XCUIElement *> *)fb_matchingElementsWithItem : (FBClassChainItem *)item query : (XCUIElementQuery *)query shouldReturnAfterFirstMatch : (nullable NSNumber *)shouldReturnAfterFirstMatch
98+ + (NSArray <id<FBXCElementSnapshot>> *)fb_matchingSnapshotsWithItem : (FBClassChainItem *)item candidates : ( NSArray <id<FBXCElementSnapshot>> *)candidates shouldReturnAfterFirstMatch : (nullable NSNumber *)shouldReturnAfterFirstMatch
8199{
82100 if (1 == item.position .integerValue || (0 == item.position .integerValue && shouldReturnAfterFirstMatch.boolValue )) {
83- XCUIElement * result = query. fb_firstMatch ;
101+ id <FBXCElementSnapshot> result = candidates. firstObject ;
84102 return result ? @[result] : @[];
85103 }
86- NSArray <XCUIElement *> *allMatches = query.fb_allMatches ;
87104 if (0 == item.position .integerValue ) {
88- return allMatches ;
105+ return candidates ;
89106 }
90- if (allMatches .count >= (NSUInteger )ABS (item.position .integerValue )) {
107+ if (candidates .count >= (NSUInteger )ABS (item.position .integerValue )) {
91108 return item.position .integerValue > 0
92- ? @[[allMatches objectAtIndex:item.position.integerValue - 1]]
93- : @[[allMatches objectAtIndex:allMatches .count + item.position.integerValue]] ;
109+ ? @[[candidates objectAtIndex:item.position.integerValue - 1]]
110+ : @[[candidates objectAtIndex:candidates .count + item.position.integerValue]] ;
94111 }
95112 return @[];
96113}
0 commit comments