FBXPath.m 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  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 "FBXPath.h"
  10. #import "FBConfiguration.h"
  11. #import "FBExceptions.h"
  12. #import "FBElementUtils.h"
  13. #import "FBLogger.h"
  14. #import "FBMacros.h"
  15. #import "FBXMLGenerationOptions.h"
  16. #import "FBXCElementSnapshotWrapper+Helpers.h"
  17. #import "NSString+FBXMLSafeString.h"
  18. #import "XCUIElement.h"
  19. #import "XCUIElement+FBCaching.h"
  20. #import "XCUIElement+FBUtilities.h"
  21. #import "XCUIElement+FBWebDriverAttributes.h"
  22. #import "XCTestPrivateSymbols.h"
  23. #import "FBElementHelpers.h"
  24. @interface FBElementAttribute : NSObject
  25. @property (nonatomic, readonly) id<FBElement> element;
  26. + (nonnull NSString *)name;
  27. + (nullable NSString *)valueForElement:(id<FBElement>)element;
  28. + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element;
  29. + (NSArray<Class> *)supportedAttributes;
  30. @end
  31. @interface FBTypeAttribute : FBElementAttribute
  32. @end
  33. @interface FBValueAttribute : FBElementAttribute
  34. @end
  35. @interface FBNameAttribute : FBElementAttribute
  36. @end
  37. @interface FBLabelAttribute : FBElementAttribute
  38. @end
  39. @interface FBEnabledAttribute : FBElementAttribute
  40. @end
  41. @interface FBVisibleAttribute : FBElementAttribute
  42. @end
  43. @interface FBAccessibleAttribute : FBElementAttribute
  44. @end
  45. @interface FBDimensionAttribute : FBElementAttribute
  46. @end
  47. @interface FBXAttribute : FBDimensionAttribute
  48. @end
  49. @interface FBYAttribute : FBDimensionAttribute
  50. @end
  51. @interface FBWidthAttribute : FBDimensionAttribute
  52. @end
  53. @interface FBHeightAttribute : FBDimensionAttribute
  54. @end
  55. @interface FBIndexAttribute : FBElementAttribute
  56. @end
  57. @interface FBHittableAttribute : FBElementAttribute
  58. @end
  59. @interface FBInternalIndexAttribute : FBElementAttribute
  60. @property (nonatomic, nonnull, readonly) NSString* indexValue;
  61. + (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value;
  62. @end
  63. @interface FBPlaceholderValueAttribute : FBElementAttribute
  64. @end
  65. @interface FBNativeFrameAttribute : FBElementAttribute
  66. @end
  67. @interface FBTraitsAttribute : FBElementAttribute
  68. @end
  69. @interface FBMinValueAttribute : FBElementAttribute
  70. @end
  71. @interface FBMaxValueAttribute : FBElementAttribute
  72. @end
  73. #if TARGET_OS_TV
  74. @interface FBFocusedAttribute : FBElementAttribute
  75. @end
  76. #endif
  77. const static char *_UTF8Encoding = "UTF-8";
  78. static NSString *const kXMLIndexPathKey = @"private_indexPath";
  79. static NSString *const topNodeIndexPath = @"top";
  80. @implementation FBXPath
  81. + (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery
  82. {
  83. NSString *reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery];
  84. @throw [NSException exceptionWithName:name reason:reason userInfo:@{}];
  85. return nil;
  86. }
  87. + (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
  88. options:(nullable FBXMLGenerationOptions *)options
  89. {
  90. xmlDocPtr doc;
  91. xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
  92. int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
  93. if (rc < 0) {
  94. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
  95. } else {
  96. BOOL hasScope = nil != options.scope && [options.scope length] > 0;
  97. if (hasScope) {
  98. rc = xmlTextWriterStartElement(writer,
  99. (xmlChar *)[[self safeXmlStringWithString:options.scope] UTF8String]);
  100. if (rc < 0) {
  101. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", options.scope, rc];
  102. }
  103. }
  104. if (rc >= 0) {
  105. [self waitUntilStableWithElement:root];
  106. // If 'includeHittableInPageSource' setting is enabled, then use native snapshots
  107. // to calculate a more accurate value for the 'hittable' attribute.
  108. rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root
  109. useNative:FBConfiguration.includeHittableInPageSource]
  110. writer:writer
  111. elementStore:nil
  112. query:nil
  113. excludingAttributes:options.excludedAttributes];
  114. }
  115. if (rc >= 0 && hasScope) {
  116. rc = xmlTextWriterEndElement(writer);
  117. if (rc < 0) {
  118. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
  119. }
  120. }
  121. if (rc >= 0) {
  122. rc = xmlTextWriterEndDocument(writer);
  123. if (rc < 0) {
  124. [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc];
  125. }
  126. }
  127. }
  128. if (rc < 0) {
  129. xmlFreeTextWriter(writer);
  130. xmlFreeDoc(doc);
  131. return nil;
  132. }
  133. int buffersize;
  134. xmlChar *xmlbuff;
  135. xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1);
  136. xmlFreeTextWriter(writer);
  137. xmlFreeDoc(doc);
  138. NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding];
  139. xmlFree(xmlbuff);
  140. return result;
  141. }
  142. + (NSArray<id<FBXCElementSnapshot>> *)matchesWithRootElement:(id<FBElement>)root
  143. forQuery:(NSString *)xpathQuery
  144. {
  145. xmlDocPtr doc;
  146. xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
  147. if (NULL == writer) {
  148. [FBLogger logFmt:@"Failed to invoke libxml2>xmlNewTextWriterDoc for XPath query \"%@\"", xpathQuery];
  149. return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
  150. }
  151. NSMutableDictionary *elementStore = [NSMutableDictionary dictionary];
  152. int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
  153. id<FBXCElementSnapshot> lookupScopeSnapshot = nil;
  154. id<FBXCElementSnapshot> contextRootSnapshot = nil;
  155. BOOL useNativeSnapshot = nil == xpathQuery
  156. ? NO
  157. : [[self.class elementAttributesWithXPathQuery:xpathQuery] containsObject:FBHittableAttribute.class];
  158. if (rc < 0) {
  159. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
  160. } else {
  161. [self waitUntilStableWithElement:root];
  162. if (FBConfiguration.limitXpathContextScope) {
  163. lookupScopeSnapshot = [self snapshotWithRoot:root useNative:useNativeSnapshot];
  164. } else {
  165. if ([root isKindOfClass:XCUIElement.class]) {
  166. lookupScopeSnapshot = [self snapshotWithRoot:[(XCUIElement *)root application]
  167. useNative:useNativeSnapshot];
  168. contextRootSnapshot = [root isKindOfClass:XCUIApplication.class]
  169. ? nil
  170. : ([(XCUIElement *)root lastSnapshot] ?: [self snapshotWithRoot:(XCUIElement *)root
  171. useNative:useNativeSnapshot]);
  172. } else {
  173. lookupScopeSnapshot = (id<FBXCElementSnapshot>)root;
  174. contextRootSnapshot = nil == lookupScopeSnapshot.parent ? nil : (id<FBXCElementSnapshot>)root;
  175. while (nil != lookupScopeSnapshot.parent) {
  176. lookupScopeSnapshot = lookupScopeSnapshot.parent;
  177. }
  178. }
  179. }
  180. rc = [self xmlRepresentationWithRootElement:lookupScopeSnapshot
  181. writer:writer
  182. elementStore:elementStore
  183. query:xpathQuery
  184. excludingAttributes:nil];
  185. if (rc >= 0) {
  186. rc = xmlTextWriterEndDocument(writer);
  187. if (rc < 0) {
  188. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndDocument. Error code: %d", rc];
  189. }
  190. }
  191. }
  192. if (rc < 0) {
  193. xmlFreeTextWriter(writer);
  194. xmlFreeDoc(doc);
  195. return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
  196. }
  197. xmlXPathObjectPtr contextNodeQueryResult = [self matchNodeInDocument:doc
  198. elementStore:elementStore.copy
  199. forSnapshot:contextRootSnapshot];
  200. xmlNodePtr contextNode = NULL;
  201. if (NULL != contextNodeQueryResult) {
  202. xmlNodeSetPtr nodeSet = contextNodeQueryResult->nodesetval;
  203. if (!xmlXPathNodeSetIsEmpty(nodeSet)) {
  204. contextNode = nodeSet->nodeTab[0];
  205. }
  206. }
  207. xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery
  208. document:doc
  209. contextNode:contextNode];
  210. if (NULL != contextNodeQueryResult) {
  211. xmlXPathFreeObject(contextNodeQueryResult);
  212. }
  213. if (NULL == queryResult) {
  214. xmlFreeTextWriter(writer);
  215. xmlFreeDoc(doc);
  216. return [self throwException:FBInvalidXPathException forQuery:xpathQuery];
  217. }
  218. NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval
  219. elementStore:elementStore];
  220. xmlXPathFreeObject(queryResult);
  221. xmlFreeTextWriter(writer);
  222. xmlFreeDoc(doc);
  223. if (nil == matchingSnapshots) {
  224. return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
  225. }
  226. return matchingSnapshots;
  227. }
  228. + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet
  229. elementStore:(NSMutableDictionary *)elementStore
  230. {
  231. if (xmlXPathNodeSetIsEmpty(nodeSet)) {
  232. return @[];
  233. }
  234. NSMutableArray *matchingSnapshots = [NSMutableArray array];
  235. const xmlChar *indexPathKeyName = (xmlChar *)[kXMLIndexPathKey UTF8String];
  236. for (NSInteger i = 0; i < nodeSet->nodeNr; i++) {
  237. xmlNodePtr currentNode = nodeSet->nodeTab[i];
  238. xmlChar *attrValue = xmlGetProp(currentNode, indexPathKeyName);
  239. if (NULL == attrValue) {
  240. [FBLogger log:@"Failed to invoke libxml2>xmlGetProp"];
  241. return nil;
  242. }
  243. id<FBXCElementSnapshot> element = [elementStore objectForKey:(id)[NSString stringWithCString:(const char *)attrValue encoding:NSUTF8StringEncoding]];
  244. if (element) {
  245. [matchingSnapshots addObject:element];
  246. }
  247. xmlFree(attrValue);
  248. }
  249. return matchingSnapshots.copy;
  250. }
  251. + (nullable xmlXPathObjectPtr)matchNodeInDocument:(xmlDocPtr)doc
  252. elementStore:(NSDictionary<NSString *, id<FBXCElementSnapshot>> *)elementStore
  253. forSnapshot:(nullable id<FBXCElementSnapshot>)snapshot
  254. {
  255. if (nil == snapshot) {
  256. return NULL;
  257. }
  258. NSString *contextRootUid = [FBElementUtils uidWithAccessibilityElement:[(id)snapshot accessibilityElement]];
  259. if (nil == contextRootUid) {
  260. return NULL;
  261. }
  262. for (NSString *key in elementStore) {
  263. id<FBXCElementSnapshot> value = [elementStore objectForKey:key];
  264. NSString *snapshotUid = [FBElementUtils uidWithAccessibilityElement:[value accessibilityElement]];
  265. if (nil == snapshotUid || ![snapshotUid isEqualToString:contextRootUid]) {
  266. continue;
  267. }
  268. NSString *indexQuery = [NSString stringWithFormat:@"//*[@%@=\"%@\"]", kXMLIndexPathKey, key];
  269. xmlXPathObjectPtr queryResult = [self evaluate:indexQuery
  270. document:doc
  271. contextNode:NULL];
  272. if (NULL != queryResult) {
  273. return queryResult;
  274. }
  275. }
  276. return NULL;
  277. }
  278. + (NSSet<Class> *)elementAttributesWithXPathQuery:(NSString *)query
  279. {
  280. if ([query rangeOfString:@"[^\\w@]@\\*[^\\w]" options:NSRegularExpressionSearch].location != NSNotFound) {
  281. // read all element attributes if 'star' attribute name pattern is used in xpath query
  282. return [NSSet setWithArray:FBElementAttribute.supportedAttributes];
  283. }
  284. NSMutableSet<Class> *result = [NSMutableSet set];
  285. for (Class attributeCls in FBElementAttribute.supportedAttributes) {
  286. if ([query rangeOfString:[NSString stringWithFormat:@"[^\\w@]@%@[^\\w]", [attributeCls name]] options:NSRegularExpressionSearch].location != NSNotFound) {
  287. [result addObject:attributeCls];
  288. }
  289. }
  290. return result.copy;
  291. }
  292. + (int)xmlRepresentationWithRootElement:(id<FBXCElementSnapshot>)root
  293. writer:(xmlTextWriterPtr)writer
  294. elementStore:(nullable NSMutableDictionary *)elementStore
  295. query:(nullable NSString*)query
  296. excludingAttributes:(nullable NSArray<NSString *> *)excludedAttributes
  297. {
  298. // Trying to be smart here and only including attributes, that were asked in the query, to the resulting document.
  299. // This may speed up the lookup significantly in some cases
  300. NSMutableSet<Class> *includedAttributes;
  301. if (nil == query) {
  302. includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes];
  303. if (!FBConfiguration.includeHittableInPageSource) {
  304. // The hittable attribute is expensive to calculate for each snapshot item
  305. // thus we only include it when requested explicitly
  306. [includedAttributes removeObject:FBHittableAttribute.class];
  307. }
  308. if (!FBConfiguration.includeNativeFrameInPageSource) {
  309. // Include nativeFrame only when requested
  310. [includedAttributes removeObject:FBNativeFrameAttribute.class];
  311. }
  312. if (!FBConfiguration.includeMinMaxValueInPageSource) {
  313. // minValue/maxValue are retrieved from private APIs and may be slow on deep trees
  314. [includedAttributes removeObject:FBMinValueAttribute.class];
  315. [includedAttributes removeObject:FBMaxValueAttribute.class];
  316. }
  317. if (nil != excludedAttributes) {
  318. for (NSString *excludedAttributeName in excludedAttributes) {
  319. for (Class supportedAttribute in FBElementAttribute.supportedAttributes) {
  320. if ([[supportedAttribute name] caseInsensitiveCompare:excludedAttributeName] == NSOrderedSame) {
  321. [includedAttributes removeObject:supportedAttribute];
  322. break;
  323. }
  324. }
  325. }
  326. }
  327. } else {
  328. includedAttributes = [self.class elementAttributesWithXPathQuery:query].mutableCopy;
  329. }
  330. [FBLogger logFmt:@"The following attributes were requested to be included into the XML: %@", includedAttributes];
  331. int rc = [self writeXmlWithRootElement:root
  332. indexPath:(elementStore != nil ? topNodeIndexPath : nil)
  333. elementStore:elementStore
  334. includedAttributes:includedAttributes.copy
  335. writer:writer];
  336. if (rc < 0) {
  337. [FBLogger log:@"Failed to generate XML presentation of a screen element"];
  338. return rc;
  339. }
  340. return 0;
  341. }
  342. + (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
  343. document:(xmlDocPtr)doc
  344. contextNode:(nullable xmlNodePtr)contextNode
  345. {
  346. xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
  347. if (NULL == xpathCtx) {
  348. [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery];
  349. return NULL;
  350. }
  351. xpathCtx->node = NULL == contextNode ? doc->children : contextNode;
  352. xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx);
  353. if (NULL == xpathObj) {
  354. xmlXPathFreeContext(xpathCtx);
  355. [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery];
  356. return NULL;
  357. }
  358. xmlXPathFreeContext(xpathCtx);
  359. return xpathObj;
  360. }
  361. + (nullable NSString *)safeXmlStringWithString:(NSString *)str
  362. {
  363. return [str fb_xmlSafeStringWithReplacement:@""];
  364. }
  365. + (int)recordElementAttributes:(xmlTextWriterPtr)writer
  366. forElement:(id<FBXCElementSnapshot>)element
  367. indexPath:(nullable NSString *)indexPath
  368. includedAttributes:(nullable NSSet<Class> *)includedAttributes
  369. {
  370. for (Class attributeCls in FBElementAttribute.supportedAttributes) {
  371. // include all supported attributes by default unless enumerated explicitly
  372. if (includedAttributes && ![includedAttributes containsObject:attributeCls]) {
  373. continue;
  374. }
  375. // Text-input placeholder (only for elements that support inner text)
  376. if ((attributeCls == FBPlaceholderValueAttribute.class) &&
  377. !FBDoesElementSupportInnerText(element.elementType)) {
  378. continue;
  379. }
  380. // Only for elements that support min/max value
  381. if ((attributeCls == FBMinValueAttribute.class || attributeCls == FBMaxValueAttribute.class) &&
  382. !FBDoesElementSupportMinMaxValue(element.elementType)) {
  383. continue;
  384. }
  385. int rc = [attributeCls recordWithWriter:writer
  386. forElement:[FBXCElementSnapshotWrapper ensureWrapped:element]];
  387. if (rc < 0) {
  388. return rc;
  389. }
  390. }
  391. if (nil != indexPath) {
  392. // index path is the special case
  393. return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath];
  394. }
  395. return 0;
  396. }
  397. + (int)writeXmlWithRootElement:(id<FBXCElementSnapshot>)root
  398. indexPath:(nullable NSString *)indexPath
  399. elementStore:(nullable NSMutableDictionary *)elementStore
  400. includedAttributes:(nullable NSSet<Class> *)includedAttributes
  401. writer:(xmlTextWriterPtr)writer
  402. {
  403. NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil);
  404. NSArray<id<FBXCElementSnapshot>> *children = root.children;
  405. if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) {
  406. [elementStore setObject:root forKey:topNodeIndexPath];
  407. }
  408. FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:root];
  409. int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]);
  410. if (rc < 0) {
  411. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc];
  412. return rc;
  413. }
  414. rc = [self recordElementAttributes:writer
  415. forElement:root
  416. indexPath:indexPath
  417. includedAttributes:includedAttributes];
  418. if (rc < 0) {
  419. return rc;
  420. }
  421. for (NSUInteger i = 0; i < [children count]; i++) {
  422. @autoreleasepool {
  423. id<FBXCElementSnapshot> childSnapshot = [children objectAtIndex:i];
  424. NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil;
  425. if (elementStore != nil && newIndexPath != nil) {
  426. [elementStore setObject:childSnapshot forKey:(id)newIndexPath];
  427. }
  428. rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot]
  429. indexPath:newIndexPath
  430. elementStore:elementStore
  431. includedAttributes:includedAttributes
  432. writer:writer];
  433. if (rc < 0) {
  434. return rc;
  435. }
  436. }
  437. }
  438. rc = xmlTextWriterEndElement(writer);
  439. if (rc < 0) {
  440. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
  441. return rc;
  442. }
  443. return 0;
  444. }
  445. + (id<FBXCElementSnapshot>)snapshotWithRoot:(id<FBElement>)root
  446. useNative:(BOOL)useNative
  447. {
  448. if (![root isKindOfClass:XCUIElement.class]) {
  449. return (id<FBXCElementSnapshot>)root;
  450. }
  451. if (useNative) {
  452. return [(XCUIElement *)root fb_nativeSnapshot];
  453. }
  454. return [root isKindOfClass:XCUIApplication.class]
  455. ? [(XCUIElement *)root fb_standardSnapshot]
  456. : [(XCUIElement *)root fb_customSnapshot];
  457. }
  458. + (void)waitUntilStableWithElement:(id<FBElement>)root
  459. {
  460. if ([root isKindOfClass:XCUIElement.class]) {
  461. // If the app is not idle state while we retrieve the visiblity state
  462. // then the snapshot retrieval operation might freeze and time out
  463. [[(XCUIElement *)root application] fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
  464. }
  465. }
  466. @end
  467. static NSString *const FBAbstractMethodInvocationException = @"AbstractMethodInvocationException";
  468. @implementation FBElementAttribute
  469. - (instancetype)initWithElement:(id<FBElement>)element
  470. {
  471. self = [super init];
  472. if (self) {
  473. _element = element;
  474. }
  475. return self;
  476. }
  477. + (NSString *)name
  478. {
  479. NSString *errMsg = [NSString stringWithFormat:@"The abstract method +(NSString *)name is expected to be overriden by %@", NSStringFromClass(self.class)];
  480. @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
  481. }
  482. + (NSString *)valueForElement:(id<FBElement>)element
  483. {
  484. NSString *errMsg = [NSString stringWithFormat:@"The abstract method -(NSString *)value is expected to be overriden by %@", NSStringFromClass(self.class)];
  485. @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
  486. }
  487. + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element
  488. {
  489. NSString *value = [self valueForElement:element];
  490. if (nil == value) {
  491. // Skip the attribute if the value equals to nil
  492. return 0;
  493. }
  494. int rc = xmlTextWriterWriteAttribute(writer,
  495. (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String],
  496. (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]);
  497. if (rc < 0) {
  498. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc];
  499. }
  500. return rc;
  501. }
  502. + (NSArray<Class> *)supportedAttributes
  503. {
  504. // The list of attributes to be written for each XML node
  505. // The enumeration order does matter here
  506. return @[FBTypeAttribute.class,
  507. FBValueAttribute.class,
  508. FBNameAttribute.class,
  509. FBLabelAttribute.class,
  510. FBEnabledAttribute.class,
  511. FBVisibleAttribute.class,
  512. FBAccessibleAttribute.class,
  513. #if TARGET_OS_TV
  514. FBFocusedAttribute.class,
  515. #endif
  516. FBXAttribute.class,
  517. FBYAttribute.class,
  518. FBWidthAttribute.class,
  519. FBHeightAttribute.class,
  520. FBIndexAttribute.class,
  521. FBHittableAttribute.class,
  522. FBPlaceholderValueAttribute.class,
  523. FBTraitsAttribute.class,
  524. FBNativeFrameAttribute.class,
  525. FBMinValueAttribute.class,
  526. FBMaxValueAttribute.class,
  527. ];
  528. }
  529. @end
  530. @implementation FBTypeAttribute
  531. + (NSString *)name
  532. {
  533. return @"type";
  534. }
  535. + (NSString *)valueForElement:(id<FBElement>)element
  536. {
  537. return element.wdType;
  538. }
  539. @end
  540. @implementation FBValueAttribute
  541. + (NSString *)name
  542. {
  543. return @"value";
  544. }
  545. + (NSString *)valueForElement:(id<FBElement>)element
  546. {
  547. id idValue = element.wdValue;
  548. if ([idValue isKindOfClass:[NSValue class]]) {
  549. return [idValue stringValue];
  550. } else if ([idValue isKindOfClass:[NSString class]]) {
  551. return idValue;
  552. }
  553. return [idValue description];
  554. }
  555. @end
  556. @implementation FBNameAttribute
  557. + (NSString *)name
  558. {
  559. return @"name";
  560. }
  561. + (NSString *)valueForElement:(id<FBElement>)element
  562. {
  563. return element.wdName;
  564. }
  565. @end
  566. @implementation FBLabelAttribute
  567. + (NSString *)name
  568. {
  569. return @"label";
  570. }
  571. + (NSString *)valueForElement:(id<FBElement>)element
  572. {
  573. return element.wdLabel;
  574. }
  575. @end
  576. @implementation FBEnabledAttribute
  577. + (NSString *)name
  578. {
  579. return @"enabled";
  580. }
  581. + (NSString *)valueForElement:(id<FBElement>)element
  582. {
  583. return FBBoolToString(element.wdEnabled);
  584. }
  585. @end
  586. @implementation FBVisibleAttribute
  587. + (NSString *)name
  588. {
  589. return @"visible";
  590. }
  591. + (NSString *)valueForElement:(id<FBElement>)element
  592. {
  593. return FBBoolToString(element.wdVisible);
  594. }
  595. @end
  596. @implementation FBAccessibleAttribute
  597. + (NSString *)name
  598. {
  599. return @"accessible";
  600. }
  601. + (NSString *)valueForElement:(id<FBElement>)element
  602. {
  603. return FBBoolToString(element.wdAccessible);
  604. }
  605. @end
  606. #if TARGET_OS_TV
  607. @implementation FBFocusedAttribute
  608. + (NSString *)name
  609. {
  610. return @"focused";
  611. }
  612. + (NSString *)valueForElement:(id<FBElement>)element
  613. {
  614. return FBBoolToString(element.wdFocused);
  615. }
  616. @end
  617. #endif
  618. @implementation FBDimensionAttribute
  619. + (NSString *)valueForElement:(id<FBElement>)element
  620. {
  621. return [NSString stringWithFormat:@"%@", [element.wdRect objectForKey:[self name]]];
  622. }
  623. @end
  624. @implementation FBXAttribute
  625. + (NSString *)name
  626. {
  627. return @"x";
  628. }
  629. @end
  630. @implementation FBYAttribute
  631. + (NSString *)name
  632. {
  633. return @"y";
  634. }
  635. @end
  636. @implementation FBWidthAttribute
  637. + (NSString *)name
  638. {
  639. return @"width";
  640. }
  641. @end
  642. @implementation FBHeightAttribute
  643. + (NSString *)name
  644. {
  645. return @"height";
  646. }
  647. @end
  648. @implementation FBIndexAttribute
  649. + (NSString *)name
  650. {
  651. return @"index";
  652. }
  653. + (NSString *)valueForElement:(id<FBElement>)element
  654. {
  655. return [NSString stringWithFormat:@"%lu", element.wdIndex];
  656. }
  657. @end
  658. @implementation FBHittableAttribute
  659. + (NSString *)name
  660. {
  661. return @"hittable";
  662. }
  663. + (NSString *)valueForElement:(id<FBElement>)element
  664. {
  665. return FBBoolToString(element.wdHittable);
  666. }
  667. @end
  668. @implementation FBInternalIndexAttribute
  669. + (NSString *)name
  670. {
  671. return kXMLIndexPathKey;
  672. }
  673. + (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value
  674. {
  675. if (nil == value) {
  676. // Skip the attribute if the value equals to nil
  677. return 0;
  678. }
  679. int rc = xmlTextWriterWriteAttribute(writer,
  680. (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String],
  681. (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]);
  682. if (rc < 0) {
  683. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc];
  684. }
  685. return rc;
  686. }
  687. @end
  688. @implementation FBPlaceholderValueAttribute
  689. + (NSString *)name
  690. {
  691. return @"placeholderValue";
  692. }
  693. + (NSString *)valueForElement:(id<FBElement>)element
  694. {
  695. return element.wdPlaceholderValue;
  696. }
  697. @end
  698. @implementation FBNativeFrameAttribute
  699. + (NSString *)name
  700. {
  701. return @"nativeFrame";
  702. }
  703. + (NSString *)valueForElement:(id<FBElement>)element
  704. {
  705. return NSStringFromCGRect(element.wdNativeFrame);
  706. }
  707. @end
  708. @implementation FBTraitsAttribute
  709. + (NSString *)name
  710. {
  711. return @"traits";
  712. }
  713. + (NSString *)valueForElement:(id<FBElement>)element
  714. {
  715. return element.wdTraits;
  716. }
  717. @end
  718. @implementation FBMinValueAttribute
  719. + (NSString *)name
  720. {
  721. return @"minValue";
  722. }
  723. + (NSString *)valueForElement:(id<FBElement>)element
  724. {
  725. return [element.wdMinValue stringValue];
  726. }
  727. @end
  728. @implementation FBMaxValueAttribute
  729. + (NSString *)name
  730. {
  731. return @"maxValue";
  732. }
  733. + (NSString *)valueForElement:(id<FBElement>)element
  734. {
  735. return [element.wdMaxValue stringValue];
  736. }
  737. @end