/** * 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 "FBLogger.h" #import "XCAXClient_iOS.h" #import "XCTestDriver.h" #import "XCTestPrivateSymbols.h" #import "XCUIElement.h" #import "XCUIElement+FBWebDriverAttributes.h" #import "NSString+FBXMLSafeString.h" @interface FBElementAttribute : NSObject @property (nonatomic, readonly) id element; + (nonnull NSString *)name; + (nullable NSString *)valueForElement:(id)element; + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element; + (NSArray *)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 FBDimensionAttribute : FBElementAttribute @end @interface FBXAttribute : FBDimensionAttribute @end @interface FBYAttribute : FBDimensionAttribute @end @interface FBWidthAttribute : FBDimensionAttribute @end @interface FBHeightAttribute : FBDimensionAttribute @end @interface FBIndexAttribute : FBElementAttribute @property (nonatomic, nonnull, readonly) NSString* indexValue; + (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value; @end const static char *_UTF8Encoding = "UTF-8"; static NSString *const kXMLIndexPathKey = @"private_indexPath"; static NSString *const topNodeIndexPath = @"top"; NSString *const FBInvalidXPathException = @"FBInvalidXPathException"; NSString *const FBXPathQueryEvaluationException = @"FBXPathQueryEvaluationException"; @implementation FBXPath + (void)throwException:(NSString *)name forQuery:(NSString *)xpathQuery __attribute__((noreturn)) { NSString *reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery]; @throw [NSException exceptionWithName:name reason:reason userInfo:@{}]; } + (nullable NSString *)xmlStringWithSnapshot:(XCElementSnapshot *)root { xmlDocPtr doc; xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); int rc = [FBXPath getSnapshotAsXML:(XCElementSnapshot *)root writer:writer elementStore:nil query:nil]; if (rc < 0) { xmlFreeTextWriter(writer); xmlFreeDoc(doc); return nil; } int buffersize; xmlChar *xmlbuff; xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1); xmlFreeTextWriter(writer); xmlFreeDoc(doc); return [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding]; } + (NSArray *)findMatchesIn:(XCElementSnapshot *)root xpathQuery:(NSString *)xpathQuery { xmlDocPtr doc; xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); if (NULL == writer) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlNewTextWriterDoc for XPath query \"%@\"", xpathQuery]; [FBXPath throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; return nil; } NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; int rc = [FBXPath getSnapshotAsXML:root writer:writer elementStore:elementStore query:xpathQuery]; if (rc < 0) { xmlFreeTextWriter(writer); xmlFreeDoc(doc); [FBXPath throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; return nil; } xmlXPathObjectPtr queryResult = [FBXPath evaluate:xpathQuery document:doc]; if (NULL == queryResult) { xmlFreeTextWriter(writer); xmlFreeDoc(doc); [FBXPath throwException:FBInvalidXPathException forQuery:xpathQuery]; return nil; } NSArray *matchingSnapshots = [FBXPath collectMatchingSnapshots:queryResult->nodesetval elementStore:elementStore]; xmlXPathFreeObject(queryResult); xmlFreeTextWriter(writer); xmlFreeDoc(doc); if (nil == matchingSnapshots) { [FBXPath throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; return nil; } return matchingSnapshots; } + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet elementStore:(NSMutableDictionary *)elementStore { if (xmlXPathNodeSetIsEmpty(nodeSet)) { return @[]; } NSMutableArray *matchingSnapshots = [NSMutableArray array]; const xmlChar *indexPathKeyName = [FBXPath xmlCharPtrForInput:[kXMLIndexPathKey cStringUsingEncoding:NSUTF8StringEncoding]]; 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; } XCElementSnapshot *element = [elementStore objectForKey:(id)[NSString stringWithCString:(const char *)attrValue encoding:NSUTF8StringEncoding]]; if (element) { [matchingSnapshots addObject:element]; } } return matchingSnapshots; } + (NSSet *)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 *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)getSnapshotAsXML:(XCElementSnapshot *)root writer:(xmlTextWriterPtr)writer elementStore:(nullable NSMutableDictionary *)elementStore query:(nullable NSString*)query { int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL); if (rc < 0) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc]; return rc; } // 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 rc = [FBXPath generateXMLPresentation:root indexPath:(elementStore != nil ? topNodeIndexPath : nil) elementStore:elementStore includedAttributes:(query == nil ? nil : [self.class elementAttributesWithXPathQuery:query]) writer:writer]; if (rc < 0) { [FBLogger log:@"Failed to generate XML presentation of a screen element"]; return rc; } if (nil != elementStore) { // The current node should be in the store as well elementStore[topNodeIndexPath] = root; } rc = xmlTextWriterEndDocument(writer); if (rc < 0) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc]; return rc; } return 0; } + (xmlChar *)xmlCharPtrForInput:(const char *)input { if (0 == input) { return NULL; } xmlCharEncodingHandlerPtr handler = xmlFindCharEncodingHandler(_UTF8Encoding); if (!handler) { [FBLogger log:@"Failed to invoke libxml2>xmlFindCharEncodingHandler"]; return NULL; } int size = (int) strlen(input) + 1; int outputSize = size * 2 - 1; xmlChar *output = (unsigned char *) xmlMalloc((size_t) outputSize); if (0 != output) { int temp = size - 1; int ret = handler->input(output, &outputSize, (const xmlChar *) input, &temp); if ((ret < 0) || (temp - size + 1)) { xmlFree(output); output = 0; } else { output = (unsigned char *) xmlRealloc(output, outputSize + 1); output[outputSize] = 0; } } return output; } + (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([FBXPath xmlCharPtrForInput:[xpathQuery cStringUsingEncoding:NSUTF8StringEncoding]], xpathCtx); if (NULL == xpathObj) { xmlXPathFreeContext(xpathCtx); [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery]; return NULL; } xmlXPathFreeContext(xpathCtx); return xpathObj; } + (xmlChar *)safeXmlStringWithString:(NSString *)str { if (nil == str) { return NULL; } NSString *safeString = [str fb_xmlSafeStringWithReplacement:@""]; return [self.class xmlCharPtrForInput:[safeString cStringUsingEncoding:NSUTF8StringEncoding]]; } + (int)recordElementAttributes:(xmlTextWriterPtr)writer forElement:(XCElementSnapshot *)element indexPath:(nullable NSString *)indexPath includedAttributes:(nullable NSSet *)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:element]; if (rc < 0) { return rc; } } if (nil != indexPath) { // index path is the special case return [FBIndexAttribute recordWithWriter:writer forValue:indexPath]; } return 0; } + (int)generateXMLPresentation:(XCElementSnapshot *)root indexPath:(nullable NSString *)indexPath elementStore:(nullable NSMutableDictionary *)elementStore includedAttributes:(nullable NSSet *)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); int rc = xmlTextWriterStartElement(writer, [FBXPath xmlCharPtrForInput:[root.wdType cStringUsingEncoding:NSUTF8StringEncoding]]); if (rc < 0) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement. Error code: %d", rc]; return rc; } rc = [FBXPath recordElementAttributes:writer forElement:root indexPath:indexPath includedAttributes:includedAttributes]; if (rc < 0) { return rc; } NSArray *children = root.children; for (NSUInteger i = 0; i < [children count]; i++) { XCElementSnapshot *childSnapshot = children[i]; NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil; if (elementStore != nil && newIndexPath != nil) { elementStore[newIndexPath] = childSnapshot; } rc = [self generateXMLPresentation: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)element { self = [super init]; if (self) { _element = element; } return self; } + (NSString *)name { NSString *errMsg = [NSString stringWithFormat:@"The asbtract method +(NSString *)name is expected to be overriden by %@", NSStringFromClass(self.class)]; @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil]; } + (NSString *)valueForElement:(id)element { NSString *errMsg = [NSString stringWithFormat:@"The asbtract 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)element { NSString *value = [self valueForElement:element]; if (nil == value) { // Skip the attribute if the value equals to nil return 0; } int rc = xmlTextWriterWriteAttribute(writer, [FBXPath safeXmlStringWithString:[self name]], [FBXPath safeXmlStringWithString:value]); if (rc < 0) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc]; } return rc; } + (NSArray *)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, FBXAttribute.class, FBYAttribute.class, FBWidthAttribute.class, FBHeightAttribute.class]; } @end @implementation FBTypeAttribute + (NSString *)name { return @"type"; } + (NSString *)valueForElement:(id)element { return element.wdType; } @end @implementation FBValueAttribute + (NSString *)name { return @"value"; } + (NSString *)valueForElement:(id)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)element { return element.wdName; } @end @implementation FBLabelAttribute + (NSString *)name { return @"label"; } + (NSString *)valueForElement:(id)element { return element.wdLabel; } @end @implementation FBEnabledAttribute + (NSString *)name { return @"enabled"; } + (NSString *)valueForElement:(id)element { return element.wdEnabled ? @"true" : @"false"; } @end @implementation FBVisibleAttribute + (NSString *)name { return @"visible"; } + (NSString *)valueForElement:(id)element { return element.wdVisible ? @"true" : @"false"; } @end @implementation FBDimensionAttribute + (NSString *)valueForElement:(id)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 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, [FBXPath safeXmlStringWithString:[self name]], [FBXPath safeXmlStringWithString:value]); if (rc < 0) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc]; } return rc; } @end