955 lines
27 KiB
Objective-C
955 lines
27 KiB
Objective-C
/**
|
|
* 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.
|
|
*/
|
|
|
|
#import "FBXPath.h"
|
|
|
|
#import "FBConfiguration.h"
|
|
#import "FBExceptions.h"
|
|
#import "FBElementUtils.h"
|
|
#import "FBLogger.h"
|
|
#import "FBMacros.h"
|
|
#import "FBXMLGenerationOptions.h"
|
|
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
|
#import "NSString+FBXMLSafeString.h"
|
|
#import "XCUIApplication.h"
|
|
#import "XCUIElement.h"
|
|
#import "XCUIElement+FBCaching.h"
|
|
#import "XCUIElement+FBUtilities.h"
|
|
#import "XCUIElement+FBWebDriverAttributes.h"
|
|
#import "XCTestPrivateSymbols.h"
|
|
#import "FBElementHelpers.h"
|
|
#import "FBXCAXClientProxy.h"
|
|
#import "FBXCAccessibilityElement.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;
|
|
+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value;
|
|
|
|
+ (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;
|
|
|
|
@end
|
|
|
|
@interface FBApplicationBundleIdAttribute : FBElementAttribute
|
|
|
|
@end
|
|
|
|
@interface FBApplicationPidAttribute : FBElementAttribute
|
|
|
|
@end
|
|
|
|
@interface FBPlaceholderValueAttribute : FBElementAttribute
|
|
|
|
@end
|
|
|
|
@interface FBNativeFrameAttribute : FBElementAttribute
|
|
|
|
@end
|
|
|
|
@interface FBTraitsAttribute : FBElementAttribute
|
|
|
|
@end
|
|
|
|
@interface FBMinValueAttribute : FBElementAttribute
|
|
|
|
@end
|
|
|
|
@interface FBMaxValueAttribute : FBElementAttribute
|
|
|
|
@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) {
|
|
[self waitUntilStableWithElement:root];
|
|
// If 'includeHittableInPageSource' setting is enabled, then use native snapshots
|
|
// to calculate a more accurate value for the 'hittable' attribute.
|
|
rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root
|
|
useNative:FBConfiguration.includeHittableInPageSource]
|
|
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);
|
|
id<FBXCElementSnapshot> lookupScopeSnapshot = nil;
|
|
id<FBXCElementSnapshot> contextRootSnapshot = nil;
|
|
BOOL useNativeSnapshot = nil == xpathQuery
|
|
? NO
|
|
: [[self.class elementAttributesWithXPathQuery:xpathQuery] containsObject:FBHittableAttribute.class];
|
|
if (rc < 0) {
|
|
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
|
|
} else {
|
|
[self waitUntilStableWithElement:root];
|
|
if (FBConfiguration.limitXpathContextScope) {
|
|
lookupScopeSnapshot = [self snapshotWithRoot:root useNative:useNativeSnapshot];
|
|
} else {
|
|
if ([root isKindOfClass:XCUIElement.class]) {
|
|
lookupScopeSnapshot = [self snapshotWithRoot:[(XCUIElement *)root application]
|
|
useNative:useNativeSnapshot];
|
|
contextRootSnapshot = [root isKindOfClass:XCUIApplication.class]
|
|
? nil
|
|
: ([(XCUIElement *)root lastSnapshot] ?: [self snapshotWithRoot:(XCUIElement *)root
|
|
useNative:useNativeSnapshot]);
|
|
} else {
|
|
lookupScopeSnapshot = (id<FBXCElementSnapshot>)root;
|
|
contextRootSnapshot = nil == lookupScopeSnapshot.parent ? nil : (id<FBXCElementSnapshot>)root;
|
|
while (nil != lookupScopeSnapshot.parent) {
|
|
lookupScopeSnapshot = lookupScopeSnapshot.parent;
|
|
}
|
|
}
|
|
}
|
|
|
|
rc = [self xmlRepresentationWithRootElement:lookupScopeSnapshot
|
|
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 contextNodeQueryResult = [self matchNodeInDocument:doc
|
|
elementStore:elementStore.copy
|
|
forSnapshot:contextRootSnapshot];
|
|
xmlNodePtr contextNode = NULL;
|
|
if (NULL != contextNodeQueryResult) {
|
|
xmlNodeSetPtr nodeSet = contextNodeQueryResult->nodesetval;
|
|
if (!xmlXPathNodeSetIsEmpty(nodeSet)) {
|
|
contextNode = nodeSet->nodeTab[0];
|
|
}
|
|
}
|
|
xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery
|
|
document:doc
|
|
contextNode:contextNode];
|
|
if (NULL != contextNodeQueryResult) {
|
|
xmlXPathFreeObject(contextNodeQueryResult);
|
|
}
|
|
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;
|
|
}
|
|
|
|
+ (nullable xmlXPathObjectPtr)matchNodeInDocument:(xmlDocPtr)doc
|
|
elementStore:(NSDictionary<NSString *, id<FBXCElementSnapshot>> *)elementStore
|
|
forSnapshot:(nullable id<FBXCElementSnapshot>)snapshot
|
|
{
|
|
if (nil == snapshot) {
|
|
return NULL;
|
|
}
|
|
|
|
NSString *contextRootUid = [FBElementUtils uidWithAccessibilityElement:[(id)snapshot accessibilityElement]];
|
|
if (nil == contextRootUid) {
|
|
return NULL;
|
|
}
|
|
|
|
for (NSString *key in elementStore) {
|
|
id<FBXCElementSnapshot> value = [elementStore objectForKey:key];
|
|
NSString *snapshotUid = [FBElementUtils uidWithAccessibilityElement:[value accessibilityElement]];
|
|
if (nil == snapshotUid || ![snapshotUid isEqualToString:contextRootUid]) {
|
|
continue;
|
|
}
|
|
NSString *indexQuery = [NSString stringWithFormat:@"//*[@%@=\"%@\"]", kXMLIndexPathKey, key];
|
|
xmlXPathObjectPtr queryResult = [self evaluate:indexQuery
|
|
document:doc
|
|
contextNode:NULL];
|
|
if (NULL != queryResult) {
|
|
return queryResult;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
+ (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<FBXCElementSnapshot>)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];
|
|
if (!FBConfiguration.includeHittableInPageSource) {
|
|
// The hittable attribute is expensive to calculate for each snapshot item
|
|
// thus we only include it when requested explicitly
|
|
[includedAttributes removeObject:FBHittableAttribute.class];
|
|
}
|
|
if (!FBConfiguration.includeNativeFrameInPageSource) {
|
|
// Include nativeFrame only when requested
|
|
[includedAttributes removeObject:FBNativeFrameAttribute.class];
|
|
}
|
|
if (!FBConfiguration.includeMinMaxValueInPageSource) {
|
|
// minValue/maxValue are retrieved from private APIs and may be slow on deep trees
|
|
[includedAttributes removeObject:FBMinValueAttribute.class];
|
|
[includedAttributes removeObject:FBMaxValueAttribute.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
|
|
contextNode:(nullable xmlNodePtr)contextNode
|
|
{
|
|
xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
|
|
if (NULL == xpathCtx) {
|
|
[FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery];
|
|
return NULL;
|
|
}
|
|
xpathCtx->node = NULL == contextNode ? doc->children : contextNode;
|
|
|
|
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;
|
|
}
|
|
// Text-input placeholder (only for elements that support inner text)
|
|
if ((attributeCls == FBPlaceholderValueAttribute.class) &&
|
|
!FBDoesElementSupportInnerText(element.elementType)) {
|
|
continue;
|
|
}
|
|
// Only for elements that support min/max value
|
|
if ((attributeCls == FBMinValueAttribute.class || attributeCls == FBMaxValueAttribute.class) &&
|
|
!FBDoesElementSupportMinMaxValue(element.elementType)) {
|
|
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];
|
|
}
|
|
if (element.elementType == XCUIElementTypeApplication) {
|
|
// only record process identifier and bundle identifier for the application element
|
|
int pid = [element.accessibilityElement processIdentifier];
|
|
if (pid > 0) {
|
|
int rc = [FBApplicationPidAttribute recordWithWriter:writer
|
|
forValue:[NSString stringWithFormat:@"%d", pid]];
|
|
if (rc < 0) {
|
|
return rc;
|
|
}
|
|
XCUIApplication *app = [[FBXCAXClientProxy sharedClient]
|
|
monitoredApplicationWithProcessIdentifier:pid];
|
|
NSString *bundleID = [app bundleID];
|
|
if (nil != bundleID) {
|
|
rc = [FBApplicationBundleIdAttribute recordWithWriter:writer
|
|
forValue:bundleID];
|
|
if (rc < 0) {
|
|
return rc;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
+ (int)writeXmlWithRootElement:(id<FBXCElementSnapshot>)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);
|
|
|
|
NSArray<id<FBXCElementSnapshot>> *children = root.children;
|
|
|
|
if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) {
|
|
[elementStore setObject:root forKey:topNodeIndexPath];
|
|
}
|
|
|
|
FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:root];
|
|
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:root
|
|
indexPath:indexPath
|
|
includedAttributes:includedAttributes];
|
|
if (rc < 0) {
|
|
return rc;
|
|
}
|
|
|
|
for (NSUInteger i = 0; i < [children count]; i++) {
|
|
@autoreleasepool {
|
|
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;
|
|
}
|
|
|
|
+ (id<FBXCElementSnapshot>)snapshotWithRoot:(id<FBElement>)root
|
|
useNative:(BOOL)useNative
|
|
{
|
|
if (![root isKindOfClass:XCUIElement.class]) {
|
|
return (id<FBXCElementSnapshot>)root;
|
|
}
|
|
|
|
if (useNative) {
|
|
return [(XCUIElement *)root fb_nativeSnapshot];
|
|
}
|
|
return [root isKindOfClass:XCUIApplication.class]
|
|
? [(XCUIElement *)root fb_standardSnapshot]
|
|
: [(XCUIElement *)root fb_customSnapshot];
|
|
}
|
|
|
|
+ (void)waitUntilStableWithElement:(id<FBElement>)root
|
|
{
|
|
if ([root isKindOfClass:XCUIElement.class]) {
|
|
// If the app is not idle state while we retrieve the visiblity state
|
|
// then the snapshot retrieval operation might freeze and time out
|
|
[[(XCUIElement *)root application] fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
|
|
}
|
|
}
|
|
|
|
@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];
|
|
return [self recordWithWriter:writer forValue:value];
|
|
}
|
|
|
|
+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable 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;
|
|
}
|
|
|
|
+ (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,
|
|
FBPlaceholderValueAttribute.class,
|
|
FBTraitsAttribute.class,
|
|
FBNativeFrameAttribute.class,
|
|
FBMinValueAttribute.class,
|
|
FBMaxValueAttribute.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;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation FBApplicationBundleIdAttribute : FBElementAttribute
|
|
|
|
+ (NSString *)name
|
|
{
|
|
return @"bundleId";
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation FBApplicationPidAttribute : FBElementAttribute
|
|
|
|
+ (NSString *)name
|
|
{
|
|
return @"processId";
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation FBPlaceholderValueAttribute
|
|
|
|
+ (NSString *)name
|
|
{
|
|
return @"placeholderValue";
|
|
}
|
|
|
|
+ (NSString *)valueForElement:(id<FBElement>)element
|
|
{
|
|
return element.wdPlaceholderValue;
|
|
}
|
|
@end
|
|
|
|
@implementation FBNativeFrameAttribute
|
|
|
|
+ (NSString *)name
|
|
{
|
|
return @"nativeFrame";
|
|
}
|
|
|
|
+ (NSString *)valueForElement:(id<FBElement>)element
|
|
{
|
|
return NSStringFromCGRect(element.wdNativeFrame);
|
|
}
|
|
@end
|
|
|
|
@implementation FBTraitsAttribute
|
|
|
|
+ (NSString *)name
|
|
{
|
|
return @"traits";
|
|
}
|
|
|
|
+ (NSString *)valueForElement:(id<FBElement>)element
|
|
{
|
|
return element.wdTraits;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation FBMinValueAttribute
|
|
|
|
+ (NSString *)name
|
|
{
|
|
return @"minValue";
|
|
}
|
|
|
|
+ (NSString *)valueForElement:(id<FBElement>)element
|
|
{
|
|
return [element.wdMinValue stringValue];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation FBMaxValueAttribute
|
|
|
|
+ (NSString *)name
|
|
{
|
|
return @"maxValue";
|
|
}
|
|
|
|
+ (NSString *)valueForElement:(id<FBElement>)element
|
|
{
|
|
return [element.wdMaxValue stringValue];
|
|
}
|
|
|
|
@end
|