FBXPath.m 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  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 "FBLogger.h"
  13. #import "FBMacros.h"
  14. #import "FBXMLGenerationOptions.h"
  15. #import "FBXCElementSnapshotWrapper+Helpers.h"
  16. #import "NSString+FBXMLSafeString.h"
  17. #import "XCUIElement.h"
  18. #import "XCUIElement+FBCaching.h"
  19. #import "XCUIElement+FBUtilities.h"
  20. #import "XCUIElement+FBWebDriverAttributes.h"
  21. #import "XCTestPrivateSymbols.h"
  22. @interface FBElementAttribute : NSObject
  23. @property (nonatomic, readonly) id<FBElement> element;
  24. + (nonnull NSString *)name;
  25. + (nullable NSString *)valueForElement:(id<FBElement>)element;
  26. + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element;
  27. + (NSArray<Class> *)supportedAttributes;
  28. @end
  29. @interface FBTypeAttribute : FBElementAttribute
  30. @end
  31. @interface FBValueAttribute : FBElementAttribute
  32. @end
  33. @interface FBNameAttribute : FBElementAttribute
  34. @end
  35. @interface FBLabelAttribute : FBElementAttribute
  36. @end
  37. @interface FBEnabledAttribute : FBElementAttribute
  38. @end
  39. @interface FBVisibleAttribute : FBElementAttribute
  40. @end
  41. @interface FBAccessibleAttribute : FBElementAttribute
  42. @end
  43. @interface FBDimensionAttribute : FBElementAttribute
  44. @end
  45. @interface FBXAttribute : FBDimensionAttribute
  46. @end
  47. @interface FBYAttribute : FBDimensionAttribute
  48. @end
  49. @interface FBWidthAttribute : FBDimensionAttribute
  50. @end
  51. @interface FBHeightAttribute : FBDimensionAttribute
  52. @end
  53. @interface FBIndexAttribute : FBElementAttribute
  54. @end
  55. @interface FBHittableAttribute : FBElementAttribute
  56. @end
  57. @interface FBInternalIndexAttribute : FBElementAttribute
  58. @property (nonatomic, nonnull, readonly) NSString* indexValue;
  59. + (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value;
  60. @end
  61. #if TARGET_OS_TV
  62. @interface FBFocusedAttribute : FBElementAttribute
  63. @end
  64. #endif
  65. const static char *_UTF8Encoding = "UTF-8";
  66. static NSString *const kXMLIndexPathKey = @"private_indexPath";
  67. static NSString *const topNodeIndexPath = @"top";
  68. @implementation FBXPath
  69. + (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery
  70. {
  71. NSString *reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery];
  72. @throw [NSException exceptionWithName:name reason:reason userInfo:@{}];
  73. return nil;
  74. }
  75. + (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
  76. options:(nullable FBXMLGenerationOptions *)options
  77. {
  78. xmlDocPtr doc;
  79. xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
  80. int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
  81. if (rc < 0) {
  82. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
  83. } else {
  84. BOOL hasScope = nil != options.scope && [options.scope length] > 0;
  85. if (hasScope) {
  86. rc = xmlTextWriterStartElement(writer,
  87. (xmlChar *)[[self safeXmlStringWithString:options.scope] UTF8String]);
  88. if (rc < 0) {
  89. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", options.scope, rc];
  90. }
  91. }
  92. if (rc >= 0) {
  93. rc = [self xmlRepresentationWithRootElement:root
  94. writer:writer
  95. elementStore:nil
  96. query:nil
  97. excludingAttributes:options.excludedAttributes];
  98. }
  99. if (rc >= 0 && hasScope) {
  100. rc = xmlTextWriterEndElement(writer);
  101. if (rc < 0) {
  102. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
  103. }
  104. }
  105. if (rc >= 0) {
  106. rc = xmlTextWriterEndDocument(writer);
  107. if (rc < 0) {
  108. [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc];
  109. }
  110. }
  111. }
  112. if (rc < 0) {
  113. xmlFreeTextWriter(writer);
  114. xmlFreeDoc(doc);
  115. return nil;
  116. }
  117. int buffersize;
  118. xmlChar *xmlbuff;
  119. xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1);
  120. xmlFreeTextWriter(writer);
  121. xmlFreeDoc(doc);
  122. NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding];
  123. xmlFree(xmlbuff);
  124. return result;
  125. }
  126. + (NSArray<id<FBXCElementSnapshot>> *)matchesWithRootElement:(id<FBElement>)root
  127. forQuery:(NSString *)xpathQuery
  128. {
  129. xmlDocPtr doc;
  130. xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
  131. if (NULL == writer) {
  132. [FBLogger logFmt:@"Failed to invoke libxml2>xmlNewTextWriterDoc for XPath query \"%@\"", xpathQuery];
  133. return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
  134. }
  135. NSMutableDictionary *elementStore = [NSMutableDictionary dictionary];
  136. int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
  137. if (rc < 0) {
  138. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
  139. } else {
  140. rc = [self xmlRepresentationWithRootElement:root
  141. writer:writer
  142. elementStore:elementStore
  143. query:xpathQuery
  144. excludingAttributes:nil];
  145. if (rc >= 0) {
  146. rc = xmlTextWriterEndDocument(writer);
  147. if (rc < 0) {
  148. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndDocument. Error code: %d", rc];
  149. }
  150. }
  151. }
  152. if (rc < 0) {
  153. xmlFreeTextWriter(writer);
  154. xmlFreeDoc(doc);
  155. return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
  156. }
  157. xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery document:doc];
  158. if (NULL == queryResult) {
  159. xmlFreeTextWriter(writer);
  160. xmlFreeDoc(doc);
  161. return [self throwException:FBInvalidXPathException forQuery:xpathQuery];
  162. }
  163. NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval
  164. elementStore:elementStore];
  165. xmlXPathFreeObject(queryResult);
  166. xmlFreeTextWriter(writer);
  167. xmlFreeDoc(doc);
  168. if (nil == matchingSnapshots) {
  169. return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
  170. }
  171. return matchingSnapshots;
  172. }
  173. + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet
  174. elementStore:(NSMutableDictionary *)elementStore
  175. {
  176. if (xmlXPathNodeSetIsEmpty(nodeSet)) {
  177. return @[];
  178. }
  179. NSMutableArray *matchingSnapshots = [NSMutableArray array];
  180. const xmlChar *indexPathKeyName = (xmlChar *)[kXMLIndexPathKey UTF8String];
  181. for (NSInteger i = 0; i < nodeSet->nodeNr; i++) {
  182. xmlNodePtr currentNode = nodeSet->nodeTab[i];
  183. xmlChar *attrValue = xmlGetProp(currentNode, indexPathKeyName);
  184. if (NULL == attrValue) {
  185. [FBLogger log:@"Failed to invoke libxml2>xmlGetProp"];
  186. return nil;
  187. }
  188. id<FBXCElementSnapshot> element = [elementStore objectForKey:(id)[NSString stringWithCString:(const char *)attrValue encoding:NSUTF8StringEncoding]];
  189. if (element) {
  190. [matchingSnapshots addObject:element];
  191. }
  192. xmlFree(attrValue);
  193. }
  194. return matchingSnapshots.copy;
  195. }
  196. + (NSSet<Class> *)elementAttributesWithXPathQuery:(NSString *)query
  197. {
  198. if ([query rangeOfString:@"[^\\w@]@\\*[^\\w]" options:NSRegularExpressionSearch].location != NSNotFound) {
  199. // read all element attributes if 'star' attribute name pattern is used in xpath query
  200. return [NSSet setWithArray:FBElementAttribute.supportedAttributes];
  201. }
  202. NSMutableSet<Class> *result = [NSMutableSet set];
  203. for (Class attributeCls in FBElementAttribute.supportedAttributes) {
  204. if ([query rangeOfString:[NSString stringWithFormat:@"[^\\w@]@%@[^\\w]", [attributeCls name]] options:NSRegularExpressionSearch].location != NSNotFound) {
  205. [result addObject:attributeCls];
  206. }
  207. }
  208. return result.copy;
  209. }
  210. + (int)xmlRepresentationWithRootElement:(id<FBElement>)root
  211. writer:(xmlTextWriterPtr)writer
  212. elementStore:(nullable NSMutableDictionary *)elementStore
  213. query:(nullable NSString*)query
  214. excludingAttributes:(nullable NSArray<NSString *> *)excludedAttributes
  215. {
  216. // Trying to be smart here and only including attributes, that were asked in the query, to the resulting document.
  217. // This may speed up the lookup significantly in some cases
  218. NSMutableSet<Class> *includedAttributes;
  219. if (nil == query) {
  220. includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes];
  221. // The hittable attribute is expensive to calculate for each snapshot item
  222. // thus we only include it when requested by an xPath query
  223. [includedAttributes removeObject:FBHittableAttribute.class];
  224. if (nil != excludedAttributes) {
  225. for (NSString *excludedAttributeName in excludedAttributes) {
  226. for (Class supportedAttribute in FBElementAttribute.supportedAttributes) {
  227. if ([[supportedAttribute name] caseInsensitiveCompare:excludedAttributeName] == NSOrderedSame) {
  228. [includedAttributes removeObject:supportedAttribute];
  229. break;
  230. }
  231. }
  232. }
  233. }
  234. } else {
  235. includedAttributes = [self.class elementAttributesWithXPathQuery:query].mutableCopy;
  236. }
  237. [FBLogger logFmt:@"The following attributes were requested to be included into the XML: %@", includedAttributes];
  238. int rc = [self writeXmlWithRootElement:root
  239. indexPath:(elementStore != nil ? topNodeIndexPath : nil)
  240. elementStore:elementStore
  241. includedAttributes:includedAttributes.copy
  242. writer:writer];
  243. if (rc < 0) {
  244. [FBLogger log:@"Failed to generate XML presentation of a screen element"];
  245. return rc;
  246. }
  247. return 0;
  248. }
  249. + (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc
  250. {
  251. xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
  252. if (NULL == xpathCtx) {
  253. [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery];
  254. return NULL;
  255. }
  256. xpathCtx->node = doc->children;
  257. xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx);
  258. if (NULL == xpathObj) {
  259. xmlXPathFreeContext(xpathCtx);
  260. [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery];
  261. return NULL;
  262. }
  263. xmlXPathFreeContext(xpathCtx);
  264. return xpathObj;
  265. }
  266. + (nullable NSString *)safeXmlStringWithString:(NSString *)str
  267. {
  268. return [str fb_xmlSafeStringWithReplacement:@""];
  269. }
  270. + (int)recordElementAttributes:(xmlTextWriterPtr)writer
  271. forElement:(id<FBXCElementSnapshot>)element
  272. indexPath:(nullable NSString *)indexPath
  273. includedAttributes:(nullable NSSet<Class> *)includedAttributes
  274. {
  275. for (Class attributeCls in FBElementAttribute.supportedAttributes) {
  276. // include all supported attributes by default unless enumerated explicitly
  277. if (includedAttributes && ![includedAttributes containsObject:attributeCls]) {
  278. continue;
  279. }
  280. int rc = [attributeCls recordWithWriter:writer
  281. forElement:[FBXCElementSnapshotWrapper ensureWrapped:element]];
  282. if (rc < 0) {
  283. return rc;
  284. }
  285. }
  286. if (nil != indexPath) {
  287. // index path is the special case
  288. return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath];
  289. }
  290. return 0;
  291. }
  292. + (int)writeXmlWithRootElement:(id<FBElement>)root
  293. indexPath:(nullable NSString *)indexPath
  294. elementStore:(nullable NSMutableDictionary *)elementStore
  295. includedAttributes:(nullable NSSet<Class> *)includedAttributes
  296. writer:(xmlTextWriterPtr)writer
  297. {
  298. NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil);
  299. id<FBXCElementSnapshot> currentSnapshot;
  300. NSArray<id<FBXCElementSnapshot>> *children;
  301. if ([root isKindOfClass:XCUIElement.class]) {
  302. XCUIElement *element = (XCUIElement *)root;
  303. NSMutableArray<NSString *> *snapshotAttributes = [NSMutableArray arrayWithArray:FBStandardAttributeNames()];
  304. if (nil == includedAttributes || [includedAttributes containsObject:FBVisibleAttribute.class]) {
  305. [snapshotAttributes addObject:FB_XCAXAIsVisibleAttributeName];
  306. // If the app is not idle state while we retrieve the visiblity state
  307. // then the snapshot retrieval operation might freeze and time out
  308. [element.application fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
  309. }
  310. currentSnapshot = [element fb_snapshotWithAttributes:snapshotAttributes.copy
  311. maxDepth:nil];
  312. children = currentSnapshot.children;
  313. } else {
  314. currentSnapshot = (id<FBXCElementSnapshot>)root;
  315. children = currentSnapshot.children;
  316. }
  317. if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) {
  318. [elementStore setObject:currentSnapshot forKey:topNodeIndexPath];
  319. }
  320. FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:currentSnapshot];
  321. int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]);
  322. if (rc < 0) {
  323. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc];
  324. return rc;
  325. }
  326. rc = [self recordElementAttributes:writer
  327. forElement:currentSnapshot
  328. indexPath:indexPath
  329. includedAttributes:includedAttributes];
  330. if (rc < 0) {
  331. return rc;
  332. }
  333. for (NSUInteger i = 0; i < [children count]; i++) {
  334. id<FBXCElementSnapshot> childSnapshot = [children objectAtIndex:i];
  335. NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil;
  336. if (elementStore != nil && newIndexPath != nil) {
  337. [elementStore setObject:childSnapshot forKey:(id)newIndexPath];
  338. }
  339. rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot]
  340. indexPath:newIndexPath
  341. elementStore:elementStore
  342. includedAttributes:includedAttributes
  343. writer:writer];
  344. if (rc < 0) {
  345. return rc;
  346. }
  347. }
  348. rc = xmlTextWriterEndElement(writer);
  349. if (rc < 0) {
  350. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
  351. return rc;
  352. }
  353. return 0;
  354. }
  355. @end
  356. static NSString *const FBAbstractMethodInvocationException = @"AbstractMethodInvocationException";
  357. @implementation FBElementAttribute
  358. - (instancetype)initWithElement:(id<FBElement>)element
  359. {
  360. self = [super init];
  361. if (self) {
  362. _element = element;
  363. }
  364. return self;
  365. }
  366. + (NSString *)name
  367. {
  368. NSString *errMsg = [NSString stringWithFormat:@"The abstract method +(NSString *)name is expected to be overriden by %@", NSStringFromClass(self.class)];
  369. @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
  370. }
  371. + (NSString *)valueForElement:(id<FBElement>)element
  372. {
  373. NSString *errMsg = [NSString stringWithFormat:@"The abstract method -(NSString *)value is expected to be overriden by %@", NSStringFromClass(self.class)];
  374. @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
  375. }
  376. + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element
  377. {
  378. NSString *value = [self valueForElement:element];
  379. if (nil == value) {
  380. // Skip the attribute if the value equals to nil
  381. return 0;
  382. }
  383. int rc = xmlTextWriterWriteAttribute(writer,
  384. (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String],
  385. (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]);
  386. if (rc < 0) {
  387. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc];
  388. }
  389. return rc;
  390. }
  391. + (NSArray<Class> *)supportedAttributes
  392. {
  393. // The list of attributes to be written for each XML node
  394. // The enumeration order does matter here
  395. return @[FBTypeAttribute.class,
  396. FBValueAttribute.class,
  397. FBNameAttribute.class,
  398. FBLabelAttribute.class,
  399. FBEnabledAttribute.class,
  400. FBVisibleAttribute.class,
  401. FBAccessibleAttribute.class,
  402. #if TARGET_OS_TV
  403. FBFocusedAttribute.class,
  404. #endif
  405. FBXAttribute.class,
  406. FBYAttribute.class,
  407. FBWidthAttribute.class,
  408. FBHeightAttribute.class,
  409. FBIndexAttribute.class,
  410. FBHittableAttribute.class,
  411. ];
  412. }
  413. @end
  414. @implementation FBTypeAttribute
  415. + (NSString *)name
  416. {
  417. return @"type";
  418. }
  419. + (NSString *)valueForElement:(id<FBElement>)element
  420. {
  421. return element.wdType;
  422. }
  423. @end
  424. @implementation FBValueAttribute
  425. + (NSString *)name
  426. {
  427. return @"value";
  428. }
  429. + (NSString *)valueForElement:(id<FBElement>)element
  430. {
  431. id idValue = element.wdValue;
  432. if ([idValue isKindOfClass:[NSValue class]]) {
  433. return [idValue stringValue];
  434. } else if ([idValue isKindOfClass:[NSString class]]) {
  435. return idValue;
  436. }
  437. return [idValue description];
  438. }
  439. @end
  440. @implementation FBNameAttribute
  441. + (NSString *)name
  442. {
  443. return @"name";
  444. }
  445. + (NSString *)valueForElement:(id<FBElement>)element
  446. {
  447. return element.wdName;
  448. }
  449. @end
  450. @implementation FBLabelAttribute
  451. + (NSString *)name
  452. {
  453. return @"label";
  454. }
  455. + (NSString *)valueForElement:(id<FBElement>)element
  456. {
  457. return element.wdLabel;
  458. }
  459. @end
  460. @implementation FBEnabledAttribute
  461. + (NSString *)name
  462. {
  463. return @"enabled";
  464. }
  465. + (NSString *)valueForElement:(id<FBElement>)element
  466. {
  467. return FBBoolToString(element.wdEnabled);
  468. }
  469. @end
  470. @implementation FBVisibleAttribute
  471. + (NSString *)name
  472. {
  473. return @"visible";
  474. }
  475. + (NSString *)valueForElement:(id<FBElement>)element
  476. {
  477. return FBBoolToString(element.wdVisible);
  478. }
  479. @end
  480. @implementation FBAccessibleAttribute
  481. + (NSString *)name
  482. {
  483. return @"accessible";
  484. }
  485. + (NSString *)valueForElement:(id<FBElement>)element
  486. {
  487. return FBBoolToString(element.wdAccessible);
  488. }
  489. @end
  490. #if TARGET_OS_TV
  491. @implementation FBFocusedAttribute
  492. + (NSString *)name
  493. {
  494. return @"focused";
  495. }
  496. + (NSString *)valueForElement:(id<FBElement>)element
  497. {
  498. return FBBoolToString(element.wdFocused);
  499. }
  500. @end
  501. #endif
  502. @implementation FBDimensionAttribute
  503. + (NSString *)valueForElement:(id<FBElement>)element
  504. {
  505. return [NSString stringWithFormat:@"%@", [element.wdRect objectForKey:[self name]]];
  506. }
  507. @end
  508. @implementation FBXAttribute
  509. + (NSString *)name
  510. {
  511. return @"x";
  512. }
  513. @end
  514. @implementation FBYAttribute
  515. + (NSString *)name
  516. {
  517. return @"y";
  518. }
  519. @end
  520. @implementation FBWidthAttribute
  521. + (NSString *)name
  522. {
  523. return @"width";
  524. }
  525. @end
  526. @implementation FBHeightAttribute
  527. + (NSString *)name
  528. {
  529. return @"height";
  530. }
  531. @end
  532. @implementation FBIndexAttribute
  533. + (NSString *)name
  534. {
  535. return @"index";
  536. }
  537. + (NSString *)valueForElement:(id<FBElement>)element
  538. {
  539. return [NSString stringWithFormat:@"%lu", element.wdIndex];
  540. }
  541. @end
  542. @implementation FBHittableAttribute
  543. + (NSString *)name
  544. {
  545. return @"hittable";
  546. }
  547. + (NSString *)valueForElement:(id<FBElement>)element
  548. {
  549. return FBBoolToString(element.wdHittable);
  550. }
  551. @end
  552. @implementation FBInternalIndexAttribute
  553. + (NSString *)name
  554. {
  555. return kXMLIndexPathKey;
  556. }
  557. + (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value
  558. {
  559. if (nil == value) {
  560. // Skip the attribute if the value equals to nil
  561. return 0;
  562. }
  563. int rc = xmlTextWriterWriteAttribute(writer,
  564. (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String],
  565. (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]);
  566. if (rc < 0) {
  567. [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc];
  568. }
  569. return rc;
  570. }
  571. @end