FBW3CActionsSynthesizer.m 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921
  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. BOOL isSelfMetaModifier = FBIsMetaModifier(self.value);
  340. for (NSInteger index = currentItemIndex - 1; index >= 0; index--) {
  341. FBW3CKeyItem *item = [allItems objectAtIndex:index];
  342. BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
  343. BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
  344. if (!isKeyUp && !isKeyDown) {
  345. if (isSelfMetaModifier) {
  346. continue;
  347. } else {
  348. break;
  349. }
  350. }
  351. NSString *value = [item performSelector:@selector(value)];
  352. if (isKeyDown && [value isEqualToString:self.value]) {
  353. balance--;
  354. }
  355. if (isKeyUp && [value isEqualToString:self.value]) {
  356. balance++;
  357. }
  358. }
  359. return 0 == balance;
  360. }
  361. - (NSUInteger)collectModifersWithItems:(NSArray *)allItems
  362. currentItemIndex:(NSUInteger)currentItemIndex
  363. {
  364. NSUInteger modifiers = 0;
  365. for (NSUInteger index = 0; index < currentItemIndex; index++) {
  366. FBW3CKeyItem *item = [allItems objectAtIndex:index];
  367. BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
  368. BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
  369. if (!isKeyUp && !isKeyDown) {
  370. continue;
  371. }
  372. NSString *value = [item performSelector:@selector(value)];
  373. NSUInteger modifier = FBToMetaModifier(value);
  374. if (modifier > 0) {
  375. if (isKeyDown) {
  376. modifiers |= modifier;
  377. } else if (item.offset < self.offset) {
  378. // only cancel the modifier if it is not in the same group
  379. modifiers &= ~modifier;
  380. }
  381. }
  382. }
  383. return modifiers;
  384. }
  385. - (NSString *)collectTextWithItems:(NSArray *)allItems
  386. currentItemIndex:(NSUInteger)currentItemIndex
  387. {
  388. NSMutableArray *result = [NSMutableArray array];
  389. for (NSInteger index = currentItemIndex; index >= 0; index--) {
  390. FBW3CKeyItem *item = [allItems objectAtIndex:index];
  391. BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
  392. BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
  393. if (!isKeyUp && !isKeyDown) {
  394. break;
  395. }
  396. NSString *value = [item performSelector:@selector(value)];
  397. if (FBIsMetaModifier(value)) {
  398. continue;
  399. }
  400. if (isKeyUp) {
  401. [result addObject:value];
  402. }
  403. }
  404. return [result.reverseObjectEnumerator.allObjects componentsJoinedByString:@""];
  405. }
  406. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  407. allItems:(NSArray *)allItems
  408. currentItemIndex:(NSUInteger)currentItemIndex
  409. error:(NSError **)error
  410. {
  411. if (![self hasDownPairInItems:allItems currentItemIndex:currentItemIndex]) {
  412. NSString *description = [NSString stringWithFormat:@"Key Up action '%@' is not balanced with a preceding Key Down one in '%@'", self.value, self.actionItem];
  413. if (error) {
  414. *error = [[FBErrorBuilder.builder withDescription:description] build];
  415. }
  416. return nil;
  417. }
  418. if (FBIsMetaModifier(self.value)) {
  419. return @[];
  420. }
  421. BOOL isLastKeyUpInGroup = currentItemIndex == allItems.count - 1
  422. || [[allItems objectAtIndex:currentItemIndex + 1] isKindOfClass:FBKeyPauseItem.class];
  423. if (!isLastKeyUpInGroup) {
  424. return @[];
  425. }
  426. NSString *text = [self collectTextWithItems:allItems currentItemIndex:currentItemIndex];
  427. NSTimeInterval offset = FBMillisToSeconds(self.offset);
  428. XCPointerEventPath *resultPath = [[XCPointerEventPath alloc] initForTextInput];
  429. // TODO: Figure out how meta modifiers could be applied
  430. // TODO: The current approach throws zero division error on execution
  431. // NSUInteger modifiers = [self collectModifersWithItems:allItems currentItemIndex:currentItemIndex];
  432. // [resultPath setModifiers:modifiers mergeWithCurrentModifierFlags:NO atOffset:0];
  433. [resultPath typeText:text
  434. atOffset:offset
  435. typingSpeed:FBConfiguration.maxTypingFrequency
  436. shouldRedact:YES];
  437. return @[resultPath];
  438. }
  439. @end
  440. @implementation FBKeyDownItem : FBW3CKeyItem
  441. - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
  442. application:(XCUIApplication *)application
  443. previousItem:(nullable FBW3CKeyItem *)previousItem
  444. offset:(double)offset
  445. error:(NSError **)error
  446. {
  447. self = [super initWithActionItem:actionItem
  448. application:application
  449. previousItem:previousItem
  450. offset:offset
  451. error:error];
  452. if (self) {
  453. NSString *value = FBRequireValue(actionItem, error);
  454. if (nil == value) {
  455. return nil;
  456. }
  457. _value = value;
  458. }
  459. return self;
  460. }
  461. + (NSString *)actionName
  462. {
  463. return FB_ACTION_ITEM_TYPE_KEY_DOWN;
  464. }
  465. - (BOOL)hasUpPairInItems:(NSArray *)allItems
  466. currentItemIndex:(NSUInteger)currentItemIndex
  467. {
  468. NSInteger balance = 1;
  469. BOOL isSelfMetaModifier = FBIsMetaModifier(self.value);
  470. for (NSUInteger index = currentItemIndex + 1; index < allItems.count; index++) {
  471. FBW3CKeyItem *item = [allItems objectAtIndex:index];
  472. BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
  473. BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
  474. if (!isKeyUp && !isKeyDown) {
  475. if (isSelfMetaModifier) {
  476. continue;
  477. } else {
  478. break;
  479. }
  480. }
  481. NSString *value = [item performSelector:@selector(value)];
  482. if (isKeyUp && [value isEqualToString:self.value]) {
  483. balance--;
  484. }
  485. if (isKeyDown && [value isEqualToString:self.value]) {
  486. balance++;
  487. }
  488. }
  489. return 0 == balance;
  490. }
  491. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  492. allItems:(NSArray *)allItems
  493. currentItemIndex:(NSUInteger)currentItemIndex
  494. error:(NSError **)error
  495. {
  496. if (![self hasUpPairInItems:allItems currentItemIndex:currentItemIndex]) {
  497. NSString *description = [NSString stringWithFormat:@"Key Down action '%@' must have a closing Key Up successor in '%@'", self.value, self.actionItem];
  498. if (error) {
  499. *error = [[FBErrorBuilder.builder withDescription:description] build];
  500. }
  501. return nil;
  502. }
  503. return @[];
  504. }
  505. @end
  506. @implementation FBKeyPauseItem
  507. - (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
  508. application:(XCUIApplication *)application
  509. previousItem:(nullable FBW3CKeyItem *)previousItem
  510. offset:(double)offset
  511. error:(NSError **)error
  512. {
  513. self = [super initWithActionItem:actionItem
  514. application:application
  515. previousItem:previousItem
  516. offset:offset
  517. error:error];
  518. if (self) {
  519. NSNumber *duration = FBOptDuration(actionItem, nil, error);
  520. if (nil == duration) {
  521. return nil;
  522. }
  523. _duration = [duration doubleValue];
  524. }
  525. return self;
  526. }
  527. + (NSString *)actionName
  528. {
  529. return FB_ACTION_ITEM_TYPE_PAUSE;
  530. }
  531. - (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
  532. allItems:(NSArray *)allItems
  533. currentItemIndex:(NSUInteger)currentItemIndex
  534. error:(NSError **)error
  535. {
  536. return @[];
  537. }
  538. @end
  539. @interface FBW3CGestureItemsChain : FBBaseActionItemsChain
  540. @end
  541. @implementation FBW3CGestureItemsChain
  542. - (void)addItem:(FBBaseActionItem *)item
  543. {
  544. self.durationOffset += ((FBBaseGestureItem *)item).duration;
  545. [self.items addObject:item];
  546. }
  547. @end
  548. @interface FBW3CKeyItemsChain : FBBaseActionItemsChain
  549. @end
  550. @implementation FBW3CKeyItemsChain
  551. - (void)addItem:(FBBaseActionItem *)item
  552. {
  553. if ([item isKindOfClass:FBKeyPauseItem.class]) {
  554. self.durationOffset += ((FBKeyPauseItem *)item).duration;
  555. }
  556. [self.items addObject:item];
  557. }
  558. @end
  559. @implementation FBW3CActionsSynthesizer
  560. - (NSArray<NSDictionary<NSString *, id> *> *)preprocessedActionItemsWith:(NSArray<NSDictionary<NSString *, id> *> *)actionItems
  561. {
  562. NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
  563. BOOL shouldCancelNextItem = NO;
  564. for (NSDictionary<NSString *, id> *actionItem in [actionItems reverseObjectEnumerator]) {
  565. if (shouldCancelNextItem) {
  566. shouldCancelNextItem = NO;
  567. continue;
  568. }
  569. NSString *actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE];
  570. if (actionItemType != nil && [actionItemType isEqualToString:FB_ACTION_ITEM_TYPE_POINTER_CANCEL]) {
  571. shouldCancelNextItem = YES;
  572. continue;
  573. }
  574. if (nil == self.elementCache) {
  575. [result addObject:actionItem];
  576. continue;
  577. }
  578. id origin = [actionItem objectForKey:FB_ACTION_ITEM_KEY_ORIGIN];
  579. if (nil == origin || [@[FB_ORIGIN_TYPE_POINTER, FB_ORIGIN_TYPE_VIEWPORT] containsObject:origin]) {
  580. [result addObject:actionItem];
  581. continue;
  582. }
  583. // Selenium Python client passes 'origin' element in the following format:
  584. //
  585. // if isinstance(origin, WebElement):
  586. // action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
  587. if ([origin isKindOfClass:NSDictionary.class]) {
  588. id element = FBExtractElement(origin);
  589. if (nil != element) {
  590. origin = element;
  591. }
  592. }
  593. XCUIElement *instance;
  594. if ([origin isKindOfClass:XCUIElement.class]) {
  595. instance = origin;
  596. } else if ([origin isKindOfClass:NSString.class]) {
  597. instance = [self.elementCache elementForUUID:(NSString *)origin];
  598. } else {
  599. [result addObject:actionItem];
  600. continue;
  601. }
  602. NSMutableDictionary<NSString *, id> *processedItem = actionItem.mutableCopy;
  603. [processedItem setObject:instance forKey:FB_ACTION_ITEM_KEY_ORIGIN];
  604. [result addObject:processedItem.copy];
  605. }
  606. return [[result reverseObjectEnumerator] allObjects];
  607. }
  608. - (nullable NSArray<XCPointerEventPath *> *)eventPathsWithKeyAction:(NSDictionary<NSString *, id> *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error
  609. {
  610. static NSDictionary<NSString *, Class> *keyItemsMapping;
  611. static NSArray<NSString *> *supportedActionItemTypes;
  612. static dispatch_once_t onceToken;
  613. dispatch_once(&onceToken, ^{
  614. NSMutableDictionary<NSString *, Class> *itemsMapping = [NSMutableDictionary dictionary];
  615. for (Class cls in @[FBKeyDownItem.class,
  616. FBKeyPauseItem.class,
  617. FBKeyUpItem.class]) {
  618. [itemsMapping setObject:cls forKey:[cls actionName]];
  619. }
  620. keyItemsMapping = itemsMapping.copy;
  621. supportedActionItemTypes = @[FB_ACTION_ITEM_TYPE_PAUSE,
  622. FB_ACTION_ITEM_TYPE_KEY_UP,
  623. FB_ACTION_ITEM_TYPE_KEY_DOWN];
  624. });
  625. NSArray<NSDictionary<NSString *, id> *> *actionItems = [actionDescription objectForKey:FB_KEY_ACTIONS];
  626. if (nil == actionItems || 0 == actionItems.count) {
  627. NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one item defined for each action. Action with id '%@' contains none", actionId];
  628. if (error) {
  629. *error = [[FBErrorBuilder.builder withDescription:description] build];
  630. }
  631. return nil;
  632. }
  633. FBW3CKeyItemsChain *chain = [[FBW3CKeyItemsChain alloc] init];
  634. NSArray<NSDictionary<NSString *, id> *> *processedItems = [self preprocessedActionItemsWith:actionItems];
  635. for (NSDictionary<NSString *, id> *actionItem in processedItems) {
  636. id actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE];
  637. if (![actionItemType isKindOfClass:NSString.class]) {
  638. NSString *description = [NSString stringWithFormat:@"The %@ property is mandatory to set for '%@' action item", FB_ACTION_ITEM_KEY_TYPE, actionItem];
  639. if (error) {
  640. *error = [[FBErrorBuilder.builder withDescription:description] build];
  641. }
  642. return nil;
  643. }
  644. Class keyItemClass = [keyItemsMapping objectForKey:actionItemType];
  645. if (nil == keyItemClass) {
  646. NSString *description = [NSString stringWithFormat:@"'%@' action item type '%@' is not supported. Only the following action item types are supported: %@", actionId, actionItemType, supportedActionItemTypes];
  647. if (error) {
  648. *error = [[FBErrorBuilder.builder withDescription:description] build];
  649. }
  650. return nil;
  651. }
  652. FBW3CKeyItem *keyItem = [[keyItemClass alloc] initWithActionItem:actionItem
  653. application:self.application
  654. previousItem:[chain.items lastObject]
  655. offset:chain.durationOffset
  656. error:error];
  657. if (nil == keyItem) {
  658. return nil;
  659. }
  660. [chain addItem:keyItem];
  661. }
  662. return [chain asEventPathsWithError:error];
  663. }
  664. - (nullable NSArray<XCPointerEventPath *> *)eventPathsWithGestureAction:(NSDictionary<NSString *, id> *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error
  665. {
  666. static NSDictionary<NSString *, Class> *gestureItemsMapping;
  667. static NSArray<NSString *> *supportedActionItemTypes;
  668. static dispatch_once_t onceToken;
  669. dispatch_once(&onceToken, ^{
  670. NSMutableDictionary<NSString *, Class> *itemsMapping = [NSMutableDictionary dictionary];
  671. for (Class cls in @[FBPointerDownItem.class,
  672. FBPointerMoveItem.class,
  673. FBPointerPauseItem.class,
  674. FBPointerUpItem.class]) {
  675. [itemsMapping setObject:cls forKey:[cls actionName]];
  676. }
  677. gestureItemsMapping = itemsMapping.copy;
  678. supportedActionItemTypes = @[FB_ACTION_ITEM_TYPE_PAUSE,
  679. FB_ACTION_ITEM_TYPE_POINTER_UP,
  680. FB_ACTION_ITEM_TYPE_POINTER_DOWN,
  681. FB_ACTION_ITEM_TYPE_POINTER_MOVE];
  682. });
  683. id parameters = [actionDescription objectForKey:FB_KEY_PARAMETERS];
  684. id pointerType = FB_POINTER_TYPE_MOUSE;
  685. if ([parameters isKindOfClass:NSDictionary.class]) {
  686. pointerType = [parameters objectForKey:FB_PARAMETERS_KEY_POINTER_TYPE] ?: FB_POINTER_TYPE_MOUSE;
  687. }
  688. if (![pointerType isKindOfClass:NSString.class] || ![pointerType isEqualToString:FB_POINTER_TYPE_TOUCH]) {
  689. NSString *description = [NSString stringWithFormat:@"Only pointer type '%@' is supported. '%@' is given instead for action with id '%@'", FB_POINTER_TYPE_TOUCH, pointerType, actionId];
  690. if (error) {
  691. *error = [[FBErrorBuilder.builder withDescription:description] build];
  692. }
  693. return nil;
  694. }
  695. NSArray<NSDictionary<NSString *, id> *> *actionItems = [actionDescription objectForKey:FB_KEY_ACTIONS];
  696. if (nil == actionItems || 0 == actionItems.count) {
  697. NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one gesture item defined for each action. Action with id '%@' contains none", actionId];
  698. if (error) {
  699. *error = [[FBErrorBuilder.builder withDescription:description] build];
  700. }
  701. return nil;
  702. }
  703. FBW3CGestureItemsChain *chain = [[FBW3CGestureItemsChain alloc] init];
  704. NSArray<NSDictionary<NSString *, id> *> *processedItems = [self preprocessedActionItemsWith:actionItems];
  705. for (NSDictionary<NSString *, id> *actionItem in processedItems) {
  706. id actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE];
  707. if (![actionItemType isKindOfClass:NSString.class]) {
  708. NSString *description = [NSString stringWithFormat:@"The %@ property is mandatory to set for '%@' action item", FB_ACTION_ITEM_KEY_TYPE, actionItem];
  709. if (error) {
  710. *error = [[FBErrorBuilder.builder withDescription:description] build];
  711. }
  712. return nil;
  713. }
  714. Class gestureItemClass = [gestureItemsMapping objectForKey:actionItemType];
  715. if (nil == gestureItemClass) {
  716. NSString *description = [NSString stringWithFormat:@"'%@' action item type '%@' is not supported. Only the following action item types are supported: %@", actionId, actionItemType, supportedActionItemTypes];
  717. if (error) {
  718. *error = [[FBErrorBuilder.builder withDescription:description] build];
  719. }
  720. return nil;
  721. }
  722. FBW3CGestureItem *gestureItem = [[gestureItemClass alloc] initWithActionItem:actionItem application:self.application previousItem:[chain.items lastObject] offset:chain.durationOffset error:error];
  723. if (nil == gestureItem) {
  724. return nil;
  725. }
  726. [chain addItem:gestureItem];
  727. }
  728. return [chain asEventPathsWithError:error];
  729. }
  730. - (nullable NSArray<XCPointerEventPath *> *)eventPathsWithActionDescription:(NSDictionary<NSString *, id> *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error
  731. {
  732. id actionType = [actionDescription objectForKey:FB_KEY_TYPE];
  733. if (![actionType isKindOfClass:NSString.class] ||
  734. !([actionType isEqualToString:FB_ACTION_TYPE_POINTER]
  735. || ([XCPointerEvent.class fb_areKeyEventsSupported] && [actionType isEqualToString:FB_ACTION_TYPE_KEY]))) {
  736. 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];
  737. if (error) {
  738. *error = [[FBErrorBuilder.builder withDescription:description] build];
  739. }
  740. return nil;
  741. }
  742. if ([actionType isEqualToString:FB_ACTION_TYPE_POINTER]) {
  743. return [self eventPathsWithGestureAction:actionDescription forActionId:actionId error:error];
  744. }
  745. return [self eventPathsWithKeyAction:actionDescription forActionId:actionId error:error];
  746. }
  747. - (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error
  748. {
  749. XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc]
  750. initWithName:@"W3C Touch Action"
  751. interfaceOrientation:self.application.interfaceOrientation];
  752. NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *actionsMapping = [NSMutableDictionary new];
  753. NSMutableArray<NSString *> *actionIds = [NSMutableArray new];
  754. for (NSDictionary<NSString *, id> *action in self.actions) {
  755. id actionId = [action objectForKey:FB_KEY_ID];
  756. if (![actionId isKindOfClass:NSString.class] || 0 == [actionId length]) {
  757. if (error) {
  758. NSString *description = [NSString stringWithFormat:@"The mandatory action %@ field is missing or empty for '%@'", FB_KEY_ID, action];
  759. *error = [[FBErrorBuilder.builder withDescription:description] build];
  760. }
  761. return nil;
  762. }
  763. if (nil != [actionsMapping objectForKey:actionId]) {
  764. if (error) {
  765. NSString *description = [NSString stringWithFormat:@"Action %@ '%@' is not unique for '%@'", FB_KEY_ID, actionId, action];
  766. *error = [[FBErrorBuilder.builder withDescription:description] build];
  767. }
  768. return nil;
  769. }
  770. [actionIds addObject:actionId];
  771. [actionsMapping setObject:action forKey:actionId];
  772. }
  773. for (NSString *actionId in actionIds.copy) {
  774. NSDictionary<NSString *, id> *actionDescription = [actionsMapping objectForKey:actionId];
  775. NSArray<XCPointerEventPath *> *eventPaths = [self eventPathsWithActionDescription:actionDescription forActionId:actionId error:error];
  776. if (nil == eventPaths) {
  777. return nil;
  778. }
  779. for (XCPointerEventPath *eventPath in eventPaths) {
  780. [eventRecord addPointerEventPath:eventPath];
  781. }
  782. }
  783. return eventRecord;
  784. }
  785. @end
  786. #endif