FBW3CActionsSynthesizer.m 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  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 "FBW3CActionsSynthesizer.h"
  10. #import "FBErrorBuilder.h"
  11. #import "FBElementCache.h"
  12. #import "FBConfiguration.h"
  13. #import "FBLogger.h"
  14. #import "FBMacros.h"
  15. #import "FBMathUtils.h"
  16. #import "FBProtocolHelpers.h"
  17. #import "FBW3CActionsHelpers.h"
  18. #import "FBXCodeCompatibility.h"
  19. #import "FBXCTestDaemonsProxy.h"
  20. #import "FBXCElementSnapshotWrapper+Helpers.h"
  21. #import "XCUIApplication+FBHelpers.h"
  22. #import "XCUIDevice.h"
  23. #import "XCUIElement+FBCaching.h"
  24. #import "XCUIElement+FBIsVisible.h"
  25. #import "XCUIElement+FBUtilities.h"
  26. #import "XCUIElement.h"
  27. #import "XCSynthesizedEventRecord.h"
  28. #import "XCPointerEventPath.h"
  29. #import "XCPointerEvent.h"
  30. static NSString *const FB_KEY_TYPE = @"type";
  31. static NSString *const FB_ACTION_TYPE_POINTER = @"pointer";
  32. static NSString *const FB_ACTION_TYPE_KEY = @"key";
  33. static NSString *const FB_ACTION_TYPE_NONE = @"none";
  34. static NSString *const FB_PARAMETERS_KEY_POINTER_TYPE = @"pointerType";
  35. static NSString *const FB_POINTER_TYPE_MOUSE = @"mouse";
  36. static NSString *const FB_POINTER_TYPE_PEN = @"pen";
  37. static NSString *const FB_POINTER_TYPE_TOUCH = @"touch";
  38. static NSString *const FB_ACTION_ITEM_KEY_ORIGIN = @"origin";
  39. static NSString *const FB_ORIGIN_TYPE_VIEWPORT = @"viewport";
  40. static NSString *const FB_ORIGIN_TYPE_POINTER = @"pointer";
  41. static NSString *const FB_ACTION_ITEM_KEY_TYPE = @"type";
  42. static NSString *const FB_ACTION_ITEM_TYPE_POINTER_MOVE = @"pointerMove";
  43. static NSString *const FB_ACTION_ITEM_TYPE_POINTER_DOWN = @"pointerDown";
  44. static NSString *const FB_ACTION_ITEM_TYPE_POINTER_UP = @"pointerUp";
  45. static NSString *const FB_ACTION_ITEM_TYPE_POINTER_CANCEL = @"pointerCancel";
  46. static NSString *const FB_ACTION_ITEM_TYPE_PAUSE = @"pause";
  47. static NSString *const FB_ACTION_ITEM_TYPE_KEY_UP = @"keyUp";
  48. static NSString *const FB_ACTION_ITEM_TYPE_KEY_DOWN = @"keyDown";
  49. static NSString *const FB_ACTION_ITEM_KEY_X = @"x";
  50. static NSString *const FB_ACTION_ITEM_KEY_Y = @"y";
  51. static NSString *const FB_ACTION_ITEM_KEY_BUTTON = @"button";
  52. static NSString *const FB_ACTION_ITEM_KEY_PRESSURE = @"pressure";
  53. static NSString *const FB_KEY_ID = @"id";
  54. static NSString *const FB_KEY_PARAMETERS = @"parameters";
  55. static NSString *const FB_KEY_ACTIONS = @"actions";
  56. #if !TARGET_OS_TV
  57. @interface FBW3CGestureItem : FBBaseGestureItem
  58. @property (nullable, readonly, nonatomic) FBBaseGestureItem *previousItem;
  59. @end
  60. @interface FBPointerDownItem : FBW3CGestureItem
  61. @property (nullable, readonly, nonatomic) NSNumber *pressure;
  62. @end
  63. @interface FBPointerMoveItem : FBW3CGestureItem
  64. @end
  65. @interface FBPointerUpItem : FBW3CGestureItem
  66. @end
  67. @interface FBPointerPauseItem : FBW3CGestureItem
  68. @end
  69. @interface FBW3CKeyItem : FBBaseActionItem
  70. @property (nullable, readonly, nonatomic) FBW3CKeyItem *previousItem;
  71. @end
  72. @interface FBKeyUpItem : FBW3CKeyItem
  73. @property (readonly, nonatomic) NSString *value;
  74. @end
  75. @interface FBKeyDownItem : FBW3CKeyItem
  76. @property (readonly, nonatomic) NSString *value;
  77. @end
  78. @interface FBKeyPauseItem : FBW3CKeyItem
  79. @property (readonly, nonatomic) double duration;
  80. @end
  81. @implementation FBW3CGestureItem
  82. - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
  83. application:(XCUIApplication *)application
  84. previousItem:(nullable FBBaseGestureItem *)previousItem
  85. offset:(double)offset
  86. error:(NSError **)error
  87. {
  88. self = [super init];
  89. if (self) {
  90. self.actionItem = actionItem;
  91. self.application = application;
  92. self.offset = offset;
  93. _previousItem = previousItem;
  94. NSNumber *durationObj = FBOptDuration(actionItem, @0, error);
  95. if (nil == durationObj) {
  96. return nil;
  97. }
  98. self.duration = durationObj.doubleValue;
  99. XCUICoordinate *position = [self positionWithError:error];
  100. if (nil == position) {
  101. return nil;
  102. }
  103. self.atPosition = position;
  104. }
  105. return self;
  106. }
  107. - (nullable XCUICoordinate *)positionWithError:(NSError **)error
  108. {
  109. if (nil == self.previousItem) {
  110. NSString *errorDescription = [NSString stringWithFormat:@"The '%@' action item must be preceded by %@ item", self.actionItem, FB_ACTION_ITEM_TYPE_POINTER_MOVE];
  111. if (error) {
  112. *error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
  113. }
  114. return nil;
  115. }
  116. return self.previousItem.atPosition;
  117. }
  118. - (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element
  119. positionOffset:(nullable NSValue *)positionOffset
  120. error:(NSError **)error
  121. {
  122. if (nil == element || nil == positionOffset) {
  123. return [super hitpointWithElement:element positionOffset:positionOffset error:error];
  124. }
  125. // An offset relative to the element is defined
  126. if (CGRectIsEmpty(element.frame)) {
  127. [FBLogger log:self.application.fb_descriptionRepresentation];
  128. NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable",
  129. element.description];
  130. if (error) {
  131. *error = [[FBErrorBuilder.builder withDescription:description] build];
  132. }
  133. return nil;
  134. }
  135. // W3C standard requires that relative element coordinates start at the center of the element's rectangle
  136. CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y);
  137. // TODO: Shall we throw an exception if hitPoint is out of the element frame?
  138. return [[element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)] coordinateWithOffset:offset];
  139. }
  140. @end
  141. @implementation FBPointerDownItem
  142. - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
  143. application:(XCUIApplication *)application
  144. previousItem:(nullable FBW3CGestureItem *)previousItem
  145. offset:(double)offset
  146. error:(NSError **)error
  147. {
  148. self = [super initWithActionItem:actionItem application:application previousItem:previousItem offset:offset error:error];
  149. if (self) {
  150. _pressure = [actionItem objectForKey:FB_ACTION_ITEM_KEY_PRESSURE];
  151. }
  152. return self;
  153. }
  154. + (NSString *)actionName
  155. {
  156. return FB_ACTION_ITEM_TYPE_POINTER_DOWN;
  157. }
  158. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  159. allItems:(NSArray *)allItems
  160. currentItemIndex:(NSUInteger)currentItemIndex
  161. error:(NSError **)error
  162. {
  163. if (nil != eventPath && currentItemIndex == 1) {
  164. FBW3CGestureItem *preceedingItem = [allItems objectAtIndex:currentItemIndex - 1];
  165. if ([preceedingItem isKindOfClass:FBPointerMoveItem.class]) {
  166. return @[];
  167. }
  168. }
  169. if (nil == self.pressure) {
  170. XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint
  171. offset:FBMillisToSeconds(self.offset)];
  172. return @[result];
  173. }
  174. if (nil == eventPath) {
  175. NSString *description = [NSString stringWithFormat:@"'%@' action with pressure must be preceeded with at least one '%@' action without this option", self.class.actionName, self.class.actionName];
  176. if (error) {
  177. *error = [[FBErrorBuilder.builder withDescription:description] build];
  178. }
  179. return nil;
  180. }
  181. if (![XCUIDevice sharedDevice].supportsPressureInteraction) {
  182. if (error) {
  183. *error = [[FBErrorBuilder.builder withDescription:@"This device does not support force press interactions"] build];
  184. }
  185. return nil;
  186. }
  187. [eventPath pressDownWithPressure:self.pressure.doubleValue
  188. atOffset:FBMillisToSeconds(self.offset)];
  189. return @[];
  190. }
  191. @end
  192. @implementation FBPointerMoveItem
  193. - (nullable XCUICoordinate *)positionWithError:(NSError **)error
  194. {
  195. static NSArray<NSString *> *supportedOriginTypes;
  196. static dispatch_once_t onceToken;
  197. dispatch_once(&onceToken, ^{
  198. supportedOriginTypes = @[FB_ORIGIN_TYPE_POINTER, FB_ORIGIN_TYPE_VIEWPORT];
  199. });
  200. id origin = [self.actionItem objectForKey:FB_ACTION_ITEM_KEY_ORIGIN] ?: FB_ORIGIN_TYPE_VIEWPORT;
  201. BOOL isOriginAnElement = [origin isKindOfClass:XCUIElement.class] && [(XCUIElement *)origin exists];
  202. if (!isOriginAnElement && ![supportedOriginTypes containsObject:origin]) {
  203. NSString *description = [NSString stringWithFormat:@"Unsupported %@ type '%@' is set for '%@' action item. Supported origin types: %@ or an element instance", FB_ACTION_ITEM_KEY_ORIGIN, origin, self.actionItem, supportedOriginTypes];
  204. if (error) {
  205. *error = [[FBErrorBuilder.builder withDescription:description] build];
  206. }
  207. return nil;
  208. }
  209. XCUIElement *element = isOriginAnElement ? (XCUIElement *)origin : nil;
  210. NSNumber *x = [self.actionItem objectForKey:FB_ACTION_ITEM_KEY_X];
  211. NSNumber *y = [self.actionItem objectForKey:FB_ACTION_ITEM_KEY_Y];
  212. if ((nil != x && nil == y) || (nil != y && nil == x) ||
  213. ([origin isKindOfClass:NSString.class] && [origin isEqualToString:FB_ORIGIN_TYPE_VIEWPORT] && (nil == x || nil == y))) {
  214. NSString *errorDescription = [NSString stringWithFormat:@"Both 'x' and 'y' options should be set for '%@' action item", self.actionItem];
  215. if (error) {
  216. *error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
  217. }
  218. return nil;
  219. }
  220. if (nil != element) {
  221. if (nil == x && nil == y) {
  222. return [self hitpointWithElement:element positionOffset:nil error:error];
  223. }
  224. return [self hitpointWithElement:element positionOffset:[NSValue valueWithCGPoint:CGPointMake(x.floatValue, y.floatValue)] error:error];
  225. }
  226. if ([origin isKindOfClass:NSString.class] && [origin isEqualToString:FB_ORIGIN_TYPE_VIEWPORT]) {
  227. return [self hitpointWithElement:nil positionOffset:[NSValue valueWithCGPoint:CGPointMake(x.floatValue, y.floatValue)] error:error];
  228. }
  229. // origin == FB_ORIGIN_TYPE_POINTER
  230. if (nil == self.previousItem) {
  231. NSString *errorDescription = [NSString stringWithFormat:@"There is no previous item for '%@' action item, however %@ is set to '%@'", self.actionItem, FB_ACTION_ITEM_KEY_ORIGIN, FB_ORIGIN_TYPE_POINTER];
  232. if (error) {
  233. *error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
  234. }
  235. return nil;
  236. }
  237. XCUICoordinate *recentPosition = self.previousItem.atPosition;
  238. CGVector offsetRelativeToRecentPosition = (nil == x && nil == y) ? CGVectorMake(0, 0) : CGVectorMake(x.floatValue, y.floatValue);
  239. return [recentPosition coordinateWithOffset:offsetRelativeToRecentPosition];
  240. }
  241. + (NSString *)actionName
  242. {
  243. return FB_ACTION_ITEM_TYPE_POINTER_MOVE;
  244. }
  245. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  246. allItems:(NSArray *)allItems
  247. currentItemIndex:(NSUInteger)currentItemIndex
  248. error:(NSError **)error
  249. {
  250. if (nil == eventPath) {
  251. return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint
  252. offset:FBMillisToSeconds(self.offset + self.duration)]];
  253. }
  254. [eventPath moveToPoint:self.atPosition.screenPoint
  255. atOffset:FBMillisToSeconds(self.offset + self.duration)];
  256. return @[];
  257. }
  258. @end
  259. @implementation FBPointerPauseItem
  260. + (NSString *)actionName
  261. {
  262. return FB_ACTION_ITEM_TYPE_PAUSE;
  263. }
  264. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  265. allItems:(NSArray *)allItems
  266. currentItemIndex:(NSUInteger)currentItemIndex
  267. error:(NSError **)error
  268. {
  269. return @[];
  270. }
  271. @end
  272. @implementation FBPointerUpItem
  273. + (NSString *)actionName
  274. {
  275. return FB_ACTION_ITEM_TYPE_POINTER_UP;
  276. }
  277. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  278. allItems:(NSArray *)allItems
  279. currentItemIndex:(NSUInteger)currentItemIndex
  280. error:(NSError **)error
  281. {
  282. if (nil == eventPath) {
  283. NSString *description = [NSString stringWithFormat:@"Pointer Up must not be the first action in '%@'", self.actionItem];
  284. if (error) {
  285. *error = [[FBErrorBuilder.builder withDescription:description] build];
  286. }
  287. return nil;
  288. }
  289. [eventPath liftUpAtOffset:FBMillisToSeconds(self.offset)];
  290. return @[];
  291. }
  292. @end
  293. @implementation FBW3CKeyItem
  294. - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
  295. application:(XCUIApplication *)application
  296. previousItem:(nullable FBW3CKeyItem *)previousItem
  297. offset:(double)offset
  298. error:(NSError **)error
  299. {
  300. self = [super init];
  301. if (self) {
  302. self.actionItem = actionItem;
  303. self.application = application;
  304. self.offset = offset;
  305. _previousItem = previousItem;
  306. }
  307. return self;
  308. }
  309. @end
  310. @implementation FBKeyUpItem : FBW3CKeyItem
  311. - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
  312. application:(XCUIApplication *)application
  313. previousItem:(nullable FBW3CKeyItem *)previousItem
  314. offset:(double)offset
  315. error:(NSError **)error
  316. {
  317. self = [super initWithActionItem:actionItem
  318. application:application
  319. previousItem:previousItem
  320. offset:offset
  321. error:error];
  322. if (self) {
  323. NSString *value = FBRequireValue(actionItem, error);
  324. if (nil == value) {
  325. return nil;
  326. }
  327. _value = value;
  328. }
  329. return self;
  330. }
  331. + (NSString *)actionName
  332. {
  333. return FB_ACTION_ITEM_TYPE_KEY_UP;
  334. }
  335. - (BOOL)hasDownPairInItems:(NSArray *)allItems
  336. currentItemIndex:(NSUInteger)currentItemIndex
  337. {
  338. NSInteger balance = 1;
  339. for (NSInteger index = currentItemIndex - 1; index >= 0; index--) {
  340. FBW3CKeyItem *item = [allItems objectAtIndex:index];
  341. BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
  342. BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
  343. if (!isKeyUp && !isKeyDown) {
  344. break;
  345. }
  346. NSString *value = [item performSelector:@selector(value)];
  347. if (isKeyDown && [value isEqualToString:self.value]) {
  348. balance--;
  349. }
  350. if (isKeyUp && [value isEqualToString:self.value]) {
  351. balance++;
  352. }
  353. }
  354. return 0 == balance;
  355. }
  356. - (NSString *)collectTextWithItems:(NSArray *)allItems
  357. currentItemIndex:(NSUInteger)currentItemIndex
  358. {
  359. NSMutableArray *result = [NSMutableArray array];
  360. for (NSInteger index = currentItemIndex; index >= 0; index--) {
  361. FBW3CKeyItem *item = [allItems objectAtIndex:index];
  362. BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
  363. BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
  364. if (!isKeyUp && !isKeyDown) {
  365. break;
  366. }
  367. NSString *value = [item performSelector:@selector(value)];
  368. if (isKeyUp) {
  369. [result addObject:FBMapIfSpecialCharacter(value)];
  370. }
  371. }
  372. return [result.reverseObjectEnumerator.allObjects componentsJoinedByString:@""];
  373. }
  374. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  375. allItems:(NSArray *)allItems
  376. currentItemIndex:(NSUInteger)currentItemIndex
  377. error:(NSError **)error
  378. {
  379. if (![self hasDownPairInItems:allItems currentItemIndex:currentItemIndex]) {
  380. NSString *description = [NSString stringWithFormat:@"Key Up action '%@' is not balanced with a preceding Key Down one in '%@'", self.value, self.actionItem];
  381. if (error) {
  382. *error = [[FBErrorBuilder.builder withDescription:description] build];
  383. }
  384. return nil;
  385. }
  386. BOOL isLastKeyUpInGroup = currentItemIndex == allItems.count - 1
  387. || [[allItems objectAtIndex:currentItemIndex + 1] isKindOfClass:FBKeyPauseItem.class];
  388. if (!isLastKeyUpInGroup) {
  389. return @[];
  390. }
  391. NSString *text = [self collectTextWithItems:allItems currentItemIndex:currentItemIndex];
  392. NSTimeInterval offset = FBMillisToSeconds(self.offset);
  393. XCPointerEventPath *resultPath = [[XCPointerEventPath alloc] initForTextInput];
  394. [resultPath typeText:text
  395. atOffset:offset
  396. typingSpeed:FBConfiguration.maxTypingFrequency
  397. shouldRedact:YES];
  398. return @[resultPath];
  399. }
  400. @end
  401. @implementation FBKeyDownItem : FBW3CKeyItem
  402. - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
  403. application:(XCUIApplication *)application
  404. previousItem:(nullable FBW3CKeyItem *)previousItem
  405. offset:(double)offset
  406. error:(NSError **)error
  407. {
  408. self = [super initWithActionItem:actionItem
  409. application:application
  410. previousItem:previousItem
  411. offset:offset
  412. error:error];
  413. if (self) {
  414. NSString *value = FBRequireValue(actionItem, error);
  415. if (nil == value) {
  416. return nil;
  417. }
  418. _value = value;
  419. }
  420. return self;
  421. }
  422. + (NSString *)actionName
  423. {
  424. return FB_ACTION_ITEM_TYPE_KEY_DOWN;
  425. }
  426. - (BOOL)hasUpPairInItems:(NSArray *)allItems
  427. currentItemIndex:(NSUInteger)currentItemIndex
  428. {
  429. NSInteger balance = 1;
  430. for (NSUInteger index = currentItemIndex + 1; index < allItems.count; index++) {
  431. FBW3CKeyItem *item = [allItems objectAtIndex:index];
  432. BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
  433. BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
  434. if (!isKeyUp && !isKeyDown) {
  435. break;
  436. }
  437. NSString *value = [item performSelector:@selector(value)];
  438. if (isKeyUp && [value isEqualToString:self.value]) {
  439. balance--;
  440. }
  441. if (isKeyDown && [value isEqualToString:self.value]) {
  442. balance++;
  443. }
  444. }
  445. return 0 == balance;
  446. }
  447. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  448. allItems:(NSArray *)allItems
  449. currentItemIndex:(NSUInteger)currentItemIndex
  450. error:(NSError **)error
  451. {
  452. if (![self hasUpPairInItems:allItems currentItemIndex:currentItemIndex]) {
  453. NSString *description = [NSString stringWithFormat:@"Key Down action '%@' must have a closing Key Up successor in '%@'", self.value, self.actionItem];
  454. if (error) {
  455. *error = [[FBErrorBuilder.builder withDescription:description] build];
  456. }
  457. return nil;
  458. }
  459. return @[];
  460. }
  461. @end
  462. @implementation FBKeyPauseItem
  463. - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
  464. application:(XCUIApplication *)application
  465. previousItem:(nullable FBW3CKeyItem *)previousItem
  466. offset:(double)offset
  467. error:(NSError **)error
  468. {
  469. self = [super initWithActionItem:actionItem
  470. application:application
  471. previousItem:previousItem
  472. offset:offset
  473. error:error];
  474. if (self) {
  475. NSNumber *duration = FBOptDuration(actionItem, nil, error);
  476. if (nil == duration) {
  477. return nil;
  478. }
  479. _duration = [duration doubleValue];
  480. }
  481. return self;
  482. }
  483. + (NSString *)actionName
  484. {
  485. return FB_ACTION_ITEM_TYPE_PAUSE;
  486. }
  487. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  488. allItems:(NSArray *)allItems
  489. currentItemIndex:(NSUInteger)currentItemIndex
  490. error:(NSError **)error
  491. {
  492. return @[];
  493. }
  494. @end
  495. @interface FBW3CGestureItemsChain : FBBaseActionItemsChain
  496. @end
  497. @implementation FBW3CGestureItemsChain
  498. - (void)addItem:(FBBaseActionItem *)item
  499. {
  500. self.durationOffset += ((FBBaseGestureItem *)item).duration;
  501. [self.items addObject:item];
  502. }
  503. @end
  504. @interface FBW3CKeyItemsChain : FBBaseActionItemsChain
  505. @end
  506. @implementation FBW3CKeyItemsChain
  507. - (void)addItem:(FBBaseActionItem *)item
  508. {
  509. if ([item isKindOfClass:FBKeyPauseItem.class]) {
  510. self.durationOffset += ((FBKeyPauseItem *)item).duration;
  511. }
  512. [self.items addObject:item];
  513. }
  514. @end
  515. @implementation FBW3CActionsSynthesizer
  516. - (NSArray<NSDictionary<NSString *, id> *> *)preprocessedActionItemsWith:(NSArray<NSDictionary<NSString *, id> *> *)actionItems
  517. {
  518. NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
  519. BOOL shouldCancelNextItem = NO;
  520. for (NSDictionary<NSString *, id> *actionItem in [actionItems reverseObjectEnumerator]) {
  521. if (shouldCancelNextItem) {
  522. shouldCancelNextItem = NO;
  523. continue;
  524. }
  525. NSString *actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE];
  526. if (actionItemType != nil && [actionItemType isEqualToString:FB_ACTION_ITEM_TYPE_POINTER_CANCEL]) {
  527. shouldCancelNextItem = YES;
  528. continue;
  529. }
  530. if (nil == self.elementCache) {
  531. [result addObject:actionItem];
  532. continue;
  533. }
  534. id origin = [actionItem objectForKey:FB_ACTION_ITEM_KEY_ORIGIN];
  535. if (nil == origin || [@[FB_ORIGIN_TYPE_POINTER, FB_ORIGIN_TYPE_VIEWPORT] containsObject:origin]) {
  536. [result addObject:actionItem];
  537. continue;
  538. }
  539. // Selenium Python client passes 'origin' element in the following format:
  540. //
  541. // if isinstance(origin, WebElement):
  542. // action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
  543. if ([origin isKindOfClass:NSDictionary.class]) {
  544. id element = FBExtractElement(origin);
  545. if (nil != element) {
  546. origin = element;
  547. }
  548. }
  549. XCUIElement *instance;
  550. if ([origin isKindOfClass:XCUIElement.class]) {
  551. instance = origin;
  552. } else if ([origin isKindOfClass:NSString.class]) {
  553. instance = [self.elementCache elementForUUID:(NSString *)origin checkStaleness:YES];
  554. } else {
  555. [result addObject:actionItem];
  556. continue;
  557. }
  558. NSMutableDictionary<NSString *, id> *processedItem = actionItem.mutableCopy;
  559. [processedItem setObject:instance forKey:FB_ACTION_ITEM_KEY_ORIGIN];
  560. [result addObject:processedItem.copy];
  561. }
  562. return [[result reverseObjectEnumerator] allObjects];
  563. }
  564. - (nullable NSArray<XCPointerEventPath *> *)eventPathsWithKeyAction:(NSDictionary<NSString *, id> *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error
  565. {
  566. static NSDictionary<NSString *, Class> *keyItemsMapping;
  567. static NSArray<NSString *> *supportedActionItemTypes;
  568. static dispatch_once_t onceToken;
  569. dispatch_once(&onceToken, ^{
  570. NSMutableDictionary<NSString *, Class> *itemsMapping = [NSMutableDictionary dictionary];
  571. for (Class cls in @[FBKeyDownItem.class,
  572. FBKeyPauseItem.class,
  573. FBKeyUpItem.class]) {
  574. [itemsMapping setObject:cls forKey:[cls actionName]];
  575. }
  576. keyItemsMapping = itemsMapping.copy;
  577. supportedActionItemTypes = @[FB_ACTION_ITEM_TYPE_PAUSE,
  578. FB_ACTION_ITEM_TYPE_KEY_UP,
  579. FB_ACTION_ITEM_TYPE_KEY_DOWN];
  580. });
  581. NSArray<NSDictionary<NSString *, id> *> *actionItems = [actionDescription objectForKey:FB_KEY_ACTIONS];
  582. if (nil == actionItems || 0 == actionItems.count) {
  583. NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one item defined for each action. Action with id '%@' contains none", actionId];
  584. if (error) {
  585. *error = [[FBErrorBuilder.builder withDescription:description] build];
  586. }
  587. return nil;
  588. }
  589. FBW3CKeyItemsChain *chain = [[FBW3CKeyItemsChain alloc] init];
  590. NSArray<NSDictionary<NSString *, id> *> *processedItems = [self preprocessedActionItemsWith:actionItems];
  591. for (NSDictionary<NSString *, id> *actionItem in processedItems) {
  592. id actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE];
  593. if (![actionItemType isKindOfClass:NSString.class]) {
  594. NSString *description = [NSString stringWithFormat:@"The %@ property is mandatory to set for '%@' action item", FB_ACTION_ITEM_KEY_TYPE, actionItem];
  595. if (error) {
  596. *error = [[FBErrorBuilder.builder withDescription:description] build];
  597. }
  598. return nil;
  599. }
  600. Class keyItemClass = [keyItemsMapping objectForKey:actionItemType];
  601. if (nil == keyItemClass) {
  602. NSString *description = [NSString stringWithFormat:@"'%@' action item type '%@' is not supported. Only the following action item types are supported: %@", actionId, actionItemType, supportedActionItemTypes];
  603. if (error) {
  604. *error = [[FBErrorBuilder.builder withDescription:description] build];
  605. }
  606. return nil;
  607. }
  608. FBW3CKeyItem *keyItem = [[keyItemClass alloc] initWithActionItem:actionItem
  609. application:self.application
  610. previousItem:[chain.items lastObject]
  611. offset:chain.durationOffset
  612. error:error];
  613. if (nil == keyItem) {
  614. return nil;
  615. }
  616. [chain addItem:keyItem];
  617. }
  618. return [chain asEventPathsWithError:error];
  619. }
  620. - (nullable NSArray<XCPointerEventPath *> *)eventPathsWithGestureAction:(NSDictionary<NSString *, id> *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error
  621. {
  622. static NSDictionary<NSString *, Class> *gestureItemsMapping;
  623. static NSArray<NSString *> *supportedActionItemTypes;
  624. static dispatch_once_t onceToken;
  625. dispatch_once(&onceToken, ^{
  626. NSMutableDictionary<NSString *, Class> *itemsMapping = [NSMutableDictionary dictionary];
  627. for (Class cls in @[FBPointerDownItem.class,
  628. FBPointerMoveItem.class,
  629. FBPointerPauseItem.class,
  630. FBPointerUpItem.class]) {
  631. [itemsMapping setObject:cls forKey:[cls actionName]];
  632. }
  633. gestureItemsMapping = itemsMapping.copy;
  634. supportedActionItemTypes = @[FB_ACTION_ITEM_TYPE_PAUSE,
  635. FB_ACTION_ITEM_TYPE_POINTER_UP,
  636. FB_ACTION_ITEM_TYPE_POINTER_DOWN,
  637. FB_ACTION_ITEM_TYPE_POINTER_MOVE];
  638. });
  639. id parameters = [actionDescription objectForKey:FB_KEY_PARAMETERS];
  640. id pointerType = FB_POINTER_TYPE_MOUSE;
  641. if ([parameters isKindOfClass:NSDictionary.class]) {
  642. pointerType = [parameters objectForKey:FB_PARAMETERS_KEY_POINTER_TYPE] ?: FB_POINTER_TYPE_MOUSE;
  643. }
  644. if (![pointerType isKindOfClass:NSString.class] || ![pointerType isEqualToString:FB_POINTER_TYPE_TOUCH]) {
  645. NSString *description = [NSString stringWithFormat:@"Only pointer type '%@' is supported. '%@' is given instead for action with id '%@'", FB_POINTER_TYPE_TOUCH, pointerType, actionId];
  646. if (error) {
  647. *error = [[FBErrorBuilder.builder withDescription:description] build];
  648. }
  649. return nil;
  650. }
  651. NSArray<NSDictionary<NSString *, id> *> *actionItems = [actionDescription objectForKey:FB_KEY_ACTIONS];
  652. if (nil == actionItems || 0 == actionItems.count) {
  653. NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one gesture item defined for each action. Action with id '%@' contains none", actionId];
  654. if (error) {
  655. *error = [[FBErrorBuilder.builder withDescription:description] build];
  656. }
  657. return nil;
  658. }
  659. FBW3CGestureItemsChain *chain = [[FBW3CGestureItemsChain alloc] init];
  660. NSArray<NSDictionary<NSString *, id> *> *processedItems = [self preprocessedActionItemsWith:actionItems];
  661. for (NSDictionary<NSString *, id> *actionItem in processedItems) {
  662. id actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE];
  663. if (![actionItemType isKindOfClass:NSString.class]) {
  664. NSString *description = [NSString stringWithFormat:@"The %@ property is mandatory to set for '%@' action item", FB_ACTION_ITEM_KEY_TYPE, actionItem];
  665. if (error) {
  666. *error = [[FBErrorBuilder.builder withDescription:description] build];
  667. }
  668. return nil;
  669. }
  670. Class gestureItemClass = [gestureItemsMapping objectForKey:actionItemType];
  671. if (nil == gestureItemClass) {
  672. NSString *description = [NSString stringWithFormat:@"'%@' action item type '%@' is not supported. Only the following action item types are supported: %@", actionId, actionItemType, supportedActionItemTypes];
  673. if (error) {
  674. *error = [[FBErrorBuilder.builder withDescription:description] build];
  675. }
  676. return nil;
  677. }
  678. FBW3CGestureItem *gestureItem = [[gestureItemClass alloc] initWithActionItem:actionItem application:self.application previousItem:[chain.items lastObject] offset:chain.durationOffset error:error];
  679. if (nil == gestureItem) {
  680. return nil;
  681. }
  682. [chain addItem:gestureItem];
  683. }
  684. return [chain asEventPathsWithError:error];
  685. }
  686. - (nullable NSArray<XCPointerEventPath *> *)eventPathsWithActionDescription:(NSDictionary<NSString *, id> *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error
  687. {
  688. id actionType = [actionDescription objectForKey:FB_KEY_TYPE];
  689. if (![actionType isKindOfClass:NSString.class] ||
  690. !([actionType isEqualToString:FB_ACTION_TYPE_POINTER]
  691. || ([XCPointerEvent.class fb_areKeyEventsSupported] && [actionType isEqualToString:FB_ACTION_TYPE_KEY]))) {
  692. NSString *description = [NSString stringWithFormat:@"Only actions of '%@' types are supported. '%@' is given instead for action with id '%@'", @[FB_ACTION_TYPE_POINTER, FB_ACTION_TYPE_KEY], actionType, actionId];
  693. if (error) {
  694. *error = [[FBErrorBuilder.builder withDescription:description] build];
  695. }
  696. return nil;
  697. }
  698. if ([actionType isEqualToString:FB_ACTION_TYPE_POINTER]) {
  699. return [self eventPathsWithGestureAction:actionDescription forActionId:actionId error:error];
  700. }
  701. return [self eventPathsWithKeyAction:actionDescription forActionId:actionId error:error];
  702. }
  703. - (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error
  704. {
  705. XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc]
  706. initWithName:@"W3C Touch Action"
  707. interfaceOrientation:self.application.interfaceOrientation];
  708. NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *actionsMapping = [NSMutableDictionary new];
  709. NSMutableArray<NSString *> *actionIds = [NSMutableArray new];
  710. for (NSDictionary<NSString *, id> *action in self.actions) {
  711. id actionId = [action objectForKey:FB_KEY_ID];
  712. if (![actionId isKindOfClass:NSString.class] || 0 == [actionId length]) {
  713. if (error) {
  714. NSString *description = [NSString stringWithFormat:@"The mandatory action %@ field is missing or empty for '%@'", FB_KEY_ID, action];
  715. *error = [[FBErrorBuilder.builder withDescription:description] build];
  716. }
  717. return nil;
  718. }
  719. if (nil != [actionsMapping objectForKey:actionId]) {
  720. if (error) {
  721. NSString *description = [NSString stringWithFormat:@"Action %@ '%@' is not unique for '%@'", FB_KEY_ID, actionId, action];
  722. *error = [[FBErrorBuilder.builder withDescription:description] build];
  723. }
  724. return nil;
  725. }
  726. NSArray<NSDictionary<NSString *, id> *> *actionItems = [action objectForKey:FB_KEY_ACTIONS];
  727. if (nil == actionItems) {
  728. NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one item defined for each action. Action with id '%@' contains none", actionId];
  729. if (error) {
  730. *error = [[FBErrorBuilder.builder withDescription:description] build];
  731. }
  732. return nil;
  733. }
  734. if (0 == actionItems.count) {
  735. [FBLogger logFmt:@"Action items in the action id '%@' had an empty array. Skipping the action.", actionId];
  736. continue;
  737. }
  738. [actionIds addObject:actionId];
  739. [actionsMapping setObject:action forKey:actionId];
  740. }
  741. for (NSString *actionId in actionIds.copy) {
  742. NSDictionary<NSString *, id> *actionDescription = [actionsMapping objectForKey:actionId];
  743. NSArray<XCPointerEventPath *> *eventPaths = [self eventPathsWithActionDescription:actionDescription forActionId:actionId error:error];
  744. if (nil == eventPaths) {
  745. return nil;
  746. }
  747. for (XCPointerEventPath *eventPath in eventPaths) {
  748. [eventRecord addPointerEventPath:eventPath];
  749. }
  750. }
  751. return eventRecord;
  752. }
  753. @end
  754. #endif