XCUIElement+FBScrolling.m 15 KB

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