FBElementCommands.m 33 KB

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