mirror of
https://github.com/wu736139669/Monkey-Test.git
synced 2026-08-05 05:55:46 +00:00
555 lines
15 KiB
Objective-C
Executable File
555 lines
15 KiB
Objective-C
Executable File
/**
|
|
* 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<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 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<XCElementSnapshot *> *)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<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)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<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: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<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);
|
|
|
|
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<FBElement>)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<FBElement>)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<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, [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<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,
|
|
FBXAttribute.class,
|
|
FBYAttribute.class,
|
|
FBWidthAttribute.class,
|
|
FBHeightAttribute.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 element.wdEnabled ? @"true" : @"false";
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation FBVisibleAttribute
|
|
|
|
+ (NSString *)name
|
|
{
|
|
return @"visible";
|
|
}
|
|
|
|
+ (NSString *)valueForElement:(id<FBElement>)element
|
|
{
|
|
return element.wdVisible ? @"true" : @"false";
|
|
}
|
|
|
|
@end
|
|
|
|
@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 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
|
|
|
|
|
|
|