| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714 |
- /**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
- #import "FBXPath.h"
- #import "FBConfiguration.h"
- #import "FBExceptions.h"
- #import "FBLogger.h"
- #import "FBMacros.h"
- #import "FBXMLGenerationOptions.h"
- #import "FBXCElementSnapshotWrapper+Helpers.h"
- #import "NSString+FBXMLSafeString.h"
- #import "XCUIElement.h"
- #import "XCUIElement+FBCaching.h"
- #import "XCUIElement+FBUtilities.h"
- #import "XCUIElement+FBWebDriverAttributes.h"
- #import "XCTestPrivateSymbols.h"
- @interface FBElementAttribute : NSObject
- @property (nonatomic, readonly) id<FBElement> element;
- + (nonnull NSString *)name;
- + (nullable NSString *)valueForElement:(id<FBElement>)element;
- + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element;
- + (NSArray<Class> *)supportedAttributes;
- @end
- @interface FBTypeAttribute : FBElementAttribute
- @end
- @interface FBValueAttribute : FBElementAttribute
- @end
- @interface FBNameAttribute : FBElementAttribute
- @end
- @interface FBLabelAttribute : FBElementAttribute
- @end
- @interface FBEnabledAttribute : FBElementAttribute
- @end
- @interface FBVisibleAttribute : FBElementAttribute
- @end
- @interface FBAccessibleAttribute : FBElementAttribute
- @end
- @interface FBDimensionAttribute : FBElementAttribute
- @end
- @interface FBXAttribute : FBDimensionAttribute
- @end
- @interface FBYAttribute : FBDimensionAttribute
- @end
- @interface FBWidthAttribute : FBDimensionAttribute
- @end
- @interface FBHeightAttribute : FBDimensionAttribute
- @end
- @interface FBIndexAttribute : FBElementAttribute
- @end
- @interface FBHittableAttribute : FBElementAttribute
- @end
- @interface FBInternalIndexAttribute : FBElementAttribute
- @property (nonatomic, nonnull, readonly) NSString* indexValue;
- + (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value;
- @end
- #if TARGET_OS_TV
- @interface FBFocusedAttribute : FBElementAttribute
- @end
- #endif
- const static char *_UTF8Encoding = "UTF-8";
- static NSString *const kXMLIndexPathKey = @"private_indexPath";
- static NSString *const topNodeIndexPath = @"top";
- @implementation FBXPath
- + (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery
- {
- NSString *reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery];
- @throw [NSException exceptionWithName:name reason:reason userInfo:@{}];
- return nil;
- }
- + (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
- options:(nullable FBXMLGenerationOptions *)options
- {
- xmlDocPtr doc;
- xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
- int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
- } else {
- BOOL hasScope = nil != options.scope && [options.scope length] > 0;
- if (hasScope) {
- rc = xmlTextWriterStartElement(writer,
- (xmlChar *)[[self safeXmlStringWithString:options.scope] UTF8String]);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", options.scope, rc];
- }
- }
- if (rc >= 0) {
- rc = [self xmlRepresentationWithRootElement:root
- writer:writer
- elementStore:nil
- query:nil
- excludingAttributes:options.excludedAttributes];
- }
- if (rc >= 0 && hasScope) {
- rc = xmlTextWriterEndElement(writer);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
- }
- }
- if (rc >= 0) {
- rc = xmlTextWriterEndDocument(writer);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc];
- }
- }
- }
- if (rc < 0) {
- xmlFreeTextWriter(writer);
- xmlFreeDoc(doc);
- return nil;
- }
- int buffersize;
- xmlChar *xmlbuff;
- xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1);
- xmlFreeTextWriter(writer);
- xmlFreeDoc(doc);
- NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding];
- xmlFree(xmlbuff);
- return result;
- }
- + (NSArray<id<FBXCElementSnapshot>> *)matchesWithRootElement:(id<FBElement>)root
- forQuery:(NSString *)xpathQuery
- {
- xmlDocPtr doc;
- xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
- if (NULL == writer) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlNewTextWriterDoc for XPath query \"%@\"", xpathQuery];
- return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
- }
- NSMutableDictionary *elementStore = [NSMutableDictionary dictionary];
- int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
- } else {
- rc = [self xmlRepresentationWithRootElement:root
- writer:writer
- elementStore:elementStore
- query:xpathQuery
- excludingAttributes:nil];
- if (rc >= 0) {
- rc = xmlTextWriterEndDocument(writer);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndDocument. Error code: %d", rc];
- }
- }
- }
- if (rc < 0) {
- xmlFreeTextWriter(writer);
- xmlFreeDoc(doc);
- return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
- }
- xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery document:doc];
- if (NULL == queryResult) {
- xmlFreeTextWriter(writer);
- xmlFreeDoc(doc);
- return [self throwException:FBInvalidXPathException forQuery:xpathQuery];
- }
- NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval
- elementStore:elementStore];
- xmlXPathFreeObject(queryResult);
- xmlFreeTextWriter(writer);
- xmlFreeDoc(doc);
- if (nil == matchingSnapshots) {
- return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
- }
- return matchingSnapshots;
- }
- + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet
- elementStore:(NSMutableDictionary *)elementStore
- {
- if (xmlXPathNodeSetIsEmpty(nodeSet)) {
- return @[];
- }
- NSMutableArray *matchingSnapshots = [NSMutableArray array];
- const xmlChar *indexPathKeyName = (xmlChar *)[kXMLIndexPathKey UTF8String];
- for (NSInteger i = 0; i < nodeSet->nodeNr; i++) {
- xmlNodePtr currentNode = nodeSet->nodeTab[i];
- xmlChar *attrValue = xmlGetProp(currentNode, indexPathKeyName);
- if (NULL == attrValue) {
- [FBLogger log:@"Failed to invoke libxml2>xmlGetProp"];
- return nil;
- }
- id<FBXCElementSnapshot> element = [elementStore objectForKey:(id)[NSString stringWithCString:(const char *)attrValue encoding:NSUTF8StringEncoding]];
- if (element) {
- [matchingSnapshots addObject:element];
- }
- xmlFree(attrValue);
- }
- return matchingSnapshots.copy;
- }
- + (NSSet<Class> *)elementAttributesWithXPathQuery:(NSString *)query
- {
- if ([query rangeOfString:@"[^\\w@]@\\*[^\\w]" options:NSRegularExpressionSearch].location != NSNotFound) {
- // read all element attributes if 'star' attribute name pattern is used in xpath query
- return [NSSet setWithArray:FBElementAttribute.supportedAttributes];
- }
- NSMutableSet<Class> *result = [NSMutableSet set];
- for (Class attributeCls in FBElementAttribute.supportedAttributes) {
- if ([query rangeOfString:[NSString stringWithFormat:@"[^\\w@]@%@[^\\w]", [attributeCls name]] options:NSRegularExpressionSearch].location != NSNotFound) {
- [result addObject:attributeCls];
- }
- }
- return result.copy;
- }
- + (int)xmlRepresentationWithRootElement:(id<FBElement>)root
- writer:(xmlTextWriterPtr)writer
- elementStore:(nullable NSMutableDictionary *)elementStore
- query:(nullable NSString*)query
- excludingAttributes:(nullable NSArray<NSString *> *)excludedAttributes
- {
- // Trying to be smart here and only including attributes, that were asked in the query, to the resulting document.
- // This may speed up the lookup significantly in some cases
- NSMutableSet<Class> *includedAttributes;
- if (nil == query) {
- includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes];
- // The hittable attribute is expensive to calculate for each snapshot item
- // thus we only include it when requested by an xPath query
- [includedAttributes removeObject:FBHittableAttribute.class];
- if (nil != excludedAttributes) {
- for (NSString *excludedAttributeName in excludedAttributes) {
- for (Class supportedAttribute in FBElementAttribute.supportedAttributes) {
- if ([[supportedAttribute name] caseInsensitiveCompare:excludedAttributeName] == NSOrderedSame) {
- [includedAttributes removeObject:supportedAttribute];
- break;
- }
- }
- }
- }
- } else {
- includedAttributes = [self.class elementAttributesWithXPathQuery:query].mutableCopy;
- }
- [FBLogger logFmt:@"The following attributes were requested to be included into the XML: %@", includedAttributes];
- int rc = [self writeXmlWithRootElement:root
- indexPath:(elementStore != nil ? topNodeIndexPath : nil)
- elementStore:elementStore
- includedAttributes:includedAttributes.copy
- writer:writer];
- if (rc < 0) {
- [FBLogger log:@"Failed to generate XML presentation of a screen element"];
- return rc;
- }
- return 0;
- }
- + (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc
- {
- xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
- if (NULL == xpathCtx) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery];
- return NULL;
- }
- xpathCtx->node = doc->children;
- xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx);
- if (NULL == xpathObj) {
- xmlXPathFreeContext(xpathCtx);
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery];
- return NULL;
- }
- xmlXPathFreeContext(xpathCtx);
- return xpathObj;
- }
- + (nullable NSString *)safeXmlStringWithString:(NSString *)str
- {
- return [str fb_xmlSafeStringWithReplacement:@""];
- }
- + (int)recordElementAttributes:(xmlTextWriterPtr)writer
- forElement:(id<FBXCElementSnapshot>)element
- indexPath:(nullable NSString *)indexPath
- includedAttributes:(nullable NSSet<Class> *)includedAttributes
- {
- for (Class attributeCls in FBElementAttribute.supportedAttributes) {
- // include all supported attributes by default unless enumerated explicitly
- if (includedAttributes && ![includedAttributes containsObject:attributeCls]) {
- continue;
- }
- int rc = [attributeCls recordWithWriter:writer
- forElement:[FBXCElementSnapshotWrapper ensureWrapped:element]];
- if (rc < 0) {
- return rc;
- }
- }
- if (nil != indexPath) {
- // index path is the special case
- return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath];
- }
- return 0;
- }
- + (int)writeXmlWithRootElement:(id<FBElement>)root
- indexPath:(nullable NSString *)indexPath
- elementStore:(nullable NSMutableDictionary *)elementStore
- includedAttributes:(nullable NSSet<Class> *)includedAttributes
- writer:(xmlTextWriterPtr)writer
- {
- NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil);
- id<FBXCElementSnapshot> currentSnapshot;
- NSArray<id<FBXCElementSnapshot>> *children;
- if ([root isKindOfClass:XCUIElement.class]) {
- XCUIElement *element = (XCUIElement *)root;
- NSMutableArray<NSString *> *snapshotAttributes = [NSMutableArray arrayWithArray:FBStandardAttributeNames()];
- if (nil == includedAttributes || [includedAttributes containsObject:FBVisibleAttribute.class]) {
- [snapshotAttributes addObject:FB_XCAXAIsVisibleAttributeName];
- // If the app is not idle state while we retrieve the visiblity state
- // then the snapshot retrieval operation might freeze and time out
- [element.application fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
- }
- currentSnapshot = [element fb_snapshotWithAttributes:snapshotAttributes.copy
- maxDepth:nil];
- children = currentSnapshot.children;
- } else {
- currentSnapshot = (id<FBXCElementSnapshot>)root;
- children = currentSnapshot.children;
- }
- if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) {
- [elementStore setObject:currentSnapshot forKey:topNodeIndexPath];
- }
- FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:currentSnapshot];
- int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc];
- return rc;
- }
- rc = [self recordElementAttributes:writer
- forElement:currentSnapshot
- indexPath:indexPath
- includedAttributes:includedAttributes];
- if (rc < 0) {
- return rc;
- }
- for (NSUInteger i = 0; i < [children count]; i++) {
- id<FBXCElementSnapshot> childSnapshot = [children objectAtIndex:i];
- NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil;
- if (elementStore != nil && newIndexPath != nil) {
- [elementStore setObject:childSnapshot forKey:(id)newIndexPath];
- }
- rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot]
- indexPath:newIndexPath
- elementStore:elementStore
- includedAttributes:includedAttributes
- writer:writer];
- if (rc < 0) {
- return rc;
- }
- }
- rc = xmlTextWriterEndElement(writer);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
- return rc;
- }
- return 0;
- }
- @end
- static NSString *const FBAbstractMethodInvocationException = @"AbstractMethodInvocationException";
- @implementation FBElementAttribute
- - (instancetype)initWithElement:(id<FBElement>)element
- {
- self = [super init];
- if (self) {
- _element = element;
- }
- return self;
- }
- + (NSString *)name
- {
- NSString *errMsg = [NSString stringWithFormat:@"The abstract method +(NSString *)name is expected to be overriden by %@", NSStringFromClass(self.class)];
- @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- NSString *errMsg = [NSString stringWithFormat:@"The abstract method -(NSString *)value is expected to be overriden by %@", NSStringFromClass(self.class)];
- @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
- }
- + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element
- {
- NSString *value = [self valueForElement:element];
- if (nil == value) {
- // Skip the attribute if the value equals to nil
- return 0;
- }
- int rc = xmlTextWriterWriteAttribute(writer,
- (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String],
- (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc];
- }
- return rc;
- }
- + (NSArray<Class> *)supportedAttributes
- {
- // The list of attributes to be written for each XML node
- // The enumeration order does matter here
- return @[FBTypeAttribute.class,
- FBValueAttribute.class,
- FBNameAttribute.class,
- FBLabelAttribute.class,
- FBEnabledAttribute.class,
- FBVisibleAttribute.class,
- FBAccessibleAttribute.class,
- #if TARGET_OS_TV
- FBFocusedAttribute.class,
- #endif
- FBXAttribute.class,
- FBYAttribute.class,
- FBWidthAttribute.class,
- FBHeightAttribute.class,
- FBIndexAttribute.class,
- FBHittableAttribute.class,
- ];
- }
- @end
- @implementation FBTypeAttribute
- + (NSString *)name
- {
- return @"type";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return element.wdType;
- }
- @end
- @implementation FBValueAttribute
- + (NSString *)name
- {
- return @"value";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- id idValue = element.wdValue;
- if ([idValue isKindOfClass:[NSValue class]]) {
- return [idValue stringValue];
- } else if ([idValue isKindOfClass:[NSString class]]) {
- return idValue;
- }
- return [idValue description];
- }
- @end
- @implementation FBNameAttribute
- + (NSString *)name
- {
- return @"name";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return element.wdName;
- }
- @end
- @implementation FBLabelAttribute
- + (NSString *)name
- {
- return @"label";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return element.wdLabel;
- }
- @end
- @implementation FBEnabledAttribute
- + (NSString *)name
- {
- return @"enabled";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return FBBoolToString(element.wdEnabled);
- }
- @end
- @implementation FBVisibleAttribute
- + (NSString *)name
- {
- return @"visible";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return FBBoolToString(element.wdVisible);
- }
- @end
- @implementation FBAccessibleAttribute
- + (NSString *)name
- {
- return @"accessible";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return FBBoolToString(element.wdAccessible);
- }
- @end
- #if TARGET_OS_TV
- @implementation FBFocusedAttribute
- + (NSString *)name
- {
- return @"focused";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return FBBoolToString(element.wdFocused);
- }
- @end
- #endif
- @implementation FBDimensionAttribute
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return [NSString stringWithFormat:@"%@", [element.wdRect objectForKey:[self name]]];
- }
- @end
- @implementation FBXAttribute
- + (NSString *)name
- {
- return @"x";
- }
- @end
- @implementation FBYAttribute
- + (NSString *)name
- {
- return @"y";
- }
- @end
- @implementation FBWidthAttribute
- + (NSString *)name
- {
- return @"width";
- }
- @end
- @implementation FBHeightAttribute
- + (NSString *)name
- {
- return @"height";
- }
- @end
- @implementation FBIndexAttribute
- + (NSString *)name
- {
- return @"index";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return [NSString stringWithFormat:@"%lu", element.wdIndex];
- }
- @end
- @implementation FBHittableAttribute
- + (NSString *)name
- {
- return @"hittable";
- }
- + (NSString *)valueForElement:(id<FBElement>)element
- {
- return FBBoolToString(element.wdHittable);
- }
- @end
- @implementation FBInternalIndexAttribute
- + (NSString *)name
- {
- return kXMLIndexPathKey;
- }
- + (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value
- {
- if (nil == value) {
- // Skip the attribute if the value equals to nil
- return 0;
- }
- int rc = xmlTextWriterWriteAttribute(writer,
- (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String],
- (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]);
- if (rc < 0) {
- [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc];
- }
- return rc;
- }
- @end
|