初始化提交
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) 2018-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 "FBXCElementSnapshotWrapper.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (Helpers)
|
||||
|
||||
/**
|
||||
Returns an array of descendants matching given type
|
||||
|
||||
@param type requested descendant type
|
||||
@return an array of descendants matching given type
|
||||
*/
|
||||
- (NSArray<id<FBXCElementSnapshot>> *)fb_descendantsMatchingType:(XCUIElementType)type;
|
||||
|
||||
/**
|
||||
Returns first (going up element tree) parent that matches given type. If non found returns nil.
|
||||
|
||||
@param type requested parent type
|
||||
@return parent element matching given type
|
||||
*/
|
||||
- (nullable id<FBXCElementSnapshot>)fb_parentMatchingType:(XCUIElementType)type;
|
||||
|
||||
/**
|
||||
Returns first (going up element tree) parent that matches one of given types. If non found returns nil.
|
||||
|
||||
@param types possible parent types
|
||||
@return parent element matching one of given types
|
||||
*/
|
||||
- (nullable id<FBXCElementSnapshot>)fb_parentMatchingOneOfTypes:(NSArray<NSNumber *> *)types;
|
||||
|
||||
/**
|
||||
Returns first (going up element tree) visible parent that matches one of given types and has more than one child. If non found returns nil.
|
||||
|
||||
@param types possible parent types
|
||||
@param filter will filter results even further after matching one of given types
|
||||
@return parent element matching one of given types and satisfying filter condition
|
||||
*/
|
||||
- (nullable id<FBXCElementSnapshot>)fb_parentMatchingOneOfTypes:(NSArray<NSNumber *> *)types filter:(BOOL(^)(id<FBXCElementSnapshot> snapshot))filter;
|
||||
|
||||
/**
|
||||
Retrieves the list of all element ancestors in the snapshot hierarchy.
|
||||
|
||||
@return the list of element ancestors or an empty list if the snapshot has no parent.
|
||||
*/
|
||||
- (NSArray<id<FBXCElementSnapshot>> *)fb_ancestors;
|
||||
|
||||
/**
|
||||
Returns value for given accessibility property identifier.
|
||||
|
||||
@param attribute attribute's accessibility identifier. Can be one of
|
||||
`XC_kAXXCAttribute`-prefixed attribute names.
|
||||
@param error Error instance in case of a failure
|
||||
@return value for given accessibility property identifier or nil in case of failure
|
||||
*/
|
||||
- (nullable id)fb_attributeValue:(NSString *)attribute
|
||||
error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Method used to determine whether given element matches receiver by comparing it's parameters except frame.
|
||||
|
||||
@param snapshot element's snapshot to compare against
|
||||
@return YES, if they match otherwise NO
|
||||
*/
|
||||
- (BOOL)fb_framelessFuzzyMatchesElement:(id<FBXCElementSnapshot>)snapshot;
|
||||
|
||||
/**
|
||||
Returns an array of descendants cell snapshots
|
||||
|
||||
@return an array of descendants cell snapshots
|
||||
*/
|
||||
- (NSArray<id<FBXCElementSnapshot>> *)fb_descendantsCellSnapshots;
|
||||
|
||||
/**
|
||||
Returns itself if it is either XCUIElementTypeIcon or XCUIElementTypeCell. Otherwise, returns first (going up element tree) parent that matches cell (XCUIElementTypeCell or XCUIElementTypeIcon). If non found returns nil.
|
||||
|
||||
@return parent element matching either XCUIElementTypeIcon or XCUIElementTypeCell.
|
||||
*/
|
||||
- (nullable id<FBXCElementSnapshot>)fb_parentCellSnapshot;
|
||||
|
||||
/**! Human-readable snapshot description */
|
||||
- (NSString *)fb_description;
|
||||
|
||||
/**
|
||||
Wrapper for Apple's hitpoint, thats resolves few known issues
|
||||
|
||||
@return Element's hitpoint if exists nil otherwise
|
||||
*/
|
||||
- (nullable NSValue *)fb_hitPoint;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright (c) 2018-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 "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
|
||||
#import "FBFindElementCommands.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBRunLoopSpinner.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBXCElementSnapshot.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "FBXCAXClientProxy.h"
|
||||
#import "XCTestDriver.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
#import "XCUIHitPointResult.h"
|
||||
|
||||
#define ATTRIBUTE_FETCH_WARN_TIME_LIMIT 0.05
|
||||
|
||||
inline static BOOL isSnapshotTypeAmongstGivenTypes(id<FBXCElementSnapshot> snapshot,
|
||||
NSArray<NSNumber *> *types);
|
||||
|
||||
@implementation FBXCElementSnapshotWrapper (Helpers)
|
||||
|
||||
- (NSString *)fb_description
|
||||
{
|
||||
NSString *result = [NSString stringWithFormat:@"%@", self.wdType];
|
||||
if (nil != self.wdName) {
|
||||
result = [NSString stringWithFormat:@"%@ (%@)", result, self.wdName];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray<id<FBXCElementSnapshot>> *)fb_descendantsMatchingType:(XCUIElementType)type
|
||||
{
|
||||
return [self descendantsByFilteringWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot) {
|
||||
return snapshot.elementType == type;
|
||||
}];
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_parentMatchingType:(XCUIElementType)type
|
||||
{
|
||||
NSArray *acceptedParents = @[@(type)];
|
||||
return [self fb_parentMatchingOneOfTypes:acceptedParents];
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_parentMatchingOneOfTypes:(NSArray<NSNumber *> *)types
|
||||
{
|
||||
return [self fb_parentMatchingOneOfTypes:types filter:^(id<FBXCElementSnapshot> snapshot) {
|
||||
return YES;
|
||||
}];
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_parentMatchingOneOfTypes:(NSArray<NSNumber *> *)types
|
||||
filter:(BOOL(^)(id<FBXCElementSnapshot> snapshot))filter
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = self.parent;
|
||||
while (snapshot && !(isSnapshotTypeAmongstGivenTypes(snapshot, types) && filter(snapshot))) {
|
||||
snapshot = snapshot.parent;
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
- (id)fb_attributeValue:(NSString *)attribute
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSDate *start = [NSDate date];
|
||||
NSDictionary *result = [FBXCAXClientProxy.sharedClient attributesForElement:[self accessibilityElement]
|
||||
attributes:@[attribute]
|
||||
error:error];
|
||||
NSTimeInterval elapsed = ABS([start timeIntervalSinceNow]);
|
||||
if (elapsed > ATTRIBUTE_FETCH_WARN_TIME_LIMIT) {
|
||||
NSLog(@"! Fetching of %@ value for %@ took %@s", attribute, self.fb_description, @(elapsed));
|
||||
}
|
||||
return [result objectForKey:attribute];
|
||||
}
|
||||
|
||||
inline static BOOL areValuesEqual(id value1, id value2);
|
||||
|
||||
inline static BOOL areValuesEqualOrBlank(id value1, id value2);
|
||||
|
||||
inline static BOOL isNilOrEmpty(id value);
|
||||
|
||||
- (BOOL)fb_framelessFuzzyMatchesElement:(id<FBXCElementSnapshot>)snapshot
|
||||
{
|
||||
// Pure payload-based comparison sometimes yield false negatives, therefore relying on it only if all of the identifying properties are blank
|
||||
if (isNilOrEmpty(self.identifier)
|
||||
&& isNilOrEmpty(self.title)
|
||||
&& isNilOrEmpty(self.label)
|
||||
&& isNilOrEmpty(self.value)
|
||||
&& isNilOrEmpty(self.placeholderValue)) {
|
||||
return [self.wdUID isEqualToString:([FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdUID ?: @"")];
|
||||
}
|
||||
|
||||
// Sometimes value and placeholderValue of a correct match from different snapshots are not the same (one is nil and one is a blank string)
|
||||
// Therefore taking it into account when comparing
|
||||
return self.elementType == snapshot.elementType &&
|
||||
areValuesEqual(self.identifier, snapshot.identifier) &&
|
||||
areValuesEqual(self.title, snapshot.title) &&
|
||||
areValuesEqual(self.label, snapshot.label) &&
|
||||
areValuesEqualOrBlank(self.value, snapshot.value) &&
|
||||
areValuesEqualOrBlank(self.placeholderValue, snapshot.placeholderValue);
|
||||
}
|
||||
|
||||
- (NSArray<id<FBXCElementSnapshot>> *)fb_descendantsCellSnapshots
|
||||
{
|
||||
NSArray<id<FBXCElementSnapshot>> *cellSnapshots = [self fb_descendantsMatchingType:XCUIElementTypeCell];
|
||||
|
||||
if (cellSnapshots.count == 0) {
|
||||
// For the home screen, cells are actually of type XCUIElementTypeIcon
|
||||
cellSnapshots = [self fb_descendantsMatchingType:XCUIElementTypeIcon];
|
||||
}
|
||||
|
||||
if (cellSnapshots.count == 0) {
|
||||
// In some cases XCTest will not report Cell Views. In that case grab all descendants and try to figure out scroll directon from them.
|
||||
cellSnapshots = self._allDescendants;
|
||||
}
|
||||
|
||||
return cellSnapshots;
|
||||
}
|
||||
|
||||
- (NSArray<id<FBXCElementSnapshot>> *)fb_ancestors
|
||||
{
|
||||
NSMutableArray<id<FBXCElementSnapshot>> *ancestors = [NSMutableArray array];
|
||||
id<FBXCElementSnapshot> parent = self.parent;
|
||||
while (parent) {
|
||||
[ancestors addObject:parent];
|
||||
parent = parent.parent;
|
||||
}
|
||||
return ancestors.copy;
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_parentCellSnapshot
|
||||
{
|
||||
id<FBXCElementSnapshot> targetCellSnapshot = self.snapshot;
|
||||
// XCUIElementTypeIcon is the cell type for homescreen icons
|
||||
NSArray<NSNumber *> *acceptableElementTypes = @[
|
||||
@(XCUIElementTypeCell),
|
||||
@(XCUIElementTypeIcon),
|
||||
];
|
||||
if (self.elementType != XCUIElementTypeCell && self.elementType != XCUIElementTypeIcon) {
|
||||
targetCellSnapshot = [self fb_parentMatchingOneOfTypes:acceptableElementTypes];
|
||||
}
|
||||
return targetCellSnapshot;
|
||||
}
|
||||
|
||||
- (NSValue *)fb_hitPoint
|
||||
{
|
||||
NSError *error;
|
||||
XCUIHitPointResult *result = [self hitPoint:&error];
|
||||
if (nil != error) {
|
||||
[FBLogger logFmt:@"Failed to fetch hit point for %@ - %@", self.fb_description, error.localizedDescription];
|
||||
return nil;
|
||||
}
|
||||
return [NSValue valueWithCGPoint:result.hitPoint];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
inline static BOOL isSnapshotTypeAmongstGivenTypes(id<FBXCElementSnapshot> snapshot, NSArray<NSNumber *> *types)
|
||||
{
|
||||
for (NSUInteger i = 0; i < types.count; i++) {
|
||||
if([@(snapshot.elementType) isEqual: types[i]] || [types[i] isEqual: @(XCUIElementTypeAny)]){
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
inline static BOOL areValuesEqual(id value1, id value2)
|
||||
{
|
||||
return value1 == value2 || [value1 isEqual:value2];
|
||||
}
|
||||
|
||||
inline static BOOL areValuesEqualOrBlank(id value1, id value2)
|
||||
{
|
||||
return areValuesEqual(value1, value2) || (isNilOrEmpty(value1) && isNilOrEmpty(value2));
|
||||
}
|
||||
|
||||
inline static BOOL isNilOrEmpty(id value)
|
||||
{
|
||||
if ([value isKindOfClass:NSString.class]) {
|
||||
return [(NSString*)value length] == 0;
|
||||
}
|
||||
return value == nil;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSString (FBUtf8SafeString)
|
||||
|
||||
/**
|
||||
Converts the string, so it could be properly represented in UTF-8 encoding. All non-encodable characters are replaced with
|
||||
the given `replacement`
|
||||
|
||||
@param replacement The character to use a a replacement for the lossy encoding
|
||||
@returns Either the same string or a string with non-encodable chars replaced
|
||||
*/
|
||||
- (instancetype)fb_utf8SafeStringWithReplacement:(unichar)replacement;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSDictionary (FBUtf8SafeDictionary)
|
||||
|
||||
/**
|
||||
Converts the dictionary, so it could be properly represented in UTF-8 encoding. All non-encodable characters
|
||||
in string values are replaced with the Unocde question mark characters. Nested dictionaries and arrays are
|
||||
processed recursively.
|
||||
|
||||
@returns Either the same dictionary or a dictionary with non-encodable chars in string values replaced
|
||||
*/
|
||||
- (instancetype)fb_utf8SafeDictionary;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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 "NSDictionary+FBUtf8SafeDictionary.h"
|
||||
|
||||
const unichar REPLACER = 0xfffd;
|
||||
|
||||
@implementation NSString (FBUtf8SafeString)
|
||||
|
||||
- (instancetype)fb_utf8SafeStringWithReplacement:(unichar)replacement
|
||||
{
|
||||
if ([self canBeConvertedToEncoding:NSUTF8StringEncoding]) {
|
||||
return self;
|
||||
}
|
||||
|
||||
NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
|
||||
NSString *convertedString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
NSString *replacementStr = [NSString stringWithCharacters:&replacement length:1];
|
||||
NSUInteger originalIdx = 0;
|
||||
NSUInteger convertedIdx = 0;
|
||||
while (originalIdx < [self length] && convertedIdx < [convertedString length]) {
|
||||
unichar originalChar = [self characterAtIndex:originalIdx];
|
||||
unichar convertedChar = [convertedString characterAtIndex:convertedIdx];
|
||||
|
||||
if (originalChar == convertedChar) {
|
||||
[result appendString:[NSString stringWithCharacters:&originalChar length:1]];
|
||||
originalIdx++;
|
||||
convertedIdx++;
|
||||
continue;
|
||||
}
|
||||
|
||||
while (originalChar != convertedChar && originalIdx < [self length]) {
|
||||
[result appendString:replacementStr];
|
||||
originalChar = [self characterAtIndex:++originalIdx];
|
||||
}
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSArray (FBUtf8SafeArray)
|
||||
|
||||
- (instancetype)fb_utf8SafeArray
|
||||
{
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
for (id item in self) {
|
||||
if ([item isKindOfClass:NSString.class]) {
|
||||
[result addObject:[(NSString *)item fb_utf8SafeStringWithReplacement:REPLACER]];
|
||||
} else if ([item isKindOfClass:NSDictionary.class]) {
|
||||
[result addObject:[(NSDictionary *)item fb_utf8SafeDictionary]];
|
||||
} else if ([item isKindOfClass:NSArray.class]) {
|
||||
[result addObject:[(NSArray *)item fb_utf8SafeArray]];
|
||||
} else {
|
||||
[result addObject:item];
|
||||
}
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSDictionary (FBUtf8SafeDictionary)
|
||||
|
||||
- (instancetype)fb_utf8SafeDictionary
|
||||
{
|
||||
NSMutableDictionary *result = [self mutableCopy];
|
||||
for (id key in self) {
|
||||
id value = result[key];
|
||||
if ([value isKindOfClass:NSString.class]) {
|
||||
result[key] = [(NSString *)value fb_utf8SafeStringWithReplacement:REPLACER];
|
||||
} else if ([value isKindOfClass:NSArray.class]) {
|
||||
result[key] = [(NSArray *)value fb_utf8SafeArray];
|
||||
} else if ([value isKindOfClass:NSDictionary.class]) {
|
||||
result[key] = [(NSDictionary *)value fb_utf8SafeDictionary];
|
||||
}
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
@end
|
||||
29
WebDriverAgentLib/Categories/NSExpression+FBFormat.h
Normal file
29
WebDriverAgentLib/Categories/NSExpression+FBFormat.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSExpression (FBFormat)
|
||||
|
||||
/**
|
||||
Method used to normalize/verify NSExpression expressions before passing them to WDA.
|
||||
Only expressions of NSKeyPathExpressionType are going to be verified.
|
||||
Allowed property names are only these declared in FBElement protocol (property names are received in runtime)
|
||||
and their shortcuts (without 'wd' prefix). All other property names are considered as unknown.
|
||||
|
||||
@param input expression object received from user input
|
||||
@return formatted expression
|
||||
@throw FBUnknownPredicateKeyException in case the given property name is not declared in FBElement protocol
|
||||
*/
|
||||
+ (instancetype)fb_wdExpressionWithExpression:(NSExpression *)input;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
34
WebDriverAgentLib/Categories/NSExpression+FBFormat.m
Normal file
34
WebDriverAgentLib/Categories/NSExpression+FBFormat.m
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 "NSExpression+FBFormat.h"
|
||||
|
||||
#import "FBElementUtils.h"
|
||||
|
||||
@implementation NSExpression (FBFormat)
|
||||
|
||||
+ (instancetype)fb_wdExpressionWithExpression:(NSExpression *)input
|
||||
{
|
||||
if ([input expressionType] != NSKeyPathExpressionType) {
|
||||
return input;
|
||||
}
|
||||
|
||||
NSString *propName = [input keyPath];
|
||||
NSUInteger dotPos = [propName rangeOfString:@"."].location;
|
||||
NSString *wdPropName;
|
||||
if (NSNotFound == dotPos) {
|
||||
wdPropName = [FBElementUtils wdAttributeNameForAttributeName:propName];
|
||||
} else {
|
||||
NSString *actualPropName = [propName substringToIndex:dotPos];
|
||||
NSString *suffix = [propName substringFromIndex:(dotPos + 1)];
|
||||
wdPropName = [NSString stringWithFormat:@"%@.%@", [FBElementUtils wdAttributeNameForAttributeName:actualPropName], suffix];
|
||||
}
|
||||
return [NSExpression expressionForKeyPath:wdPropName];
|
||||
}
|
||||
|
||||
@end
|
||||
18
WebDriverAgentLib/Categories/NSString+FBVisualLength.h
Normal file
18
WebDriverAgentLib/Categories/NSString+FBVisualLength.h
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
@interface NSString (FBVisualLength)
|
||||
|
||||
/**
|
||||
Helper method that returns length of string with trimmed whitespaces
|
||||
*/
|
||||
- (NSUInteger)fb_visualLength;
|
||||
|
||||
@end
|
||||
18
WebDriverAgentLib/Categories/NSString+FBVisualLength.m
Normal file
18
WebDriverAgentLib/Categories/NSString+FBVisualLength.m
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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 "NSString+FBVisualLength.h"
|
||||
|
||||
@implementation NSString (FBVisualLength)
|
||||
|
||||
- (NSUInteger)fb_visualLength
|
||||
{
|
||||
return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]].length;
|
||||
}
|
||||
|
||||
@end
|
||||
27
WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h
Normal file
27
WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSString (FBXMLSafeString)
|
||||
|
||||
/**
|
||||
Method used to normalize a string before passing it to XML document
|
||||
|
||||
@param replacement The string to be used as a replacement for invalid XML characters
|
||||
@return The string where all characters, which are not members of
|
||||
XML Character Range definition (http://www.w3.org/TR/2008/REC-xml-20081126/#charsets),
|
||||
are replaced
|
||||
*/
|
||||
- (NSString *)fb_xmlSafeStringWithReplacement:(NSString *)replacement;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
30
WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m
Normal file
30
WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 "NSString+FBXMLSafeString.h"
|
||||
|
||||
@implementation NSString (FBXMLSafeString)
|
||||
|
||||
- (NSString *)fb_xmlSafeStringWithReplacement:(NSString *)replacement
|
||||
{
|
||||
static NSMutableCharacterSet *invalidSet;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
// Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
||||
invalidSet = [NSMutableCharacterSet characterSetWithRange:NSMakeRange(0x9, 1)];
|
||||
[invalidSet addCharactersInRange:NSMakeRange(0xA, 1)];
|
||||
[invalidSet addCharactersInRange:NSMakeRange(0xD, 1)];
|
||||
[invalidSet addCharactersInRange:NSMakeRange(0x20, 0xD7FF - 0x20 + 1)];
|
||||
[invalidSet addCharactersInRange:NSMakeRange(0xE000, 0xFFFD - 0xE000 + 1)];
|
||||
[invalidSet addCharactersInRange:NSMakeRange(0x10000, 0x10FFFF - 0x10000 + 1)];
|
||||
[invalidSet invert];
|
||||
});
|
||||
return [[self componentsSeparatedByCharactersInSet:invalidSet] componentsJoinedByString:replacement];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
#import "XCAXClient_iOS.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const FBSnapshotMaxDepthKey;
|
||||
|
||||
void FBSetCustomParameterForElementSnapshot (NSString* name, id value);
|
||||
|
||||
id __nullable FBGetCustomParameterForElementSnapshot (NSString *name);
|
||||
|
||||
@interface XCAXClient_iOS (FBSnapshotReqParams)
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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 "XCAXClient_iOS+FBSnapshotReqParams.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
/**
|
||||
Available parameters with their default values for XCTest:
|
||||
@"maxChildren" : (int)2147483647
|
||||
@"traverseFromParentsToChildren" : YES
|
||||
@"maxArrayCount" : (int)2147483647
|
||||
@"snapshotKeyHonorModalViews" : NO
|
||||
@"maxDepth" : (int)2147483647
|
||||
*/
|
||||
NSString *const FBSnapshotMaxDepthKey = @"maxDepth";
|
||||
|
||||
static id (*original_defaultParameters)(id, SEL);
|
||||
static id (*original_snapshotParameters)(id, SEL);
|
||||
static NSDictionary *defaultRequestParameters;
|
||||
static NSDictionary *defaultAdditionalRequestParameters;
|
||||
static NSMutableDictionary *customRequestParameters;
|
||||
|
||||
void FBSetCustomParameterForElementSnapshot (NSString *name, id value)
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
customRequestParameters = [NSMutableDictionary new];
|
||||
});
|
||||
customRequestParameters[name] = value;
|
||||
}
|
||||
|
||||
id FBGetCustomParameterForElementSnapshot (NSString *name)
|
||||
{
|
||||
return customRequestParameters[name];
|
||||
}
|
||||
|
||||
static id swizzledDefaultParameters(id self, SEL _cmd)
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
defaultRequestParameters = original_defaultParameters(self, _cmd);
|
||||
});
|
||||
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:defaultRequestParameters];
|
||||
[result addEntriesFromDictionary:defaultAdditionalRequestParameters ?: @{}];
|
||||
[result addEntriesFromDictionary:customRequestParameters ?: @{}];
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
static id swizzledSnapshotParameters(id self, SEL _cmd)
|
||||
{
|
||||
NSDictionary *result = original_snapshotParameters(self, _cmd);
|
||||
defaultAdditionalRequestParameters = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@implementation XCAXClient_iOS (FBSnapshotReqParams)
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-load-method"
|
||||
#pragma clang diagnostic ignored "-Wcast-function-type-strict"
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
Method original_defaultParametersMethod = class_getInstanceMethod(self.class, @selector(defaultParameters));
|
||||
IMP swizzledDefaultParametersImp = (IMP)swizzledDefaultParameters;
|
||||
original_defaultParameters = (id (*)(id, SEL)) method_setImplementation(original_defaultParametersMethod, swizzledDefaultParametersImp);
|
||||
|
||||
Method original_snapshotParametersMethod = class_getInstanceMethod(NSClassFromString(@"XCTElementQuery"), NSSelectorFromString(@"snapshotParameters"));
|
||||
IMP swizzledSnapshotParametersImp = (IMP)swizzledSnapshotParameters;
|
||||
original_snapshotParameters = (id (*)(id, SEL)) method_setImplementation(original_snapshotParametersMethod, swizzledSnapshotParametersImp);
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
17
WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h
Normal file
17
WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCTIssue (AMPatcher)
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
35
WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m
Normal file
35
WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 "XCTIssue+FBPatcher.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
static _Bool swizzledShouldInterruptTest(id self, SEL _cmd)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
@implementation XCTIssue (AMPatcher)
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-load-method"
|
||||
#pragma clang diagnostic ignored "-Wcast-function-type-strict"
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
SEL originalShouldInterruptTest = NSSelectorFromString(@"shouldInterruptTest");
|
||||
if (nil == originalShouldInterruptTest) return;
|
||||
Method originalShouldInterruptTestMethod = class_getInstanceMethod(self.class, originalShouldInterruptTest);
|
||||
if (nil == originalShouldInterruptTestMethod) return;
|
||||
method_setImplementation(originalShouldInterruptTestMethod, (IMP)swizzledShouldInterruptTest);
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
23
WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h
Normal file
23
WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
@interface XCUIApplication (FBAlert)
|
||||
|
||||
/* The accessiblity label used for Safari app */
|
||||
extern NSString *const FB_SAFARI_APP_NAME;
|
||||
|
||||
/**
|
||||
Retrieve the current alert element
|
||||
|
||||
@return Alert element instance
|
||||
*/
|
||||
- (XCUIElement *)fb_alertElement;
|
||||
|
||||
@end
|
||||
109
WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m
Normal file
109
WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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 "XCUIApplication+FBAlert.h"
|
||||
|
||||
#import "FBMacros.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
|
||||
#define MAX_CENTER_DELTA 10.0
|
||||
|
||||
NSString *const FB_SAFARI_APP_NAME = @"Safari";
|
||||
|
||||
|
||||
@implementation XCUIApplication (FBAlert)
|
||||
|
||||
- (nullable XCUIElement *)fb_alertElementFromSafariWithScrollView:(XCUIElement *)scrollView
|
||||
viewSnapshot:(id<FBXCElementSnapshot>)viewSnapshot
|
||||
{
|
||||
CGRect appFrame = viewSnapshot.frame;
|
||||
NSPredicate *dstViewMatchPredicate = [NSPredicate predicateWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot, NSDictionary *bindings) {
|
||||
CGRect curFrame = snapshot.frame;
|
||||
if (!CGRectEqualToRect(appFrame, curFrame)
|
||||
&& curFrame.origin.x > 0 && curFrame.size.width < appFrame.size.width) {
|
||||
CGFloat possibleCenterX = (appFrame.size.width - curFrame.size.width) / 2;
|
||||
return fabs(possibleCenterX - curFrame.origin.x) < MAX_CENTER_DELTA;
|
||||
}
|
||||
return NO;
|
||||
}];
|
||||
NSPredicate *dstViewContainPredicate1 = [NSPredicate predicateWithFormat:@"elementType == %lu", XCUIElementTypeTextView];
|
||||
NSPredicate *dstViewContainPredicate2 = [NSPredicate predicateWithFormat:@"elementType == %lu", XCUIElementTypeButton];
|
||||
// Find the first XCUIElementTypeOther which is the grandchild of the web view
|
||||
// and is horizontally aligned to the center of the screen
|
||||
XCUIElement *candidate = [[[[[[scrollView descendantsMatchingType:XCUIElementTypeAny]
|
||||
matchingIdentifier:@"WebView"]
|
||||
descendantsMatchingType:XCUIElementTypeOther]
|
||||
matchingPredicate:dstViewMatchPredicate]
|
||||
containingPredicate:dstViewContainPredicate1]
|
||||
containingPredicate:dstViewContainPredicate2].allElementsBoundByIndex.firstObject;
|
||||
|
||||
if (nil == candidate) {
|
||||
return nil;
|
||||
}
|
||||
// ...and contains one to two buttons
|
||||
// and conatins at least one text view
|
||||
__block NSUInteger buttonsCount = 0;
|
||||
__block NSUInteger textViewsCount = 0;
|
||||
id<FBXCElementSnapshot> snapshot = candidate.fb_cachedSnapshot ?: [candidate fb_customSnapshot];
|
||||
[snapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
|
||||
XCUIElementType curType = descendant.elementType;
|
||||
if (curType == XCUIElementTypeButton) {
|
||||
buttonsCount++;
|
||||
} else if (curType == XCUIElementTypeTextView) {
|
||||
textViewsCount++;
|
||||
}
|
||||
}];
|
||||
return (buttonsCount >= 1 && buttonsCount <= 2 && textViewsCount > 0) ? candidate : nil;
|
||||
}
|
||||
|
||||
- (XCUIElement *)fb_alertElement
|
||||
{
|
||||
NSPredicate *alertCollectorPredicate = [NSPredicate predicateWithFormat:@"elementType IN {%lu,%lu,%lu}",
|
||||
XCUIElementTypeAlert, XCUIElementTypeSheet, XCUIElementTypeScrollView];
|
||||
XCUIElement *alert = [[self descendantsMatchingType:XCUIElementTypeAny]
|
||||
matchingPredicate:alertCollectorPredicate].allElementsBoundByIndex.firstObject;
|
||||
if (nil == alert) {
|
||||
return nil;
|
||||
}
|
||||
id<FBXCElementSnapshot> alertSnapshot = alert.fb_cachedSnapshot ?: [alert fb_customSnapshot];
|
||||
|
||||
if (alertSnapshot.elementType == XCUIElementTypeAlert) {
|
||||
return alert;
|
||||
}
|
||||
|
||||
if (alertSnapshot.elementType == XCUIElementTypeSheet) {
|
||||
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) {
|
||||
return alert;
|
||||
}
|
||||
|
||||
// In case of iPad we want to check if sheet isn't contained by popover.
|
||||
// In that case we ignore it.
|
||||
id<FBXCElementSnapshot> ancestor = alertSnapshot.parent;
|
||||
while (nil != ancestor) {
|
||||
if (nil != ancestor.identifier && [ancestor.identifier isEqualToString:@"PopoverDismissRegion"]) {
|
||||
return nil;
|
||||
}
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return alert;
|
||||
}
|
||||
|
||||
if (alertSnapshot.elementType == XCUIElementTypeScrollView) {
|
||||
id<FBXCElementSnapshot> app = [[FBXCElementSnapshotWrapper ensureWrapped:alertSnapshot] fb_parentMatchingType:XCUIElementTypeApplication];
|
||||
if (nil != app && [app.label isEqualToString:FB_SAFARI_APP_NAME]) {
|
||||
// Check alert presence in Safari web view
|
||||
return [self fb_alertElementFromSafariWithScrollView:alert viewSnapshot:alertSnapshot];
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
23
WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h
Normal file
23
WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2018-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 <XCTest/XCTest.h>
|
||||
#import "FBElement.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIApplication (FBFocused)
|
||||
|
||||
/**
|
||||
Return current focused element
|
||||
*/
|
||||
- (id<FBElement>)fb_focusedElement;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
171
WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h
Normal file
171
WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
@class XCElementSnapshot;
|
||||
@protocol FBXCAccessibilityElement;
|
||||
@class FBXMLGenerationOptions;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIApplication (FBHelpers)
|
||||
|
||||
/**
|
||||
Deactivates application for given time
|
||||
|
||||
@param duration amount of time application should deactivated
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_deactivateWithDuration:(NSTimeInterval)duration error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Return application elements tree in form of nested dictionaries
|
||||
*/
|
||||
- (NSDictionary *)fb_tree;
|
||||
|
||||
/**
|
||||
@param excludedAttributes Set of possible attributes to be excluded i.e frame, enabled, visible, accessible, focused. If set to nil or an empty array then no attributes will be excluded from the resulting JSON
|
||||
@return application elements tree in form of nested dictionaries
|
||||
*/
|
||||
- (NSDictionary *)fb_tree:(nullable NSSet<NSString *> *) excludedAttributes;
|
||||
|
||||
/**
|
||||
Return application elements accessibility tree in form of nested dictionaries
|
||||
*/
|
||||
- (NSDictionary *)fb_accessibilityTree;
|
||||
|
||||
/**
|
||||
Return application elements tree in a form of xml string
|
||||
with default options.
|
||||
|
||||
@return nil if there was a failure while retriveing the page source.
|
||||
*/
|
||||
- (nullable NSString *)fb_xmlRepresentation;
|
||||
|
||||
/**
|
||||
Return application elements tree in a form of xml string
|
||||
|
||||
@param options Optional values that affect the resulting XML generation process.
|
||||
@return nil if there was a failure while retriveing the page source.
|
||||
*/
|
||||
- (nullable NSString *)fb_xmlRepresentationWithOptions:(nullable FBXMLGenerationOptions *)options;
|
||||
|
||||
/**
|
||||
Return application elements tree in form of internal XCTest debugDescription string
|
||||
*/
|
||||
- (NSString *)fb_descriptionRepresentation;
|
||||
|
||||
/**
|
||||
Returns the element, which currently holds the keyboard input focus or nil if there are no such elements.
|
||||
*/
|
||||
- (nullable XCUIElement *)fb_activeElement;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
/**
|
||||
Returns the element, which currently focused.
|
||||
*/
|
||||
- (nullable XCUIElement *)fb_focusedElement;
|
||||
#endif
|
||||
|
||||
/**
|
||||
Waits until the current on-screen accessbility element belongs to the current application instance
|
||||
@param timeout The maximum time to wait for the element to appear
|
||||
@returns Either YES or NO
|
||||
*/
|
||||
- (BOOL)fb_waitForAppElement:(NSTimeInterval)timeout;
|
||||
|
||||
/**
|
||||
Retrieves the information about the applications the given accessiblity elements
|
||||
belong to
|
||||
|
||||
@param axElements the list of accessibility elements
|
||||
@returns The list of dictionaries. Each dictionary contains `bundleId` and `pid` items
|
||||
*/
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)fb_appsInfoWithAxElements:(NSArray<id<FBXCAccessibilityElement>> *)axElements;
|
||||
|
||||
/**
|
||||
Retrieves the information about the currently active apps
|
||||
|
||||
@returns The list of dictionaries. Each dictionary contains `bundleId` and `pid` items.
|
||||
*/
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)fb_activeAppsInfo;
|
||||
|
||||
/**
|
||||
Tries to dismiss the on-screen keyboard
|
||||
|
||||
@param keyNames Optional list of possible keyboard key labels to tap
|
||||
in order to dismiss the keyboard.
|
||||
@param error The resulting error object if the method fails to dismiss the keyboard
|
||||
@returns YES if the keyboard dismissal was successful or NO otherwise
|
||||
*/
|
||||
- (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray<NSString *> *)keyNames
|
||||
error:(NSError **)error;
|
||||
|
||||
/**
|
||||
A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc
|
||||
|
||||
@param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return List of found issues or nil if there was a failure
|
||||
*/
|
||||
- (nullable NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet<NSString *> *)auditTypes
|
||||
error:(NSError **)error;
|
||||
|
||||
/**
|
||||
A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc
|
||||
|
||||
@param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return List of found issues or nil if there was a failure
|
||||
*/
|
||||
- (nullable NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes
|
||||
error:(NSError **)error;
|
||||
/**
|
||||
Constructor used to get current active application
|
||||
*/
|
||||
+ (instancetype)fb_activeApplication;
|
||||
|
||||
/**
|
||||
Constructor used to get current active application
|
||||
|
||||
@param bundleId The bundle identifier of an app, which should be selected as active by default
|
||||
if it is present in the list of active applications
|
||||
*/
|
||||
+ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId;
|
||||
|
||||
/**
|
||||
Constructor used to get the system application (e.g. Springboard on iOS)
|
||||
*/
|
||||
+ (instancetype)fb_systemApplication;
|
||||
|
||||
/**
|
||||
Retrieves the list of all currently active applications
|
||||
*/
|
||||
+ (NSArray<XCUIApplication *> *)fb_activeApplications;
|
||||
|
||||
/**
|
||||
Switch to system app (called Springboard on iOS)
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
+ (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error;
|
||||
|
||||
/**
|
||||
Determines whether the other app is the same as the current one
|
||||
|
||||
@param otherApp Other app instance
|
||||
@return YES if the other app has the same identifier
|
||||
*/
|
||||
- (BOOL)fb_isSameAppAs:(nullable XCUIApplication *)otherApp;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
644
WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m
Normal file
644
WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m
Normal file
@@ -0,0 +1,644 @@
|
||||
/**
|
||||
* 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 "XCUIApplication+FBHelpers.h"
|
||||
|
||||
#import "FBActiveAppDetectionPoint.h"
|
||||
#import "FBElementTypeTransformer.h"
|
||||
#import "FBKeyboard.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBExceptions.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBMathUtils.h"
|
||||
#import "FBRunLoopSpinner.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXPath.h"
|
||||
#import "FBXCAccessibilityElement.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "FBXCAXClientProxy.h"
|
||||
#import "FBXMLGenerationOptions.h"
|
||||
#import "XCTestManager_ManagerInterface-Protocol.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
#import "XCTRunnerDaemonSession.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIApplicationImpl.h"
|
||||
#import "XCUIApplicationProcess.h"
|
||||
#import "XCUIDevice+FBHelpers.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
#import "XCUIElementQuery.h"
|
||||
#import "FBElementHelpers.h"
|
||||
|
||||
static NSString* const FBUnknownBundleId = @"unknown";
|
||||
|
||||
static NSString* const FBExclusionAttributeFrame = @"frame";
|
||||
static NSString* const FBExclusionAttributeEnabled = @"enabled";
|
||||
static NSString* const FBExclusionAttributeVisible = @"visible";
|
||||
static NSString* const FBExclusionAttributeAccessible = @"accessible";
|
||||
static NSString* const FBExclusionAttributeFocused = @"focused";
|
||||
static NSString* const FBExclusionAttributePlaceholderValue = @"placeholderValue";
|
||||
static NSString* const FBExclusionAttributeNativeFrame = @"nativeFrame";
|
||||
static NSString* const FBExclusionAttributeTraits = @"traits";
|
||||
static NSString* const FBExclusionAttributeMinValue = @"minValue";
|
||||
static NSString* const FBExclusionAttributeMaxValue = @"maxValue";
|
||||
|
||||
_Nullable id extractIssueProperty(id issue, NSString *propertyName) {
|
||||
SEL selector = NSSelectorFromString(propertyName);
|
||||
NSMethodSignature *methodSignature = [issue methodSignatureForSelector:selector];
|
||||
if (nil == methodSignature) {
|
||||
return nil;
|
||||
}
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
|
||||
[invocation setSelector:selector];
|
||||
[invocation invokeWithTarget:issue];
|
||||
id __unsafe_unretained result;
|
||||
[invocation getReturnValue:&result];
|
||||
return result;
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, NSNumber *> *auditTypeNamesToValues(void) {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSDictionary *result;
|
||||
dispatch_once(&onceToken, ^{
|
||||
// https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc
|
||||
result = @{
|
||||
@"XCUIAccessibilityAuditTypeAction": @(1UL << 32),
|
||||
@"XCUIAccessibilityAuditTypeAll": @(~0UL),
|
||||
@"XCUIAccessibilityAuditTypeContrast": @(1UL << 0),
|
||||
@"XCUIAccessibilityAuditTypeDynamicType": @(1UL << 16),
|
||||
@"XCUIAccessibilityAuditTypeElementDetection": @(1UL << 1),
|
||||
@"XCUIAccessibilityAuditTypeHitRegion": @(1UL << 2),
|
||||
@"XCUIAccessibilityAuditTypeParentChild": @(1UL << 33),
|
||||
@"XCUIAccessibilityAuditTypeSufficientElementDescription": @(1UL << 3),
|
||||
@"XCUIAccessibilityAuditTypeTextClipped": @(1UL << 17),
|
||||
@"XCUIAccessibilityAuditTypeTrait": @(1UL << 18),
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
NSDictionary<NSNumber *, NSString *> *auditTypeValuesToNames(void) {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSDictionary *result;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSMutableDictionary *inverted = [NSMutableDictionary new];
|
||||
[auditTypeNamesToValues() enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSNumber *value, BOOL *stop) {
|
||||
inverted[value] = key;
|
||||
}];
|
||||
result = inverted.copy;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, NSString *> *customExclusionAttributesMap(void) {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSDictionary *result;
|
||||
dispatch_once(&onceToken, ^{
|
||||
result = @{
|
||||
FBExclusionAttributeVisible: FB_XCAXAIsVisibleAttributeName,
|
||||
FBExclusionAttributeAccessible: FB_XCAXAIsElementAttributeName,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@implementation XCUIApplication (FBHelpers)
|
||||
|
||||
- (BOOL)fb_waitForAppElement:(NSTimeInterval)timeout
|
||||
{
|
||||
__block BOOL canDetectAxElement = YES;
|
||||
int currentProcessIdentifier = [self.accessibilityElement processIdentifier];
|
||||
BOOL result = [[[FBRunLoopSpinner new]
|
||||
timeout:timeout]
|
||||
spinUntilTrue:^BOOL{
|
||||
id<FBXCAccessibilityElement> currentAppElement = FBActiveAppDetectionPoint.sharedInstance.axElement;
|
||||
canDetectAxElement = nil != currentAppElement;
|
||||
if (!canDetectAxElement) {
|
||||
return YES;
|
||||
}
|
||||
return currentAppElement.processIdentifier == currentProcessIdentifier;
|
||||
}];
|
||||
return canDetectAxElement
|
||||
? result
|
||||
: [self waitForExistenceWithTimeout:timeout];
|
||||
}
|
||||
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)fb_appsInfoWithAxElements:(NSArray<id<FBXCAccessibilityElement>> *)axElements
|
||||
{
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
|
||||
id<XCTestManager_ManagerInterface> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
|
||||
for (id<FBXCAccessibilityElement> axElement in axElements) {
|
||||
NSMutableDictionary<NSString *, id> *appInfo = [NSMutableDictionary dictionary];
|
||||
pid_t pid = axElement.processIdentifier;
|
||||
appInfo[@"pid"] = @(pid);
|
||||
__block NSString *bundleId = nil;
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
[proxy _XCT_requestBundleIDForPID:pid
|
||||
reply:^(NSString *bundleID, NSError *error) {
|
||||
if (nil == error) {
|
||||
bundleId = bundleID;
|
||||
} else {
|
||||
[FBLogger logFmt:@"Cannot request the bundle ID for process ID %@: %@", @(pid), error.description];
|
||||
}
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)));
|
||||
appInfo[@"bundleId"] = bundleId ?: FBUnknownBundleId;
|
||||
[result addObject:appInfo.copy];
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)fb_activeAppsInfo
|
||||
{
|
||||
return [self fb_appsInfoWithAxElements:[FBXCAXClientProxy.sharedClient activeApplications]];
|
||||
}
|
||||
|
||||
- (BOOL)fb_deactivateWithDuration:(NSTimeInterval)duration error:(NSError **)error
|
||||
{
|
||||
if(![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:error]) {
|
||||
return NO;
|
||||
}
|
||||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:MAX(duration, .0)]];
|
||||
[self activate];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSDictionary *)fb_tree
|
||||
{
|
||||
return [self fb_tree:nil];
|
||||
}
|
||||
|
||||
- (NSDictionary *)fb_tree:(nullable NSSet<NSString *> *)excludedAttributes
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
return [self.class dictionaryForElement:snapshot
|
||||
recursive:YES
|
||||
excludedAttributes:excludedAttributes];
|
||||
}
|
||||
|
||||
- (NSDictionary *)fb_accessibilityTree
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
return [self.class accessibilityInfoForElement:snapshot];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)dictionaryForElement:(id<FBXCElementSnapshot>)snapshot
|
||||
recursive:(BOOL)recursive
|
||||
excludedAttributes:(nullable NSSet<NSString *> *)excludedAttributes
|
||||
{
|
||||
NSMutableDictionary *info = [[NSMutableDictionary alloc] init];
|
||||
info[@"type"] = [FBElementTypeTransformer shortStringWithElementType:snapshot.elementType];
|
||||
info[@"rawIdentifier"] = FBValueOrNull([snapshot.identifier isEqual:@""] ? nil : snapshot.identifier);
|
||||
FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
|
||||
info[@"name"] = FBValueOrNull(wrappedSnapshot.wdName);
|
||||
info[@"value"] = FBValueOrNull(wrappedSnapshot.wdValue);
|
||||
info[@"label"] = FBValueOrNull(wrappedSnapshot.wdLabel);
|
||||
info[@"rect"] = wrappedSnapshot.wdRect;
|
||||
|
||||
NSDictionary<NSString *, NSString *(^)(void)> *attributeBlocks = [self fb_attributeBlockMapForWrappedSnapshot:wrappedSnapshot];
|
||||
|
||||
NSSet *nonPrefixedKeys = [NSSet setWithObjects:
|
||||
FBExclusionAttributeFrame,
|
||||
FBExclusionAttributePlaceholderValue,
|
||||
FBExclusionAttributeNativeFrame,
|
||||
FBExclusionAttributeTraits,
|
||||
FBExclusionAttributeMinValue,
|
||||
FBExclusionAttributeMaxValue,
|
||||
nil];
|
||||
|
||||
for (NSString *key in attributeBlocks) {
|
||||
if (excludedAttributes == nil || ![excludedAttributes containsObject:key]) {
|
||||
NSString *value = ((NSString * (^)(void))attributeBlocks[key])();
|
||||
if ([nonPrefixedKeys containsObject:key]) {
|
||||
info[key] = value;
|
||||
} else {
|
||||
info[[NSString stringWithFormat:@"is%@", [key capitalizedString]]] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!recursive) {
|
||||
return info.copy;
|
||||
}
|
||||
|
||||
NSArray *childElements = snapshot.children;
|
||||
if ([childElements count]) {
|
||||
info[@"children"] = [[NSMutableArray alloc] init];
|
||||
for (id<FBXCElementSnapshot> childSnapshot in childElements) {
|
||||
@autoreleasepool {
|
||||
[info[@"children"] addObject:[self dictionaryForElement:childSnapshot
|
||||
recursive:YES
|
||||
excludedAttributes:excludedAttributes]];
|
||||
}
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
// Helper used by `dictionaryForElement:` to assemble attribute value blocks,
|
||||
// including both common attributes and conditionally included ones like placeholderValue.
|
||||
+ (NSDictionary<NSString *, NSString *(^)(void)> *)fb_attributeBlockMapForWrappedSnapshot:(FBXCElementSnapshotWrapper *)wrappedSnapshot
|
||||
|
||||
{
|
||||
// Base attributes common to every element
|
||||
NSMutableDictionary<NSString *, id(^)(void)> *blocks =
|
||||
[@{
|
||||
FBExclusionAttributeFrame: ^{
|
||||
return NSStringFromCGRect(wrappedSnapshot.wdFrame);
|
||||
},
|
||||
FBExclusionAttributeNativeFrame: ^{
|
||||
return NSStringFromCGRect(wrappedSnapshot.wdNativeFrame);
|
||||
},
|
||||
FBExclusionAttributeEnabled: ^{
|
||||
return [@([wrappedSnapshot isWDEnabled]) stringValue];
|
||||
},
|
||||
FBExclusionAttributeVisible: ^{
|
||||
return [@([wrappedSnapshot isWDVisible]) stringValue];
|
||||
},
|
||||
FBExclusionAttributeAccessible: ^{
|
||||
return [@([wrappedSnapshot isWDAccessible]) stringValue];
|
||||
},
|
||||
FBExclusionAttributeFocused: ^{
|
||||
return [@([wrappedSnapshot isWDFocused]) stringValue];
|
||||
},
|
||||
FBExclusionAttributeTraits: ^{
|
||||
return wrappedSnapshot.wdTraits;
|
||||
}
|
||||
} mutableCopy];
|
||||
|
||||
XCUIElementType elementType = wrappedSnapshot.elementType;
|
||||
|
||||
// Text-input placeholder (only for elements that support inner text)
|
||||
if (FBDoesElementSupportInnerText(elementType)) {
|
||||
blocks[FBExclusionAttributePlaceholderValue] = ^{
|
||||
return (NSString *)FBValueOrNull(wrappedSnapshot.wdPlaceholderValue);
|
||||
};
|
||||
}
|
||||
|
||||
// Only for elements that support min/max value
|
||||
if (FBDoesElementSupportMinMaxValue(elementType)) {
|
||||
blocks[FBExclusionAttributeMinValue] = ^{
|
||||
return wrappedSnapshot.wdMinValue;
|
||||
};
|
||||
blocks[FBExclusionAttributeMaxValue] = ^{
|
||||
return wrappedSnapshot.wdMaxValue;
|
||||
};
|
||||
}
|
||||
|
||||
return [blocks copy];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)accessibilityInfoForElement:(id<FBXCElementSnapshot>)snapshot
|
||||
{
|
||||
FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
|
||||
BOOL isAccessible = [wrappedSnapshot isWDAccessible];
|
||||
BOOL isVisible = [wrappedSnapshot isWDVisible];
|
||||
|
||||
NSMutableDictionary *info = [[NSMutableDictionary alloc] init];
|
||||
|
||||
if (isAccessible) {
|
||||
if (isVisible) {
|
||||
info[@"value"] = FBValueOrNull(wrappedSnapshot.wdValue);
|
||||
info[@"label"] = FBValueOrNull(wrappedSnapshot.wdLabel);
|
||||
}
|
||||
} else {
|
||||
NSMutableArray *children = [[NSMutableArray alloc] init];
|
||||
for (id<FBXCElementSnapshot> childSnapshot in snapshot.children) {
|
||||
@autoreleasepool {
|
||||
NSDictionary *childInfo = [self accessibilityInfoForElement:childSnapshot];
|
||||
if ([childInfo count]) {
|
||||
[children addObject: childInfo];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ([children count]) {
|
||||
info[@"children"] = [children copy];
|
||||
}
|
||||
}
|
||||
if ([info count]) {
|
||||
info[@"type"] = [FBElementTypeTransformer shortStringWithElementType:snapshot.elementType];
|
||||
info[@"rawIdentifier"] = FBValueOrNull([snapshot.identifier isEqual:@""] ? nil : snapshot.identifier);
|
||||
info[@"name"] = FBValueOrNull(wrappedSnapshot.wdName);
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
- (NSString *)fb_xmlRepresentation
|
||||
{
|
||||
return [self fb_xmlRepresentationWithOptions:nil];
|
||||
}
|
||||
|
||||
- (NSString *)fb_xmlRepresentationWithOptions:(FBXMLGenerationOptions *)options
|
||||
{
|
||||
return [FBXPath xmlStringWithRootElement:self options:options];
|
||||
}
|
||||
|
||||
- (NSString *)fb_descriptionRepresentation
|
||||
{
|
||||
NSMutableArray<NSString *> *childrenDescriptions = [NSMutableArray array];
|
||||
for (XCUIElement *child in [self.fb_query childrenMatchingType:XCUIElementTypeAny].allElementsBoundByIndex) {
|
||||
[childrenDescriptions addObject:child.debugDescription];
|
||||
}
|
||||
// debugDescription property of XCUIApplication instance shows descendants addresses in memory
|
||||
// instead of the actual information about them, however the representation works properly
|
||||
// for all descendant elements
|
||||
return (0 == childrenDescriptions.count) ? self.debugDescription : [childrenDescriptions componentsJoinedByString:@"\n\n"];
|
||||
}
|
||||
|
||||
- (XCUIElement *)fb_activeElement
|
||||
{
|
||||
return [[[self.fb_query descendantsMatchingType:XCUIElementTypeAny]
|
||||
matchingPredicate:[NSPredicate predicateWithFormat:@"hasKeyboardFocus == YES"]]
|
||||
fb_firstMatch];
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (XCUIElement *)fb_focusedElement
|
||||
{
|
||||
return [[[self.fb_query descendantsMatchingType:XCUIElementTypeAny]
|
||||
matchingPredicate:[NSPredicate predicateWithFormat:@"hasFocus == true"]]
|
||||
fb_firstMatch];
|
||||
}
|
||||
#endif
|
||||
|
||||
- (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray<NSString *> *)keyNames
|
||||
error:(NSError **)error
|
||||
{
|
||||
BOOL (^isKeyboardInvisible)(void) = ^BOOL(void) {
|
||||
return ![FBKeyboard waitUntilVisibleForApplication:self
|
||||
timeout:0
|
||||
error:nil];
|
||||
};
|
||||
|
||||
if (isKeyboardInvisible()) {
|
||||
// Short circuit if the keyboard is not visible
|
||||
return YES;
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
[[XCUIRemote sharedRemote] pressButton:XCUIRemoteButtonMenu];
|
||||
#else
|
||||
NSArray<XCUIElement *> *(^findMatchingKeys)(NSPredicate *) = ^NSArray<XCUIElement *> *(NSPredicate * predicate) {
|
||||
NSPredicate *keysPredicate = [NSPredicate predicateWithFormat:@"elementType == %@", @(XCUIElementTypeKey)];
|
||||
XCUIElementQuery *parentView = [[self.keyboard descendantsMatchingType:XCUIElementTypeOther]
|
||||
containingPredicate:keysPredicate];
|
||||
return [[parentView childrenMatchingType:XCUIElementTypeAny]
|
||||
matchingPredicate:predicate].allElementsBoundByIndex;
|
||||
};
|
||||
|
||||
if (nil != keyNames && keyNames.count > 0) {
|
||||
NSPredicate *searchPredicate = [NSPredicate predicateWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot, NSDictionary *bindings) {
|
||||
if (snapshot.elementType != XCUIElementTypeKey && snapshot.elementType != XCUIElementTypeButton) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return (nil != snapshot.identifier && [keyNames containsObject:snapshot.identifier])
|
||||
|| (nil != snapshot.label && [keyNames containsObject:snapshot.label]);
|
||||
}];
|
||||
NSArray *matchedKeys = findMatchingKeys(searchPredicate);
|
||||
if (matchedKeys.count > 0) {
|
||||
for (XCUIElement *matchedKey in matchedKeys) {
|
||||
if (!matchedKey.exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[matchedKey tap];
|
||||
if (isKeyboardInvisible()) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ([UIDevice.currentDevice userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
|
||||
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"elementType IN %@",
|
||||
@[@(XCUIElementTypeKey), @(XCUIElementTypeButton)]];
|
||||
NSArray *matchedKeys = findMatchingKeys(searchPredicate);
|
||||
if (matchedKeys.count > 0) {
|
||||
[matchedKeys[matchedKeys.count - 1] tap];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
NSString *errorDescription = @"Did not know how to dismiss the keyboard. Try to dismiss it in the way supported by your application under test.";
|
||||
return [[[[FBRunLoopSpinner new]
|
||||
timeout:3]
|
||||
timeoutErrorMessage:errorDescription]
|
||||
spinUntilTrue:isKeyboardInvisible
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet<NSString *> *)auditTypes
|
||||
error:(NSError **)error;
|
||||
{
|
||||
uint64_t numTypes = 0;
|
||||
NSDictionary *namesMap = auditTypeNamesToValues();
|
||||
for (NSString *value in auditTypes) {
|
||||
NSNumber *typeValue = namesMap[value];
|
||||
if (nil == typeValue) {
|
||||
NSString *reason = [NSString stringWithFormat:@"Audit type value '%@' is not known. Only the following audit types are supported: %@", value, namesMap.allKeys];
|
||||
@throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:@{}];
|
||||
}
|
||||
numTypes |= [typeValue unsignedLongLongValue];
|
||||
}
|
||||
return [self fb_performAccessibilityAuditWithAuditTypes:numTypes error:error];
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes
|
||||
error:(NSError **)error;
|
||||
{
|
||||
SEL selector = NSSelectorFromString(@"performAccessibilityAuditWithAuditTypes:issueHandler:error:");
|
||||
if (![self respondsToSelector:selector]) {
|
||||
[[[FBErrorBuilder alloc]
|
||||
withDescription:@"Accessibility audit is only supported since iOS 17/Xcode 15"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
|
||||
// These custom attributes could take too long to fetch, thus excluded
|
||||
NSSet *customAttributesToExclude = [NSSet setWithArray:[customExclusionAttributesMap() allKeys]];
|
||||
NSMutableArray<NSDictionary *> *resultArray = [NSMutableArray array];
|
||||
NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
|
||||
[invocation setSelector:selector];
|
||||
[invocation setArgument:&auditTypes atIndex:2];
|
||||
BOOL (^issueHandler)(id) = ^BOOL(id issue) {
|
||||
@autoreleasepool {
|
||||
NSString *auditType = @"";
|
||||
NSDictionary *valuesToNamesMap = auditTypeValuesToNames();
|
||||
NSNumber *auditTypeValue = [issue valueForKey:@"auditType"];
|
||||
if (nil != auditTypeValue) {
|
||||
auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue];
|
||||
}
|
||||
|
||||
id extractedElement = extractIssueProperty(issue, @"element");
|
||||
|
||||
id<FBXCElementSnapshot> elementSnapshot = [extractedElement fb_cachedSnapshot] ?: [extractedElement fb_standardSnapshot];
|
||||
NSDictionary *elementAttributes = elementSnapshot
|
||||
? [self.class dictionaryForElement:elementSnapshot
|
||||
recursive:NO
|
||||
excludedAttributes:customAttributesToExclude]
|
||||
: @{};
|
||||
|
||||
[resultArray addObject:@{
|
||||
@"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"",
|
||||
@"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"",
|
||||
@"auditType": auditType,
|
||||
@"element": [extractedElement description] ?: @"",
|
||||
@"elementDescription": [extractedElement debugDescription] ?: @"",
|
||||
@"elementAttributes": elementAttributes ?: @{},
|
||||
}];
|
||||
return YES;
|
||||
}
|
||||
};
|
||||
[invocation setArgument:&issueHandler atIndex:3];
|
||||
[invocation setArgument:&error atIndex:4];
|
||||
[invocation invokeWithTarget:self];
|
||||
BOOL isSuccessful;
|
||||
[invocation getReturnValue:&isSuccessful];
|
||||
return isSuccessful ? resultArray.copy : nil;
|
||||
}
|
||||
|
||||
+ (instancetype)fb_activeApplication
|
||||
{
|
||||
return [self fb_activeApplicationWithDefaultBundleId:nil];
|
||||
}
|
||||
|
||||
+ (NSArray<XCUIApplication *> *)fb_activeApplications
|
||||
{
|
||||
NSArray<id<FBXCAccessibilityElement>> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications];
|
||||
NSMutableArray<XCUIApplication *> *result = [NSMutableArray array];
|
||||
if (activeApplicationElements.count > 0) {
|
||||
for (id<FBXCAccessibilityElement> applicationElement in activeApplicationElements) {
|
||||
XCUIApplication *app = [XCUIApplication fb_applicationWithPID:applicationElement.processIdentifier];
|
||||
if (nil != app) {
|
||||
[result addObject:app];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.count > 0 ? result.copy : @[self.class.fb_systemApplication];
|
||||
}
|
||||
|
||||
+ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId
|
||||
{
|
||||
NSArray<id<FBXCAccessibilityElement>> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications];
|
||||
id<FBXCAccessibilityElement> activeApplicationElement = nil;
|
||||
id<FBXCAccessibilityElement> currentElement = nil;
|
||||
if (nil != bundleId) {
|
||||
currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement;
|
||||
if (nil != currentElement) {
|
||||
NSArray<NSDictionary *> *appInfos = [self fb_appsInfoWithAxElements:@[currentElement]];
|
||||
[FBLogger logFmt:@"Detected on-screen application: %@", appInfos.firstObject[@"bundleId"]];
|
||||
if ([[appInfos.firstObject objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) {
|
||||
activeApplicationElement = currentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nil == activeApplicationElement && activeApplicationElements.count > 1) {
|
||||
if (nil != bundleId) {
|
||||
NSArray<NSDictionary *> *appInfos = [self fb_appsInfoWithAxElements:activeApplicationElements];
|
||||
NSMutableArray<NSString *> *bundleIds = [NSMutableArray array];
|
||||
for (NSDictionary *appInfo in appInfos) {
|
||||
[bundleIds addObject:(NSString *)appInfo[@"bundleId"]];
|
||||
}
|
||||
[FBLogger logFmt:@"Detected system active application(s): %@", bundleIds];
|
||||
// Try to select the desired application first
|
||||
for (NSUInteger appIdx = 0; appIdx < appInfos.count; appIdx++) {
|
||||
if ([[[appInfos objectAtIndex:appIdx] objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) {
|
||||
activeApplicationElement = [activeApplicationElements objectAtIndex:appIdx];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to the "normal" algorithm if the desired application is either
|
||||
// not set or is not active
|
||||
if (nil == activeApplicationElement) {
|
||||
if (nil == currentElement) {
|
||||
currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement;
|
||||
}
|
||||
if (nil == currentElement) {
|
||||
[FBLogger log:@"Cannot precisely detect the current application. Will use the system's recently active one"];
|
||||
if (nil == bundleId) {
|
||||
[FBLogger log:@"Consider changing the 'defaultActiveApplication' setting to the bundle identifier of the desired application under test"];
|
||||
}
|
||||
} else {
|
||||
for (id<FBXCAccessibilityElement> appElement in activeApplicationElements) {
|
||||
if (appElement.processIdentifier == currentElement.processIdentifier) {
|
||||
activeApplicationElement = appElement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nil != activeApplicationElement) {
|
||||
XCUIApplication *application = [XCUIApplication fb_applicationWithPID:activeApplicationElement.processIdentifier];
|
||||
if (nil != application) {
|
||||
return application;
|
||||
}
|
||||
[FBLogger log:@"Cannot translate the active process identifier into an application object"];
|
||||
}
|
||||
|
||||
if (activeApplicationElements.count > 0) {
|
||||
[FBLogger logFmt:@"Getting the most recent active application (out of %@ total items)", @(activeApplicationElements.count)];
|
||||
for (id<FBXCAccessibilityElement> appElement in activeApplicationElements) {
|
||||
XCUIApplication *application = [XCUIApplication fb_applicationWithPID:appElement.processIdentifier];
|
||||
if (nil != application) {
|
||||
return application;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[FBLogger log:@"Cannot retrieve any active applications. Assuming the system application is the active one"];
|
||||
return [self fb_systemApplication];
|
||||
}
|
||||
|
||||
+ (instancetype)fb_systemApplication
|
||||
{
|
||||
return [self fb_applicationWithPID:
|
||||
[[FBXCAXClientProxy.sharedClient systemApplication] processIdentifier]];
|
||||
}
|
||||
|
||||
+ (instancetype)fb_applicationWithPID:(pid_t)processID
|
||||
{
|
||||
return [FBXCAXClientProxy.sharedClient monitoredApplicationWithProcessIdentifier:processID];
|
||||
}
|
||||
|
||||
+ (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error
|
||||
{
|
||||
XCUIApplication *systemApp = self.fb_systemApplication;
|
||||
@try {
|
||||
if (systemApp.running) {
|
||||
[systemApp activate];
|
||||
} else {
|
||||
[systemApp launch];
|
||||
}
|
||||
} @catch (NSException *e) {
|
||||
return [[[FBErrorBuilder alloc]
|
||||
withDescription:nil == e ? @"Cannot open the home screen" : e.reason]
|
||||
buildError:error];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)fb_isSameAppAs:(nullable XCUIApplication *)otherApp
|
||||
{
|
||||
if (nil == otherApp) {
|
||||
return NO;
|
||||
}
|
||||
return self == otherApp || [self.bundleID isEqualToString:(NSString *)otherApp.bundleID];
|
||||
}
|
||||
|
||||
@end
|
||||
24
WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h
Normal file
24
WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
#import "XCUIApplication.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIApplication (FBQuiescence)
|
||||
|
||||
/**
|
||||
It allows to turn on/off waiting for application quiescence, while performing queries. Defaults to YES.
|
||||
This value mirrors the corresponding property of the connected XCUIApplicationProcess instance.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL fb_shouldWaitForQuiescence;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
28
WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m
Normal file
28
WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 "XCUIApplication+FBQuiescence.h"
|
||||
|
||||
#import "XCUIApplicationImpl.h"
|
||||
#import "XCUIApplicationProcess.h"
|
||||
#import "XCUIApplicationProcess+FBQuiescence.h"
|
||||
|
||||
|
||||
@implementation XCUIApplication (FBQuiescence)
|
||||
|
||||
- (BOOL)fb_shouldWaitForQuiescence
|
||||
{
|
||||
return [[self applicationImpl] currentProcess].fb_shouldWaitForQuiescence.boolValue;
|
||||
}
|
||||
|
||||
- (void)setFb_shouldWaitForQuiescence:(BOOL)value
|
||||
{
|
||||
[[self applicationImpl] currentProcess].fb_shouldWaitForQuiescence = @(value);
|
||||
}
|
||||
|
||||
@end
|
||||
29
WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h
Normal file
29
WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
#import "FBElementCache.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIApplication (FBTouchAction)
|
||||
|
||||
/**
|
||||
Perform complex touch action in scope of the current application.
|
||||
|
||||
@param actions Array of dictionaries, whose format is described in W3C spec (https://github.com/jlipps/simple-wd-spec#perform-actions)
|
||||
@param elementCache Cached elements mapping for the currrent application. The method assumes all elements are already represented by their actual instances if nil value is set
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem
|
||||
@return YES If the touch action has been successfully performed without errors
|
||||
*/
|
||||
- (BOOL)fb_performW3CActions:(NSArray *)actions elementCache:(nullable FBElementCache *)elementCache error:(NSError * _Nullable*)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
75
WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m
Normal file
75
WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 "XCUIApplication+FBTouchAction.h"
|
||||
|
||||
#import "FBBaseActionsSynthesizer.h"
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBExceptions.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBRunLoopSpinner.h"
|
||||
#import "FBW3CActionsSynthesizer.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "XCEventGenerator.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
@implementation XCUIApplication (FBTouchAction)
|
||||
|
||||
+ (BOOL)handleEventSynthesWithError:(NSError *)error
|
||||
{
|
||||
if ([error.localizedDescription containsString:@"not visible"]) {
|
||||
[[NSException exceptionWithName:FBElementNotVisibleException
|
||||
reason:error.localizedDescription
|
||||
userInfo:error.userInfo] raise];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)fb_performActionsWithSynthesizerType:(Class)synthesizerType
|
||||
actions:(NSArray *)actions
|
||||
elementCache:(FBElementCache *)elementCache
|
||||
error:(NSError **)error
|
||||
{
|
||||
FBBaseActionsSynthesizer *synthesizer = [[synthesizerType alloc] initWithActions:actions
|
||||
forApplication:self
|
||||
elementCache:elementCache
|
||||
error:error];
|
||||
if (nil == synthesizer) {
|
||||
return NO;
|
||||
}
|
||||
XCSynthesizedEventRecord *eventRecord = [synthesizer synthesizeWithError:error];
|
||||
if (nil == eventRecord) {
|
||||
return [self.class handleEventSynthesWithError:*error];
|
||||
}
|
||||
return [self fb_synthesizeEvent:eventRecord error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_performW3CActions:(NSArray *)actions
|
||||
elementCache:(FBElementCache *)elementCache
|
||||
error:(NSError **)error
|
||||
{
|
||||
if (![self fb_performActionsWithSynthesizerType:FBW3CActionsSynthesizer.class
|
||||
actions:actions
|
||||
elementCache:elementCache
|
||||
error:error]) {
|
||||
return NO;
|
||||
}
|
||||
[self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)fb_synthesizeEvent:(XCSynthesizedEventRecord *)event error:(NSError *__autoreleasing*)error
|
||||
{
|
||||
return [FBXCTestDaemonsProxy synthesizeEventWithRecord:event error:error];
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIApplication (FBUIInterruptions)
|
||||
|
||||
/**
|
||||
* Disables automatic UI interruptions handling for all applications.
|
||||
*/
|
||||
+ (void)fb_disableUIInterruptionsHandling;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 "XCUIApplication+FBUIInterruptions.h"
|
||||
|
||||
#import "FBReflectionUtils.h"
|
||||
#import "XCUIApplication.h"
|
||||
|
||||
@implementation XCUIApplication (FBUIInterruptions)
|
||||
|
||||
- (BOOL)fb_doesNotHandleUIInterruptions
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (void)fb_disableUIInterruptionsHandling
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
FBReplaceMethod([self class],
|
||||
@selector(doesNotHandleUIInterruptions),
|
||||
@selector(fb_doesNotHandleUIInterruptions));
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
#import "XCUIApplicationProcess.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIApplicationProcess (FBQuiescence)
|
||||
|
||||
/*! Defines wtether the process should perform quiescence checks. YES by default */
|
||||
@property (nonatomic) NSNumber* fb_shouldWaitForQuiescence;
|
||||
|
||||
/**
|
||||
@param waitForAnimations Set it to YES if XCTest should also wait for application animations to complete
|
||||
*/
|
||||
- (void)fb_waitForQuiescenceIncludingAnimationsIdle:(bool)waitForAnimations;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 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 "XCUIApplicationProcess+FBQuiescence.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBExceptions.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBSettings.h"
|
||||
|
||||
static void (*original_waitForQuiescenceIncludingAnimationsIdle)(id, SEL, BOOL);
|
||||
static void (*original_waitForQuiescenceIncludingAnimationsIdlePreEvent)(id, SEL, BOOL, BOOL);
|
||||
|
||||
static void swizzledWaitForQuiescenceIncludingAnimationsIdle(id self, SEL _cmd, BOOL includingAnimations)
|
||||
{
|
||||
NSString *bundleId = [self bundleID];
|
||||
if (![[self fb_shouldWaitForQuiescence] boolValue] || FBConfiguration.waitForIdleTimeout < DBL_EPSILON) {
|
||||
[FBLogger logFmt:@"Quiescence checks are disabled for %@ application. Making it to believe it is idling",
|
||||
bundleId];
|
||||
return;
|
||||
}
|
||||
|
||||
NSTimeInterval desiredTimeout = FBConfiguration.waitForIdleTimeout;
|
||||
NSTimeInterval previousTimeout = _XCTApplicationStateTimeout();
|
||||
_XCTSetApplicationStateTimeout(desiredTimeout);
|
||||
[FBLogger logFmt:@"Waiting up to %@s until %@ is in idle state (%@ animations)",
|
||||
@(desiredTimeout), bundleId, includingAnimations ? @"including" : @"excluding"];
|
||||
@try {
|
||||
original_waitForQuiescenceIncludingAnimationsIdle(self, _cmd, includingAnimations);
|
||||
} @finally {
|
||||
_XCTSetApplicationStateTimeout(previousTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
static void swizzledWaitForQuiescenceIncludingAnimationsIdlePreEvent(id self, SEL _cmd, BOOL includingAnimations, BOOL isPreEvent)
|
||||
{
|
||||
NSString *bundleId = [self bundleID];
|
||||
if (![[self fb_shouldWaitForQuiescence] boolValue] || FBConfiguration.waitForIdleTimeout < DBL_EPSILON) {
|
||||
[FBLogger logFmt:@"Quiescence checks are disabled for %@ application. Making it to believe it is idling",
|
||||
bundleId];
|
||||
return;
|
||||
}
|
||||
|
||||
NSTimeInterval desiredTimeout = FBConfiguration.waitForIdleTimeout;
|
||||
NSTimeInterval previousTimeout = _XCTApplicationStateTimeout();
|
||||
_XCTSetApplicationStateTimeout(desiredTimeout);
|
||||
[FBLogger logFmt:@"Waiting up to %@s until %@ is in idle state (%@ animations)",
|
||||
@(desiredTimeout), bundleId, includingAnimations ? @"including" : @"excluding"];
|
||||
@try {
|
||||
original_waitForQuiescenceIncludingAnimationsIdlePreEvent(self, _cmd, includingAnimations, isPreEvent);
|
||||
} @finally {
|
||||
_XCTSetApplicationStateTimeout(previousTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
@implementation XCUIApplicationProcess (FBQuiescence)
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-load-method"
|
||||
#pragma clang diagnostic ignored "-Wcast-function-type-strict"
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
Method waitForQuiescenceIncludingAnimationsIdleMethod = class_getInstanceMethod(self.class, @selector(waitForQuiescenceIncludingAnimationsIdle:));
|
||||
Method waitForQuiescenceIncludingAnimationsIdlePreEventMethod = class_getInstanceMethod(self.class, @selector(waitForQuiescenceIncludingAnimationsIdle:isPreEvent:));
|
||||
if (nil != waitForQuiescenceIncludingAnimationsIdleMethod) {
|
||||
IMP swizzledImp = (IMP)swizzledWaitForQuiescenceIncludingAnimationsIdle;
|
||||
original_waitForQuiescenceIncludingAnimationsIdle = (void (*)(id, SEL, BOOL)) method_setImplementation(waitForQuiescenceIncludingAnimationsIdleMethod, swizzledImp);
|
||||
} else if (nil != waitForQuiescenceIncludingAnimationsIdlePreEventMethod) {
|
||||
IMP swizzledImp = (IMP)swizzledWaitForQuiescenceIncludingAnimationsIdlePreEvent;
|
||||
original_waitForQuiescenceIncludingAnimationsIdlePreEvent = (void (*)(id, SEL, BOOL, BOOL)) method_setImplementation(waitForQuiescenceIncludingAnimationsIdlePreEventMethod, swizzledImp);
|
||||
} else {
|
||||
[FBLogger log:@"Could not find method -[XCUIApplicationProcess waitForQuiescenceIncludingAnimationsIdle:]"];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
static char XCUIAPPLICATIONPROCESS_SHOULD_WAIT_FOR_QUIESCENCE;
|
||||
|
||||
@dynamic fb_shouldWaitForQuiescence;
|
||||
|
||||
- (NSNumber *)fb_shouldWaitForQuiescence
|
||||
{
|
||||
id result = objc_getAssociatedObject(self, &XCUIAPPLICATIONPROCESS_SHOULD_WAIT_FOR_QUIESCENCE);
|
||||
if (nil == result) {
|
||||
return @(YES);
|
||||
}
|
||||
return (NSNumber *)result;
|
||||
}
|
||||
|
||||
- (void)setFb_shouldWaitForQuiescence:(NSNumber *)value
|
||||
{
|
||||
objc_setAssociatedObject(self, &XCUIAPPLICATIONPROCESS_SHOULD_WAIT_FOR_QUIESCENCE, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
- (void)fb_waitForQuiescenceIncludingAnimationsIdle:(bool)waitForAnimations
|
||||
{
|
||||
if ([self respondsToSelector:@selector(waitForQuiescenceIncludingAnimationsIdle:)]) {
|
||||
[self waitForQuiescenceIncludingAnimationsIdle:waitForAnimations];
|
||||
} else if ([self respondsToSelector:@selector(waitForQuiescenceIncludingAnimationsIdle:isPreEvent:)]) {
|
||||
[self waitForQuiescenceIncludingAnimationsIdle:waitForAnimations isPreEvent:NO];
|
||||
} else {
|
||||
@throw [NSException exceptionWithName:FBIncompatibleWdaException
|
||||
reason:@"The current WebDriverAgent build is not compatible to your device OS version"
|
||||
userInfo:@{}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
30
WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h
Normal file
30
WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
@interface XCUIDevice (FBHealthCheck)
|
||||
|
||||
/**
|
||||
Checks health of XCTest by:
|
||||
1) Querying application for some elements,
|
||||
2) Triggering some device events.
|
||||
|
||||
!!! Health check might modify simulator state so it should only be called in-between testing sessions
|
||||
|
||||
@param application application used to issue queries
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_healthCheckWithApplication:(nullable XCUIApplication *)application;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
47
WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m
Normal file
47
WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 "XCUIDevice+FBHealthCheck.h"
|
||||
|
||||
#import "XCUIDevice+FBRotation.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
|
||||
@implementation XCUIDevice (FBHealthCheck)
|
||||
|
||||
- (BOOL)fb_healthCheckWithApplication:(nullable XCUIApplication *)application
|
||||
{
|
||||
if (![self fb_elementQueryCheckWithApplication:application]) {
|
||||
return NO;
|
||||
}
|
||||
if (![self fb_deviceInteractionCheck]) {
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)fb_elementQueryCheckWithApplication:(nullable XCUIApplication *)application
|
||||
{
|
||||
if (!application) {
|
||||
return NO;
|
||||
}
|
||||
if (!application.label) {
|
||||
return NO;
|
||||
}
|
||||
if ([application descendantsMatchingType:XCUIElementTypeAny].count == 0 ) {
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)fb_deviceInteractionCheck
|
||||
{
|
||||
[self pressButton:XCUIDeviceButtonHome];
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
193
WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h
Normal file
193
WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) {
|
||||
FBUIInterfaceAppearanceUnspecified,
|
||||
FBUIInterfaceAppearanceLight,
|
||||
FBUIInterfaceAppearanceDark
|
||||
};
|
||||
|
||||
@interface XCUIDevice (FBHelpers)
|
||||
|
||||
/**
|
||||
Matches or mismatches TouchID request
|
||||
|
||||
@param shouldMatch determines if TouchID should be matched
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_fingerTouchShouldMatch:(BOOL)shouldMatch;
|
||||
|
||||
/**
|
||||
Forces the device under test to switch to the home screen
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_goToHomescreenWithError:(NSError **)error;
|
||||
|
||||
/**
|
||||
Checks if the screen is locked or not.
|
||||
|
||||
@return YES if screen is locked
|
||||
*/
|
||||
- (BOOL)fb_isScreenLocked;
|
||||
|
||||
/**
|
||||
Forces the device under test to switch to the lock screen. An immediate return will happen if the device is already locked and an error is going to be thrown if the screen has not been locked after the timeout.
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_lockScreen:(NSError **)error;
|
||||
|
||||
/**
|
||||
Forces the device under test to unlock. An immediate return will happen if the device is already unlocked and an error is going to be thrown if the screen has not been unlocked after the timeout.
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_unlockScreen:(NSError **)error;
|
||||
|
||||
/**
|
||||
Returns screenshot
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return Device screenshot as PNG-encoded data or nil in case of failure
|
||||
*/
|
||||
- (nullable NSData *)fb_screenshotWithError:(NSError*__autoreleasing*)error;
|
||||
|
||||
/**
|
||||
Returns device's current wifi ip4 address
|
||||
*/
|
||||
- (nullable NSString *)fb_wifiIPAddress;
|
||||
|
||||
/**
|
||||
Opens the particular url scheme using the default application assigned to it.
|
||||
This API only works since XCode 14.3/iOS 16.4
|
||||
Older Xcode/iOS version try to use Siri fallback.
|
||||
|
||||
@param url The url scheme represented as a string, for example https://apple.com
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation was successful
|
||||
*/
|
||||
- (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Opens the particular url scheme using the given application
|
||||
This API only works since XCode 14.3/iOS 16.4
|
||||
|
||||
@param url The url scheme represented as a string, for example https://apple.com
|
||||
@param bundleId The bundle identifier of an application to use in order to open the given URL
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation was successful
|
||||
*/
|
||||
- (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Presses the corresponding hardware button on the device with duration.
|
||||
|
||||
@param buttonName One of the supported button names: volumeUp (real devices only), volumeDown (real device only), home
|
||||
@param duration Duration in seconds or nil.
|
||||
This argument works only on tvOS. When this argument is nil on tvOS,
|
||||
https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton will be called.
|
||||
Others are https://developer.apple.com/documentation/xctest/xcuiremote/1627475-pressbutton.
|
||||
A single tap when this argument is `nil` is equal to when the duration is 0.005 seconds in XCTest.
|
||||
On iOS, this value will be ignored. It always calls https://developer.apple.com/documentation/xctest/xcuidevice/1619052-pressbutton
|
||||
@return YES if the button has been pressed
|
||||
*/
|
||||
- (BOOL)fb_pressButton:(NSString *)buttonName forDuration:(nullable NSNumber *)duration error:(NSError **)error;
|
||||
|
||||
|
||||
/**
|
||||
Activates Siri service voice recognition with the given text to parse
|
||||
|
||||
@param text The actual string to parse
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES the command has been successfully executed by Siri voice recognition service
|
||||
*/
|
||||
- (BOOL)fb_activateSiriVoiceRecognitionWithText:(NSString *)text error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Emulated triggering of the given low-level IOHID device event. The constants for possible events are defined
|
||||
in https://unix.superglobalmegacorp.com/xnu/newsrc/iokit/IOKit/hidsystem/IOHIDUsageTables.h.html
|
||||
Popular constants:
|
||||
- kHIDPage_Consumer = 0x0C
|
||||
- kHIDUsage_Csmr_VolumeIncrement = 0xE9 (Volume Up)
|
||||
- kHIDUsage_Csmr_VolumeDecrement = 0xEA (Volume Down)
|
||||
- kHIDUsage_Csmr_Menu = 0x40 (Home)
|
||||
- kHIDUsage_Csmr_Power = 0x30 (Power)
|
||||
- kHIDUsage_Csmr_Snapshot = 0x65 (Power + Home)
|
||||
|
||||
@param page The event page identifier
|
||||
@param usage The event usage identifier (usages are defined per-page)
|
||||
@param duration The event duration in float seconds (XCTest uses 0.005 for a single press event)
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES the event has successfully been triggered
|
||||
*/
|
||||
- (BOOL)fb_performIOHIDEventWithPage:(unsigned int)page
|
||||
usage:(unsigned int)usage
|
||||
duration:(NSTimeInterval)duration
|
||||
error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Allows to set device appearance
|
||||
|
||||
@param appearance The desired appearance value
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the appearance has been successfully set
|
||||
*/
|
||||
- (BOOL)fb_setAppearance:(FBUIInterfaceAppearance)appearance error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Get current appearance prefefence.
|
||||
|
||||
@return 0 (automatic), 1 (light) or 2 (dark), or nil
|
||||
*/
|
||||
- (nullable NSNumber *)fb_getAppearance;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
/**
|
||||
Allows to set a simulated geolocation coordinates.
|
||||
Only works since Xcode 14.3/iOS 16.4
|
||||
|
||||
@param location The simlated location coordinates to set
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the simulated location has been successfully set
|
||||
*/
|
||||
- (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Allows to get a simulated geolocation coordinates.
|
||||
Only works since Xcode 14.3/iOS 16.4
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return The current simulated location or nil in case of failure or if no location has previously been seet
|
||||
(the returned error will be nil in the latter case)
|
||||
*/
|
||||
- (nullable CLLocation *)fb_getSimulatedLocation:(NSError **)error;
|
||||
|
||||
/**
|
||||
Allows to clear a previosuly set simulated geolocation coordinates.
|
||||
Only works since Xcode 14.3/iOS 16.4
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the simulated location has been successfully cleared
|
||||
*/
|
||||
- (BOOL)fb_clearSimulatedLocation:(NSError **)error;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
388
WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m
Normal file
388
WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m
Normal file
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* 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 "XCUIDevice+FBHelpers.h"
|
||||
|
||||
#import <arpa/inet.h>
|
||||
#import <ifaddrs.h>
|
||||
#include <notify.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBImageUtils.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBMathUtils.h"
|
||||
#import "FBScreenshot.h"
|
||||
#import "FBXCDeviceEvent.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "XCUIDevice.h"
|
||||
|
||||
static const NSTimeInterval FBHomeButtonCoolOffTime = 1.;
|
||||
static const NSTimeInterval FBScreenLockTimeout = 5.;
|
||||
|
||||
@implementation XCUIDevice (FBHelpers)
|
||||
|
||||
static bool fb_isLocked;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-load-method"
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
[self fb_registerAppforDetectLockState];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
+ (void)fb_registerAppforDetectLockState
|
||||
{
|
||||
int notify_token;
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wstrict-prototypes"
|
||||
notify_register_dispatch("com.apple.springboard.lockstate", ¬ify_token, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token) {
|
||||
uint64_t state = UINT64_MAX;
|
||||
notify_get_state(token, &state);
|
||||
fb_isLocked = state != 0;
|
||||
});
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
- (BOOL)fb_goToHomescreenWithError:(NSError **)error
|
||||
{
|
||||
return [XCUIApplication fb_switchToSystemApplicationWithError:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_lockScreen:(NSError **)error
|
||||
{
|
||||
if (fb_isLocked) {
|
||||
return YES;
|
||||
}
|
||||
[self pressLockButton];
|
||||
return [[[[FBRunLoopSpinner new]
|
||||
timeout:FBScreenLockTimeout]
|
||||
timeoutErrorMessage:@"Timed out while waiting until the screen gets locked"]
|
||||
spinUntilTrue:^BOOL{
|
||||
return fb_isLocked;
|
||||
} error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_isScreenLocked
|
||||
{
|
||||
return fb_isLocked;
|
||||
}
|
||||
|
||||
- (BOOL)fb_unlockScreen:(NSError **)error
|
||||
{
|
||||
if (!fb_isLocked) {
|
||||
return YES;
|
||||
}
|
||||
[self pressButton:XCUIDeviceButtonHome];
|
||||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]];
|
||||
#if !TARGET_OS_TV
|
||||
[self pressButton:XCUIDeviceButtonHome];
|
||||
#else
|
||||
[self pressButton:XCUIDeviceButtonHome];
|
||||
#endif
|
||||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]];
|
||||
return [[[[FBRunLoopSpinner new]
|
||||
timeout:FBScreenLockTimeout]
|
||||
timeoutErrorMessage:@"Timed out while waiting until the screen gets unlocked"]
|
||||
spinUntilTrue:^BOOL{
|
||||
return !fb_isLocked;
|
||||
} error:error];
|
||||
}
|
||||
|
||||
- (NSData *)fb_screenshotWithError:(NSError*__autoreleasing*)error
|
||||
{
|
||||
return [FBScreenshot takeInOriginalResolutionWithQuality:FBConfiguration.screenshotQuality
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_fingerTouchShouldMatch:(BOOL)shouldMatch
|
||||
{
|
||||
const char *name;
|
||||
if (shouldMatch) {
|
||||
name = "com.apple.BiometricKit_Sim.fingerTouch.match";
|
||||
} else {
|
||||
name = "com.apple.BiometricKit_Sim.fingerTouch.nomatch";
|
||||
}
|
||||
return notify_post(name) == NOTIFY_STATUS_OK;
|
||||
}
|
||||
|
||||
- (NSString *)fb_wifiIPAddress
|
||||
{
|
||||
struct ifaddrs *interfaces = NULL;
|
||||
struct ifaddrs *temp_addr = NULL;
|
||||
int success = getifaddrs(&interfaces);
|
||||
if (success != 0) {
|
||||
freeifaddrs(interfaces);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *address = nil;
|
||||
temp_addr = interfaces;
|
||||
while(temp_addr != NULL) {
|
||||
if(temp_addr->ifa_addr->sa_family != AF_INET) {
|
||||
temp_addr = temp_addr->ifa_next;
|
||||
continue;
|
||||
}
|
||||
NSString *interfaceName = [NSString stringWithUTF8String:temp_addr->ifa_name];
|
||||
if(![interfaceName isEqualToString:@"en0"]) {
|
||||
temp_addr = temp_addr->ifa_next;
|
||||
continue;
|
||||
}
|
||||
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
|
||||
break;
|
||||
}
|
||||
freeifaddrs(interfaces);
|
||||
return address;
|
||||
}
|
||||
|
||||
- (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error
|
||||
{
|
||||
NSURL *parsedUrl = [NSURL URLWithString:url];
|
||||
if (nil == parsedUrl) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"'%@' is not a valid URL", url]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
NSError *err;
|
||||
if ([FBXCTestDaemonsProxy openDefaultApplicationForURL:parsedUrl error:&err]) {
|
||||
return YES;
|
||||
}
|
||||
if (![err.description containsString:@"does not support"]) {
|
||||
if (error) {
|
||||
*error = err;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
id siriService = [self valueForKey:@"siriService"];
|
||||
if (nil != siriService) {
|
||||
return [self fb_activateSiriVoiceRecognitionWithText:[NSString stringWithFormat:@"Open {%@}", url] error:error];
|
||||
}
|
||||
|
||||
NSString *description = [NSString stringWithFormat:@"Cannot open '%@' with the default application assigned for it. Consider upgrading to Xcode 14.3+/iOS 16.4+", url];
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"%@", description]
|
||||
buildError:error];;
|
||||
}
|
||||
|
||||
- (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error
|
||||
{
|
||||
NSURL *parsedUrl = [NSURL URLWithString:url];
|
||||
if (nil == parsedUrl) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"'%@' is not a valid URL", url]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
return [FBXCTestDaemonsProxy openURL:parsedUrl usingApplication:bundleId error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_activateSiriVoiceRecognitionWithText:(NSString *)text error:(NSError **)error
|
||||
{
|
||||
id siriService = [self valueForKey:@"siriService"];
|
||||
if (nil == siriService) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescription:@"Siri service is not available on the device under test"]
|
||||
buildError:error];
|
||||
}
|
||||
SEL selector = NSSelectorFromString(@"activateWithVoiceRecognitionText:");
|
||||
NSMethodSignature *signature = [siriService methodSignatureForSelector:selector];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
|
||||
[invocation setSelector:selector];
|
||||
[invocation setArgument:&text atIndex:2];
|
||||
@try {
|
||||
[invocation invokeWithTarget:siriService];
|
||||
return YES;
|
||||
} @catch (NSException *e) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"%@", e.reason]
|
||||
buildError:error];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)fb_pressButton:(NSString *)buttonName
|
||||
forDuration:(nullable NSNumber *)duration
|
||||
error:(NSError **)error
|
||||
{
|
||||
#if !TARGET_OS_TV
|
||||
return [self fb_pressButton:buttonName error:error];
|
||||
#else
|
||||
NSMutableArray<NSString *> *supportedButtonNames = [NSMutableArray array];
|
||||
NSInteger remoteButton = -1; // no remote button
|
||||
if ([buttonName.lowercaseString isEqualToString:@"home"]) {
|
||||
// XCUIRemoteButtonHome = 7
|
||||
remoteButton = XCUIRemoteButtonHome;
|
||||
}
|
||||
[supportedButtonNames addObject:@"home"];
|
||||
|
||||
// https://developer.apple.com/design/human-interface-guidelines/tvos/remote-and-controllers/remote/
|
||||
if ([buttonName.lowercaseString isEqualToString:@"up"]) {
|
||||
// XCUIRemoteButtonUp = 0,
|
||||
remoteButton = XCUIRemoteButtonUp;
|
||||
}
|
||||
[supportedButtonNames addObject:@"up"];
|
||||
|
||||
if ([buttonName.lowercaseString isEqualToString:@"down"]) {
|
||||
// XCUIRemoteButtonDown = 1,
|
||||
remoteButton = XCUIRemoteButtonDown;
|
||||
}
|
||||
[supportedButtonNames addObject:@"down"];
|
||||
|
||||
if ([buttonName.lowercaseString isEqualToString:@"left"]) {
|
||||
// XCUIRemoteButtonLeft = 2,
|
||||
remoteButton = XCUIRemoteButtonLeft;
|
||||
}
|
||||
[supportedButtonNames addObject:@"left"];
|
||||
|
||||
if ([buttonName.lowercaseString isEqualToString:@"right"]) {
|
||||
// XCUIRemoteButtonRight = 3,
|
||||
remoteButton = XCUIRemoteButtonRight;
|
||||
}
|
||||
[supportedButtonNames addObject:@"right"];
|
||||
|
||||
if ([buttonName.lowercaseString isEqualToString:@"menu"]) {
|
||||
// XCUIRemoteButtonMenu = 5,
|
||||
remoteButton = XCUIRemoteButtonMenu;
|
||||
}
|
||||
[supportedButtonNames addObject:@"menu"];
|
||||
|
||||
if ([buttonName.lowercaseString isEqualToString:@"playpause"]) {
|
||||
// XCUIRemoteButtonPlayPause = 6,
|
||||
remoteButton = XCUIRemoteButtonPlayPause;
|
||||
}
|
||||
[supportedButtonNames addObject:@"playpause"];
|
||||
|
||||
if ([buttonName.lowercaseString isEqualToString:@"select"]) {
|
||||
// XCUIRemoteButtonSelect = 4,
|
||||
remoteButton = XCUIRemoteButtonSelect;
|
||||
}
|
||||
[supportedButtonNames addObject:@"select"];
|
||||
|
||||
if (remoteButton == -1) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The button '%@' is unknown. Only the following button names are supported: %@", buttonName, supportedButtonNames]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
// https://developer.apple.com/documentation/xctest/xcuiremote/1627475-pressbutton
|
||||
[[XCUIRemote sharedRemote] pressButton:remoteButton forDuration:duration.doubleValue];
|
||||
} else {
|
||||
// https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton
|
||||
[[XCUIRemote sharedRemote] pressButton:remoteButton];
|
||||
}
|
||||
|
||||
return YES;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (BOOL)fb_pressButton:(NSString *)buttonName
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSMutableArray<NSString *> *supportedButtonNames = [NSMutableArray array];
|
||||
XCUIDeviceButton dstButton = 0;
|
||||
if ([buttonName.lowercaseString isEqualToString:@"home"]) {
|
||||
dstButton = XCUIDeviceButtonHome;
|
||||
}
|
||||
[supportedButtonNames addObject:@"home"];
|
||||
#if !TARGET_OS_SIMULATOR
|
||||
if ([buttonName.lowercaseString isEqualToString:@"volumeup"]) {
|
||||
dstButton = XCUIDeviceButtonVolumeUp;
|
||||
}
|
||||
if ([buttonName.lowercaseString isEqualToString:@"volumedown"]) {
|
||||
dstButton = XCUIDeviceButtonVolumeDown;
|
||||
}
|
||||
[supportedButtonNames addObject:@"volumeUp"];
|
||||
[supportedButtonNames addObject:@"volumeDown"];
|
||||
#endif
|
||||
|
||||
if (dstButton == 0) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The button '%@' is unknown. Only the following button names are supported: %@", buttonName, supportedButtonNames]
|
||||
buildError:error];
|
||||
}
|
||||
[self pressButton:dstButton];
|
||||
return YES;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (BOOL)fb_performIOHIDEventWithPage:(unsigned int)page
|
||||
usage:(unsigned int)usage
|
||||
duration:(NSTimeInterval)duration
|
||||
error:(NSError **)error
|
||||
{
|
||||
id<FBXCDeviceEvent> event = FBCreateXCDeviceEvent(page, usage, duration, error);
|
||||
return nil == event ? NO : [self performDeviceEvent:event error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_setAppearance:(FBUIInterfaceAppearance)appearance error:(NSError **)error
|
||||
{
|
||||
SEL selector = NSSelectorFromString(@"setAppearanceMode:");
|
||||
if (nil != selector && [self respondsToSelector:selector]) {
|
||||
NSMethodSignature *signature = [self methodSignatureForSelector:selector];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
|
||||
[invocation setSelector:selector];
|
||||
[invocation setTarget:self];
|
||||
[invocation setArgument:&appearance atIndex:2];
|
||||
[invocation invoke];
|
||||
return YES;
|
||||
}
|
||||
|
||||
#if __clang_major__ >= 15 || (__clang_major__ >= 14 && __clang_minor__ >= 0 && __clang_patchlevel__ >= 3)
|
||||
// Xcode 14.3.1 can build these values.
|
||||
// For iOS 17+
|
||||
if ([self respondsToSelector:NSSelectorFromString(@"appearance")]) {
|
||||
self.appearance = (XCUIDeviceAppearance) appearance;
|
||||
return YES;
|
||||
}
|
||||
#endif
|
||||
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Current Xcode SDK does not support appearance changing"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
- (NSNumber *)fb_getAppearance
|
||||
{
|
||||
#if __clang_major__ >= 15 || (__clang_major__ >= 14 && __clang_minor__ >= 0 && __clang_patchlevel__ >= 3)
|
||||
// Xcode 14.3.1 can build these values.
|
||||
// For iOS 17+
|
||||
if ([self respondsToSelector:NSSelectorFromString(@"appearance")]) {
|
||||
return [NSNumber numberWithLongLong:[self appearance]];
|
||||
}
|
||||
#endif
|
||||
|
||||
return [self respondsToSelector:@selector(appearanceMode)]
|
||||
? [NSNumber numberWithLongLong:[self appearanceMode]]
|
||||
: nil;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error
|
||||
{
|
||||
return [FBXCTestDaemonsProxy setSimulatedLocation:location error:error];
|
||||
}
|
||||
|
||||
- (nullable CLLocation *)fb_getSimulatedLocation:(NSError **)error
|
||||
{
|
||||
return [FBXCTestDaemonsProxy getSimulatedLocation:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_clearSimulatedLocation:(NSError **)error
|
||||
{
|
||||
return [FBXCTestDaemonsProxy clearSimulatedLocation:error];
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
38
WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h
Normal file
38
WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@interface XCUIDevice (FBRotation)
|
||||
|
||||
/**
|
||||
Sets requested device interface orientation.
|
||||
|
||||
@param orientation The interface orientation.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_setDeviceInterfaceOrientation:(UIDeviceOrientation)orientation;
|
||||
|
||||
/**
|
||||
Sets the devices orientation to the rotation passed.
|
||||
|
||||
@param rotationObj The rotation defining the devices orientation.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_setDeviceRotation:(NSDictionary *)rotationObj;
|
||||
|
||||
/*! The UIDeviceOrientation to rotation mappings */
|
||||
@property (strong, nonatomic, readonly) NSDictionary *fb_rotationMapping;
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
68
WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m
Normal file
68
WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 "XCUIDevice+FBRotation.h"
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
|
||||
# if !TARGET_OS_TV
|
||||
|
||||
@implementation XCUIDevice (FBRotation)
|
||||
|
||||
- (BOOL)fb_setDeviceInterfaceOrientation:(UIDeviceOrientation)orientation
|
||||
{
|
||||
XCUIApplication *application = XCUIApplication.fb_activeApplication;
|
||||
[XCUIDevice sharedDevice].orientation = orientation;
|
||||
return [self waitUntilInterfaceIsAtOrientation:orientation application:application];
|
||||
}
|
||||
|
||||
- (BOOL)fb_setDeviceRotation:(NSDictionary *)rotationObj
|
||||
{
|
||||
NSArray<NSNumber *> *keysForRotationObj = [self.fb_rotationMapping allKeysForObject:rotationObj];
|
||||
if (keysForRotationObj.count == 0) {
|
||||
return NO;
|
||||
}
|
||||
NSInteger orientation = keysForRotationObj.firstObject.integerValue;
|
||||
XCUIApplication *application = XCUIApplication.fb_activeApplication;
|
||||
[XCUIDevice sharedDevice].orientation = orientation;
|
||||
return [self waitUntilInterfaceIsAtOrientation:orientation application:application];
|
||||
}
|
||||
|
||||
- (BOOL)waitUntilInterfaceIsAtOrientation:(NSInteger)orientation application:(XCUIApplication *)application
|
||||
{
|
||||
// Tapping elements immediately after rotation may fail due to way UIKit is handling touches.
|
||||
// We should wait till UI cools off, before continuing
|
||||
[application fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
|
||||
|
||||
return application.interfaceOrientation == orientation;
|
||||
}
|
||||
|
||||
- (NSDictionary *)fb_rotationMapping
|
||||
{
|
||||
static NSDictionary *rotationMap;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
rotationMap =
|
||||
@{
|
||||
@(UIDeviceOrientationUnknown) : @{@"x" : @(-1), @"y" : @(-1), @"z" : @(-1)},
|
||||
@(UIDeviceOrientationPortrait) : @{@"x" : @(0), @"y" : @(0), @"z" : @(0)},
|
||||
@(UIDeviceOrientationPortraitUpsideDown) : @{@"x" : @(0), @"y" : @(0), @"z" : @(180)},
|
||||
@(UIDeviceOrientationLandscapeLeft) : @{@"x" : @(0), @"y" : @(0), @"z" : @(270)},
|
||||
@(UIDeviceOrientationLandscapeRight) : @{@"x" : @(0), @"y" : @(0), @"z" : @(90)},
|
||||
@(UIDeviceOrientationFaceUp) : @{@"x" : @(90), @"y" : @(0), @"z" : @(0)},
|
||||
@(UIDeviceOrientationFaceDown) : @{@"x" : @(270), @"y" : @(0), @"z" : @(0)},
|
||||
};
|
||||
});
|
||||
return rotationMap;
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
29
WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h
Normal file
29
WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 <WebDriverAgentLib/XCUIElement.h>
|
||||
#import <WebDriverAgentLib/FBXCElementSnapshotWrapper.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBAccessibility)
|
||||
|
||||
/*! Whether or not the element is accessible */
|
||||
@property (atomic, readonly) BOOL fb_isAccessibilityElement;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (FBAccessibility)
|
||||
|
||||
/*! Whether or not the element in snapshot is accessible */
|
||||
@property (atomic, readonly) BOOL fb_isAccessibilityElement;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
50
WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m
Normal file
50
WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBAccessibility.h"
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
|
||||
@implementation XCUIElement (FBAccessibility)
|
||||
|
||||
- (BOOL)fb_isAccessibilityElement
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isAccessibilityElement;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBXCElementSnapshotWrapper (FBAccessibility)
|
||||
|
||||
- (BOOL)fb_isAccessibilityElement
|
||||
{
|
||||
NSNumber *isAccessibilityElement = self.additionalAttributes[FB_XCAXAIsElementAttribute];
|
||||
if (nil != isAccessibilityElement) {
|
||||
return isAccessibilityElement.boolValue;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSNumber *attributeValue = [self fb_attributeValue:FB_XCAXAIsElementAttributeName
|
||||
error:&error];
|
||||
if (nil != attributeValue) {
|
||||
NSMutableDictionary *updatedValue = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}];
|
||||
[updatedValue setObject:attributeValue forKey:FB_XCAXAIsElementAttribute];
|
||||
self.snapshot.additionalAttributes = updatedValue.copy;
|
||||
return [attributeValue boolValue];
|
||||
}
|
||||
|
||||
NSLog(@"Cannot determine accessibility of '%@' natively: %@. Defaulting to: %@",
|
||||
self.fb_description, error.description, @(NO));
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
19
WebDriverAgentLib/Categories/XCUIElement+FBCaching.h
Normal file
19
WebDriverAgentLib/Categories/XCUIElement+FBCaching.h
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBCaching)
|
||||
|
||||
@property (nonatomic, readonly) NSString *fb_cacheId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
36
WebDriverAgentLib/Categories/XCUIElement+FBCaching.m
Normal file
36
WebDriverAgentLib/Categories/XCUIElement+FBCaching.m
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBCaching.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBUID.h"
|
||||
|
||||
@implementation XCUIElement (FBCaching)
|
||||
|
||||
static char XCUIELEMENT_CACHE_ID_KEY;
|
||||
|
||||
@dynamic fb_cacheId;
|
||||
|
||||
- (NSString *)fb_cacheId
|
||||
{
|
||||
id result = objc_getAssociatedObject(self, &XCUIELEMENT_CACHE_ID_KEY);
|
||||
if ([result isKindOfClass:NSString.class]) {
|
||||
return (NSString *)result;
|
||||
}
|
||||
|
||||
NSString *uid = self.fb_uid;
|
||||
objc_setAssociatedObject(self, &XCUIELEMENT_CACHE_ID_KEY, uid, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
return uid;
|
||||
}
|
||||
|
||||
@end
|
||||
57
WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h
Normal file
57
WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBClassChain)
|
||||
|
||||
/**
|
||||
Returns an array of descendants matching given class chain query.
|
||||
This query is similar to xpath, but can only include indexes, predicates and valid class names. Search by direct children and descendant elements is supported. Examples of direct search requests:
|
||||
XCUIElementTypeWindow/XCUIElementTypeButton[3] - select the third child button of the first child window element.
|
||||
XCUIElementTypeWindow - select all the children windows.
|
||||
XCUIElementTypeWindow[2] - select the second child window in the hierarchy. Indexing starts at 1.
|
||||
XCUIElementTypeWindow/XCUIElementTypeAny[3] - select the third child (of any type) of the first child. window
|
||||
XCUIElementTypeWindow[2]/XCUIElementTypeAny - select all the children of the second child window.
|
||||
XCUIElementTypeWindow[2]/XCUIElementTypeAny[-2] - select the second last child of the second child window.
|
||||
One may use '*' (star) character to substitute the universal 'XCUIElementTypeAny' class name.
|
||||
XCUIElementTypeWindow[`name CONTAINS[cd] "blabla"`] - select all windows, where name attribute starts with "blabla" or "BlAbla".
|
||||
XCUIElementTypeWindow[`label BEGINSWITH "blabla"`][-1] - select the last window, where label text begins with "blabla".
|
||||
XCUIElementTypeWindow/XCUIElementTypeAny[`value == "bla1" OR label == "bla2"`] - select all children of the first window, where value is "bla1" or label is "bla2".
|
||||
XCUIElementTypeWindow[`name == "you're the winner"`]/XCUIElementTypeAny[`visible == 1`] - select all visible children of the first window named "you're the winner".
|
||||
XCUIElementTypeWindow/XCUIElementTypeTable/XCUIElementTypeCell[`visible == 1`][$type == XCUIElementTypeImage AND name == 'bla'$]/XCUIElementTypeTextField - select a text field, which is a direct child of a visible table cell, which has at least one descendant image with identifier 'bla'.
|
||||
Predicate string should be always enclosed into ` or $ characters inside square brackets. Use `` or $$ to escape a single ` or $ character inside predicate expression.
|
||||
Single backtick means the predicate expression is applied to the current children. It is the direct alternative of matchingPredicate: query selector.
|
||||
Single dollar sign means the predicate expression is applied to all the descendants of the current element(s). It is the direct alternative of containingPredicate: query selector.
|
||||
Predicate expression should be always put before the index, but never after it. All predicate expressions are executed in the same exact order, which is set in the chain query.
|
||||
It is not recommended to set explicit indexes for intermediate chain elements, because it slows down the lookup speed.
|
||||
|
||||
Indirect descendant search requests are pretty similar to requests above:
|
||||
** /XCUIElementTypeCell[`name BEGINSWITH "A"`][-1]/XCUIElementTypeButton[10] - select the 10-th child button of the very last cell in the tree, whose name starts with 'A'.
|
||||
** /XCUIElementTypeCell[`name BEGINSWITH "B"`] - select all cells in the tree, where name starts with 'B'
|
||||
** /XCUIElementTypeCell[`name BEGINSWITH "C"`]/XCUIElementTypeButton[10] - select the 10-th child button of the first cell in the tree, whose name starts with 'C' and which has at least ten direct children of type XCUIElementTypeButton.
|
||||
** /XCUIElementTypeCell[`name BEGINSWITH "D"`]/ ** /XCUIElementTypeButton - select the all descendant buttons of the first cell in the tree, whose name starts with 'D'.
|
||||
|
||||
Double star and slash is the marker of the fact, that the next following item is the descendant of the previous chain item, rather than its child.
|
||||
|
||||
The matching result is similar to what XCTest's children... and descendants... selector calls of XCUIElement class instances produce when combined into a chain.
|
||||
|
||||
@param classChainQuery valid class chain query string
|
||||
@param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be resolved and returned.
|
||||
This will speed up the search significantly if the given chain matches multiple nodes in the UI tree
|
||||
@return an array of descendants matching given class chain
|
||||
@throws FBUnknownAttributeException if any of predicates in the chain contains unknown attribute(s)
|
||||
*/
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery
|
||||
shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
98
WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m
Normal file
98
WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBClassChain.h"
|
||||
|
||||
#import "FBClassChainQueryParser.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBExceptions.h"
|
||||
|
||||
@implementation XCUIElement (FBClassChain)
|
||||
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
|
||||
{
|
||||
NSError *error;
|
||||
FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:classChainQuery error:&error];
|
||||
if (nil == parsedChain) {
|
||||
@throw [NSException exceptionWithName:FBClassChainQueryParseException reason:error.localizedDescription userInfo:error.userInfo];
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray<FBClassChainItem *> *lookupChain = parsedChain.elements.mutableCopy;
|
||||
FBClassChainItem *chainItem = lookupChain.firstObject;
|
||||
XCUIElement *currentRoot = self;
|
||||
XCUIElementQuery *query = [currentRoot fb_queryWithChainItem:chainItem query:nil];
|
||||
[lookupChain removeObjectAtIndex:0];
|
||||
while (lookupChain.count > 0) {
|
||||
BOOL isRootChanged = NO;
|
||||
if (nil != chainItem.position) {
|
||||
// It is necessary to resolve the query if intermediate element index is not zero or one,
|
||||
// because predicates don't support search by indexes
|
||||
NSArray<XCUIElement *> *currentRootMatch = [self.class fb_matchingElementsWithItem:chainItem
|
||||
query:query
|
||||
shouldReturnAfterFirstMatch:nil];
|
||||
if (0 == currentRootMatch.count) {
|
||||
return @[];
|
||||
}
|
||||
currentRoot = currentRootMatch.firstObject;
|
||||
isRootChanged = YES;
|
||||
}
|
||||
chainItem = [lookupChain firstObject];
|
||||
query = [currentRoot fb_queryWithChainItem:chainItem query:isRootChanged ? nil : query];
|
||||
[lookupChain removeObjectAtIndex:0];
|
||||
}
|
||||
return [self.class fb_matchingElementsWithItem:chainItem
|
||||
query:query
|
||||
shouldReturnAfterFirstMatch:@(shouldReturnAfterFirstMatch)];
|
||||
}
|
||||
|
||||
- (XCUIElementQuery *)fb_queryWithChainItem:(FBClassChainItem *)item query:(nullable XCUIElementQuery *)query
|
||||
{
|
||||
if (item.isDescendant) {
|
||||
if (query) {
|
||||
query = [query descendantsMatchingType:item.type];
|
||||
} else {
|
||||
query = [self.fb_query descendantsMatchingType:item.type];
|
||||
}
|
||||
} else {
|
||||
if (query) {
|
||||
query = [query childrenMatchingType:item.type];
|
||||
} else {
|
||||
query = [self.fb_query childrenMatchingType:item.type];
|
||||
}
|
||||
}
|
||||
if (item.predicates) {
|
||||
for (FBAbstractPredicateItem *predicate in item.predicates) {
|
||||
if ([predicate isKindOfClass:FBSelfPredicateItem.class]) {
|
||||
query = [query matchingPredicate:predicate.value];
|
||||
} else if ([predicate isKindOfClass:FBDescendantPredicateItem.class]) {
|
||||
query = [query containingPredicate:predicate.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
+ (NSArray<XCUIElement *> *)fb_matchingElementsWithItem:(FBClassChainItem *)item query:(XCUIElementQuery *)query shouldReturnAfterFirstMatch:(nullable NSNumber *)shouldReturnAfterFirstMatch
|
||||
{
|
||||
if (1 == item.position.integerValue || (0 == item.position.integerValue && shouldReturnAfterFirstMatch.boolValue)) {
|
||||
XCUIElement *result = query.fb_firstMatch;
|
||||
return result ? @[result] : @[];
|
||||
}
|
||||
NSArray<XCUIElement *> *allMatches = query.fb_allMatches;
|
||||
if (0 == item.position.integerValue) {
|
||||
return allMatches;
|
||||
}
|
||||
if (allMatches.count >= (NSUInteger)ABS(item.position.integerValue)) {
|
||||
return item.position.integerValue > 0
|
||||
? @[[allMatches objectAtIndex:item.position.integerValue - 1]]
|
||||
: @[[allMatches objectAtIndex:allMatches.count + item.position.integerValue]];
|
||||
}
|
||||
return @[];
|
||||
}
|
||||
|
||||
@end
|
||||
75
WebDriverAgentLib/Categories/XCUIElement+FBFind.h
Normal file
75
WebDriverAgentLib/Categories/XCUIElement+FBFind.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBFind)
|
||||
|
||||
/**
|
||||
Returns an array of descendants matching given class name
|
||||
|
||||
@param className requested class name
|
||||
@param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be
|
||||
resolved and returned. This will speed up the search significantly if given class name matches multiple
|
||||
nodes in the UI tree
|
||||
@return an array of descendants matching given class name
|
||||
*/
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingClassName:(NSString *)className shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch;
|
||||
|
||||
/**
|
||||
Returns an array of descendants matching given accessibility id
|
||||
|
||||
@param accessibilityId requested accessibility id
|
||||
@param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be
|
||||
resolved and returned. This will speed up the search significantly if given id matches multiple
|
||||
nodes in the UI tree
|
||||
@return an array of descendants matching given accessibility id
|
||||
*/
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingIdentifier:(NSString *)accessibilityId shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch;
|
||||
|
||||
/**
|
||||
Returns an array of descendants matching given xpath query
|
||||
|
||||
@param xpathQuery requested xpath query
|
||||
@param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be
|
||||
resolved and returned. This will speed up the search significantly if given xpath matches multiple
|
||||
nodes in the UI tree
|
||||
@return an array of descendants matching given xpath query
|
||||
*/
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingXPathQuery:(NSString *)xpathQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch;
|
||||
|
||||
/**
|
||||
Returns an array of descendants matching given predicate.
|
||||
Allowed property names are only these declared in FBElement protocol (property names are received in runtime)
|
||||
and their shortcuts (without 'wd' prefix). All other property names are considered as unknown.
|
||||
|
||||
@param predicate requested predicate
|
||||
@param shouldReturnAfterFirstMatch set it to YES if you want only the first found element to be
|
||||
resolved and returned. This will speed up the search significantly if given predicate matches multiple
|
||||
nodes in the UI tree
|
||||
@return an array of descendants matching given predicate
|
||||
@throw FBUnknownPredicateKeyException in case the given property name is not declared in FBElement protocol
|
||||
*/
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingPredicate:(NSPredicate *)predicate shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch;
|
||||
|
||||
/**
|
||||
Returns an array of descendants with property matching given value
|
||||
|
||||
@param property requested property name
|
||||
@param value requested value of the property
|
||||
@param partialSearch determines whether it should be exact or partial match
|
||||
@return an array of descendants with property matching given value
|
||||
*/
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingProperty:(NSString *)property value:(NSString *)value partialSearch:(BOOL)partialSearch;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
133
WebDriverAgentLib/Categories/XCUIElement+FBFind.m
Normal file
133
WebDriverAgentLib/Categories/XCUIElement+FBFind.m
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBFind.h"
|
||||
|
||||
#import "FBMacros.h"
|
||||
#import "FBElementTypeTransformer.h"
|
||||
#import "FBConfiguration.h"
|
||||
#import "NSPredicate+FBFormat.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCUIElement+FBUID.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
#import "XCUIElementQuery.h"
|
||||
#import "FBElementUtils.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXPath.h"
|
||||
|
||||
@implementation XCUIElement (FBFind)
|
||||
|
||||
+ (NSArray<XCUIElement *> *)fb_extractMatchingElementsFromQuery:(XCUIElementQuery *)query
|
||||
shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
|
||||
{
|
||||
if (!shouldReturnAfterFirstMatch) {
|
||||
return query.fb_allMatches;
|
||||
}
|
||||
XCUIElement *matchedElement = query.fb_firstMatch;
|
||||
return matchedElement ? @[matchedElement] : @[];
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_cachedSnapshotWithQuery:(XCUIElementQuery *)query
|
||||
{
|
||||
return [self isKindOfClass:XCUIApplication.class] ? query.rootElementSnapshot : self.fb_cachedSnapshot;
|
||||
}
|
||||
|
||||
#pragma mark - Search by ClassName
|
||||
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingClassName:(NSString *)className
|
||||
shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
|
||||
{
|
||||
XCUIElementType type = [FBElementTypeTransformer elementTypeWithTypeName:className];
|
||||
XCUIElementQuery *query = [self.fb_query descendantsMatchingType:type];
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
[result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query
|
||||
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]];
|
||||
id<FBXCElementSnapshot> cachedSnapshot = [self fb_cachedSnapshotWithQuery:query];
|
||||
if (type == XCUIElementTypeAny || cachedSnapshot.elementType == type) {
|
||||
if (shouldReturnAfterFirstMatch || result.count == 0) {
|
||||
return @[self];
|
||||
}
|
||||
[result insertObject:self atIndex:0];
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search by property value
|
||||
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingProperty:(NSString *)property
|
||||
value:(NSString *)value
|
||||
partialSearch:(BOOL)partialSearch
|
||||
{
|
||||
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:(partialSearch ? @"%K CONTAINS %@" : @"%K == %@"), property, value];
|
||||
return [self fb_descendantsMatchingPredicate:searchPredicate shouldReturnAfterFirstMatch:NO];
|
||||
}
|
||||
|
||||
#pragma mark - Search by Predicate String
|
||||
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingPredicate:(NSPredicate *)predicate
|
||||
shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
|
||||
{
|
||||
NSPredicate *formattedPredicate = [NSPredicate fb_snapshotBlockPredicateWithPredicate:predicate];
|
||||
XCUIElementQuery *query = [[self.fb_query descendantsMatchingType:XCUIElementTypeAny] matchingPredicate:formattedPredicate];
|
||||
NSMutableArray<XCUIElement *> *result = [NSMutableArray array];
|
||||
[result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query
|
||||
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]];
|
||||
id<FBXCElementSnapshot> cachedSnapshot = [self fb_cachedSnapshotWithQuery:query];
|
||||
// Include self element into predicate search
|
||||
if ([formattedPredicate evaluateWithObject:cachedSnapshot]) {
|
||||
if (shouldReturnAfterFirstMatch || result.count == 0) {
|
||||
return @[self];
|
||||
}
|
||||
[result insertObject:self atIndex:0];
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search by xpath
|
||||
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingXPathQuery:(NSString *)xpathQuery
|
||||
shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
|
||||
{
|
||||
// XPath will try to match elements only class name, so requesting elements by XCUIElementTypeAny will not work. We should use '*' instead.
|
||||
xpathQuery = [xpathQuery stringByReplacingOccurrencesOfString:@"XCUIElementTypeAny" withString:@"*"];
|
||||
NSArray<id<FBXCElementSnapshot>> *matchingSnapshots = [FBXPath matchesWithRootElement:self forQuery:xpathQuery];
|
||||
if (0 == [matchingSnapshots count]) {
|
||||
return @[];
|
||||
}
|
||||
if (shouldReturnAfterFirstMatch) {
|
||||
id<FBXCElementSnapshot> snapshot = matchingSnapshots.firstObject;
|
||||
matchingSnapshots = @[snapshot];
|
||||
}
|
||||
XCUIElement *scopeRoot = FBConfiguration.limitXpathContextScope ? self : self.application;
|
||||
return [scopeRoot fb_filterDescendantsWithSnapshots:matchingSnapshots
|
||||
onlyChildren:NO];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search by Accessibility Id
|
||||
|
||||
- (NSArray<XCUIElement *> *)fb_descendantsMatchingIdentifier:(NSString *)accessibilityId
|
||||
shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
|
||||
{
|
||||
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot,
|
||||
NSDictionary<NSString *,id> * _Nullable bindings) {
|
||||
@autoreleasepool {
|
||||
return [[FBXCElementSnapshotWrapper wdNameWithSnapshot:snapshot] isEqualToString:accessibilityId];
|
||||
}
|
||||
}];
|
||||
return [self fb_descendantsMatchingPredicate:predicate
|
||||
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
|
||||
}
|
||||
|
||||
@end
|
||||
38
WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h
Normal file
38
WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 <WebDriverAgentLib/XCUIElement.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
@interface XCUIElement (FBForceTouch)
|
||||
|
||||
/**
|
||||
Performs force touch on element
|
||||
|
||||
@param relativeCoordinate hit point coordinate relative to the current element position.
|
||||
nil value means to use the default element hit point
|
||||
@param pressure The pressure of the force touch – valid values are [0, touch.maximumPossibleForce]
|
||||
nil value would use the default pressure value
|
||||
@param duration The duration of the gesture in float seconds
|
||||
nil value would use the default duration value
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_forceTouchCoordinate:(nullable NSValue *)relativeCoordinate
|
||||
pressure:(nullable NSNumber *)pressure
|
||||
duration:(nullable NSNumber *)duration
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
52
WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m
Normal file
52
WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBForceTouch.h"
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "XCUICoordinate.h"
|
||||
#import "XCUIDevice.h"
|
||||
|
||||
@implementation XCUIElement (FBForceTouch)
|
||||
|
||||
- (BOOL)fb_forceTouchCoordinate:(NSValue *)relativeCoordinate
|
||||
pressure:(NSNumber *)pressure
|
||||
duration:(NSNumber *)duration
|
||||
error:(NSError **)error
|
||||
{
|
||||
if (![XCUIDevice sharedDevice].supportsPressureInteraction) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Force press is not supported on this device"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
if (nil == relativeCoordinate) {
|
||||
if (nil == pressure || nil == duration) {
|
||||
[self forcePress];
|
||||
} else {
|
||||
[self pressWithPressure:[pressure doubleValue] duration:[duration doubleValue]];
|
||||
}
|
||||
} else {
|
||||
CGVector offset = CGVectorMake(relativeCoordinate.CGPointValue.x,
|
||||
relativeCoordinate.CGPointValue.y);
|
||||
XCUICoordinate *hitPoint = [[self coordinateWithNormalizedOffset:CGVectorMake(0, 0)]
|
||||
coordinateWithOffset:offset];
|
||||
if (nil == pressure || nil == duration) {
|
||||
[hitPoint forcePress];
|
||||
} else {
|
||||
[hitPoint pressWithPressure:[pressure doubleValue] duration:[duration doubleValue]];
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
28
WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h
Normal file
28
WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 <WebDriverAgentLib/FBXCElementSnapshotWrapper.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBIsVisible)
|
||||
|
||||
/*! Whether or not the element is visible */
|
||||
@property (atomic, readonly) BOOL fb_isVisible;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (FBIsVisible)
|
||||
|
||||
/*! Whether or not the element is visible */
|
||||
@property (atomic, readonly) BOOL fb_isVisible;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
78
WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m
Normal file
78
WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBIsVisible.h"
|
||||
|
||||
#import "FBElementUtils.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBVisibleFrame.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
|
||||
NSNumber* _Nullable fetchSnapshotVisibility(id<FBXCElementSnapshot> snapshot)
|
||||
{
|
||||
return nil == snapshot.additionalAttributes ? nil : snapshot.additionalAttributes[FB_XCAXAIsVisibleAttribute];
|
||||
}
|
||||
|
||||
@implementation XCUIElement (FBIsVisible)
|
||||
|
||||
- (BOOL)fb_isVisible
|
||||
{
|
||||
@autoreleasepool {
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isVisible;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBXCElementSnapshotWrapper (FBIsVisible)
|
||||
|
||||
- (BOOL)fb_hasVisibleDescendants
|
||||
{
|
||||
for (id<FBXCElementSnapshot> descendant in (self._allDescendants ?: @[])) {
|
||||
if ([fetchSnapshotVisibility(descendant) boolValue]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)fb_isVisible
|
||||
{
|
||||
NSNumber *isVisible = fetchSnapshotVisibility(self);
|
||||
if (nil != isVisible) {
|
||||
return isVisible.boolValue;
|
||||
}
|
||||
|
||||
// Fetching the attribute value is expensive.
|
||||
// Shortcircuit here to save time and assume if any of descendants
|
||||
// is already determined as visible then the container should be visible as well
|
||||
if ([self fb_hasVisibleDescendants]) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSNumber *attributeValue = [self fb_attributeValue:FB_XCAXAIsVisibleAttributeName
|
||||
error:&error];
|
||||
if (nil != attributeValue) {
|
||||
NSMutableDictionary *updatedValue = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}];
|
||||
[updatedValue setObject:attributeValue forKey:FB_XCAXAIsVisibleAttribute];
|
||||
self.snapshot.additionalAttributes = updatedValue.copy;
|
||||
@autoreleasepool {
|
||||
return [attributeValue boolValue];
|
||||
}
|
||||
}
|
||||
|
||||
NSLog(@"Cannot determine visiblity of %@ natively: %@. Defaulting to: %@",
|
||||
self.fb_description, error.description, @(NO));
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
33
WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h
Normal file
33
WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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 <WebDriverAgentLib/FBXCElementSnapshotWrapper.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBMinMax)
|
||||
|
||||
/*! Minimum value (minValue) – may be nil if the element does not have this attribute */
|
||||
@property (nonatomic, readonly, nullable) NSNumber *fb_minValue;
|
||||
|
||||
/*! Maximum value (maxValue) - may be nil if the element does not have this attribute */
|
||||
@property (nonatomic, readonly, nullable) NSNumber *fb_maxValue;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (FBMinMax)
|
||||
|
||||
/*! Minimum value (minValue) – may be nil if the element does not have this attribute */
|
||||
@property (nonatomic, readonly, nullable) NSNumber *fb_minValue;
|
||||
|
||||
/*! Maximum value (maxValue) - may be nil if the element does not have this attribute */
|
||||
@property (nonatomic, readonly, nullable) NSNumber *fb_maxValue;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
75
WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m
Normal file
75
WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 "FBLogger.h"
|
||||
#import "XCUIElement+FBMinMax.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (FBMinMaxInternal)
|
||||
|
||||
- (NSNumber *)fb_numericAttribute:(NSString *)attributeName symbol:(NSNumber *)symbol;
|
||||
|
||||
@end
|
||||
|
||||
@implementation XCUIElement (FBMinMax)
|
||||
|
||||
- (NSNumber *)fb_minValue
|
||||
{
|
||||
@autoreleasepool {
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_minValue];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSNumber *)fb_maxValue
|
||||
{
|
||||
@autoreleasepool {
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_maxValue];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBXCElementSnapshotWrapper (FBMinMax)
|
||||
|
||||
- (NSNumber *)fb_minValue
|
||||
{
|
||||
return [self fb_numericAttribute:FB_XCAXACustomMinValueAttributeName
|
||||
symbol:FB_XCAXACustomMinValueAttribute];
|
||||
}
|
||||
|
||||
- (NSNumber *)fb_maxValue
|
||||
{
|
||||
return [self fb_numericAttribute:FB_XCAXACustomMaxValueAttributeName
|
||||
symbol:FB_XCAXACustomMaxValueAttribute];
|
||||
}
|
||||
|
||||
- (NSNumber *)fb_numericAttribute:(NSString *)attributeName symbol:(NSNumber *)symbol
|
||||
{
|
||||
NSNumber *cached = (self.snapshot.additionalAttributes ?: @{})[symbol];
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
NSNumber *raw = [self fb_attributeValue:attributeName error:&error];
|
||||
if (nil != raw) {
|
||||
NSMutableDictionary *updated = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}];
|
||||
updated[symbol] = raw;
|
||||
self.snapshot.additionalAttributes = updated.copy;
|
||||
return raw;
|
||||
}
|
||||
|
||||
[FBLogger logFmt:@"[FBMinMax] Cannot determine %@ for %@: %@", attributeName, self.fb_description, error.localizedDescription];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
45
WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h
Normal file
45
WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 <WebDriverAgentLib/XCUIElement.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBPickerWheel)
|
||||
|
||||
/**
|
||||
Selects the next available option in Picker Wheel
|
||||
|
||||
@param offset the value in range [0.01, 0.5]. It defines how far from picker
|
||||
wheel's center the click should happen. The actual distance is culculated by
|
||||
multiplying this value to the actual picker wheel height. Too small offset value
|
||||
may not change the picker wheel value and too high value may cause the wheel to switch
|
||||
two or more values at once. Usually the optimal value is located in range [0.15, 0.3]
|
||||
@param error returns error object if there was an error while selecting the
|
||||
next picker value
|
||||
@return YES if the current option has been successfully switched. Otherwise NO
|
||||
*/
|
||||
- (BOOL)fb_selectNextOptionWithOffset:(CGFloat)offset error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Selects the previous available option in Picker Wheel
|
||||
|
||||
@param offset the value in range [0.01, 0.5]. It defines how far from picker
|
||||
wheel's center the click should happen. The actual distance is culculated by
|
||||
multiplying this value to the actual picker wheel height. Too small offset value
|
||||
may not change the picker wheel value and too high value may cause the wheel to switch
|
||||
two or more values at once. Usually the optimal value is located in range [0.15, 0.3]
|
||||
@param error returns error object if there was an error while selecting the
|
||||
previous picker value
|
||||
@return YES if the current option has been successfully switched. Otherwise NO
|
||||
*/
|
||||
- (BOOL)fb_selectPreviousOptionWithOffset:(CGFloat)offset error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
58
WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m
Normal file
58
WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBPickerWheel.h"
|
||||
|
||||
#import "FBRunLoopSpinner.h"
|
||||
#import "FBXCElementSnapshot.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIElement+FBUID.h"
|
||||
#import "XCUICoordinate.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCUIElement+FBResolve.h"
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@implementation XCUIElement (FBPickerWheel)
|
||||
|
||||
static const NSTimeInterval VALUE_CHANGE_TIMEOUT = 2;
|
||||
|
||||
- (BOOL)fb_scrollWithOffset:(CGFloat)relativeHeightOffset error:(NSError **)error
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
NSString *previousValue = snapshot.value;
|
||||
XCUICoordinate *startCoord = [self coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)];
|
||||
XCUICoordinate *endCoord = [startCoord coordinateWithOffset:CGVectorMake(0.0, relativeHeightOffset * snapshot.frame.size.height)];
|
||||
// If picker value is reflected in its accessiblity id
|
||||
// then fetching of the next snapshot may fail with StaleElementReferenceError
|
||||
// because we bound elements by their accessbility ids by default.
|
||||
// Fetching stable instance of an element allows it to be bounded to the
|
||||
// unique element identifier (UID), so it could be found next time even if its
|
||||
// id is different from the initial one. See https://github.com/appium/appium/issues/17569
|
||||
XCUIElement *stableInstance = [self fb_stableInstanceWithUid:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]];
|
||||
[endCoord tap];
|
||||
return [[[[FBRunLoopSpinner new]
|
||||
timeout:VALUE_CHANGE_TIMEOUT]
|
||||
timeoutErrorMessage:[NSString stringWithFormat:@"Picker wheel value has not been changed after %@ seconds timeout", @(VALUE_CHANGE_TIMEOUT)]]
|
||||
spinUntilTrue:^BOOL{
|
||||
return ![stableInstance.value isEqualToString:previousValue];
|
||||
}
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_selectNextOptionWithOffset:(CGFloat)offset error:(NSError **)error
|
||||
{
|
||||
return [self fb_scrollWithOffset:offset error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_selectPreviousOptionWithOffset:(CGFloat)offset error:(NSError **)error
|
||||
{
|
||||
return [self fb_scrollWithOffset:-offset error:error];
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
37
WebDriverAgentLib/Categories/XCUIElement+FBResolve.h
Normal file
37
WebDriverAgentLib/Categories/XCUIElement+FBResolve.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBResolve)
|
||||
|
||||
/*! This property is always true unless the element gets resolved by its internal UUID (e.g. results of an xpath query) */
|
||||
@property (nullable, nonatomic) NSNumber *fb_isResolvedNatively;
|
||||
|
||||
/**
|
||||
Returns element instance based on query by element's UUID rather than any other attributes, which
|
||||
might be a subject of change during the application life cycle. The UUID is calculated based on the PID
|
||||
of the application to which this particular element belongs and the identifier of the underlying AXElement
|
||||
instance. That usually guarantees the same element is always going to be matched in scope of the parent
|
||||
application independently of its current attribute values.
|
||||
Example: We have an element X with value Y. Our locator looks like 'value == Y'. Normally, if the element's
|
||||
value is changed to Z and we try to reuse this cached instance of it then a StaleElement error is thrown.
|
||||
Although, if the cached element instance is the one returned by this API call then the same element
|
||||
is going to be matched and no staleness exception will be thrown.
|
||||
|
||||
@param uid Element UUID
|
||||
@return Either the same element instance if `fb_isResolvedNatively` was set to NO (usually the cache for elements
|
||||
matched by xpath locators) or the stable instance of the self element based on the query by element's UUID.
|
||||
*/
|
||||
- (XCUIElement *)fb_stableInstanceWithUid:(NSString *__nullable)uid;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
51
WebDriverAgentLib/Categories/XCUIElement+FBResolve.m
Normal file
51
WebDriverAgentLib/Categories/XCUIElement+FBResolve.m
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBResolve.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "XCUIElement.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIElement+FBUID.h"
|
||||
|
||||
@implementation XCUIElement (FBResolve)
|
||||
|
||||
static char XCUIELEMENT_IS_RESOLVED_NATIVELY_KEY;
|
||||
|
||||
@dynamic fb_isResolvedNatively;
|
||||
|
||||
- (void)setFb_isResolvedNatively:(NSNumber *)isResolvedNatively
|
||||
{
|
||||
objc_setAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_NATIVELY_KEY, isResolvedNatively, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
- (NSNumber *)fb_isResolvedNatively
|
||||
{
|
||||
NSNumber *result = objc_getAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_NATIVELY_KEY);
|
||||
return nil == result ? @YES : result;
|
||||
}
|
||||
|
||||
- (XCUIElement *)fb_stableInstanceWithUid:(NSString *)uid
|
||||
{
|
||||
if (nil == uid || ![self.fb_isResolvedNatively boolValue] || [self isKindOfClass:XCUIApplication.class]) {
|
||||
return self;
|
||||
}
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@", FBStringify(FBXCElementSnapshotWrapper, fb_uid), uid];
|
||||
@autoreleasepool {
|
||||
XCUIElementQuery *query = [self.application.fb_query descendantsMatchingType:XCUIElementTypeAny];
|
||||
XCUIElement *result = [query matchingPredicate:predicate].allElementsBoundByIndex.firstObject;
|
||||
if (nil != result) {
|
||||
result.fb_isResolvedNatively = @NO;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
98
WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h
Normal file
98
WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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 <WebDriverAgentLib/XCUIElement.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Defines directions in which scrolling is possible.
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, FBXCUIElementScrollDirection) {
|
||||
FBXCUIElementScrollDirectionUnknown,
|
||||
FBXCUIElementScrollDirectionVertical,
|
||||
FBXCUIElementScrollDirectionHorizontal,
|
||||
};
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
@interface XCUIElement (FBScrolling)
|
||||
|
||||
/**
|
||||
Scrolls receiver up by one screen height
|
||||
|
||||
@param distance Normalized <0.0 - 1.0> scroll distance distance
|
||||
*/
|
||||
- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance;
|
||||
|
||||
/**
|
||||
Scrolls receiver down by one screen height
|
||||
|
||||
@param distance Normalized <0.0 - 1.0> scroll distance distance
|
||||
*/
|
||||
- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance;
|
||||
|
||||
/**
|
||||
Scrolls receiver left by one screen width
|
||||
|
||||
@param distance Normalized <0.0 - 1.0> scroll distance distance
|
||||
*/
|
||||
- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance;
|
||||
|
||||
/**
|
||||
Scrolls receiver right by one screen width
|
||||
|
||||
@param distance Normalized <0.0 - 1.0> scroll distance distance
|
||||
*/
|
||||
- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance;
|
||||
|
||||
/**
|
||||
Scrolls parent scroll view till receiver is visible.
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_scrollToVisibleWithError:(NSError **)error;
|
||||
|
||||
/**
|
||||
Scrolls parent scroll view till the current element is visible.
|
||||
This call is fast as it uses a native XCTest implementation.
|
||||
The element must be hittable.
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_nativeScrollToVisibleWithError:(NSError **)error;
|
||||
|
||||
/**
|
||||
Scrolls parent scroll view till receiver is visible. Whenever element is invisible it scrolls by normalizedScrollDistance
|
||||
in its direction. E.g. if normalizedScrollDistance is equal to 0.5, each step will scroll by half of scroll view's size.
|
||||
|
||||
@param normalizedScrollDistance single scroll step normalized (0.0 - 1.0) distance
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Scrolls parent scroll view till receiver is visible. Whenever element is invisible it scrolls by normalizedScrollDistance
|
||||
in its direction. E.g. if normalizedScrollDistance is equal to 0.5, each step will scroll by half of scroll view's size.
|
||||
|
||||
@param normalizedScrollDistance single scroll step normalized (0.0 - 1.0) distance
|
||||
@param scrollDirection the direction in which the scroll view should be scrolled, or FBXCUIElementScrollDirectionUnknown
|
||||
to attempt to determine it automatically
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance scrollDirection:(FBXCUIElementScrollDirection)scrollDirection error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
342
WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m
Normal file
342
WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBScrolling.h"
|
||||
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBMathUtils.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXCElementSnapshotWrapper.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUICoordinate.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "XCUIElement+FBVisibleFrame.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
|
||||
const CGFloat FBFuzzyPointThreshold = 20.f; //Smallest determined value that is not interpreted as touch
|
||||
const CGFloat FBScrollToVisibleNormalizedDistance = .5f;
|
||||
const CGFloat FBTouchEventDelay = 0.5f;
|
||||
const CGFloat FBTouchVelocity = 300; // pixels per sec
|
||||
const CGFloat FBScrollTouchProportion = 0.75f;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (FBScrolling)
|
||||
|
||||
- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
|
||||
- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
|
||||
- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
|
||||
- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application;
|
||||
- (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector inApplication:(XCUIApplication *)application;
|
||||
- (BOOL)fb_scrollByVector:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
@implementation XCUIElement (FBScrolling)
|
||||
|
||||
- (BOOL)fb_nativeScrollToVisibleWithError:(NSError **)error
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_customSnapshot];
|
||||
return nil != [self _hitPointByAttemptingToScrollToVisibleSnapshot:snapshot
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_customSnapshot];
|
||||
[[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollUpByNormalizedDistance:distance
|
||||
inApplication:self.application];
|
||||
}
|
||||
|
||||
- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_customSnapshot];
|
||||
[[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollDownByNormalizedDistance:distance
|
||||
inApplication:self.application];
|
||||
}
|
||||
|
||||
- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_customSnapshot];
|
||||
[[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollLeftByNormalizedDistance:distance
|
||||
inApplication:self.application];
|
||||
}
|
||||
|
||||
- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_customSnapshot];
|
||||
[[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollRightByNormalizedDistance:distance
|
||||
inApplication:self.application];
|
||||
}
|
||||
|
||||
- (BOOL)fb_scrollToVisibleWithError:(NSError **)error
|
||||
{
|
||||
return [self fb_scrollToVisibleWithNormalizedScrollDistance:FBScrollToVisibleNormalizedDistance error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance
|
||||
error:(NSError **)error
|
||||
{
|
||||
return [self fb_scrollToVisibleWithNormalizedScrollDistance:normalizedScrollDistance
|
||||
scrollDirection:FBXCUIElementScrollDirectionUnknown
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance
|
||||
scrollDirection:(FBXCUIElementScrollDirection)scrollDirection
|
||||
error:(NSError **)error
|
||||
{
|
||||
FBXCElementSnapshotWrapper *prescrollSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]];
|
||||
|
||||
if (prescrollSnapshot.isWDVisible) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
static NSArray *acceptedParents;
|
||||
dispatch_once(&onceToken, ^{
|
||||
acceptedParents = @[
|
||||
@(XCUIElementTypeScrollView),
|
||||
@(XCUIElementTypeCollectionView),
|
||||
@(XCUIElementTypeTable),
|
||||
@(XCUIElementTypeWebView),
|
||||
];
|
||||
});
|
||||
|
||||
__block NSArray<id<FBXCElementSnapshot>> *cellSnapshots;
|
||||
__block NSMutableArray<id<FBXCElementSnapshot>> *visibleCellSnapshots = [NSMutableArray new];
|
||||
id<FBXCElementSnapshot> scrollView = [prescrollSnapshot fb_parentMatchingOneOfTypes:acceptedParents
|
||||
filter:^(id<FBXCElementSnapshot> snapshot) {
|
||||
FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
|
||||
|
||||
if (![wrappedSnapshot isWDVisible]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
cellSnapshots = [wrappedSnapshot fb_descendantsCellSnapshots];
|
||||
|
||||
for (id<FBXCElementSnapshot> cellSnapshot in cellSnapshots) {
|
||||
FBXCElementSnapshotWrapper *wrappedCellSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:cellSnapshot];
|
||||
if (wrappedCellSnapshot.wdVisible) {
|
||||
[visibleCellSnapshots addObject:cellSnapshot];
|
||||
if (visibleCellSnapshots.count > 1) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}];
|
||||
|
||||
if (scrollView == nil) {
|
||||
return
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Failed to find scrollable visible parent with 2 visible children"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
id<FBXCElementSnapshot> targetCellSnapshot = [prescrollSnapshot fb_parentCellSnapshot];
|
||||
id<FBXCElementSnapshot> lastSnapshot = visibleCellSnapshots.lastObject;
|
||||
// Can't just do indexOfObject, because targetCellSnapshot may represent the same object represented by a member of cellSnapshots, yet be a different object
|
||||
// than that member. This reflects the fact that targetCellSnapshot came out of self.fb_parentCellSnapshot, not out of cellSnapshots directly.
|
||||
// If the result is NSNotFound, we'll just proceed by scrolling downward/rightward, since NSNotFound will always be larger than the current index.
|
||||
NSUInteger targetCellIndex = [cellSnapshots indexOfObjectPassingTest:^BOOL(id<FBXCElementSnapshot> _Nonnull obj,
|
||||
NSUInteger idx, BOOL *_Nonnull stop) {
|
||||
return [obj _matchesElement:targetCellSnapshot];
|
||||
}];
|
||||
NSUInteger visibleCellIndex = [cellSnapshots indexOfObject:lastSnapshot];
|
||||
|
||||
if (scrollDirection == FBXCUIElementScrollDirectionUnknown) {
|
||||
// Try to determine the scroll direction by determining the vector between the first and last visible cells
|
||||
id<FBXCElementSnapshot> firstVisibleCell = visibleCellSnapshots.firstObject;
|
||||
id<FBXCElementSnapshot> lastVisibleCell = visibleCellSnapshots.lastObject;
|
||||
CGVector cellGrowthVector = CGVectorMake(firstVisibleCell.frame.origin.x - lastVisibleCell.frame.origin.x,
|
||||
firstVisibleCell.frame.origin.y - lastVisibleCell.frame.origin.y
|
||||
);
|
||||
if (ABS(cellGrowthVector.dy) > ABS(cellGrowthVector.dx)) {
|
||||
scrollDirection = FBXCUIElementScrollDirectionVertical;
|
||||
} else {
|
||||
scrollDirection = FBXCUIElementScrollDirectionHorizontal;
|
||||
}
|
||||
}
|
||||
|
||||
const NSUInteger maxScrollCount = 25;
|
||||
NSUInteger scrollCount = 0;
|
||||
FBXCElementSnapshotWrapper *scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:scrollView];
|
||||
// Scrolling till cell is visible and get current value of frames
|
||||
while (![self fb_isEquivalentElementSnapshotVisible:prescrollSnapshot] && scrollCount < maxScrollCount) {
|
||||
@autoreleasepool {
|
||||
if (targetCellIndex < visibleCellIndex) {
|
||||
scrollDirection == FBXCUIElementScrollDirectionVertical ?
|
||||
[scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance
|
||||
inApplication:self.application] :
|
||||
[scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance
|
||||
inApplication:self.application];
|
||||
}
|
||||
else {
|
||||
scrollDirection == FBXCUIElementScrollDirectionVertical ?
|
||||
[scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance
|
||||
inApplication:self.application] :
|
||||
[scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance
|
||||
inApplication:self.application];
|
||||
}
|
||||
scrollCount++;
|
||||
// Wait for scroll animation
|
||||
[self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollCount >= maxScrollCount) {
|
||||
return
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Failed to perform scroll with visible cell due to max scroll count reached"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
// Cell is now visible, but it might be only partialy visible, scrolling till whole frame is visible.
|
||||
// Sometimes, attempting to grab the parent snapshot of the target cell after scrolling is complete causes a stale element reference exception.
|
||||
// Trying fb_cachedSnapshot first
|
||||
FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]];
|
||||
targetCellSnapshot = [targetCellSnapshotWrapped fb_parentCellSnapshot];
|
||||
CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrame;
|
||||
|
||||
CGVector scrollVector = CGVectorMake(visibleFrame.size.width - targetCellSnapshot.frame.size.width,
|
||||
visibleFrame.size.height - targetCellSnapshot.frame.size.height
|
||||
);
|
||||
return [scrollViewWrapped fb_scrollByVector:scrollVector
|
||||
inApplication:self.application
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_isEquivalentElementSnapshotVisible:(id<FBXCElementSnapshot>)snapshot
|
||||
{
|
||||
FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
|
||||
|
||||
if (wrappedSnapshot.isWDVisible) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
id<FBXCElementSnapshot> appSnapshot = [self.application fb_standardSnapshot];
|
||||
for (id<FBXCElementSnapshot> elementSnapshot in appSnapshot._allDescendants.copy) {
|
||||
FBXCElementSnapshotWrapper *wrappedElementSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:elementSnapshot];
|
||||
// We are comparing pre-scroll snapshot so frames are irrelevant.
|
||||
if ([wrappedSnapshot fb_framelessFuzzyMatchesElement:elementSnapshot]
|
||||
&& wrappedElementSnapshot.isWDVisible) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBXCElementSnapshotWrapper (FBScrolling)
|
||||
|
||||
- (CGRect)scrollingFrame
|
||||
{
|
||||
return self.visibleFrame;
|
||||
}
|
||||
|
||||
- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance
|
||||
inApplication:(XCUIApplication *)application
|
||||
{
|
||||
[self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) inApplication:application];
|
||||
}
|
||||
|
||||
- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance
|
||||
inApplication:(XCUIApplication *)application
|
||||
{
|
||||
[self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) inApplication:application];
|
||||
}
|
||||
|
||||
- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance
|
||||
inApplication:(XCUIApplication *)application
|
||||
{
|
||||
[self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) inApplication:application];
|
||||
}
|
||||
|
||||
- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance
|
||||
inApplication:(XCUIApplication *)application
|
||||
{
|
||||
[self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) inApplication:application];
|
||||
}
|
||||
|
||||
- (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector
|
||||
inApplication:(XCUIApplication *)application
|
||||
{
|
||||
CGVector scrollVector = CGVectorMake(CGRectGetWidth(self.scrollingFrame) * normalizedScrollVector.dx,
|
||||
CGRectGetHeight(self.scrollingFrame) * normalizedScrollVector.dy
|
||||
);
|
||||
return [self fb_scrollByVector:scrollVector inApplication:application error:nil];
|
||||
}
|
||||
|
||||
- (BOOL)fb_scrollByVector:(CGVector)vector
|
||||
inApplication:(XCUIApplication *)application
|
||||
error:(NSError **)error
|
||||
{
|
||||
CGVector scrollBoundingVector = CGVectorMake(
|
||||
CGRectGetWidth(self.scrollingFrame) * FBScrollTouchProportion,
|
||||
CGRectGetHeight(self.scrollingFrame) * FBScrollTouchProportion
|
||||
);
|
||||
scrollBoundingVector.dx = (CGFloat)floor(copysign(scrollBoundingVector.dx, vector.dx));
|
||||
scrollBoundingVector.dy = (CGFloat)floor(copysign(scrollBoundingVector.dy, vector.dy));
|
||||
|
||||
NSInteger preciseScrollAttemptsCount = 20;
|
||||
CGVector CGZeroVector = CGVectorMake(0, 0);
|
||||
BOOL shouldFinishScrolling = NO;
|
||||
while (!shouldFinishScrolling) {
|
||||
CGVector scrollVector = CGVectorMake(fabs(vector.dx) > fabs(scrollBoundingVector.dx) ? scrollBoundingVector.dx : vector.dx,
|
||||
fabs(vector.dy) > fabs(scrollBoundingVector.dy) ? scrollBoundingVector.dy : vector.dy);
|
||||
vector = CGVectorMake(vector.dx - scrollVector.dx, vector.dy - scrollVector.dy);
|
||||
shouldFinishScrolling = FBVectorFuzzyEqualToVector(vector, CGZeroVector, 1) || --preciseScrollAttemptsCount <= 0;
|
||||
if (![self fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:scrollVector inApplication:application error:error]){
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGVector)fb_hitPointOffsetForScrollingVector:(CGVector)scrollingVector
|
||||
{
|
||||
CGFloat x = CGRectGetMinX(self.scrollingFrame) + CGRectGetWidth(self.scrollingFrame) * (scrollingVector.dx < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion));
|
||||
CGFloat y = CGRectGetMinY(self.scrollingFrame) + CGRectGetHeight(self.scrollingFrame) * (scrollingVector.dy < 0.0f ? FBScrollTouchProportion : (1 - FBScrollTouchProportion));
|
||||
return CGVectorMake((CGFloat)floor(x), (CGFloat)floor(y));
|
||||
}
|
||||
|
||||
- (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector
|
||||
inApplication:(XCUIApplication *)application
|
||||
error:(NSError **)error
|
||||
{
|
||||
CGVector hitpointOffset = [self fb_hitPointOffsetForScrollingVector:vector];
|
||||
|
||||
XCUICoordinate *appCoordinate = [[XCUICoordinate alloc] initWithElement:application normalizedOffset:CGVectorMake(0.0, 0.0)];
|
||||
XCUICoordinate *startCoordinate = [[XCUICoordinate alloc] initWithCoordinate:appCoordinate pointsOffset:hitpointOffset];
|
||||
XCUICoordinate *endCoordinate = [[XCUICoordinate alloc] initWithCoordinate:startCoordinate pointsOffset:vector];
|
||||
|
||||
if (FBPointFuzzyEqualToPoint(startCoordinate.screenPoint, endCoordinate.screenPoint, FBFuzzyPointThreshold)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
[startCoordinate pressForDuration:FBTouchEventDelay
|
||||
thenDragToCoordinate:endCoordinate
|
||||
withVelocity:FBTouchVelocity
|
||||
thenHoldForDuration:FBTouchEventDelay];
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
38
WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h
Normal file
38
WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBSwiping)
|
||||
|
||||
/**
|
||||
* Performs swipe gesture on the element
|
||||
*
|
||||
* @param direction Swipe direction. The following values are supported: up, down, left and right
|
||||
* @param velocity Swipe speed in pixels per second
|
||||
*/
|
||||
- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity;
|
||||
|
||||
@end
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@interface XCUICoordinate (FBSwiping)
|
||||
|
||||
/**
|
||||
* Performs swipe gesture on the coordinate
|
||||
*
|
||||
* @param direction Swipe direction. The following values are supported: up, down, left and right
|
||||
* @param velocity Swipe speed in pixels per second
|
||||
*/
|
||||
- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity;
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
56
WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m
Normal file
56
WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBSwiping.h"
|
||||
|
||||
#import "FBLogger.h"
|
||||
#import "XCUIElement.h"
|
||||
|
||||
void swipeWithDirection(NSObject *target, NSString *direction, NSNumber* _Nullable velocity) {
|
||||
double velocityValue = .0;
|
||||
if (nil != velocity) {
|
||||
velocityValue = [velocity doubleValue];
|
||||
}
|
||||
|
||||
if (velocityValue > 0) {
|
||||
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@WithVelocity:",
|
||||
direction.lowercaseString.capitalizedString]);
|
||||
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
|
||||
[invocation setSelector:selector];
|
||||
[invocation setArgument:&velocityValue atIndex:2];
|
||||
[invocation invokeWithTarget:target];
|
||||
} else {
|
||||
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@",
|
||||
direction.lowercaseString.capitalizedString]);
|
||||
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
|
||||
[invocation setSelector:selector];
|
||||
[invocation invokeWithTarget:target];
|
||||
}
|
||||
}
|
||||
|
||||
@implementation XCUIElement (FBSwiping)
|
||||
|
||||
- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity
|
||||
{
|
||||
swipeWithDirection(self, direction, velocity);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@implementation XCUICoordinate (FBSwiping)
|
||||
|
||||
- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity
|
||||
{
|
||||
swipeWithDirection(self, direction, velocity);
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
35
WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h
Normal file
35
WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2018-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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@interface XCUIElement (FBTVFocuse)
|
||||
|
||||
/**
|
||||
Sets focus
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_setFocusWithError:(NSError**) error;
|
||||
|
||||
/**
|
||||
Select a focused element
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_selectWithError:(NSError**) error;
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
71
WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m
Normal file
71
WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2018-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 "XCUIElement+FBTVFocuse.h"
|
||||
|
||||
#import <XCTest/XCUIRemote.h>
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import <FBTVNavigationTracker.h>
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
|
||||
#if TARGET_OS_TV
|
||||
|
||||
int const MAX_ITERATIONS_COUNT = 100;
|
||||
|
||||
@implementation XCUIElement (FBTVFocuse)
|
||||
|
||||
- (BOOL)fb_setFocusWithError:(NSError**) error
|
||||
{
|
||||
[XCUIApplication.fb_activeApplication fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
|
||||
|
||||
if (!self.wdEnabled) {
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:
|
||||
[NSString stringWithFormat:@"'%@' element cannot be focused because it is disabled", self.description]] build];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
FBTVNavigationTracker *tracker = [FBTVNavigationTracker trackerWithTargetElement:self];
|
||||
for (int i = 0; i < MAX_ITERATIONS_COUNT; i++) {
|
||||
// Here hasFocus works so far. Maybe, it is because it is handled by `XCUIRemote`...
|
||||
if (self.hasFocus) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (!self.exists) {
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:
|
||||
[NSString stringWithFormat:@"'%@' element is not reachable because it does not exist. Try to use XCUIRemote commands.", self.description]] build];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
FBTVDirection direction = tracker.directionToFocusedElement;
|
||||
if (direction != FBTVDirectionNone) {
|
||||
[[XCUIRemote sharedRemote] pressButton: (XCUIRemoteButton)direction];
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)fb_selectWithError:(NSError**) error
|
||||
{
|
||||
BOOL result = [self fb_setFocusWithError: error];
|
||||
if (result) {
|
||||
[[XCUIRemote sharedRemote] pressButton:XCUIRemoteButtonSelect];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@end
|
||||
|
||||
#endif
|
||||
64
WebDriverAgentLib/Categories/XCUIElement+FBTyping.h
Normal file
64
WebDriverAgentLib/Categories/XCUIElement+FBTyping.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Types a text into the currently focused element.
|
||||
|
||||
@param text text that should be typed
|
||||
@param typingSpeed Frequency of typing (letters per sec)
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error);
|
||||
|
||||
@interface XCUIElement (FBTyping)
|
||||
|
||||
/**
|
||||
Types a text into element.
|
||||
It will try to activate keyboard on element, if element has no keyboard focus.
|
||||
|
||||
@param text text that should be typed
|
||||
@param shouldClear Whether to clear the input field before start typing
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_typeText:(NSString *)text
|
||||
shouldClear:(BOOL)shouldClear
|
||||
error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Types a text into element.
|
||||
It will try to activate keyboard on element, if element has no keyboard focus.
|
||||
|
||||
@param text text that should be typed
|
||||
@param shouldClear Whether to clear the input field before start typing
|
||||
@param frequency Frequency of typing (letters per sec)
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_typeText:(NSString *)text
|
||||
shouldClear:(BOOL)shouldClear
|
||||
frequency:(NSUInteger)frequency
|
||||
error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Clears text on element.
|
||||
It will try to activate keyboard on element, if element has no keyboard focus.
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation succeeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)fb_clearTextWithError:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
199
WebDriverAgentLib/Categories/XCUIElement+FBTyping.m
Normal file
199
WebDriverAgentLib/Categories/XCUIElement+FBTyping.m
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBTyping.h"
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBXCElementSnapshotWrapper.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "NSString+FBVisualLength.h"
|
||||
#import "XCUIDevice+FBHelpers.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCSynthesizedEventRecord.h"
|
||||
#import "XCPointerEventPath.h"
|
||||
|
||||
#define MAX_TEXT_ABBR_LEN 12
|
||||
#define MAX_CLEAR_RETRIES 3
|
||||
|
||||
BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error)
|
||||
{
|
||||
NSString *name = text.length <= MAX_TEXT_ABBR_LEN
|
||||
? [NSString stringWithFormat:@"Type '%@'", text]
|
||||
: [NSString stringWithFormat:@"Type '%@…'", [text substringToIndex:MAX_TEXT_ABBR_LEN]];
|
||||
XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:name];
|
||||
XCPointerEventPath *ep = [[XCPointerEventPath alloc] initForTextInput];
|
||||
[ep typeText:text
|
||||
atOffset:0.0
|
||||
typingSpeed:typingSpeed
|
||||
shouldRedact:NO];
|
||||
[eventRecord addPointerEventPath:ep];
|
||||
return [FBXCTestDaemonsProxy synthesizeEventWithRecord:eventRecord error:error];
|
||||
}
|
||||
|
||||
@interface NSString (FBRepeat)
|
||||
|
||||
- (NSString *)fb_repeatTimes:(NSUInteger)times;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSString (FBRepeat)
|
||||
|
||||
- (NSString *)fb_repeatTimes:(NSUInteger)times {
|
||||
return [@"" stringByPaddingToLength:times * self.length
|
||||
withString:self
|
||||
startingAtIndex:0];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (FBKeyboardFocus)
|
||||
|
||||
- (BOOL)fb_hasKeyboardFocus;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBXCElementSnapshotWrapper (FBKeyboardFocus)
|
||||
|
||||
- (BOOL)fb_hasKeyboardFocus
|
||||
{
|
||||
// https://developer.apple.com/documentation/xctest/xcuielement/1500968-typetext?language=objc
|
||||
// > The element or a descendant must have keyboard focus; otherwise an error is raised.
|
||||
return self.hasKeyboardFocus || [self descendantsByFilteringWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot) {
|
||||
return snapshot.hasKeyboardFocus;
|
||||
}].count > 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation XCUIElement (FBTyping)
|
||||
|
||||
- (void)fb_prepareForTextInputWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot
|
||||
{
|
||||
if (snapshot.fb_hasKeyboardFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
[FBLogger logFmt:@"Neither the \"%@\" element itself nor its accessible descendants have the keyboard input focus", snapshot.fb_description];
|
||||
// There is no possibility to open the keyboard by tapping a field in TvOS
|
||||
#if !TARGET_OS_TV
|
||||
[FBLogger logFmt:@"Trying to tap the \"%@\" element to have it focused", snapshot.fb_description];
|
||||
[self tap];
|
||||
// It might take some time to update the UI
|
||||
[self fb_standardSnapshot];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (BOOL)fb_typeText:(NSString *)text
|
||||
shouldClear:(BOOL)shouldClear
|
||||
error:(NSError **)error
|
||||
{
|
||||
return [self fb_typeText:text
|
||||
shouldClear:shouldClear
|
||||
frequency:FBConfiguration.maxTypingFrequency
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_typeText:(NSString *)text
|
||||
shouldClear:(BOOL)shouldClear
|
||||
frequency:(NSUInteger)frequency
|
||||
error:(NSError **)error
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
FBXCElementSnapshotWrapper *wrapped = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
|
||||
[self fb_prepareForTextInputWithSnapshot:wrapped];
|
||||
if (shouldClear && ![self fb_clearTextWithSnapshot:wrapped shouldPrepareForInput:NO error:error]) {
|
||||
return NO;
|
||||
}
|
||||
return FBTypeText(text, frequency, error);
|
||||
}
|
||||
|
||||
- (BOOL)fb_clearTextWithError:(NSError **)error
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
return [self fb_clearTextWithSnapshot:[FBXCElementSnapshotWrapper ensureWrapped:snapshot]
|
||||
shouldPrepareForInput:YES
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot
|
||||
shouldPrepareForInput:(BOOL)shouldPrepareForInput
|
||||
error:(NSError **)error
|
||||
{
|
||||
id currentValue = snapshot.value;
|
||||
if (nil != currentValue && ![currentValue isKindOfClass:NSString.class]) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The value of '%@' is not a string and thus cannot be edited", snapshot.fb_description]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
if (nil == currentValue || 0 == [currentValue fb_visualLength]) {
|
||||
// Short circuit if the content is not present
|
||||
return YES;
|
||||
}
|
||||
|
||||
static NSString *backspaceDeleteSequence;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
backspaceDeleteSequence = [[NSString alloc] initWithData:(NSData *)[@"\\u0008\\u007F" dataUsingEncoding:NSASCIIStringEncoding]
|
||||
encoding:NSNonLossyASCIIStringEncoding];
|
||||
});
|
||||
|
||||
NSUInteger preClearTextLength = [currentValue fb_visualLength];
|
||||
NSString *backspacesToType = [backspaceDeleteSequence fb_repeatTimes:preClearTextLength];
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
NSUInteger retry = 0;
|
||||
NSString *placeholderValue = snapshot.placeholderValue;
|
||||
do {
|
||||
// the ios needs to have keyboard focus to clear text
|
||||
if (shouldPrepareForInput && 0 == retry) {
|
||||
[self fb_prepareForTextInputWithSnapshot:snapshot];
|
||||
}
|
||||
|
||||
if (retry == 0 && FBConfiguration.useClearTextShortcut) {
|
||||
// 1st attempt is via the IOHIDEvent as the fastest operation
|
||||
// https://github.com/appium/appium/issues/19389
|
||||
[[XCUIDevice sharedDevice] fb_performIOHIDEventWithPage:0x07 // kHIDPage_KeyboardOrKeypad
|
||||
usage:0x9c // kHIDUsage_KeyboardClear
|
||||
duration:0.01
|
||||
error:nil];
|
||||
} else if (retry >= MAX_CLEAR_RETRIES - 1) {
|
||||
// Last chance retry. Tripple-tap the field to select its content
|
||||
[self tapWithNumberOfTaps:3 numberOfTouches:1];
|
||||
return FBTypeText(backspaceDeleteSequence, FBConfiguration.defaultTypingFrequency, error);
|
||||
} else if (!FBTypeText(backspacesToType, FBConfiguration.defaultTypingFrequency, error)) {
|
||||
// 2nd operation
|
||||
return NO;
|
||||
}
|
||||
|
||||
currentValue = [self fb_standardSnapshot].value;
|
||||
if (nil != placeholderValue && [currentValue isEqualToString:placeholderValue]) {
|
||||
// Short circuit if only the placeholder value left
|
||||
return YES;
|
||||
}
|
||||
preClearTextLength = [currentValue fb_visualLength];
|
||||
|
||||
retry++;
|
||||
} while (preClearTextLength > 0);
|
||||
return YES;
|
||||
#else
|
||||
// tvOS does not need a focus.
|
||||
// kHIDPage_KeyboardOrKeypad did not work for tvOS's search field. (tvOS 17 at least)
|
||||
// Tested XCUIElementTypeSearchField and XCUIElementTypeTextView whch were
|
||||
// common search field and email/passowrd input in tvOS apps.
|
||||
return FBTypeText(backspacesToType, FBConfiguration.defaultTypingFrequency, error);
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
42
WebDriverAgentLib/Categories/XCUIElement+FBUID.h
Normal file
42
WebDriverAgentLib/Categories/XCUIElement+FBUID.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 "FBXCElementSnapshotWrapper.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBUID)
|
||||
|
||||
/*! Represents unique internal element identifier, which is the same for an element and its snapshot as UUIDv4 */
|
||||
@property (nonatomic, nullable, readonly, copy) NSString *fb_uid;
|
||||
|
||||
/*! Represents unique internal element identifier, which is the same for an element and its snapshot */
|
||||
@property (nonatomic, readonly) unsigned long long fb_accessibiltyId;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (FBUID)
|
||||
|
||||
/*! Represents unique internal element identifier, which is the same for an element and its snapshot as UUIDv4 */
|
||||
@property (nonatomic, nullable, readonly, copy) NSString *fb_uid;
|
||||
|
||||
/*! Represents unique internal element identifier, which is the same for an element and its snapshot */
|
||||
@property (nonatomic, readonly) unsigned long long fb_accessibiltyId;
|
||||
|
||||
/**
|
||||
Fetches wdUID attribute value for the given snapshot instance
|
||||
|
||||
@param snapshot snapshot instance
|
||||
@return UID attribute value
|
||||
*/
|
||||
+ (nullable NSString *)wdUIDWithSnapshot:(id<FBXCElementSnapshot>)snapshot;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
85
WebDriverAgentLib/Categories/XCUIElement+FBUID.m
Normal file
85
WebDriverAgentLib/Categories/XCUIElement+FBUID.m
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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 <objc/runtime.h>
|
||||
|
||||
#import "XCUIElement+FBUID.h"
|
||||
|
||||
#import "FBElementUtils.h"
|
||||
#import "FBLogger.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
|
||||
@implementation XCUIElement (FBUID)
|
||||
|
||||
- (unsigned long long)fb_accessibiltyId
|
||||
{
|
||||
return [FBElementUtils idWithAccessibilityElement:([self isKindOfClass:XCUIApplication.class]
|
||||
? [(XCUIApplication *)self accessibilityElement]
|
||||
: [self fb_standardSnapshot].accessibilityElement)];
|
||||
}
|
||||
|
||||
- (NSString *)fb_uid
|
||||
{
|
||||
return [self isKindOfClass:XCUIApplication.class]
|
||||
? [FBElementUtils uidWithAccessibilityElement:[(XCUIApplication *)self accessibilityElement]]
|
||||
: [FBXCElementSnapshotWrapper ensureWrapped:[self fb_standardSnapshot]].fb_uid;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBXCElementSnapshotWrapper (FBUID)
|
||||
|
||||
static void swizzled_validatePredicateWithExpressionsAllowed(id self, SEL _cmd, id predicate, BOOL withExpressionsAllowed)
|
||||
{
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-load-method"
|
||||
#pragma clang diagnostic ignored "-Wcast-function-type-strict"
|
||||
+ (void)load
|
||||
{
|
||||
Class XCElementSnapshotCls = objc_lookUpClass("XCElementSnapshot");
|
||||
NSAssert(XCElementSnapshotCls != nil, @"Could not locate XCElementSnapshot class");
|
||||
Method uidMethod = class_getInstanceMethod(self.class, @selector(fb_uid));
|
||||
class_addMethod(XCElementSnapshotCls, @selector(fb_uid), method_getImplementation(uidMethod), method_getTypeEncoding(uidMethod));
|
||||
|
||||
// Support for Xcode 14.3 requires disabling the new predicate validator, see https://github.com/appium/appium/issues/18444
|
||||
Class XCTElementQueryTransformerPredicateValidatorCls = objc_lookUpClass("XCTElementQueryTransformerPredicateValidator");
|
||||
if (XCTElementQueryTransformerPredicateValidatorCls == nil) {
|
||||
return;
|
||||
}
|
||||
Method validatePredicateMethod = class_getClassMethod(XCTElementQueryTransformerPredicateValidatorCls, NSSelectorFromString(@"validatePredicate:withExpressionsAllowed:"));
|
||||
if (validatePredicateMethod == nil) {
|
||||
[FBLogger log:@"Could not find method +[XCTElementQueryTransformerPredicateValidator validatePredicate:withExpressionsAllowed:]"];
|
||||
return;
|
||||
}
|
||||
IMP swizzledImp = (IMP)swizzled_validatePredicateWithExpressionsAllowed;
|
||||
method_setImplementation(validatePredicateMethod, swizzledImp);
|
||||
}
|
||||
#pragma diagnostic pop
|
||||
|
||||
- (unsigned long long)fb_accessibiltyId
|
||||
{
|
||||
return [FBElementUtils idWithAccessibilityElement:self.accessibilityElement];
|
||||
}
|
||||
|
||||
+ (nullable NSString *)wdUIDWithSnapshot:(id<FBXCElementSnapshot>)snapshot
|
||||
{
|
||||
return [FBElementUtils uidWithAccessibilityElement:[snapshot accessibilityElement]];
|
||||
}
|
||||
|
||||
- (NSString *)fb_uid
|
||||
{
|
||||
if ([self isKindOfClass:FBXCElementSnapshotWrapper.class]) {
|
||||
return [self.class wdUIDWithSnapshot:self.snapshot];
|
||||
}
|
||||
return [FBElementUtils uidWithAccessibilityElement:[self accessibilityElement]];
|
||||
}
|
||||
|
||||
@end
|
||||
103
WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h
Normal file
103
WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 <XCTest/XCTest.h>
|
||||
#import <WebDriverAgentLib/FBElement.h>
|
||||
#import <WebDriverAgentLib/FBXCElementSnapshot.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBUtilities)
|
||||
|
||||
/**
|
||||
Gets the most recent snapshot of the current element. The element will be
|
||||
automatically resolved if the snapshot is not available yet.
|
||||
Calls to this method mutate the `lastSnapshot` instance property.
|
||||
The snapshot is taken by the native API provided by XCTest.
|
||||
The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth`
|
||||
|
||||
Snapshot specifics:
|
||||
- Most performant
|
||||
- Memory-friedly
|
||||
- `children` property is set to `nil` if not taken from XCUIApplication
|
||||
- `value` property is cut off to max 512 bytes
|
||||
|
||||
@return The recent snapshot of the element
|
||||
@throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made
|
||||
*/
|
||||
- (id<FBXCElementSnapshot>)fb_standardSnapshot;
|
||||
|
||||
/**
|
||||
Gets the most recent snapshot of the current element. The element will be
|
||||
automatically resolved if the snapshot is not available yet.
|
||||
Calls to this method mutate the `lastSnapshot` instance property.
|
||||
The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth`
|
||||
|
||||
Snapshot specifics:
|
||||
- Less performant in comparison to the standard one
|
||||
- `children` property is always defined
|
||||
- `value` property is not cut off
|
||||
|
||||
@return The recent snapshot of the element
|
||||
@throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made
|
||||
*/
|
||||
- (id<FBXCElementSnapshot>)fb_customSnapshot;
|
||||
|
||||
/**
|
||||
Gets the most recent snapshot of the current element. The element will be
|
||||
automatically resolved if the snapshot is not available yet.
|
||||
Calls to this method mutate the `lastSnapshot` instance property.
|
||||
The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth`
|
||||
|
||||
Snapshot specifics:
|
||||
- Less performant in comparison to the standard one
|
||||
- The `hittable` property calculation is aligned with the native calculation logic
|
||||
|
||||
@return The recent snapshot of the element
|
||||
@throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made
|
||||
*/
|
||||
- (id<FBXCElementSnapshot>)fb_nativeSnapshot;
|
||||
|
||||
/**
|
||||
Extracts the cached element snapshot from its query.
|
||||
No requests to the accessiblity framework is made.
|
||||
It is only safe to use this call right after element lookup query
|
||||
has been executed.
|
||||
|
||||
@return Either the cached snapshot or nil
|
||||
*/
|
||||
- (nullable id<FBXCElementSnapshot>)fb_cachedSnapshot;
|
||||
|
||||
/**
|
||||
Filters elements by matching them to snapshots from the corresponding array
|
||||
|
||||
@param snapshots Array of snapshots to be matched with
|
||||
@param onlyChildren Whether to only look for direct element children
|
||||
|
||||
@return Array of filtered elements, which have matches in snapshots array
|
||||
*/
|
||||
- (NSArray<XCUIElement *> *)fb_filterDescendantsWithSnapshots:(NSArray<id<FBXCElementSnapshot>> *)snapshots
|
||||
onlyChildren:(BOOL)onlyChildren;
|
||||
|
||||
/**
|
||||
Waits until element snapshot is stable to avoid "Error copying attributes -25202 error".
|
||||
This error usually happens for testmanagerd if there is an active UI animation in progress and
|
||||
causes 15-seconds delay while getting hitpoint value of element's snapshot.
|
||||
*/
|
||||
- (void)fb_waitUntilStable;
|
||||
|
||||
/**
|
||||
Waits for receiver's snapshot to become stable with the given timeout
|
||||
|
||||
@param timeout The max time to wait util the snapshot is stable
|
||||
*/
|
||||
- (void)fb_waitUntilStableWithTimeout:(NSTimeInterval)timeout;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
170
WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m
Normal file
170
WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBUtilities.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBExceptions.h"
|
||||
#import "FBImageUtils.h"
|
||||
#import "FBElementUtils.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBMathUtils.h"
|
||||
#import "FBRunLoopSpinner.h"
|
||||
#import "FBSettings.h"
|
||||
#import "FBScreenshot.h"
|
||||
#import "FBXCAXClientProxy.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXCElementSnapshot.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIApplication+FBQuiescence.h"
|
||||
#import "XCUIApplicationImpl.h"
|
||||
#import "XCUIApplicationProcess.h"
|
||||
#import "XCTElementSetTransformer-Protocol.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
#import "XCTRunnerDaemonSession.h"
|
||||
#import "XCUIApplicationProcess+FBQuiescence.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
#import "XCUIElementQuery.h"
|
||||
#import "XCUIElementQuery+FBHelpers.h"
|
||||
#import "XCUIElement+FBUID.h"
|
||||
#import "XCUIScreen.h"
|
||||
#import "XCUIElement+FBResolve.h"
|
||||
|
||||
@implementation XCUIElement (FBUtilities)
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_takeSnapshot:(BOOL)isCustom
|
||||
{
|
||||
__block id<FBXCElementSnapshot> snapshot = nil;
|
||||
@autoreleasepool {
|
||||
NSError *error = nil;
|
||||
snapshot = isCustom
|
||||
? [self.fb_query fb_uniqueSnapshotWithError:&error]
|
||||
: (id<FBXCElementSnapshot>)[self snapshotWithError:&error];
|
||||
if (nil == snapshot) {
|
||||
[self fb_raiseStaleElementExceptionWithError:error];
|
||||
}
|
||||
}
|
||||
self.lastSnapshot = snapshot;
|
||||
return self.lastSnapshot;
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_standardSnapshot
|
||||
{
|
||||
return [self fb_takeSnapshot:NO];
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_customSnapshot
|
||||
{
|
||||
return [self fb_takeSnapshot:YES];
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_nativeSnapshot
|
||||
{
|
||||
NSError *error = nil;
|
||||
BOOL isSuccessful = [self resolveOrRaiseTestFailure:NO error:&error];
|
||||
if (nil == self.lastSnapshot || !isSuccessful) {
|
||||
[self fb_raiseStaleElementExceptionWithError:error];
|
||||
}
|
||||
return self.lastSnapshot;
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_cachedSnapshot
|
||||
{
|
||||
return [self.query fb_cachedSnapshot];
|
||||
}
|
||||
|
||||
- (NSArray<XCUIElement *> *)fb_filterDescendantsWithSnapshots:(NSArray<id<FBXCElementSnapshot>> *)snapshots
|
||||
onlyChildren:(BOOL)onlyChildren
|
||||
{
|
||||
if (0 == snapshots.count) {
|
||||
return @[];
|
||||
}
|
||||
NSMutableArray<NSString *> *matchedIds = [NSMutableArray new];
|
||||
for (id<FBXCElementSnapshot> snapshot in snapshots) {
|
||||
@autoreleasepool {
|
||||
NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot];
|
||||
if (nil != uid) {
|
||||
[matchedIds addObject:uid];
|
||||
}
|
||||
}
|
||||
}
|
||||
NSMutableArray<XCUIElement *> *matchedElements = [NSMutableArray array];
|
||||
NSString *uid = nil == self.lastSnapshot
|
||||
? self.fb_uid
|
||||
: [FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot];
|
||||
if (nil != uid && [matchedIds containsObject:uid]) {
|
||||
XCUIElement *stableSelf = [self fb_stableInstanceWithUid:uid];
|
||||
if (1 == snapshots.count) {
|
||||
return @[stableSelf];
|
||||
}
|
||||
[matchedElements addObject:stableSelf];
|
||||
}
|
||||
XCUIElementType type = XCUIElementTypeAny;
|
||||
NSArray<NSNumber *> *uniqueTypes = [snapshots valueForKeyPath:[NSString stringWithFormat:@"@distinctUnionOfObjects.%@", FBStringify(XCUIElement, elementType)]];
|
||||
if (uniqueTypes && [uniqueTypes count] == 1) {
|
||||
type = [uniqueTypes.firstObject intValue];
|
||||
}
|
||||
XCUIElementQuery *query = onlyChildren
|
||||
? [self.fb_query childrenMatchingType:type]
|
||||
: [self.fb_query descendantsMatchingType:type];
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K IN %@",FBStringify(FBXCElementSnapshotWrapper, fb_uid), matchedIds];
|
||||
[matchedElements addObjectsFromArray:[query matchingPredicate:predicate].allElementsBoundByIndex];
|
||||
|
||||
for (XCUIElement *el in matchedElements) {
|
||||
el.fb_isResolvedNatively = @NO;
|
||||
}
|
||||
return matchedElements.copy;
|
||||
}
|
||||
|
||||
- (void)fb_waitUntilStable
|
||||
{
|
||||
[self fb_waitUntilStableWithTimeout:FBConfiguration.waitForIdleTimeout];
|
||||
}
|
||||
|
||||
- (void)fb_waitUntilStableWithTimeout:(NSTimeInterval)timeout
|
||||
{
|
||||
if (timeout < DBL_EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSTimeInterval previousTimeout = FBConfiguration.waitForIdleTimeout;
|
||||
BOOL previousQuiescence = self.application.fb_shouldWaitForQuiescence;
|
||||
FBConfiguration.waitForIdleTimeout = timeout;
|
||||
if (!previousQuiescence) {
|
||||
self.application.fb_shouldWaitForQuiescence = YES;
|
||||
}
|
||||
[[[self.application applicationImpl] currentProcess]
|
||||
fb_waitForQuiescenceIncludingAnimationsIdle:YES];
|
||||
if (previousQuiescence != self.application.fb_shouldWaitForQuiescence) {
|
||||
self.application.fb_shouldWaitForQuiescence = previousQuiescence;
|
||||
}
|
||||
FBConfiguration.waitForIdleTimeout = previousTimeout;
|
||||
}
|
||||
|
||||
- (void)fb_raiseStaleElementExceptionWithError:(NSError *)error __attribute__((noreturn))
|
||||
{
|
||||
NSString *hintText = @"Make sure the application UI has the expected state";
|
||||
if (nil != error && [error.localizedDescription containsString:@"Identity Binding"]) {
|
||||
hintText = [NSString stringWithFormat:@"%@. You could also try to switch the binding strategy using the 'boundElementsByIndex' setting for the element lookup", hintText];
|
||||
}
|
||||
NSString *reason = [NSString stringWithFormat:@"The previously found element \"%@\" is not present in the current view anymore. %@",
|
||||
self.description, hintText];
|
||||
if (nil != error) {
|
||||
reason = [NSString stringWithFormat:@"%@. Original error: %@", reason, error.localizedDescription];
|
||||
}
|
||||
@throw [NSException exceptionWithName:FBStaleElementException reason:reason userInfo:@{}];
|
||||
}
|
||||
|
||||
@end
|
||||
35
WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h
Normal file
35
WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 "FBXCElementSnapshotWrapper.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (FBVisibleFrame)
|
||||
|
||||
/**
|
||||
Returns the snapshot visibleFrame with a fallback to direct attribute retrieval from FBXCAXClient in case of a snapshot fault (nil visibleFrame)
|
||||
|
||||
@return the snapshot visibleFrame
|
||||
*/
|
||||
- (CGRect)fb_visibleFrame;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (FBVisibleFrame)
|
||||
|
||||
/**
|
||||
Returns the snapshot visibleFrame with a fallback to direct attribute retrieval from FBXCAXClient in case of a snapshot fault (nil visibleFrame)
|
||||
|
||||
@return the snapshot visibleFrame
|
||||
*/
|
||||
- (CGRect)fb_visibleFrame;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
52
WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m
Normal file
52
WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBVisibleFrame.h"
|
||||
#import "FBElementUtils.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
|
||||
@implementation XCUIElement (FBVisibleFrame)
|
||||
|
||||
- (CGRect)fb_visibleFrame
|
||||
{
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_standardSnapshot];
|
||||
return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_visibleFrame;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBXCElementSnapshotWrapper (FBVisibleFrame)
|
||||
|
||||
- (CGRect)fb_visibleFrame
|
||||
{
|
||||
CGRect thisVisibleFrame = [self visibleFrame];
|
||||
if (!CGRectIsEmpty(thisVisibleFrame)) {
|
||||
return thisVisibleFrame;
|
||||
}
|
||||
|
||||
NSDictionary *visibleFrameDict = [self fb_attributeValue:FB_XCAXAVisibleFrameAttributeName
|
||||
error:nil];
|
||||
if (nil == visibleFrameDict) {
|
||||
return thisVisibleFrame;
|
||||
}
|
||||
|
||||
id x = [visibleFrameDict objectForKey:@"X"];
|
||||
id y = [visibleFrameDict objectForKey:@"Y"];
|
||||
id height = [visibleFrameDict objectForKey:@"Height"];
|
||||
id width = [visibleFrameDict objectForKey:@"Width"];
|
||||
if (x != nil && y != nil && height != nil && width != nil) {
|
||||
return CGRectMake([x doubleValue], [y doubleValue], [width doubleValue], [height doubleValue]);
|
||||
}
|
||||
|
||||
return thisVisibleFrame;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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 <WebDriverAgentLib/FBElement.h>
|
||||
#import <WebDriverAgentLib/XCUIElement.h>
|
||||
#import <WebDriverAgentLib/FBXCElementSnapshotWrapper.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElement (WebDriverAttributes) <FBElement>
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBXCElementSnapshotWrapper (WebDriverAttributes) <FBElement>
|
||||
|
||||
/**
|
||||
Fetches wdName attribute value for the given snapshot instance
|
||||
|
||||
@param snapshot snapshot instance
|
||||
@return wdName attribute value or nil
|
||||
*/
|
||||
+ (nullable NSString *)wdNameWithSnapshot:(id<FBXCElementSnapshot>)snapshot;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
283
WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m
Normal file
283
WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 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 "XCUIElement+FBWebDriverAttributes.h"
|
||||
|
||||
#import "FBElementTypeTransformer.h"
|
||||
#import "FBElementHelpers.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBXCElementSnapshotWrapper.h"
|
||||
#import "XCUIElement+FBAccessibility.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "XCUIElement+FBUID.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "FBElementUtils.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
#import "XCUIHitPointResult.h"
|
||||
#import "FBAccessibilityTraits.h"
|
||||
#import "XCUIElement+FBMinMax.h"
|
||||
|
||||
#define BROKEN_RECT CGRectMake(-1, -1, 0, 0)
|
||||
|
||||
@implementation XCUIElement (WebDriverAttributesForwarding)
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_snapshotForAttributeName:(NSString *)name
|
||||
{
|
||||
// https://github.com/appium/appium-xcuitest-driver/pull/2565
|
||||
if ([name isEqualToString:FBStringify(XCUIElement, isWDHittable)]) {
|
||||
return [self fb_nativeSnapshot];
|
||||
}
|
||||
// https://github.com/appium/appium-xcuitest-driver/issues/2552
|
||||
BOOL isValueRequest = [name isEqualToString:FBStringify(XCUIElement, wdValue)];
|
||||
if ([self isKindOfClass:XCUIApplication.class] && !isValueRequest) {
|
||||
return [self fb_standardSnapshot];
|
||||
}
|
||||
BOOL isCustomSnapshot = [name isEqualToString:FBStringify(XCUIElement, isWDAccessible)]
|
||||
|| [name isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)]
|
||||
|| [name isEqualToString:FBStringify(XCUIElement, wdIndex)]
|
||||
|| isValueRequest;
|
||||
return isCustomSnapshot ? [self fb_customSnapshot] : [self fb_standardSnapshot];
|
||||
}
|
||||
|
||||
- (id)fb_valueForWDAttributeName:(NSString *)name
|
||||
{
|
||||
NSString *wdAttributeName = [FBElementUtils wdAttributeNameForAttributeName:name];
|
||||
id<FBXCElementSnapshot> snapshot = [self fb_snapshotForAttributeName:wdAttributeName];
|
||||
return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_valueForWDAttributeName:name];
|
||||
}
|
||||
|
||||
- (id)forwardingTargetForSelector:(SEL)aSelector
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static NSSet<NSString *> *fbElementAttributeNames;
|
||||
dispatch_once(&onceToken, ^{
|
||||
fbElementAttributeNames = [FBElementUtils selectorNamesWithProtocol:@protocol(FBElement)];
|
||||
});
|
||||
NSString* attributeName = NSStringFromSelector(aSelector);
|
||||
return [fbElementAttributeNames containsObject:attributeName]
|
||||
? [FBXCElementSnapshotWrapper ensureWrapped:[self fb_snapshotForAttributeName:attributeName]]
|
||||
: nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBXCElementSnapshotWrapper (WebDriverAttributes)
|
||||
|
||||
- (id)fb_valueForWDAttributeName:(NSString *)name
|
||||
{
|
||||
return [self valueForKey:[FBElementUtils wdAttributeNameForAttributeName:name]];
|
||||
}
|
||||
|
||||
- (NSNumber *)wdMinValue
|
||||
{
|
||||
return self.fb_minValue;
|
||||
}
|
||||
|
||||
- (NSNumber *)wdMaxValue
|
||||
{
|
||||
return self.fb_maxValue;
|
||||
}
|
||||
|
||||
- (NSString *)wdValue
|
||||
{
|
||||
id value = self.value;
|
||||
XCUIElementType elementType = self.elementType;
|
||||
if (elementType == XCUIElementTypeStaticText) {
|
||||
NSString *label = self.label;
|
||||
value = FBFirstNonEmptyValue(value, label);
|
||||
} else if (elementType == XCUIElementTypeButton) {
|
||||
NSNumber *isSelected = self.isSelected ? @YES : nil;
|
||||
value = FBFirstNonEmptyValue(value, isSelected);
|
||||
} else if (elementType == XCUIElementTypeSwitch) {
|
||||
value = @([value boolValue]);
|
||||
} else if (FBDoesElementSupportInnerText(elementType)) {
|
||||
NSString *placeholderValue = self.placeholderValue;
|
||||
value = FBFirstNonEmptyValue(value, placeholderValue);
|
||||
}
|
||||
value = FBTransferEmptyStringToNil(value);
|
||||
if (value) {
|
||||
value = [NSString stringWithFormat:@"%@", value];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
+ (NSString *)wdNameWithSnapshot:(id<FBXCElementSnapshot>)snapshot
|
||||
{
|
||||
NSString *identifier = snapshot.identifier;
|
||||
if (nil != identifier && identifier.length != 0) {
|
||||
return identifier;
|
||||
}
|
||||
NSString *label = snapshot.label;
|
||||
return FBTransferEmptyStringToNil(label);
|
||||
}
|
||||
|
||||
- (NSString *)wdName
|
||||
{
|
||||
return [self.class wdNameWithSnapshot:self.snapshot];
|
||||
}
|
||||
|
||||
- (NSString *)wdLabel
|
||||
{
|
||||
XCUIElementType elementType = self.elementType;
|
||||
return (elementType == XCUIElementTypeTextField
|
||||
|| elementType == XCUIElementTypeSecureTextField)
|
||||
? self.label
|
||||
: FBTransferEmptyStringToNil(self.label);
|
||||
}
|
||||
|
||||
- (NSString *)wdPlaceholderValue
|
||||
{
|
||||
return FBDoesElementSupportInnerText(self.elementType)
|
||||
? self.placeholderValue
|
||||
: FBTransferEmptyStringToNil(self.placeholderValue);
|
||||
}
|
||||
|
||||
- (NSString *)wdType
|
||||
{
|
||||
return [FBElementTypeTransformer stringWithElementType:self.elementType];
|
||||
}
|
||||
|
||||
- (NSString *)wdUID
|
||||
{
|
||||
return self.fb_uid;
|
||||
}
|
||||
|
||||
- (CGRect)wdFrame
|
||||
{
|
||||
CGRect frame = self.frame;
|
||||
// It is mandatory to replace all Infinity values with numbers to avoid JSON parsing
|
||||
// exceptions like https://github.com/facebook/WebDriverAgent/issues/639#issuecomment-314421206
|
||||
// caused by broken element dimensions returned by XCTest
|
||||
return (isinf(frame.size.width) || isinf(frame.size.height)
|
||||
|| isinf(frame.origin.x) || isinf(frame.origin.y))
|
||||
? CGRectIntegral(BROKEN_RECT)
|
||||
: CGRectIntegral(frame);
|
||||
}
|
||||
|
||||
- (CGRect)wdNativeFrame
|
||||
{
|
||||
// To avoid confusion regarding the frame returned by `wdFrame`,
|
||||
// the current property is provided to represent the element's
|
||||
// actual rendered frame.
|
||||
return self.frame;
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a comma-separated string of accessibility traits for the element.
|
||||
This method converts the element's accessibility traits bitmask into human-readable strings
|
||||
using FBAccessibilityTraitsToStringsArray. The traits represent various accessibility
|
||||
characteristics of the element such as Button, Link, Image, etc.
|
||||
You can find the list of possible traits in the Apple documentation:
|
||||
https://developer.apple.com/documentation/uikit/uiaccessibilitytraits?language=objc
|
||||
|
||||
@return A comma-separated string of accessibility traits, or an empty string if no traits are set
|
||||
*/
|
||||
- (NSString *)wdTraits
|
||||
{
|
||||
NSArray<NSString *> *traits = FBAccessibilityTraitsToStringsArray(self.snapshot.traits);
|
||||
return [traits componentsJoinedByString:@", "];
|
||||
}
|
||||
|
||||
- (BOOL)isWDVisible
|
||||
{
|
||||
return self.fb_isVisible;
|
||||
}
|
||||
|
||||
- (BOOL)isWDFocused
|
||||
{
|
||||
return self.hasFocus;
|
||||
}
|
||||
|
||||
- (BOOL)isWDAccessible
|
||||
{
|
||||
XCUIElementType elementType = self.elementType;
|
||||
// Special cases:
|
||||
// Table view cell: we consider it accessible if it's container is accessible
|
||||
// Text fields: actual accessible element isn't text field itself, but nested element
|
||||
if (elementType == XCUIElementTypeCell) {
|
||||
if (!self.fb_isAccessibilityElement) {
|
||||
id<FBXCElementSnapshot> containerView = [[self children] firstObject];
|
||||
if (![FBXCElementSnapshotWrapper ensureWrapped:containerView].fb_isAccessibilityElement) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
} else if (elementType != XCUIElementTypeTextField && elementType != XCUIElementTypeSecureTextField) {
|
||||
if (!self.fb_isAccessibilityElement) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
id<FBXCElementSnapshot> parentSnapshot = self.parent;
|
||||
while (parentSnapshot) {
|
||||
// In the scenario when table provides Search results controller, table could be marked as accessible element, even though it isn't
|
||||
// As it is highly unlikely that table view should ever be an accessibility element itself,
|
||||
// for now we work around that by skipping Table View in container checks
|
||||
if (parentSnapshot.elementType != XCUIElementTypeTable
|
||||
&& [FBXCElementSnapshotWrapper ensureWrapped:parentSnapshot].fb_isAccessibilityElement) {
|
||||
return NO;
|
||||
}
|
||||
parentSnapshot = parentSnapshot.parent;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isWDAccessibilityContainer
|
||||
{
|
||||
NSArray<id<FBXCElementSnapshot>> *children = self.children;
|
||||
for (id<FBXCElementSnapshot> child in children) {
|
||||
FBXCElementSnapshotWrapper *wrappedChild = [FBXCElementSnapshotWrapper ensureWrapped:child];
|
||||
if (wrappedChild.isWDAccessibilityContainer || wrappedChild.fb_isAccessibilityElement) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isWDEnabled
|
||||
{
|
||||
return self.isEnabled;
|
||||
}
|
||||
|
||||
- (BOOL)isWDSelected
|
||||
{
|
||||
return self.isSelected;
|
||||
}
|
||||
|
||||
- (NSUInteger)wdIndex
|
||||
{
|
||||
if (nil != self.parent) {
|
||||
for (NSUInteger index = 0; index < self.parent.children.count; ++index) {
|
||||
if ([self.parent.children objectAtIndex:index] == self.snapshot) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (BOOL)isWDHittable
|
||||
{
|
||||
XCUIHitPointResult *result = [self hitPoint:nil];
|
||||
return nil == result ? NO : result.hittable;
|
||||
}
|
||||
|
||||
- (NSDictionary *)wdRect
|
||||
{
|
||||
CGRect frame = self.wdFrame;
|
||||
return @{
|
||||
@"x": @(CGRectGetMinX(frame)),
|
||||
@"y": @(CGRectGetMinY(frame)),
|
||||
@"width": @(CGRectGetWidth(frame)),
|
||||
@"height": @(CGRectGetHeight(frame)),
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
28
WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h
Normal file
28
WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2018-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 <XCTest/XCTest.h>
|
||||
#import "FBXCElementSnapshot.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElementQuery (FBHelpers)
|
||||
|
||||
/**
|
||||
Extracts the cached element snapshot from its query.
|
||||
No requests to the accessiblity framework is made.
|
||||
It is only safe to use this call right after element lookup query
|
||||
has been executed.
|
||||
|
||||
@return Either the cached snapshot or nil
|
||||
*/
|
||||
- (nullable id<FBXCElementSnapshot>)fb_cachedSnapshot;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
46
WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m
Normal file
46
WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2018-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 "XCUIElementQuery+FBHelpers.h"
|
||||
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIElementQuery.h"
|
||||
#import "FBXCElementSnapshot.h"
|
||||
|
||||
@implementation XCUIElementQuery (FBHelpers)
|
||||
|
||||
- (nullable id<FBXCElementSnapshot>)fb_cachedSnapshot
|
||||
{
|
||||
id<FBXCElementSnapshot> rootElementSnapshot = self.rootElementSnapshot;
|
||||
if (nil == rootElementSnapshot) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
XCUIElementQuery *inputQuery = self;
|
||||
NSMutableArray<id<XCTElementSetTransformer>> *transformersChain = [NSMutableArray array];
|
||||
while (nil != inputQuery && nil != inputQuery.transformer) {
|
||||
[transformersChain insertObject:inputQuery.transformer atIndex:0];
|
||||
inputQuery = inputQuery.inputQuery;
|
||||
}
|
||||
|
||||
NSMutableArray *snapshots = [NSMutableArray arrayWithObject:rootElementSnapshot];
|
||||
[snapshots addObjectsFromArray:rootElementSnapshot._allDescendants];
|
||||
NSOrderedSet *matchingSnapshots = [NSOrderedSet orderedSetWithArray:snapshots];
|
||||
@try {
|
||||
for (id<XCTElementSetTransformer> transformer in transformersChain) {
|
||||
matchingSnapshots = (NSOrderedSet *)[transformer transform:matchingSnapshots
|
||||
relatedElements:nil];
|
||||
}
|
||||
return matchingSnapshots.count == 1 ? matchingSnapshots.firstObject : nil;
|
||||
} @catch (NSException *e) {
|
||||
[FBLogger logFmt:@"Got an unexpected error while retriveing the cached snapshot: %@", e.reason];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user