XCUIElement+FBIsVisible.m 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /**
  2. * Copyright (c) 2015-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 "XCUIElement+FBIsVisible.h"
  10. #import "FBConfiguration.h"
  11. #import "FBElementUtils.h"
  12. #import "FBMathUtils.h"
  13. #import "FBActiveAppDetectionPoint.h"
  14. #import "FBSession.h"
  15. #import "FBXCAccessibilityElement.h"
  16. #import "FBXCodeCompatibility.h"
  17. #import "FBXCElementSnapshotWrapper+Helpers.h"
  18. #import "XCUIElement+FBUtilities.h"
  19. #import "XCUIElement+FBUID.h"
  20. #import "XCTestPrivateSymbols.h"
  21. @implementation XCUIElement (FBIsVisible)
  22. - (BOOL)fb_isVisible
  23. {
  24. id<FBXCElementSnapshot> snapshot = [self fb_snapshotWithAttributes:@[FB_XCAXAIsVisibleAttributeName]
  25. maxDepth:@1];
  26. return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isVisible;
  27. }
  28. @end
  29. @implementation FBXCElementSnapshotWrapper (FBIsVisible)
  30. + (NSString *)fb_uniqIdWithSnapshot:(id<FBXCElementSnapshot>)snapshot
  31. {
  32. return [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot] ?: [NSString stringWithFormat:@"%p", (void *)snapshot];
  33. }
  34. - (nullable NSNumber *)fb_cachedVisibilityValue
  35. {
  36. NSMutableDictionary *cache = FBSession.activeSession.elementsVisibilityCache;
  37. if (nil == cache) {
  38. return nil;
  39. }
  40. NSDictionary<NSString *, NSNumber *> *result = cache[@(self.generation)];
  41. if (nil == result) {
  42. // There is no need to keep the cached data for the previous generations
  43. [cache removeAllObjects];
  44. cache[@(self.generation)] = [NSMutableDictionary dictionary];
  45. return nil;
  46. }
  47. return result[[self.class fb_uniqIdWithSnapshot:self.snapshot]];
  48. }
  49. - (BOOL)fb_cacheVisibilityWithValue:(BOOL)isVisible
  50. forAncestors:(nullable NSArray<id<FBXCElementSnapshot>> *)ancestors
  51. {
  52. NSMutableDictionary *cache = FBSession.activeSession.elementsVisibilityCache;
  53. if (nil == cache) {
  54. return isVisible;
  55. }
  56. NSMutableDictionary<NSString *, NSNumber *> *destination = cache[@(self.generation)];
  57. if (nil == destination) {
  58. return isVisible;
  59. }
  60. NSNumber *visibleObj = [NSNumber numberWithBool:isVisible];
  61. destination[[self.class fb_uniqIdWithSnapshot:self.snapshot]] = visibleObj;
  62. if (isVisible && nil != ancestors) {
  63. // if an element is visible then all its ancestors must be visible as well
  64. for (id<FBXCElementSnapshot> ancestor in ancestors) {
  65. NSString *ancestorId = [self.class fb_uniqIdWithSnapshot:ancestor];
  66. if (nil == destination[ancestorId]) {
  67. destination[ancestorId] = visibleObj;
  68. }
  69. }
  70. }
  71. return isVisible;
  72. }
  73. - (CGRect)fb_frameInContainer:(id<FBXCElementSnapshot>)container
  74. hierarchyIntersection:(nullable NSValue *)intersectionRectange
  75. {
  76. CGRect currentRectangle = nil == intersectionRectange ? self.frame : [intersectionRectange CGRectValue];
  77. id<FBXCElementSnapshot> parent = self.parent;
  78. CGRect parentFrame = parent.frame;
  79. CGRect containerFrame = container.frame;
  80. if (CGSizeEqualToSize(parentFrame.size, CGSizeZero) &&
  81. CGPointEqualToPoint(parentFrame.origin, CGPointZero)) {
  82. // Special case (or XCTest bug). Shift the origin and return immediately after shift
  83. id<FBXCElementSnapshot> nextParent = parent.parent;
  84. BOOL isGrandparent = YES;
  85. while (nextParent && nextParent != container) {
  86. CGRect nextParentFrame = nextParent.frame;
  87. if (isGrandparent &&
  88. CGSizeEqualToSize(nextParentFrame.size, CGSizeZero) &&
  89. CGPointEqualToPoint(nextParentFrame.origin, CGPointZero)) {
  90. // Double zero-size container inclusion means that element coordinates are absolute
  91. return CGRectIntersection(currentRectangle, containerFrame);
  92. }
  93. isGrandparent = NO;
  94. if (!CGPointEqualToPoint(nextParentFrame.origin, CGPointZero)) {
  95. currentRectangle.origin.x += nextParentFrame.origin.x;
  96. currentRectangle.origin.y += nextParentFrame.origin.y;
  97. return CGRectIntersection(currentRectangle, containerFrame);
  98. }
  99. nextParent = nextParent.parent;
  100. }
  101. return CGRectIntersection(currentRectangle, containerFrame);
  102. }
  103. // Skip parent containers if they are outside of the viewport
  104. CGRect intersectionWithParent = CGRectIntersectsRect(parentFrame, containerFrame) || parent.elementType != XCUIElementTypeOther
  105. ? CGRectIntersection(currentRectangle, parentFrame)
  106. : currentRectangle;
  107. if (CGRectIsEmpty(intersectionWithParent) &&
  108. parent != container &&
  109. self.elementType == XCUIElementTypeOther) {
  110. // Special case (or XCTest bug). Shift the origin
  111. if (CGSizeEqualToSize(parentFrame.size, containerFrame.size) ||
  112. // The size might be inverted in landscape
  113. CGSizeEqualToSize(parentFrame.size, CGSizeMake(containerFrame.size.height, containerFrame.size.width)) ||
  114. CGSizeEqualToSize(self.frame.size, CGSizeZero)) {
  115. // Covers ActivityListView and RemoteBridgeView cases
  116. currentRectangle.origin.x += parentFrame.origin.x;
  117. currentRectangle.origin.y += parentFrame.origin.y;
  118. return CGRectIntersection(currentRectangle, containerFrame);
  119. }
  120. }
  121. if (CGRectIsEmpty(intersectionWithParent) || parent == container) {
  122. return intersectionWithParent;
  123. }
  124. return [[FBXCElementSnapshotWrapper ensureWrapped:parent] fb_frameInContainer:container
  125. hierarchyIntersection:[NSValue valueWithCGRect:intersectionWithParent]];
  126. }
  127. - (BOOL)fb_hasAnyVisibleLeafs
  128. {
  129. NSArray<id<FBXCElementSnapshot>> *children = self.children;
  130. if (0 == children.count) {
  131. return self.fb_isVisible;
  132. }
  133. for (id<FBXCElementSnapshot> child in children) {
  134. if ([FBXCElementSnapshotWrapper ensureWrapped:child].fb_hasAnyVisibleLeafs) {
  135. return YES;
  136. }
  137. }
  138. return NO;
  139. }
  140. - (BOOL)fb_isVisible
  141. {
  142. NSNumber *isVisible = self.additionalAttributes[FB_XCAXAIsVisibleAttribute];
  143. if (isVisible != nil) {
  144. return isVisible.boolValue;
  145. }
  146. NSNumber *cachedValue = [self fb_cachedVisibilityValue];
  147. if (nil != cachedValue) {
  148. return [cachedValue boolValue];
  149. }
  150. CGRect selfFrame = self.frame;
  151. if (CGRectIsEmpty(selfFrame)) {
  152. return [self fb_cacheVisibilityWithValue:NO forAncestors:nil];
  153. }
  154. NSArray<id<FBXCElementSnapshot>> *ancestors = self.fb_ancestors;
  155. if ([FBConfiguration shouldUseTestManagerForVisibilityDetection]) {
  156. BOOL visibleAttrValue = [(NSNumber *)[self fb_attributeValue:FB_XCAXAIsVisibleAttributeName] boolValue];
  157. return [self fb_cacheVisibilityWithValue:visibleAttrValue forAncestors:ancestors];
  158. }
  159. id<FBXCElementSnapshot> parentWindow = ancestors.count > 1 ? [ancestors objectAtIndex:ancestors.count - 2] : nil;
  160. CGRect visibleRect = selfFrame;
  161. if (nil != parentWindow) {
  162. visibleRect = [self fb_frameInContainer:parentWindow hierarchyIntersection:nil];
  163. }
  164. if (CGRectIsEmpty(visibleRect)) {
  165. return [self fb_cacheVisibilityWithValue:NO forAncestors:ancestors];
  166. }
  167. CGPoint midPoint = CGPointMake(visibleRect.origin.x + visibleRect.size.width / 2,
  168. visibleRect.origin.y + visibleRect.size.height / 2);
  169. id<FBXCAccessibilityElement> hitElement = [FBActiveAppDetectionPoint axElementWithPoint:midPoint];
  170. if (nil != hitElement) {
  171. if (FBIsAXElementEqualToOther(self.accessibilityElement, hitElement)) {
  172. return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors];
  173. }
  174. for (id<FBXCElementSnapshot> ancestor in ancestors) {
  175. if (FBIsAXElementEqualToOther(hitElement, ancestor.accessibilityElement)) {
  176. return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors];
  177. }
  178. }
  179. }
  180. if (self.children.count > 0) {
  181. if (nil != hitElement) {
  182. for (id<FBXCElementSnapshot> descendant in self._allDescendants) {
  183. if (FBIsAXElementEqualToOther(hitElement, descendant.accessibilityElement)) {
  184. return [self fb_cacheVisibilityWithValue:YES
  185. forAncestors:[FBXCElementSnapshotWrapper ensureWrapped:descendant].fb_ancestors];
  186. }
  187. }
  188. }
  189. if (self.fb_hasAnyVisibleLeafs) {
  190. return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors];
  191. }
  192. } else if (nil == hitElement) {
  193. // Sometimes XCTest returns nil for leaf elements hit test even if such elements are hittable
  194. // Assume such elements are visible if their rectInContainer is visible
  195. return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors];
  196. }
  197. return [self fb_cacheVisibilityWithValue:NO forAncestors:ancestors];
  198. }
  199. @end