Skip to content

Commit

Permalink
feat: Switch the XML-related stuff to use NSXMLDocument (#314)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Oct 13, 2024
1 parent a196ee4 commit 5ef3581
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 189 deletions.
10 changes: 10 additions & 0 deletions WebDriverAgentMac/IntegrationTests/AMFindElementTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ - (void)testMultipleDescendantsWithXPath
XCTAssertEqualObjects([matches objectAtIndex:2].identifier, @"_XCUI:MinimizeWindow");
}

- (void)testMultipleDescendantsWithXPath2
{
NSString *query = @"*//XCUIElementTypeButton[matches(@identifier, \"_xcui:\", \"i\")]";
NSArray<XCUIElement *> *matches = [self.testedApplication fb_descendantsMatchingXPathQuery:query
shouldReturnAfterFirstMatch:NO];
XCTAssertTrue(matches.count >= 3);
XCTAssertEqualObjects(matches.firstObject.identifier, @"_XCUI:CloseWindow");
XCTAssertEqualObjects([matches objectAtIndex:2].identifier, @"_XCUI:MinimizeWindow");
}

- (void)testSingleDescendantWithClassChain
{
NSString *query = @"**/XCUIElementTypeButton[`identifier == '_XCUI:CloseWindow'`]";
Expand Down
252 changes: 65 additions & 187 deletions WebDriverAgentMac/WebDriverAgentLib/Utilities/FBXPath.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,6 @@
#import "NSString+FBXMLSafeString.h"
#import "XCUIElementQuery+AMHelpers.h"

#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpadded"
#endif

#import <libxml/tree.h>
#import <libxml/parser.h>
#import <libxml/xpath.h>
#import <libxml/xpathInternals.h>
#import <libxml/encoding.h>
#import <libxml/xmlwriter.h>

#ifdef __clang__
#pragma clang diagnostic pop
#endif


@interface FBElementAttribute : NSObject

Expand All @@ -43,7 +27,7 @@ @interface FBElementAttribute : NSObject
+ (nonnull NSString *)name;
+ (nullable NSString *)valueForElement:(id<XCUIElementSnapshot>)element;

+ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<XCUIElementSnapshot>)element;
+ (void)recordWithNode:(NSXMLElement *)node forElement:(id<XCUIElementSnapshot>)element;

+ (NSArray<Class> *)supportedAttributes;

Expand Down Expand Up @@ -105,12 +89,10 @@ @interface FBInternalIndexAttribute : FBElementAttribute

@property (nonatomic, nonnull, readonly) NSString* indexValue;

+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value;
+ (void)recordWithNode:(NSXMLElement *)node forValue:(NSString *)value;

@end

const static char *_UTF8Encoding = "UTF-8";

static NSString *const kXMLIndexPathKey = @"private_indexPath";


Expand All @@ -131,25 +113,8 @@ + (nullable NSString *)xmlStringWithRootElement:(XCUIElement *)root
[FBLogger logFmt:@"The snapshot of %@ cannot be taken. Original error: %@", root.description, error.description];
return nil;
}

xmlDocPtr doc;
xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
int rc = [self xmlRepresentationWithRootElement:snapshot
writer:writer
query:nil];
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;

return [self xmlRepresentationWithRootElement:snapshot].XMLString;
}

+ (NSArray<XCUIElement *> *)matchesWithRootElement:(XCUIElement *)root
Expand All @@ -165,64 +130,44 @@ + (nullable NSString *)xmlStringWithRootElement:(XCUIElement *)root
userInfo:@{}];
}

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];
}
int rc = [self xmlRepresentationWithRootElement:snapshot
writer:writer
query:xpathQuery];
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];
NSXMLElement *rootElement = [self writeXmlWithRootSnapshot:snapshot
indexPath:[AMSnapshotUtils hashWithSnapshot:snapshot]];
NSArray<__kindof NSXMLNode *> *matches = [rootElement nodesForXPath:xpathQuery error:&error];
if (nil == matches) {
@throw [NSException exceptionWithName:FBInvalidXPathException
reason:error.description
userInfo:@{}];
}

