XCUIElement+FBScrolling.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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+FBScrolling.h"
  10. #import "FBErrorBuilder.h"
  11. #import "FBLogger.h"
  12. #import "FBMacros.h"
  13. #import "FBMathUtils.h"
  14. #import "FBXCodeCompatibility.h"
  15. #import "FBXCElementSnapshotWrapper.h"
  16. #import "FBXCElementSnapshotWrapper+Helpers.h"
  17. #import "XCUIElement+FBCaching.h"
  18. #import "XCUIApplication.h"
  19. #import "XCUICoordinate.h"
  20. #import "XCUIElement+FBIsVisible.h"
  21. #import "XCUIElement.h"
  22. #import "XCUIElement+FBUtilities.h"
  23. #import "XCUIElement+FBWebDriverAttributes.h"
  24. const CGFloat FBFuzzyPointThreshold = 20.f; //Smallest determined value that is not interpreted as touch
  25. const CGFloat FBScrollToVisibleNormalizedDistance = .5f;
  26. const CGFloat FBTouchEventDelay = 0.5f;
  27. const CGFloat FBTouchVelocity = 300; // pixels per sec
  28. const CGFloat FBScrollTouchProportion = 0.75f;
  29. #if !TARGET_OS_TV
  30. @interface FBXCElementSnapshotWrapper (FBScrolling)
  31. - (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
  32. - (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
  33. - (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
  34. - (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
  35. - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector inApplication:(XCUIApplication *)application;
  36. - (BOOL)fb_scrollByVector:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error;
  37. @end
  38. @implementation XCUIElement (FBScrolling)
  39. - (BOOL)fb_nativeScrollToVisibleWithError:(NSError **)error
  40. {
  41. id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
  42. ? self.lastSnapshot
  43. : self.fb_takeSnapshot;
  44. return nil != [self _hitPointByAttemptingToScrollToVisibleSnapshot:snapshot
  45. error:error];
  46. }
  47. - (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance
  48. {
  49. id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
  50. ? self.lastSnapshot
  51. : self.fb_takeSnapshot;
  52. [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollUpByNormalizedDistance:distance
  53. inApplication:self.application];
  54. }
  55. - (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance
  56. {
  57. id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
  58. ? self.lastSnapshot
  59. : self.fb_takeSnapshot;
  60. [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollDownByNormalizedDistance:distance
  61. inApplication:self.application];
  62. }
  63. - (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance
  64. {
  65. id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
  66. ? self.lastSnapshot
  67. : self.fb_takeSnapshot;
  68. [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollLeftByNormalizedDistance:distance
  69. inApplication:self.application];
  70. }
  71. - (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance
  72. {
  73. id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
  74. ? self.lastSnapshot
  75. : self.fb_takeSnapshot;
  76. [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollRightByNormalizedDistance:distance
  77. inApplication:self.application];
  78. }
  79. - (BOOL)fb_scrollToVisibleWithError:(NSError **)error
  80. {
  81. return [self fb_scrollToVisibleWithNormalizedScrollDistance:FBScrollToVisibleNormalizedDistance error:error];
  82. }
  83. - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance error:(NSError **)error
  84. {
  85. return [self fb_scrollToVisibleWithNormalizedScrollDistance:normalizedScrollDistance
  86. scrollDirection:FBXCUIElementScrollDirectionUnknown
  87. error:error];
  88. }
  89. - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance
  90. scrollDirection:(FBXCUIElementScrollDirection)scrollDirection
  91. error:(NSError **)error
  92. {
  93. FBXCElementSnapshotWrapper *prescrollSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_takeSnapshot]];
  94. if (prescrollSnapshot.isWDVisible) {
  95. return YES;
  96. }
  97. static dispatch_once_t onceToken;
  98. static NSArray *acceptedParents;
  99. dispatch_once(&onceToken, ^{
  100. acceptedParents = @[
  101. @(XCUIElementTypeScrollView),
  102. @(XCUIElementTypeCollectionView),
  103. @(XCUIElementTypeTable),
  104. @(XCUIElementTypeWebView),
  105. ];
  106. });
  107. __block NSArray<id<FBXCElementSnapshot>> *cellSnapshots;
  108. __block NSMutableArray<id<FBXCElementSnapshot>> *visibleCellSnapshots = [NSMutableArray new];
  109. id<FBXCElementSnapshot> scrollView = [prescrollSnapshot fb_parentMatchingOneOfTypes:acceptedParents
  110. filter:^(id<FBXCElementSnapshot> snapshot) {
  111. FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
  112. if (![wrappedSnapshot isWDVisible]) {
  113. return NO;
  114. }
  115. cellSnapshots = [wrappedSnapshot fb_descendantsCellSnapshots];
  116. for (id<FBXCElementSnapshot> cellSnapshot in cellSnapshots) {
  117. FBXCElementSnapshotWrapper *wrappedCellSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:cellSnapshot];
  118. if (wrappedCellSnapshot.wdVisible) {
  119. [visibleCellSnapshots addObject:cellSnapshot];
  120. }
  121. }
  122. if (visibleCellSnapshots.count > 1) {
  123. return YES;
  124. }
  125. return NO;
  126. }];
  127. if (scrollView == nil) {
  128. return
  129. [[[FBErrorBuilder builder]
  130. withDescriptionFormat:@"Failed to find scrollable visible parent with 2 visible children"]
  131. buildError:error];
  132. }
  133. id<FBXCElementSnapshot> targetCellSnapshot = [prescrollSnapshot fb_parentCellSnapshot];
  134. id<FBXCElementSnapshot> lastSnapshot = visibleCellSnapshots.lastObject;
  135. // Can't just do indexOfObject, because targetCellSnapshot may represent the same object represented by a member of cellSnapshots, yet be a different object
  136. // than that member. This reflects the fact that targetCellSnapshot came out of self.fb_parentCellSnapshot, not out of cellSnapshots directly.
  137. // If the result is NSNotFound, we'll just proceed by scrolling downward/rightward, since NSNotFound will always be larger than the current index.
  138. NSUInteger targetCellIndex = [cellSnapshots indexOfObjectPassingTest:^BOOL(id<FBXCElementSnapshot> _Nonnull obj,
  139. NSUInteger idx, BOOL *_Nonnull stop) {
  140. return [obj _matchesElement:targetCellSnapshot];
  141. }];
  142. NSUInteger visibleCellIndex = [cellSnapshots indexOfObject:lastSnapshot];
  143. if (scrollDirection == FBXCUIElementScrollDirectionUnknown) {
  144. // Try to determine the scroll direction by determining the vector between the first and last visible cells
  145. id<FBXCElementSnapshot> firstVisibleCell = visibleCellSnapshots.firstObject;
  146. id<FBXCElementSnapshot> lastVisibleCell = visibleCellSnapshots.lastObject;
  147. CGVector cellGrowthVector = CGVectorMake(firstVisibleCell.frame.origin.x - lastVisibleCell.frame.origin.x,
  148. firstVisibleCell.frame.origin.y - lastVisibleCell.frame.origin.y
  149. );
  150. if (ABS(cellGrowthVector.dy) > ABS(cellGrowthVector.dx)) {
  151. scrollDirection = FBXCUIElementScrollDirectionVertical;
  152. } else {
  153. scrollDirection = FBXCUIElementScrollDirectionHorizontal;
  154. }
  155. }
  156. const NSUInteger maxScrollCount = 25;
  157. NSUInteger scrollCount = 0;
  158. FBXCElementSnapshotWrapper *scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:scrollView];
  159. // Scrolling till cell is visible and get current value of frames
  160. while (![self fb_isEquivalentElementSnapshotVisible:prescrollSnapshot] && scrollCount < maxScrollCount) {
  161. if (targetCellIndex < visibleCellIndex) {
  162. scrollDirection == FBXCUIElementScrollDirectionVertical ?
  163. [scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance
  164. inApplication:self.application] :
  165. [scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance
  166. inApplication:self.application];
  167. }
  168. else {
  169. scrollDirection == FBXCUIElementScrollDirectionVertical ?
  170. [scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance
  171. inApplication:self.application] :
  172. [scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance
  173. inApplication:self.application];
  174. }
  175. scrollCount++;
  176. // Wait for scroll animation
  177. [self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
  178. }
  179. if (scrollCount >= maxScrollCount) {
  180. return
  181. [[[FBErrorBuilder builder]
  182. withDescriptionFormat:@"Failed to perform scroll with visible cell due to max scroll count reached"]
  183. buildError:error];
  184. }
  185. // Cell is now visible, but it might be only partialy visible, scrolling till whole frame is visible.
  186. // Sometimes, attempting to grab the parent snapshot of the target cell after scrolling is complete causes a stale element reference exception.
  187. // Trying fb_cachedSnapshot first
  188. FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:([self fb_cachedSnapshot] ?: [self fb_takeSnapshot])];
  189. targetCellSnapshot = [targetCellSnapshotWrapped fb_parentCellSnapshot];
  190. CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrameWithFallback;
  191. CGVector scrollVector = CGVectorMake(visibleFrame.size.width - targetCellSnapshot.frame.size.width,
  192. visibleFrame.size.height - targetCellSnapshot.frame.size.height
  193. );
  194. return [scrollViewWrapped fb_scrollByVector:scrollVector
  195. inApplication:self.application
  196. error:error];
  197. }
  198. - (BOOL)fb_isEquivalentElementSnapshotVisible:(id<FBXCElementSnapshot>)snapshot
  199. {
  200. FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
  201. if (wrappedSnapshot.isWDVisible) {
  202. return YES;
  203. }
  204. id<FBXCElementSnapshot> appSnapshot = [self.application fb_takeSnapshot];
  205. for (id<FBXCElementSnapshot> elementSnapshot in appSnapshot._allDescendants.copy) {
  206. FBXCElementSnapshotWrapper *wrappedElementSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:elementSnapshot];
  207. // We are comparing pre-scroll snapshot so frames are irrelevant.
  208. if ([wrappedSnapshot fb_framelessFuzzyMatchesElement:elementSnapshot]
  209. && wrappedElementSnapshot.isWDVisible) {
  210. return YES;
  211. }
  212. }
  213. return NO;
  214. }
  215. @end
  216. @implementation FBXCElementSnapshotWrapper (FBScrolling)
  217. - (CGRect)scrollingFrame
  218. {
  219. return self.visibleFrame;
  220. }
  221. - (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application
  222. {
  223. [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) inApplication:application];
  224. }
  225. - (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application
  226. {
  227. [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) inApplication:application];
  228. }
  229. - (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application
  230. {
  231. [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) inApplication:application];
  232. }
  233. - (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application
  234. {
  235. [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) inApplication:application];
  236. }
  237. - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector inApplication:(XCUIApplication *)application
  238. {
  239. CGVector scrollVector = CGVectorMake(CGRectGetWidth(self.scrollingFrame) * normalizedScrollVector.dx,
  240. CGRectGetHeight(self.scrollingFrame) * normalizedScrollVector.dy
  241. );
  242. return [self fb_scrollByVector:scrollVector inApplication:application error:nil];
  243. }
  244. - (BOOL)fb_scrollByVector:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error
  245. {
  246. CGVector scrollBoundingVector = CGVectorMake(
  247. CGRectGetWidth(self.scrollingFrame) * FBScrollTouchProportion,
  248. CGRectGetHeight(self.scrollingFrame) * FBScrollTouchProportion
  249. );
  250. scrollBoundingVector.dx = (CGFloat)floor(copysign(scrollBoundingVector.dx, vector.dx));
  251. scrollBoundingVector.dy = (CGFloat)floor(copysign(scrollBoundingVector.dy, vector.dy));
  252. NSInteger preciseScrollAttemptsCount = 20;
  253. CGVector CGZeroVector = CGVectorMake(0, 0);
  254. BOOL shouldFinishScrolling = NO;
  255. while (!shouldFinishScrolling) {
  256. CGVector scrollVector = CGVectorMake(fabs(vector.dx) > fabs(scrollBoundingVector.dx) ? scrollBoundingVector.dx : vector.dx,
  257. fabs(vector.dy) > fabs(scrollBoundingVector.dy) ? scrollBoundingVector.dy : vector.dy);
  258. vector = CGVectorMake(vector.dx - scrollVector.dx, vector.dy - scrollVector.dy);
  259. shouldFinishScrolling = FBVectorFuzzyEqualToVector(vector, CGZeroVector, 1) || --preciseScrollAttemptsCount <= 0;
  260. if (![self fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:scrollVector inApplication:application error:error]){
  261. return NO;
  262. }
  263. }
  264. return YES;
  265. }
  266. - (CGVector)fb_hitPointOffsetForScrollingVector:(CGVector)scrollingVector
  267. {
  268. CGFloat x = CGRectGetMinX(self.scrollingFrame) + CGRectGetWidth(self.scrollingFrame) * (scrollingVector.dx < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion));
  269. CGFloat y = CGRectGetMinY(self.scrollingFrame) + CGRectGetHeight(self.scrollingFrame) * (scrollingVector.dy < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion));
  270. return CGVectorMake((CGFloat)floor(x), (CGFloat)floor(y));
  271. }
  272. - (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error
  273. {
  274. CGVector hitpointOffset = [self fb_hitPointOffsetForScrollingVector:vector];
  275. XCUICoordinate *appCoordinate = [[XCUICoordinate alloc] initWithElement:application normalizedOffset:CGVectorMake(0.0, 0.0)];
  276. XCUICoordinate *startCoordinate = [[XCUICoordinate alloc] initWithCoordinate:appCoordinate pointsOffset:hitpointOffset];
  277. XCUICoordinate *endCoordinate = [[XCUICoordinate alloc] initWithCoordinate:startCoordinate pointsOffset:vector];
  278. if (FBPointFuzzyEqualToPoint(startCoordinate.screenPoint, endCoordinate.screenPoint, FBFuzzyPointThreshold)) {
  279. return YES;
  280. }
  281. [startCoordinate pressForDuration:FBTouchEventDelay
  282. thenDragToCoordinate:endCoordinate
  283. withVelocity:FBTouchVelocity
  284. thenHoldForDuration:FBTouchEventDelay];
  285. return YES;
  286. }
  287. @end
  288. #endif