FBElementCommands.m 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  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 "FBElementCommands.h"
  10. #import "FBConfiguration.h"
  11. #import "FBKeyboard.h"
  12. #import "FBRoute.h"
  13. #import "FBRouteRequest.h"
  14. #import "FBRunLoopSpinner.h"
  15. #import "FBElementCache.h"
  16. #import "FBErrorBuilder.h"
  17. #import "FBSession.h"
  18. #import "FBElementUtils.h"
  19. #import "FBMacros.h"
  20. #import "FBMathUtils.h"
  21. #import "FBRuntimeUtils.h"
  22. #import "NSPredicate+FBFormat.h"
  23. #import "XCTestPrivateSymbols.h"
  24. #import "XCUICoordinate.h"
  25. #import "XCUIDevice.h"
  26. #import "XCUIElement+FBIsVisible.h"
  27. #import "XCUIElement+FBPickerWheel.h"
  28. #import "XCUIElement+FBScrolling.h"
  29. #import "XCUIElement+FBForceTouch.h"
  30. #import "XCUIElement+FBSwiping.h"
  31. #import "XCUIElement+FBTyping.h"
  32. #import "XCUIElement+FBUtilities.h"
  33. #import "XCUIElement+FBWebDriverAttributes.h"
  34. #import "XCUIElement+FBTVFocuse.h"
  35. #import "XCUIElement+FBResolve.h"
  36. #import "FBElementTypeTransformer.h"
  37. #import "XCUIElement.h"
  38. #import "XCUIElementQuery.h"
  39. #import "FBXCodeCompatibility.h"
  40. @interface FBElementCommands ()
  41. @end
  42. @implementation FBElementCommands
  43. #pragma mark - <FBCommandHandler>
  44. + (NSArray *)routes
  45. {
  46. return
  47. @[
  48. [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)],
  49. [[FBRoute GET:@"/window/size"].withoutSession respondWithTarget:self action:@selector(handleGetWindowSize:)],
  50. [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)],
  51. [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)],
  52. [[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)],
  53. [[FBRoute GET:@"/element/:uuid/text"] respondWithTarget:self action:@selector(handleGetText:)],
  54. [[FBRoute GET:@"/element/:uuid/displayed"] respondWithTarget:self action:@selector(handleGetDisplayed:)],
  55. [[FBRoute GET:@"/element/:uuid/selected"] respondWithTarget:self action:@selector(handleGetSelected:)],
  56. [[FBRoute GET:@"/element/:uuid/name"] respondWithTarget:self action:@selector(handleGetName:)],
  57. [[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)],
  58. [[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)],
  59. [[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)],
  60. // W3C element screenshot
  61. [[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)],
  62. // JSONWP element screenshot
  63. [[FBRoute GET:@"/screenshot/:uuid"] respondWithTarget:self action:@selector(handleElementScreenshot:)],
  64. [[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)],
  65. [[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)],
  66. #if TARGET_OS_TV
  67. [[FBRoute GET:@"/element/:uuid/attribute/focused"] respondWithTarget:self action:@selector(handleGetFocused:)],
  68. [[FBRoute POST:@"/wda/element/:uuid/focuse"] respondWithTarget:self action:@selector(handleFocuse:)],
  69. #else
  70. [[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)],
  71. [[FBRoute POST:@"/wda/swipe"] respondWithTarget:self action:@selector(handleSwipe:)],
  72. [[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)],
  73. [[FBRoute POST:@"/wda/pinch"] respondWithTarget:self action:@selector(handlePinch:)],
  74. [[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)],
  75. [[FBRoute POST:@"/wda/rotate"] respondWithTarget:self action:@selector(handleRotate:)],
  76. [[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)],
  77. [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)],
  78. [[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)],
  79. [[FBRoute POST:@"/wda/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)],
  80. [[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self
  81. action:@selector(handleTapWithNumberOfTaps:)],
  82. [[FBRoute POST:@"/wda/tapWithNumberOfTaps"] respondWithTarget:self
  83. action:@selector(handleTapWithNumberOfTaps:)],
  84. [[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)],
  85. [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)],
  86. [[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)],
  87. [[FBRoute POST:@"/wda/scroll"] respondWithTarget:self action:@selector(handleScroll:)],
  88. [[FBRoute POST:@"/wda/element/:uuid/scrollTo"] respondWithTarget:self action:@selector(handleScrollTo:)],
  89. [[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)],
  90. [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)],
  91. [[FBRoute POST:@"/wda/element/:uuid/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragWithVelocity:)],
  92. [[FBRoute POST:@"/wda/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragCoordinateWithVelocity:)],
  93. [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)],
  94. [[FBRoute POST:@"/wda/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)],
  95. [[FBRoute POST:@"/wda/element/:uuid/tap"] respondWithTarget:self action:@selector(handleTap:)],
  96. [[FBRoute POST:@"/wda/tap"] respondWithTarget:self action:@selector(handleTap:)],
  97. [[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)],
  98. #endif
  99. [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)],
  100. ];
  101. }
  102. #pragma mark - Commands
  103. + (id<FBResponsePayload>)handleGetEnabled:(FBRouteRequest *)request
  104. {
  105. FBElementCache *elementCache = request.session.elementCache;
  106. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  107. BOOL isEnabled = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDEnabled;
  108. return FBResponseWithObject(isEnabled ? @YES : @NO);
  109. }
  110. + (id<FBResponsePayload>)handleGetRect:(FBRouteRequest *)request
  111. {
  112. FBElementCache *elementCache = request.session.elementCache;
  113. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  114. return FBResponseWithObject([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdRect);
  115. }
  116. + (id<FBResponsePayload>)handleGetAttribute:(FBRouteRequest *)request
  117. {
  118. FBElementCache *elementCache = request.session.elementCache;
  119. NSString *attributeName = request.parameters[@"name"];
  120. NSString *wdAttributeName = [FBElementUtils wdAttributeNameForAttributeName:attributeName];
  121. NSArray *additionalAttributes = nil;
  122. NSNumber *maxDepth = nil;
  123. if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDVisible)]) {
  124. additionalAttributes = @[FB_XCAXAIsVisibleAttributeName];
  125. maxDepth = @1;
  126. } else if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDEnabled)]) {
  127. additionalAttributes = @[FB_XCAXAIsElementAttributeName];
  128. maxDepth = @1;
  129. } else if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)]) {
  130. additionalAttributes = @[FB_XCAXAIsElementAttributeName];
  131. }
  132. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
  133. resolveForAdditionalAttributes:additionalAttributes
  134. andMaxDepth:maxDepth];
  135. FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot];
  136. id attributeValue = [wrappedSnapshot fb_valueForWDAttributeName:attributeName];
  137. return FBResponseWithObject(attributeValue ?: [NSNull null]);
  138. }
  139. + (id<FBResponsePayload>)handleGetText:(FBRouteRequest *)request
  140. {
  141. FBElementCache *elementCache = request.session.elementCache;
  142. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  143. FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot];
  144. id text = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel);
  145. return FBResponseWithObject(text ?: @"");
  146. }
  147. + (id<FBResponsePayload>)handleGetDisplayed:(FBRouteRequest *)request
  148. {
  149. FBElementCache *elementCache = request.session.elementCache;
  150. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
  151. resolveForAdditionalAttributes:@[FB_XCAXAIsVisibleAttributeName]
  152. andMaxDepth:@1];
  153. return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDVisible));
  154. }
  155. + (id<FBResponsePayload>)handleGetAccessible:(FBRouteRequest *)request
  156. {
  157. FBElementCache *elementCache = request.session.elementCache;
  158. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
  159. resolveForAdditionalAttributes:@[FB_XCAXAIsElementAttributeName]
  160. andMaxDepth:@1];
  161. return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDAccessible));
  162. }
  163. + (id<FBResponsePayload>)handleGetIsAccessibilityContainer:(FBRouteRequest *)request
  164. {
  165. FBElementCache *elementCache = request.session.elementCache;
  166. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
  167. resolveForAdditionalAttributes:@[FB_XCAXAIsElementAttributeName]
  168. andMaxDepth:nil];
  169. return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDAccessibilityContainer));
  170. }
  171. + (id<FBResponsePayload>)handleGetName:(FBRouteRequest *)request
  172. {
  173. FBElementCache *elementCache = request.session.elementCache;
  174. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  175. return FBResponseWithObject([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdType);
  176. }
  177. + (id<FBResponsePayload>)handleGetSelected:(FBRouteRequest *)request
  178. {
  179. FBElementCache *elementCache = request.session.elementCache;
  180. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  181. return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdSelected));
  182. }
  183. + (id<FBResponsePayload>)handleSetValue:(FBRouteRequest *)request
  184. {
  185. FBElementCache *elementCache = request.session.elementCache;
  186. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  187. id value = request.arguments[@"value"] ?: request.arguments[@"text"];
  188. if (!value) {
  189. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Neither 'value' nor 'text' parameter is provided" traceback:nil]);
  190. }
  191. NSString *textToType = [value isKindOfClass:NSArray.class]
  192. ? [value componentsJoinedByString:@""]
  193. : value;
  194. XCUIElementType elementType = [(id<FBXCElementSnapshot>)element.lastSnapshot elementType];
  195. #if !TARGET_OS_TV
  196. if (elementType == XCUIElementTypePickerWheel) {
  197. [element adjustToPickerWheelValue:textToType];
  198. return FBResponseWithOK();
  199. }
  200. #endif
  201. if (elementType == XCUIElementTypeSlider) {
  202. CGFloat sliderValue = textToType.floatValue;
  203. if (sliderValue < 0.0 || sliderValue > 1.0 ) {
  204. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Value of slider should be in 0..1 range" traceback:nil]);
  205. }
  206. [element adjustToNormalizedSliderPosition:sliderValue];
  207. return FBResponseWithOK();
  208. }
  209. NSUInteger frequency = (NSUInteger)[request.arguments[@"frequency"] longLongValue] ?: [FBConfiguration maxTypingFrequency];
  210. NSError *error = nil;
  211. if (![element fb_typeText:textToType
  212. shouldClear:NO
  213. frequency:frequency
  214. error:&error]) {
  215. return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
  216. }
  217. return FBResponseWithOK();
  218. }
  219. + (id<FBResponsePayload>)handleClick:(FBRouteRequest *)request
  220. {
  221. FBElementCache *elementCache = request.session.elementCache;
  222. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  223. #if TARGET_OS_IOS
  224. [element tap];
  225. #elif TARGET_OS_TV
  226. NSError *error = nil;
  227. if (![element fb_selectWithError:&error]) {
  228. return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
  229. }
  230. #endif
  231. return FBResponseWithOK();
  232. }
  233. + (id<FBResponsePayload>)handleClear:(FBRouteRequest *)request
  234. {
  235. FBElementCache *elementCache = request.session.elementCache;
  236. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  237. NSError *error;
  238. if (![element fb_clearTextWithError:&error]) {
  239. return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
  240. }
  241. return FBResponseWithOK();
  242. }
  243. #if TARGET_OS_TV
  244. + (id<FBResponsePayload>)handleGetFocused:(FBRouteRequest *)request
  245. {
  246. // `BOOL isFocused = [elementCache elementForUUID:request.parameters[@"uuid"]];`
  247. // returns wrong true/false after moving focus by key up/down, for example.
  248. // Thus, ensure the focus compares the status with `fb_focusedElement`.
  249. BOOL isFocused = NO;
  250. XCUIElement *focusedElement = request.session.activeApplication.fb_focusedElement;
  251. if (focusedElement != nil) {
  252. FBElementCache *elementCache = request.session.elementCache;
  253. BOOL useNativeCachingStrategy = request.session.useNativeCachingStrategy;
  254. NSString *focusedUUID = [elementCache storeElement:(useNativeCachingStrategy ? focusedElement : focusedElement.fb_stableInstance)];
  255. if (focusedUUID && [focusedUUID isEqualToString:(id)request.parameters[@"uuid"]]) {
  256. isFocused = YES;
  257. }
  258. }
  259. return FBResponseWithObject(@(isFocused));
  260. }
  261. + (id<FBResponsePayload>)handleFocuse:(FBRouteRequest *)request
  262. {
  263. FBElementCache *elementCache = request.session.elementCache;
  264. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  265. NSError *error;
  266. if (![element fb_setFocusWithError:&error]) {
  267. return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
  268. }
  269. return FBResponseWithStatus([FBCommandStatus okWithValue: FBDictionaryResponseWithElement(element, FBConfiguration.shouldUseCompactResponses)]);
  270. }
  271. #else
  272. + (id<FBResponsePayload>)handleDoubleTap:(FBRouteRequest *)request
  273. {
  274. NSError *error;
  275. id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
  276. if (nil == target) {
  277. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
  278. traceback:nil]);
  279. }
  280. [target doubleTap];
  281. return FBResponseWithOK();
  282. }
  283. + (id<FBResponsePayload>)handleTwoFingerTap:(FBRouteRequest *)request
  284. {
  285. XCUIElement *element = [self targetFromRequest:request];
  286. [element twoFingerTap];
  287. return FBResponseWithOK();
  288. }
  289. + (id<FBResponsePayload>)handleTapWithNumberOfTaps:(FBRouteRequest *)request
  290. {
  291. if (nil == request.arguments[@"numberOfTaps"] || nil == request.arguments[@"numberOfTouches"]) {
  292. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both 'numberOfTaps' and 'numberOfTouches' arguments must be provided"
  293. traceback:nil]);
  294. }
  295. XCUIElement *element = [self targetFromRequest:request];
  296. [element tapWithNumberOfTaps:[request.arguments[@"numberOfTaps"] integerValue]
  297. numberOfTouches:[request.arguments[@"numberOfTouches"] integerValue]];
  298. return FBResponseWithOK();
  299. }
  300. + (id<FBResponsePayload>)handleTouchAndHold:(FBRouteRequest *)request
  301. {
  302. NSError *error;
  303. id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
  304. if (nil == target) {
  305. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
  306. traceback:nil]);
  307. }
  308. [target pressForDuration:[request.arguments[@"duration"] doubleValue]];
  309. return FBResponseWithOK();
  310. }
  311. + (id<FBResponsePayload>)handlePressAndDragWithVelocity:(FBRouteRequest *)request
  312. {
  313. FBElementCache *elementCache = request.session.elementCache;
  314. XCUIElement *element = [self targetFromRequest:request];
  315. if (![element respondsToSelector:@selector(pressForDuration:thenDragToElement:withVelocity:thenHoldForDuration:)]) {
  316. return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"This method is only supported in Xcode 12 and above"
  317. traceback:nil]);
  318. }
  319. [element pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
  320. thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"]]
  321. withVelocity:[request.arguments[@"velocity"] doubleValue]
  322. thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]];
  323. return FBResponseWithOK();
  324. }
  325. + (id<FBResponsePayload>)handlePressAndDragCoordinateWithVelocity:(FBRouteRequest *)request
  326. {
  327. FBSession *session = request.session;
  328. CGVector startOffset = CGVectorMake((CGFloat)[request.arguments[@"fromX"] doubleValue],
  329. (CGFloat)[request.arguments[@"fromY"] doubleValue]);
  330. XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset
  331. element:session.activeApplication];
  332. if (![startCoordinate respondsToSelector:@selector(pressForDuration:thenDragToCoordinate:withVelocity:thenHoldForDuration:)]) {
  333. return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"This method is only supported in Xcode 12 and above"
  334. traceback:nil]);
  335. }
  336. CGVector endOffset = CGVectorMake((CGFloat)[request.arguments[@"toX"] doubleValue],
  337. (CGFloat)[request.arguments[@"toY"] doubleValue]);
  338. XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset
  339. element:session.activeApplication];
  340. [startCoordinate pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
  341. thenDragToCoordinate:endCoordinate
  342. withVelocity:[request.arguments[@"velocity"] doubleValue]
  343. thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]];
  344. return FBResponseWithOK();
  345. }
  346. + (id<FBResponsePayload>)handleScroll:(FBRouteRequest *)request
  347. {
  348. XCUIElement *element = [self targetFromRequest:request];
  349. // Using presence of arguments as a way to convey control flow seems like a pretty bad idea but it's
  350. // what ios-driver did and sadly, we must copy them.
  351. NSString *const name = request.arguments[@"name"];
  352. if (name) {
  353. XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny]
  354. matchingIdentifier:name] allElementsBoundByIndex] lastObject];
  355. if (!childElement) {
  356. return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' identifier didn't match any elements", name]
  357. traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
  358. }
  359. return [self.class handleScrollElementToVisible:childElement withRequest:request];
  360. }
  361. NSString *const direction = request.arguments[@"direction"];
  362. if (direction) {
  363. NSString *const distanceString = request.arguments[@"distance"] ?: @"1.0";
  364. CGFloat distance = (CGFloat)distanceString.doubleValue;
  365. if ([direction isEqualToString:@"up"]) {
  366. [element fb_scrollUpByNormalizedDistance:distance];
  367. } else if ([direction isEqualToString:@"down"]) {
  368. [element fb_scrollDownByNormalizedDistance:distance];
  369. } else if ([direction isEqualToString:@"left"]) {
  370. [element fb_scrollLeftByNormalizedDistance:distance];
  371. } else if ([direction isEqualToString:@"right"]) {
  372. [element fb_scrollRightByNormalizedDistance:distance];
  373. }
  374. return FBResponseWithOK();
  375. }
  376. NSString *const predicateString = request.arguments[@"predicateString"];
  377. if (predicateString) {
  378. NSPredicate *formattedPredicate = [NSPredicate fb_snapshotBlockPredicateWithPredicate:[NSPredicate
  379. predicateWithFormat:predicateString]];
  380. XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny]
  381. matchingPredicate:formattedPredicate] allElementsBoundByIndex] lastObject];
  382. if (!childElement) {
  383. return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' predicate didn't match any elements", predicateString]
  384. traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
  385. }
  386. return [self.class handleScrollElementToVisible:childElement withRequest:request];
  387. }
  388. if (request.arguments[@"toVisible"]) {
  389. return [self.class handleScrollElementToVisible:element withRequest:request];
  390. }
  391. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Unsupported scroll type" traceback:nil]);
  392. }
  393. + (id<FBResponsePayload>)handleScrollTo:(FBRouteRequest *)request
  394. {
  395. FBElementCache *elementCache = request.session.elementCache;
  396. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  397. NSError *error;
  398. return [element fb_nativeScrollToVisibleWithError:&error]
  399. ? FBResponseWithOK()
  400. : FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
  401. traceback:nil]);
  402. }
  403. + (id<FBResponsePayload>)handleDrag:(FBRouteRequest *)request
  404. {
  405. NSString *elementUdid = (NSString *)request.parameters[@"uuid"];
  406. XCUIElement *target = nil == elementUdid
  407. ? request.session.activeApplication
  408. : [request.session.elementCache elementForUUID:elementUdid];
  409. CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue],
  410. [request.arguments[@"fromY"] doubleValue]);
  411. XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:target];
  412. CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue],
  413. [request.arguments[@"toY"] doubleValue]);
  414. XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset element:target];
  415. NSTimeInterval duration = [request.arguments[@"duration"] doubleValue];
  416. [startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate];
  417. return FBResponseWithOK();
  418. }
  419. + (id<FBResponsePayload>)handleSwipe:(FBRouteRequest *)request
  420. {
  421. NSString *const direction = request.arguments[@"direction"];
  422. if (!direction) {
  423. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'direction' parameter" traceback:nil]);
  424. }
  425. NSArray<NSString *> *supportedDirections = @[@"up", @"down", @"left", @"right"];
  426. if (![supportedDirections containsObject:direction.lowercaseString]) {
  427. NSString *message = [NSString stringWithFormat:@"Unsupported swipe direction '%@'. Only the following directions are supported: %@", direction, supportedDirections];
  428. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
  429. traceback:nil]);
  430. }
  431. NSError *error;
  432. id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
  433. if (nil == target) {
  434. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
  435. traceback:nil]);
  436. }
  437. [target fb_swipeWithDirection:direction velocity:request.arguments[@"velocity"]];
  438. return FBResponseWithOK();
  439. }
  440. + (id<FBResponsePayload>)handleTap:(FBRouteRequest *)request
  441. {
  442. NSError *error;
  443. id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
  444. if (nil == target) {
  445. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
  446. traceback:nil]);
  447. }
  448. [target tap];
  449. return FBResponseWithOK();
  450. }
  451. + (id<FBResponsePayload>)handlePinch:(FBRouteRequest *)request
  452. {
  453. XCUIElement *element = [self targetFromRequest:request];
  454. CGFloat scale = (CGFloat)[request.arguments[@"scale"] doubleValue];
  455. CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue];
  456. [element pinchWithScale:scale velocity:velocity];
  457. return FBResponseWithOK();
  458. }
  459. + (id<FBResponsePayload>)handleRotate:(FBRouteRequest *)request
  460. {
  461. XCUIElement *element = [self targetFromRequest:request];
  462. CGFloat rotation = (CGFloat)[request.arguments[@"rotation"] doubleValue];
  463. CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue];
  464. [element rotate:rotation withVelocity:velocity];
  465. return FBResponseWithOK();
  466. }
  467. + (id<FBResponsePayload>)handleForceTouch:(FBRouteRequest *)request
  468. {
  469. XCUIElement *element = [self targetFromRequest:request];
  470. NSNumber *pressure = request.arguments[@"pressure"];
  471. NSNumber *duration = request.arguments[@"duration"];
  472. NSNumber *x = request.arguments[@"x"];
  473. NSNumber *y = request.arguments[@"y"];
  474. NSValue *hitPoint = (nil == x || nil == y)
  475. ? nil
  476. : [NSValue valueWithCGPoint:CGPointMake((CGFloat)[x doubleValue], (CGFloat)[y doubleValue])];
  477. NSError *error;
  478. BOOL didSucceed = [element fb_forceTouchCoordinate:hitPoint
  479. pressure:pressure
  480. duration:duration
  481. error:&error];
  482. return didSucceed
  483. ? FBResponseWithOK()
  484. : FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
  485. traceback:nil]);
  486. }
  487. #endif
  488. + (id<FBResponsePayload>)handleKeys:(FBRouteRequest *)request
  489. {
  490. NSString *textToType = [request.arguments[@"value"] componentsJoinedByString:@""];
  491. NSUInteger frequency = [request.arguments[@"frequency"] unsignedIntegerValue] ?: [FBConfiguration maxTypingFrequency];
  492. NSError *error;
  493. if (!FBTypeText(textToType, frequency, &error)) {
  494. return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
  495. traceback:nil]);
  496. }
  497. return FBResponseWithOK();
  498. }
  499. + (id<FBResponsePayload>)handleGetWindowSize:(FBRouteRequest *)request
  500. {
  501. XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
  502. #if TARGET_OS_TV
  503. CGSize screenSize = app.frame.size;
  504. #else
  505. CGRect frame = app.wdFrame;
  506. CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation);
  507. #endif
  508. return FBResponseWithObject(@{
  509. @"width": @(screenSize.width),
  510. @"height": @(screenSize.height),
  511. });
  512. }
  513. + (id<FBResponsePayload>)handleElementScreenshot:(FBRouteRequest *)request
  514. {
  515. FBElementCache *elementCache = request.session.elementCache;
  516. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  517. NSData *screenshotData = [element.screenshot PNGRepresentation];
  518. if (nil == screenshotData) {
  519. NSString *errMsg = [NSString stringWithFormat:@"Cannot take a screenshot of %@", element.description];
  520. return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:errMsg
  521. traceback:nil]);
  522. }
  523. NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0];
  524. return FBResponseWithObject(screenshot);
  525. }
  526. #if !TARGET_OS_TV
  527. static const CGFloat DEFAULT_PICKER_OFFSET = (CGFloat)0.2;
  528. static const NSInteger DEFAULT_MAX_PICKER_ATTEMPTS = 25;
  529. + (id<FBResponsePayload>)handleWheelSelect:(FBRouteRequest *)request
  530. {
  531. FBElementCache *elementCache = request.session.elementCache;
  532. XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
  533. if ([element.lastSnapshot elementType] != XCUIElementTypePickerWheel) {
  534. NSString *errMsg = [NSString stringWithFormat:@"The element is expected to be a valid Picker Wheel control. '%@' was given instead", element.wdType];
  535. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg
  536. traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
  537. }
  538. NSString* order = [request.arguments[@"order"] lowercaseString];
  539. CGFloat offset = DEFAULT_PICKER_OFFSET;
  540. if (request.arguments[@"offset"]) {
  541. offset = (CGFloat)[request.arguments[@"offset"] doubleValue];
  542. if (offset <= 0.0 || offset > 0.5) {
  543. NSString *errMsg = [NSString stringWithFormat:@"'offset' value is expected to be in range (0.0, 0.5]. '%@' was given instead", request.arguments[@"offset"]];
  544. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg
  545. traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
  546. }
  547. }
  548. NSNumber *maxAttempts = request.arguments[@"maxAttempts"] ?: @(DEFAULT_MAX_PICKER_ATTEMPTS);
  549. NSString *expectedValue = request.arguments[@"value"];
  550. NSInteger attempt = 0;
  551. while (attempt < [maxAttempts integerValue]) {
  552. BOOL isSuccessful = false;
  553. NSError *error;
  554. if ([order isEqualToString:@"next"]) {
  555. isSuccessful = [element fb_selectNextOptionWithOffset:offset error:&error];
  556. } else if ([order isEqualToString:@"previous"]) {
  557. isSuccessful = [element fb_selectPreviousOptionWithOffset:offset error:&error];
  558. } else {
  559. NSString *errMsg = [NSString stringWithFormat:@"Only 'previous' and 'next' order values are supported. '%@' was given instead", request.arguments[@"order"]];
  560. return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg
  561. traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
  562. }
  563. if (!isSuccessful) {
  564. return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
  565. }
  566. if (nil == expectedValue || [element.wdValue isEqualToString:expectedValue]) {
  567. return FBResponseWithOK();
  568. }
  569. attempt++;
  570. }
  571. NSString *errMsg = [NSString stringWithFormat:@"Cannot select the expected picker wheel value '%@' after %ld attempts", expectedValue, attempt];
  572. return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:errMsg traceback:nil]);
  573. }
  574. #pragma mark - Helpers
  575. + (id<FBResponsePayload>)handleScrollElementToVisible:(XCUIElement *)element withRequest:(FBRouteRequest *)request
  576. {
  577. NSError *error;
  578. if (!element.exists) {
  579. return FBResponseWithStatus([FBCommandStatus elementNotVisibleErrorWithMessage:@"Can't scroll to element that does not exist" traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
  580. }
  581. if (![element fb_scrollToVisibleWithError:&error]) {
  582. return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
  583. traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
  584. }
  585. return FBResponseWithOK();
  586. }
  587. /**
  588. Returns gesture coordinate for the element based on absolute coordinate
  589. @param offset absolute screen offset for the given application
  590. @param element the element instance to perform the gesture on
  591. @return translated gesture coordinates ready to be passed to XCUICoordinate methods
  592. */
  593. + (XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset
  594. element:(XCUIElement *)element
  595. {
  596. return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset];
  597. }
  598. /**
  599. Returns either coordinates or the target element for the given request that expects 'x' and 'y' coordannates
  600. @param request HTTP request object
  601. @param error Error instance if any
  602. @return Either XCUICoordinate or XCUIElement instance. nil if the input data is invalid
  603. */
  604. + (nullable id)targetWithXyCoordinatesFromRequest:(FBRouteRequest *)request error:(NSError **)error
  605. {
  606. NSNumber *x = request.arguments[@"x"];
  607. NSNumber *y = request.arguments[@"y"];
  608. if (nil == x && nil == y) {
  609. return [self targetFromRequest:request];
  610. }
  611. if ((nil == x && nil != y) || (nil != x && nil == y)) {
  612. [[[FBErrorBuilder alloc]
  613. withDescription:@"Both x and y coordinates must be provided"]
  614. buildError:error];
  615. return nil;
  616. }
  617. return [self gestureCoordinateWithOffset:CGVectorMake(x.doubleValue, y.doubleValue)
  618. element:[self targetFromRequest:request]];
  619. }
  620. /**
  621. Returns the target element for the given request
  622. @param request HTTP request object
  623. @return Matching XCUIElement instance
  624. */
  625. + (XCUIElement *)targetFromRequest:(FBRouteRequest *)request
  626. {
  627. FBElementCache *elementCache = request.session.elementCache;
  628. NSString *elementUuid = (NSString *)request.parameters[@"uuid"];
  629. return nil == elementUuid
  630. ? request.session.activeApplication
  631. : [elementCache elementForUUID:elementUuid];
  632. }
  633. #endif
  634. @end