NSArray *matchingElements = [self collectMatchingElementsWithNodeSet:queryResult->nodesetval
rootElement:root
rootSnapshot:snapshot
includeOnlyFirstMatch:firstMatch];
xmlXPathFreeObject(queryResult);
xmlFreeTextWriter(writer);
xmlFreeDoc(doc);
NSArray *matchingElements = [self collectMatchingElementsWithNodes:matches
rootElement:root
rootSnapshot:snapshot
includeOnlyFirstMatch:firstMatch];
if (nil == matchingElements) {
return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
}
return matchingElements;
}

+ (NSArray *)collectMatchingElementsWithNodeSet:(xmlNodeSetPtr)nodeSet
rootElement:(XCUIElement *)rootElement
rootSnapshot:(id<XCUIElementSnapshot>)rootSnapshot
includeOnlyFirstMatch:(BOOL)firstMatch
+ (NSArray *)collectMatchingElementsWithNodes:(NSArray<__kindof NSXMLNode *> *)nodes
rootElement:(XCUIElement *)rootElement
rootSnapshot:(id<XCUIElementSnapshot>)rootSnapshot
includeOnlyFirstMatch:(BOOL)firstMatch
{
if (xmlXPathNodeSetIsEmpty(nodeSet)) {
if (0 == nodes.count) {
return @[];
}

const xmlChar *indexPathKeyName = (xmlChar *)[kXMLIndexPathKey UTF8String];
NSMutableArray<NSString *> *hashes = [NSMutableArray array];
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;
for (NSXMLNode *node in nodes) {
if (![node isKindOfClass:NSXMLElement.class]) {
continue;
}

NSString *hash = [NSString stringWithCString:(const char *)attrValue
encoding:NSUTF8StringEncoding];
[hashes addObject:hash];
xmlFree(attrValue);
NSString *attrValue = [[(NSXMLElement *)node attributeForName:kXMLIndexPathKey] stringValue];
if (nil == attrValue) {
continue;
}
[hashes addObject:attrValue];
}
NSMutableArray<XCUIElement *> *matchingElements = [NSMutableArray array];
NSString *selfHash = [AMSnapshotUtils hashWithSnapshot:rootSnapshot];
Expand All @@ -241,114 +186,53 @@ + (NSArray *)collectMatchingElementsWithNodeSet:(xmlNodeSetPtr)nodeSet
: matchingElements.copy;
}

+ (int)xmlRepresentationWithRootElement:(id<XCUIElementSnapshot>)root
writer:(xmlTextWriterPtr)writer
query:(nullable NSString*)query
+ (NSXMLDocument *)xmlRepresentationWithRootElement:(id<XCUIElementSnapshot>)root
{
int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
if (rc < 0) {
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
return rc;
}

NSString *index = [AMSnapshotUtils hashWithSnapshot:root];
rc = [self writeXmlWithRootElement:root
indexPath:(query != nil ? index : nil)
writer:writer];
if (rc < 0) {
[FBLogger log:@"Failed to generate XML presentation of a screen element"];
return rc;
}
rc = xmlTextWriterEndDocument(writer);
if (rc < 0) {
[FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc];
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((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;
NSXMLElement *rootElement = [self writeXmlWithRootSnapshot:root indexPath:nil];
NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] initWithRootElement:rootElement];
[xmlDoc setVersion:@"1.0"];
[xmlDoc setCharacterEncoding:@"UTF-8"];
return xmlDoc;
}

+ (nullable NSString *)safeXmlStringWithString:(nullable NSString *)str
{
return [str fb_xmlSafeStringWithReplacement:@""];
}

+ (int)recordElementAttributes:(xmlTextWriterPtr)writer
forElement:(id<XCUIElementSnapshot>)element
indexPath:(nullable NSString *)indexPath
+ (void)recordElementAttributes:(NSXMLElement *)node
forSnapshot:(id<XCUIElementSnapshot>)snapshot
indexPath:(nullable NSString *)indexPath
{
for (Class attributeCls in FBElementAttribute.supportedAttributes) {
int rc = [attributeCls recordWithWriter:writer forElement:element];
if (rc < 0) {
return rc;
}
[attributeCls recordWithNode:node forElement:snapshot];
}

if (nil != indexPath) {
// index path is the special case
return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath];
[FBInternalIndexAttribute recordWithNode:node forValue:indexPath];
}
return 0;
}

+ (int)writeXmlWithRootElement:(id<XCUIElementSnapshot>)root
indexPath:(nullable NSString *)indexPath
writer:(xmlTextWriterPtr)writer
+ (NSXMLElement *)writeXmlWithRootSnapshot:(id<XCUIElementSnapshot>)root
indexPath:(nullable NSString *)indexPath
{
id<XCUIElementSnapshot> currentSnapshot = root;
NSArray<id<XCUIElementSnapshot>> *children = root.children;
NSString *type = [FBElementTypeTransformer stringWithElementType:root.elementType];
NSXMLElement *rootElement = [NSXMLElement elementWithName:type];
[self recordElementAttributes:rootElement
forSnapshot:root
indexPath:indexPath];

NSString *type = [FBElementTypeTransformer stringWithElementType:currentSnapshot.elementType];
int rc = xmlTextWriterStartElement(writer, (xmlChar *)[type UTF8String]);
if (rc < 0) {
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", type, rc];
return rc;
}

rc = [self recordElementAttributes:writer
forElement:currentSnapshot
indexPath:indexPath];
if (rc < 0) {
return rc;
}

for (NSUInteger i = 0; i < [children count]; i++) {
id<XCUIElementSnapshot> childSnapshot = [children objectAtIndex:i];
NSArray<id<XCUIElementSnapshot>> *children = root.children;
for (id<XCUIElementSnapshot> childSnapshot in children) {
NSString *newIndexPath = (indexPath != nil)
? [AMSnapshotUtils hashWithSnapshot:childSnapshot]
: nil;
rc = [self writeXmlWithRootElement:childSnapshot
indexPath:newIndexPath
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;
NSXMLElement *childElement = [self writeXmlWithRootSnapshot:childSnapshot
indexPath:newIndexPath];
[rootElement addChild:childElement];
}
return 0;
return rootElement;
}

@end
Expand Down Expand Up @@ -379,20 +263,17 @@ + (NSString *)valueForElement:(id<XCUIElementSnapshot>)element
@throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
}

+ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<XCUIElementSnapshot>)element
+ (void)recordWithNode:(NSXMLElement *)node forElement:(id<XCUIElementSnapshot>)element
{
NSString *value = [self valueForElement:element];
if (nil == value) {
// Skip the attribute if the value equals to nil
return 0;
return;
}
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;

NSString *attrName = [FBXPath safeXmlStringWithString:self.name];
NSString *attrValue = [FBXPath safeXmlStringWithString:value];
[node addAttribute:[NSXMLNode attributeWithName:attrName stringValue:attrValue]];
}

+ (NSArray<Class> *)supportedAttributes
Expand Down Expand Up @@ -579,26 +460,23 @@ + (NSString *)valueForElement:(id<XCUIElementSnapshot>)element

@end

@implementation FBInternalIndexAttribute
@implementation FBInternalIndexAttribute: FBElementAttribute

+ (NSString *)name
{
return kXMLIndexPathKey;
}

+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value
+ (void)recordWithNode:(NSXMLElement *)node forValue:(NSString *)value
{
if (nil == value) {
// Skip the attribute if the value equals to nil
return 0;
return;
}
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;

NSString *attrName = [FBXPath safeXmlStringWithString:self.name];
NSString *attrValue = [FBXPath safeXmlStringWithString:value];
[node addAttribute:[NSXMLNode attributeWithName:attrName stringValue:attrValue]];
}

@end
2 changes: 0 additions & 2 deletions WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@
714CA7022566475200353B27 /* XCUIApplication+AMSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 714CA7002566475200353B27 /* XCUIApplication+AMSource.m */; };
714CA7062566487B00353B27 /* FBXPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 714CA7042566487B00353B27 /* FBXPath.h */; };
714CA7072566487B00353B27 /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 714CA7052566487B00353B27 /* FBXPath.m */; };
714CA70A256648B200353B27 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 714CA709256648A100353B27 /* libxml2.tbd */; };
71688A98256461ED0007F55B /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 71688A97256461ED0007F55B /* AppDelegate.m */; };
71688A9B256461ED0007F55B /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 71688A9A256461ED0007F55B /* ViewController.m */; };
71688A9D256461F00007F55B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 71688A9C256461F00007F55B /* Assets.xcassets */; };
Expand Down Expand Up @@ -455,7 +454,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
714CA70A256648B200353B27 /* libxml2.tbd in Frameworks */,
7199B3CD2565B1CD000B5C51 /* XCTest.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down

0 comments on commit 5ef3581

Please sign in to comment.