FBElementCommands.m 35 KB

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