FBXCElementSnapshotWrapper+Helpers.m 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. /**
  2. * Copyright (c) 2018-present, Facebook, Inc.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. */
  9. #import "FBXCElementSnapshotWrapper+Helpers.h"
  10. #import "FBFindElementCommands.h"
  11. #import "FBErrorBuilder.h"
  12. #import "FBRunLoopSpinner.h"
  13. #import "FBLogger.h"
  14. #import "FBXCElementSnapshot.h"
  15. #import "FBXCTestDaemonsProxy.h"
  16. #import "FBXCAXClientProxy.h"
  17. #import "XCTestDriver.h"
  18. #import "XCTestPrivateSymbols.h"
  19. #import "XCUIElement.h"
  20. #import "XCUIElement+FBWebDriverAttributes.h"
  21. #import "XCUIHitPointResult.h"
  22. #define ATTRIBUTE_FETCH_WARN_TIME_LIMIT 0.05
  23. inline static BOOL isSnapshotTypeAmongstGivenTypes(id<FBXCElementSnapshot> snapshot,
  24. NSArray<NSNumber *> *types);
  25. @implementation FBXCElementSnapshotWrapper (Helpers)
  26. - (NSString *)fb_description
  27. {
  28. NSString *result = [NSString stringWithFormat:@"%@", self.wdType];
  29. if (nil != self.wdName) {
  30. result = [NSString stringWithFormat:@"%@ (%@)", result, self.wdName];
  31. }
  32. return result;
  33. }
  34. - (NSArray<id<FBXCElementSnapshot>> *)fb_descendantsMatchingType:(XCUIElementType)type
  35. {
  36. return [self descendantsByFilteringWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot) {
  37. return snapshot.elementType == type;
  38. }];
  39. }
  40. - (id<FBXCElementSnapshot>)fb_parentMatchingType:(XCUIElementType)type
  41. {
  42. NSArray *acceptedParents = @[@(type)];
  43. return [self fb_parentMatchingOneOfTypes:acceptedParents];
  44. }
  45. - (id<FBXCElementSnapshot>)fb_parentMatchingOneOfTypes:(NSArray<NSNumber *> *)types
  46. {
  47. return [self fb_parentMatchingOneOfTypes:types filter:^(id<FBXCElementSnapshot> snapshot) {
  48. return YES;
  49. }];
  50. }
  51. - (id<FBXCElementSnapshot>)fb_parentMatchingOneOfTypes:(NSArray<NSNumber *> *)types
  52. filter:(BOOL(^)(id<FBXCElementSnapshot> snapshot))filter
  53. {
  54. id<FBXCElementSnapshot> snapshot = self.parent;
  55. while (snapshot && !(isSnapshotTypeAmongstGivenTypes(snapshot, types) && filter(snapshot))) {
  56. snapshot = snapshot.parent;
  57. }
  58. return snapshot;
  59. }
  60. - (id)fb_attributeValue:(NSString *)attribute
  61. error:(NSError **)error
  62. {
  63. NSDate *start = [NSDate date];
  64. NSDictionary *result = [FBXCAXClientProxy.sharedClient attributesForElement:[self accessibilityElement]
  65. attributes:@[attribute]
  66. error:error];
  67. NSTimeInterval elapsed = ABS([start timeIntervalSinceNow]);
  68. if (elapsed > ATTRIBUTE_FETCH_WARN_TIME_LIMIT) {
  69. NSLog(@"! Fetching of %@ value for %@ took %@s", attribute, self.fb_description, @(elapsed));
  70. }
  71. return [result objectForKey:attribute];
  72. }
  73. inline static BOOL areValuesEqual(id value1, id value2);
  74. inline static BOOL areValuesEqualOrBlank(id value1, id value2);
  75. inline static BOOL isNilOrEmpty(id value);
  76. - (BOOL)fb_framelessFuzzyMatchesElement:(id<FBXCElementSnapshot>)snapshot
  77. {
  78. // Pure payload-based comparison sometimes yield false negatives, therefore relying on it only if all of the identifying properties are blank
  79. if (isNilOrEmpty(self.identifier)
  80. && isNilOrEmpty(self.title)
  81. && isNilOrEmpty(self.label)
  82. && isNilOrEmpty(self.value)
  83. && isNilOrEmpty(self.placeholderValue)) {
  84. return [self.wdUID isEqualToString:([FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdUID ?: @"")];
  85. }
  86. // Sometimes value and placeholderValue of a correct match from different snapshots are not the same (one is nil and one is a blank string)
  87. // Therefore taking it into account when comparing
  88. return self.elementType == snapshot.elementType &&
  89. areValuesEqual(self.identifier, snapshot.identifier) &&
  90. areValuesEqual(self.title, snapshot.title) &&
  91. areValuesEqual(self.label, snapshot.label) &&
  92. areValuesEqualOrBlank(self.value, snapshot.value) &&
  93. areValuesEqualOrBlank(self.placeholderValue, snapshot.placeholderValue);
  94. }
  95. - (NSArray<id<FBXCElementSnapshot>> *)fb_descendantsCellSnapshots
  96. {
  97. NSArray<id<FBXCElementSnapshot>> *cellSnapshots = [self fb_descendantsMatchingType:XCUIElementTypeCell];
  98. if (cellSnapshots.count == 0) {
  99. // For the home screen, cells are actually of type XCUIElementTypeIcon
  100. cellSnapshots = [self fb_descendantsMatchingType:XCUIElementTypeIcon];
  101. }
  102. if (cellSnapshots.count == 0) {
  103. // In some cases XCTest will not report Cell Views. In that case grab all descendants and try to figure out scroll directon from them.
  104. cellSnapshots = self._allDescendants;
  105. }
  106. return cellSnapshots;
  107. }
  108. - (NSArray<id<FBXCElementSnapshot>> *)fb_ancestors
  109. {
  110. NSMutableArray<id<FBXCElementSnapshot>> *ancestors = [NSMutableArray array];
  111. id<FBXCElementSnapshot> parent = self.parent;
  112. while (parent) {
  113. [ancestors addObject:parent];
  114. parent = parent.parent;
  115. }
  116. return ancestors.copy;
  117. }
  118. - (id<FBXCElementSnapshot>)fb_parentCellSnapshot
  119. {
  120. id<FBXCElementSnapshot> targetCellSnapshot = self.snapshot;
  121. // XCUIElementTypeIcon is the cell type for homescreen icons
  122. NSArray<NSNumber *> *acceptableElementTypes = @[
  123. @(XCUIElementTypeCell),
  124. @(XCUIElementTypeIcon),
  125. ];
  126. if (self.elementType != XCUIElementTypeCell && self.elementType != XCUIElementTypeIcon) {
  127. targetCellSnapshot = [self fb_parentMatchingOneOfTypes:acceptableElementTypes];
  128. }
  129. return targetCellSnapshot;
  130. }
  131. - (NSValue *)fb_hitPoint
  132. {
  133. NSError *error;
  134. XCUIHitPointResult *result = [self hitPoint:&error];
  135. if (nil != error) {
  136. [FBLogger logFmt:@"Failed to fetch hit point for %@ - %@", self.fb_description, error.localizedDescription];
  137. return nil;
  138. }
  139. return [NSValue valueWithCGPoint:result.hitPoint];
  140. }
  141. @end
  142. inline static BOOL isSnapshotTypeAmongstGivenTypes(id<FBXCElementSnapshot> snapshot, NSArray<NSNumber *> *types)
  143. {
  144. for (NSUInteger i = 0; i < types.count; i++) {
  145. if([@(snapshot.elementType) isEqual: types[i]] || [types[i] isEqual: @(XCUIElementTypeAny)]){
  146. return YES;
  147. }
  148. }
  149. return NO;
  150. }
  151. inline static BOOL areValuesEqual(id value1, id value2)
  152. {
  153. return value1 == value2 || [value1 isEqual:value2];
  154. }
  155. inline static BOOL areValuesEqualOrBlank(id value1, id value2)
  156. {
  157. return areValuesEqual(value1, value2) || (isNilOrEmpty(value1) && isNilOrEmpty(value2));
  158. }
  159. inline static BOOL isNilOrEmpty(id value)
  160. {
  161. if ([value isKindOfClass:NSString.class]) {
  162. return [(NSString*)value length] == 0;
  163. }
  164. return value == nil;
  165. }