初始化提交
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
|
||||
19
WebDriverAgentLib/Commands/FBAlertViewCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBAlertViewCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBAlertViewCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
130
WebDriverAgentLib/Commands/FBAlertViewCommands.m
Normal file
130
WebDriverAgentLib/Commands/FBAlertViewCommands.m
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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 "FBAlertViewCommands.h"
|
||||
|
||||
#import "FBAlert.h"
|
||||
#import "FBRouteRequest.h"
|
||||
#import "FBSession.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
|
||||
@implementation FBAlertViewCommands
|
||||
|
||||
#pragma mark - <FBCommandHandler>
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute GET:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertGetTextCommand:)],
|
||||
[[FBRoute GET:@"/alert/text"].withoutSession respondWithTarget:self action:@selector(handleAlertGetTextCommand:)],
|
||||
[[FBRoute POST:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertSetTextCommand:)],
|
||||
[[FBRoute POST:@"/alert/accept"] respondWithTarget:self action:@selector(handleAlertAcceptCommand:)],
|
||||
[[FBRoute POST:@"/alert/accept"].withoutSession respondWithTarget:self action:@selector(handleAlertAcceptCommand:)],
|
||||
[[FBRoute POST:@"/alert/dismiss"] respondWithTarget:self action:@selector(handleAlertDismissCommand:)],
|
||||
[[FBRoute POST:@"/alert/dismiss"].withoutSession respondWithTarget:self action:@selector(handleAlertDismissCommand:)],
|
||||
[[FBRoute GET:@"/wda/alert/buttons"] respondWithTarget:self action:@selector(handleGetAlertButtonsCommand:)],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Commands
|
||||
|
||||
+ (id<FBResponsePayload>)handleAlertGetTextCommand:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
NSString *alertText = [FBAlert alertWithApplication:application].text;
|
||||
if (!alertText) {
|
||||
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithObject(alertText);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleAlertSetTextCommand:(FBRouteRequest *)request
|
||||
{
|
||||
FBSession *session = request.session;
|
||||
id value = request.arguments[@"value"];
|
||||
if (!value) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'value' parameter" traceback:nil]);
|
||||
}
|
||||
FBAlert *alert = [FBAlert alertWithApplication:session.activeApplication];
|
||||
if (!alert.isPresent) {
|
||||
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
|
||||
traceback:nil]);
|
||||
}
|
||||
NSString *textToType = value;
|
||||
if ([value isKindOfClass:[NSArray class]]) {
|
||||
textToType = [value componentsJoinedByString:@""];
|
||||
}
|
||||
NSError *error;
|
||||
if (![alert typeText:textToType error:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:error.description
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleAlertAcceptCommand:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
NSString *name = request.arguments[@"name"];
|
||||
FBAlert *alert = [FBAlert alertWithApplication:application];
|
||||
NSError *error;
|
||||
|
||||
if (!alert.isPresent) {
|
||||
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
|
||||
traceback:nil]);
|
||||
}
|
||||
if (name) {
|
||||
if (![alert clickAlertButton:name error:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
} else if (![alert acceptWithError:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleAlertDismissCommand:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
NSString *name = request.arguments[@"name"];
|
||||
FBAlert *alert = [FBAlert alertWithApplication:application];
|
||||
NSError *error;
|
||||
|
||||
if (!alert.isPresent) {
|
||||
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
|
||||
traceback:nil]);
|
||||
}
|
||||
if (name) {
|
||||
if (![alert clickAlertButton:name error:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
} else if (![alert dismissWithError:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetAlertButtonsCommand:(FBRouteRequest *)request {
|
||||
FBSession *session = request.session;
|
||||
FBAlert *alert = [FBAlert alertWithApplication:session.activeApplication];
|
||||
|
||||
if (!alert.isPresent) {
|
||||
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
|
||||
traceback:nil]);
|
||||
}
|
||||
NSArray *labels = alert.buttonLabels;
|
||||
return FBResponseWithObject(labels);
|
||||
}
|
||||
@end
|
||||
19
WebDriverAgentLib/Commands/FBCustomCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBCustomCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBCustomCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
633
WebDriverAgentLib/Commands/FBCustomCommands.m
Normal file
633
WebDriverAgentLib/Commands/FBCustomCommands.m
Normal file
@@ -0,0 +1,633 @@
|
||||
/**
|
||||
* 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 "FBCustomCommands.h"
|
||||
|
||||
#import <XCTest/XCUIDevice.h>
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBKeyboard.h"
|
||||
#import "FBNotificationsHelper.h"
|
||||
#import "FBMathUtils.h"
|
||||
#import "FBPasteboard.h"
|
||||
#import "FBResponsePayload.h"
|
||||
#import "FBRoute.h"
|
||||
#import "FBRouteRequest.h"
|
||||
#import "FBRunLoopSpinner.h"
|
||||
#import "FBScreen.h"
|
||||
#import "FBSession.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIDevice+FBHelpers.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "XCUIElementQuery.h"
|
||||
#import "FBUnattachedAppLauncher.h"
|
||||
|
||||
@implementation FBCustomCommands
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute POST:@"/timeouts"] respondWithTarget:self action:@selector(handleTimeouts:)],
|
||||
[[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)],
|
||||
[[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)],
|
||||
[[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)],
|
||||
[[FBRoute POST:@"/wda/lock"].withoutSession respondWithTarget:self action:@selector(handleLock:)],
|
||||
[[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)],
|
||||
[[FBRoute POST:@"/wda/unlock"].withoutSession respondWithTarget:self action:@selector(handleUnlock:)],
|
||||
[[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)],
|
||||
[[FBRoute GET:@"/wda/locked"].withoutSession respondWithTarget:self action:@selector(handleIsLocked:)],
|
||||
[[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)],
|
||||
[[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)],
|
||||
[[FBRoute GET:@"/wda/screen"].withoutSession respondWithTarget:self action:@selector(handleGetScreen:)],
|
||||
[[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)],
|
||||
[[FBRoute GET:@"/wda/activeAppInfo"].withoutSession respondWithTarget:self action:@selector(handleActiveAppInfo:)],
|
||||
#if !TARGET_OS_TV // tvOS does not provide relevant APIs
|
||||
[[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)],
|
||||
[[FBRoute POST:@"/wda/setPasteboard"].withoutSession respondWithTarget:self action:@selector(handleSetPasteboard:)],
|
||||
[[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)],
|
||||
[[FBRoute POST:@"/wda/getPasteboard"].withoutSession respondWithTarget:self action:@selector(handleGetPasteboard:)],
|
||||
[[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)],
|
||||
#endif
|
||||
[[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)],
|
||||
[[FBRoute POST:@"/wda/performAccessibilityAudit"] respondWithTarget:self action:@selector(handlePerformAccessibilityAudit:)],
|
||||
[[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)],
|
||||
[[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)],
|
||||
[[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)],
|
||||
[[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)],
|
||||
[[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
|
||||
[[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)],
|
||||
[[FBRoute GET:@"/wda/device/info"].withoutSession respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
|
||||
[[FBRoute POST:@"/wda/device/appearance"].withoutSession respondWithTarget:self action:@selector(handleSetDeviceAppearance:)],
|
||||
[[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)],
|
||||
[[FBRoute GET:@"/wda/device/location"].withoutSession respondWithTarget:self action:@selector(handleGetLocation:)],
|
||||
#if !TARGET_OS_TV // tvOS does not provide relevant APIs
|
||||
#if __clang_major__ >= 15
|
||||
[[FBRoute POST:@"/wda/element/:uuid/keyboardInput"] respondWithTarget:self action:@selector(handleKeyboardInput:)],
|
||||
#endif
|
||||
[[FBRoute GET:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
|
||||
[[FBRoute GET:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
|
||||
[[FBRoute POST:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleSetSimulatedLocation:)],
|
||||
[[FBRoute POST:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleSetSimulatedLocation:)],
|
||||
[[FBRoute DELETE:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleClearSimulatedLocation:)],
|
||||
[[FBRoute DELETE:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleClearSimulatedLocation:)],
|
||||
#endif
|
||||
[[FBRoute OPTIONS:@"/*"].withoutSession respondWithTarget:self action:@selector(handlePingCommand:)],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Commands
|
||||
|
||||
+ (id<FBResponsePayload>)handleHomescreenCommand:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
if (![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleDeactivateAppCommand:(FBRouteRequest *)request
|
||||
{
|
||||
NSNumber *requestedDuration = request.arguments[@"duration"];
|
||||
NSTimeInterval duration = (requestedDuration ? requestedDuration.doubleValue : 3.);
|
||||
NSError *error;
|
||||
if (![request.session.activeApplication fb_deactivateWithDuration:duration error:&error]) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleTimeouts:(FBRouteRequest *)request
|
||||
{
|
||||
// This method is intentionally not supported.
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleDismissKeyboardCommand:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
BOOL isDismissed = [request.session.activeApplication fb_dismissKeyboardWithKeyNames:request.arguments[@"keyNames"]
|
||||
error:&error];
|
||||
return isDismissed
|
||||
? FBResponseWithOK()
|
||||
: FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handlePingCommand:(FBRouteRequest *)request
|
||||
{
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetScreen:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *app = XCUIApplication.fb_systemApplication;
|
||||
|
||||
XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject;
|
||||
CGSize statusBarSize = (nil == mainStatusBar) ? CGSizeZero : mainStatusBar.frame.size;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
CGSize screenSize = app.frame.size;
|
||||
#else
|
||||
CGSize screenSize = FBAdjustDimensionsForApplication(app.wdFrame.size, app.interfaceOrientation);
|
||||
#endif
|
||||
|
||||
return FBResponseWithObject(
|
||||
@{
|
||||
@"screenSize":@{@"width": @(screenSize.width),
|
||||
@"height": @(screenSize.height)
|
||||
},
|
||||
@"statusBarSize": @{@"width": @(statusBarSize.width),
|
||||
@"height": @(statusBarSize.height),
|
||||
},
|
||||
@"scale": @([FBScreen scale]),
|
||||
});
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleLock:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
if (![[XCUIDevice sharedDevice] fb_lockScreen:&error]) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleIsLocked:(FBRouteRequest *)request
|
||||
{
|
||||
BOOL isLocked = [XCUIDevice sharedDevice].fb_isScreenLocked;
|
||||
return FBResponseWithObject(isLocked ? @YES : @NO);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleUnlock:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
if (![[XCUIDevice sharedDevice] fb_unlockScreen:&error]) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleActiveAppInfo:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
return FBResponseWithObject(@{
|
||||
@"pid": @(app.processID),
|
||||
@"bundleId": app.bundleID,
|
||||
@"name": app.identifier,
|
||||
@"processArguments": [self processArguments:app],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current active app and its arguments of active session
|
||||
*
|
||||
* @return The dictionary of current active bundleId and its process/environment argumens
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* [self currentActiveApplication]
|
||||
* //=> {
|
||||
* // "processArguments" : {
|
||||
* // "env" : {
|
||||
* // "HAPPY" : "testing"
|
||||
* // },
|
||||
* // "args" : [
|
||||
* // "happy",
|
||||
* // "tseting"
|
||||
* // ]
|
||||
* // }
|
||||
*
|
||||
* [self currentActiveApplication]
|
||||
* //=> {}
|
||||
*/
|
||||
+ (NSDictionary *)processArguments:(XCUIApplication *)app
|
||||
{
|
||||
// Can be nil if no active activation is defined by XCTest
|
||||
if (app == nil) {
|
||||
return @{};
|
||||
}
|
||||
|
||||
return
|
||||
@{
|
||||
@"args": app.launchArguments,
|
||||
@"env": app.launchEnvironment
|
||||
};
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
+ (id<FBResponsePayload>)handleSetPasteboard:(FBRouteRequest *)request
|
||||
{
|
||||
NSString *contentType = request.arguments[@"contentType"] ?: @"plaintext";
|
||||
NSData *content = [[NSData alloc] initWithBase64EncodedString:(NSString *)request.arguments[@"content"]
|
||||
options:NSDataBase64DecodingIgnoreUnknownCharacters];
|
||||
if (nil == content) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Cannot decode the pasteboard content from base64" traceback:nil]);
|
||||
}
|
||||
NSError *error;
|
||||
if (![FBPasteboard setData:content forType:contentType error:&error]) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetPasteboard:(FBRouteRequest *)request
|
||||
{
|
||||
NSString *contentType = request.arguments[@"contentType"] ?: @"plaintext";
|
||||
NSError *error;
|
||||
id result = [FBPasteboard dataForType:contentType error:&error];
|
||||
if (nil == result) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
return FBResponseWithObject([result base64EncodedStringWithOptions:0]);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetBatteryInfo:(FBRouteRequest *)request
|
||||
{
|
||||
if (![[UIDevice currentDevice] isBatteryMonitoringEnabled]) {
|
||||
[[UIDevice currentDevice] setBatteryMonitoringEnabled:YES];
|
||||
}
|
||||
return FBResponseWithObject(@{
|
||||
@"level": @([UIDevice currentDevice].batteryLevel),
|
||||
@"state": @([UIDevice currentDevice].batteryState)
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
+ (id<FBResponsePayload>)handlePressButtonCommand:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
if (![XCUIDevice.sharedDevice fb_pressButton:(id)request.arguments[@"name"]
|
||||
forDuration:(NSNumber *)request.arguments[@"duration"]
|
||||
error:&error]) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleActivateSiri:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
if (![XCUIDevice.sharedDevice fb_activateSiriVoiceRecognitionWithText:(id)request.arguments[@"text"] error:&error]) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id <FBResponsePayload>)handlePeformIOHIDEvent:(FBRouteRequest *)request
|
||||
{
|
||||
NSNumber *page = request.arguments[@"page"];
|
||||
NSNumber *usage = request.arguments[@"usage"];
|
||||
NSNumber *duration = request.arguments[@"duration"];
|
||||
NSError *error;
|
||||
if (![XCUIDevice.sharedDevice fb_performIOHIDEventWithPage:page.unsignedIntValue
|
||||
usage:usage.unsignedIntValue
|
||||
duration:duration.doubleValue
|
||||
error:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id <FBResponsePayload>)handleLaunchUnattachedApp:(FBRouteRequest *)request
|
||||
{
|
||||
NSString *bundle = (NSString *)request.arguments[@"bundleId"];
|
||||
if ([FBUnattachedAppLauncher launchAppWithBundleId:bundle]) {
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:@"LSApplicationWorkspace failed to launch app" traceback:nil]);
|
||||
}
|
||||
|
||||
+ (id <FBResponsePayload>)handleResetAppAuth:(FBRouteRequest *)request
|
||||
{
|
||||
NSNumber *resource = request.arguments[@"resource"];
|
||||
if (nil == resource) {
|
||||
NSString *errMsg = @"The 'resource' argument must be set to a valid resource identifier (numeric value). See https://developer.apple.com/documentation/xctest/xcuiprotectedresource?language=objc";
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg traceback:nil]);
|
||||
}
|
||||
[request.session.activeApplication resetAuthorizationStatusForResource:(XCUIProtectedResource)resource.longLongValue];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
/**
|
||||
Returns device location data.
|
||||
It requires to configure location access permission by manual.
|
||||
The response of 'latitude', 'longitude' and 'altitude' are always zero (0) without authorization.
|
||||
'authorizationStatus' indicates current authorization status. '3' is 'Always'.
|
||||
https://developer.apple.com/documentation/corelocation/clauthorizationstatus
|
||||
|
||||
Settings -> Privacy -> Location Service -> WebDriverAgent-Runner -> Always
|
||||
|
||||
The return value could be zero even if the permission is set to 'Always'
|
||||
since the location service needs some time to update the location data.
|
||||
*/
|
||||
+ (id<FBResponsePayload>)handleGetLocation:(FBRouteRequest *)request
|
||||
{
|
||||
#if TARGET_OS_TV
|
||||
return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"unsupported"
|
||||
traceback:nil]);
|
||||
#else
|
||||
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
|
||||
[locationManager setDistanceFilter:kCLHeadingFilterNone];
|
||||
// Always return the best acurate location data
|
||||
[locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
|
||||
[locationManager setPausesLocationUpdatesAutomatically:NO];
|
||||
[locationManager startUpdatingLocation];
|
||||
|
||||
CLAuthorizationStatus authStatus;
|
||||
if ([locationManager respondsToSelector:@selector(authorizationStatus)]) {
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[locationManager class]
|
||||
instanceMethodSignatureForSelector:@selector(authorizationStatus)]];
|
||||
[invocation setSelector:@selector(authorizationStatus)];
|
||||
[invocation setTarget:locationManager];
|
||||
[invocation invoke];
|
||||
[invocation getReturnValue:&authStatus];
|
||||
} else {
|
||||
authStatus = [CLLocationManager authorizationStatus];
|
||||
}
|
||||
|
||||
return FBResponseWithObject(@{
|
||||
@"authorizationStatus": @(authStatus),
|
||||
@"latitude": @(locationManager.location.coordinate.latitude),
|
||||
@"longitude": @(locationManager.location.coordinate.longitude),
|
||||
@"altitude": @(locationManager.location.altitude),
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleExpectNotification:(FBRouteRequest *)request
|
||||
{
|
||||
NSString *name = request.arguments[@"name"];
|
||||
if (nil == name) {
|
||||
NSString *message = @"Notification name argument must be provided";
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
|
||||
}
|
||||
NSNumber *timeout = request.arguments[@"timeout"] ?: @60;
|
||||
NSString *type = request.arguments[@"type"] ?: @"plain";
|
||||
|
||||
XCTWaiterResult result;
|
||||
if ([type isEqualToString:@"plain"]) {
|
||||
result = [FBNotificationsHelper waitForNotificationWithName:name timeout:timeout.doubleValue];
|
||||
} else if ([type isEqualToString:@"darwin"]) {
|
||||
result = [FBNotificationsHelper waitForDarwinNotificationWithName:name timeout:timeout.doubleValue];
|
||||
} else {
|
||||
NSString *message = [NSString stringWithFormat:@"Notification type could only be 'plain' or 'darwin'. Got '%@' instead", type];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
|
||||
}
|
||||
if (result != XCTWaiterResultCompleted) {
|
||||
NSString *message = [NSString stringWithFormat:@"Did not receive any expected %@ notifications within %@s",
|
||||
name, timeout];
|
||||
return FBResponseWithStatus([FBCommandStatus timeoutErrorWithMessage:message traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSetDeviceAppearance:(FBRouteRequest *)request
|
||||
{
|
||||
NSString *name = [request.arguments[@"name"] lowercaseString];
|
||||
if (nil == name || !([name isEqualToString:@"light"] || [name isEqualToString:@"dark"])) {
|
||||
NSString *message = @"The appearance name must be either 'light' or 'dark'";
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
|
||||
}
|
||||
|
||||
FBUIInterfaceAppearance appearance = [name isEqualToString:@"light"]
|
||||
? FBUIInterfaceAppearanceLight
|
||||
: FBUIInterfaceAppearanceDark;
|
||||
NSError *error;
|
||||
if (![XCUIDevice.sharedDevice fb_setAppearance:appearance error:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetDeviceInfo:(FBRouteRequest *)request
|
||||
{
|
||||
// Returns locale like ja_EN and zh-Hant_US. The format depends on OS
|
||||
// Developers should use this locale by default
|
||||
// https://developer.apple.com/documentation/foundation/nslocale/1414388-autoupdatingcurrentlocale
|
||||
NSString *currentLocale = [[NSLocale autoupdatingCurrentLocale] localeIdentifier];
|
||||
|
||||
NSMutableDictionary *deviceInfo = [NSMutableDictionary dictionaryWithDictionary:
|
||||
@{
|
||||
@"currentLocale": currentLocale,
|
||||
@"timeZone": self.timeZone,
|
||||
@"name": UIDevice.currentDevice.name,
|
||||
@"model": UIDevice.currentDevice.model,
|
||||
@"uuid": [UIDevice.currentDevice.identifierForVendor UUIDString] ?: @"unknown",
|
||||
// https://developer.apple.com/documentation/uikit/uiuserinterfaceidiom?language=objc
|
||||
@"userInterfaceIdiom": @(UIDevice.currentDevice.userInterfaceIdiom),
|
||||
@"userInterfaceStyle": self.userInterfaceStyle,
|
||||
#if TARGET_OS_SIMULATOR
|
||||
@"isSimulator": @(YES),
|
||||
#else
|
||||
@"isSimulator": @(NO),
|
||||
#endif
|
||||
}];
|
||||
|
||||
// https://developer.apple.com/documentation/foundation/nsprocessinfothermalstate
|
||||
deviceInfo[@"thermalState"] = @(NSProcessInfo.processInfo.thermalState);
|
||||
|
||||
return FBResponseWithObject(deviceInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current user interface style as a string
|
||||
*/
|
||||
+ (NSString *)userInterfaceStyle
|
||||
{
|
||||
|
||||
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) {
|
||||
// Only iOS 15+ simulators/devices return correct data while
|
||||
// the api itself works in iOS 13 and 14 that has style preference.
|
||||
NSNumber *appearance = [XCUIDevice.sharedDevice fb_getAppearance];
|
||||
if (appearance != nil) {
|
||||
return [self getAppearanceName:appearance];
|
||||
}
|
||||
}
|
||||
|
||||
static id userInterfaceStyle = nil;
|
||||
static dispatch_once_t styleOnceToken;
|
||||
dispatch_once(&styleOnceToken, ^{
|
||||
if ([UITraitCollection respondsToSelector:NSSelectorFromString(@"currentTraitCollection")]) {
|
||||
id currentTraitCollection = [UITraitCollection performSelector:NSSelectorFromString(@"currentTraitCollection")];
|
||||
if (nil != currentTraitCollection) {
|
||||
userInterfaceStyle = [currentTraitCollection valueForKey:@"userInterfaceStyle"];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (nil == userInterfaceStyle) {
|
||||
return @"unsupported";
|
||||
}
|
||||
|
||||
return [self getAppearanceName:userInterfaceStyle];
|
||||
}
|
||||
|
||||
+ (NSString *)getAppearanceName:(NSNumber *)appearance
|
||||
{
|
||||
switch ([appearance longLongValue]) {
|
||||
case FBUIInterfaceAppearanceUnspecified:
|
||||
return @"automatic";
|
||||
case FBUIInterfaceAppearanceLight:
|
||||
return @"light";
|
||||
case FBUIInterfaceAppearanceDark:
|
||||
return @"dark";
|
||||
default:
|
||||
return @"unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The string of TimeZone. Returns TZ timezone id by default. Returns TimeZone name by Apple if TZ timezone id is not available.
|
||||
*/
|
||||
+ (NSString *)timeZone
|
||||
{
|
||||
NSTimeZone *localTimeZone = [NSTimeZone localTimeZone];
|
||||
// Apple timezone name like "US/New_York"
|
||||
NSString *timeZoneAbb = [localTimeZone abbreviation];
|
||||
if (timeZoneAbb == nil) {
|
||||
return [localTimeZone name];
|
||||
}
|
||||
|
||||
// Convert timezone name to ids like "America/New_York" as TZ database Time Zones format
|
||||
// https://developer.apple.com/documentation/foundation/nstimezone
|
||||
NSString *timeZoneId = [[NSTimeZone timeZoneWithAbbreviation:timeZoneAbb] name];
|
||||
if (timeZoneId != nil) {
|
||||
return timeZoneId;
|
||||
}
|
||||
|
||||
return [localTimeZone name];
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV // tvOS does not provide relevant APIs
|
||||
+ (id<FBResponsePayload>)handleGetSimulatedLocation:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
CLLocation *location = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error];
|
||||
if (nil != error) {
|
||||
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithObject(@{
|
||||
@"latitude": location ? @(location.coordinate.latitude) : NSNull.null,
|
||||
@"longitude": location ? @(location.coordinate.longitude) : NSNull.null,
|
||||
@"altitude": location ? @(location.altitude) : NSNull.null,
|
||||
});
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSetSimulatedLocation:(FBRouteRequest *)request
|
||||
{
|
||||
NSNumber *longitude = request.arguments[@"longitude"];
|
||||
NSNumber *latitude = request.arguments[@"latitude"];
|
||||
|
||||
if (nil == longitude || nil == latitude) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both latitude and longitude must be provided"
|
||||
traceback:nil]);
|
||||
}
|
||||
NSError *error;
|
||||
CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude.doubleValue
|
||||
longitude:longitude.doubleValue];
|
||||
if (![XCUIDevice.sharedDevice fb_setSimulatedLocation:location error:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleClearSimulatedLocation:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
if (![XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
#if __clang_major__ >= 15
|
||||
+ (id<FBResponsePayload>)handleKeyboardInput:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
BOOL hasElement = ![request.parameters[@"uuid"] isEqual:@"0"];
|
||||
XCUIElement *destination = hasElement
|
||||
? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
|
||||
checkStaleness:YES]
|
||||
: request.session.activeApplication;
|
||||
id keys = request.arguments[@"keys"];
|
||||
|
||||
if (![destination respondsToSelector:@selector(typeKey:modifierFlags:)]) {
|
||||
NSString *message = @"typeKey API is only supported since Xcode15 and iPadOS 17";
|
||||
return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:message
|
||||
traceback:nil]);
|
||||
}
|
||||
|
||||
if (![keys isKindOfClass:NSArray.class]) {
|
||||
NSString *message = @"The 'keys' argument must be an array";
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
|
||||
traceback:nil]);
|
||||
}
|
||||
for (id item in (NSArray *)keys) {
|
||||
if ([item isKindOfClass:NSString.class]) {
|
||||
NSString *keyValue = [FBKeyboard keyValueForName:item] ?: item;
|
||||
[destination typeKey:keyValue modifierFlags:XCUIKeyModifierNone];
|
||||
} else if ([item isKindOfClass:NSDictionary.class]) {
|
||||
id key = [(NSDictionary *)item objectForKey:@"key"];
|
||||
if (![key isKindOfClass:NSString.class]) {
|
||||
NSString *message = [NSString stringWithFormat:@"All dictionaries of 'keys' array must have the 'key' item of type string. Got '%@' instead in the item %@", key, item];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
|
||||
traceback:nil]);
|
||||
}
|
||||
id modifiers = [(NSDictionary *)item objectForKey:@"modifierFlags"];
|
||||
NSUInteger modifierFlags = XCUIKeyModifierNone;
|
||||
if ([modifiers isKindOfClass:NSNumber.class]) {
|
||||
modifierFlags = [(NSNumber *)modifiers unsignedIntValue];
|
||||
}
|
||||
NSString *keyValue = [FBKeyboard keyValueForName:item] ?: key;
|
||||
[destination typeKey:keyValue modifierFlags:modifierFlags];
|
||||
} else {
|
||||
NSString *message = @"All items of the 'keys' array must be either dictionaries or strings";
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
|
||||
traceback:nil]);
|
||||
}
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+ (id<FBResponsePayload>)handlePerformAccessibilityAudit:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
NSArray *requestedTypes = request.arguments[@"auditTypes"];
|
||||
NSMutableSet *typesSet = [NSMutableSet set];
|
||||
if (nil == requestedTypes || 0 == [requestedTypes count]) {
|
||||
[typesSet addObject:@"XCUIAccessibilityAuditTypeAll"];
|
||||
} else {
|
||||
[typesSet addObjectsFromArray:requestedTypes];
|
||||
}
|
||||
NSArray *result = [request.session.activeApplication fb_performAccessibilityAuditWithAuditTypesSet:typesSet.copy
|
||||
error:&error];
|
||||
if (nil == result) {
|
||||
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithObject(result);
|
||||
}
|
||||
|
||||
@end
|
||||
19
WebDriverAgentLib/Commands/FBDebugCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBDebugCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBDebugCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
81
WebDriverAgentLib/Commands/FBDebugCommands.m
Normal file
81
WebDriverAgentLib/Commands/FBDebugCommands.m
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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 "FBDebugCommands.h"
|
||||
|
||||
#import "FBRouteRequest.h"
|
||||
#import "FBSession.h"
|
||||
#import "FBXMLGenerationOptions.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "FBXPath.h"
|
||||
|
||||
@implementation FBDebugCommands
|
||||
|
||||
#pragma mark - <FBCommandHandler>
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute GET:@"/source"] respondWithTarget:self action:@selector(handleGetSourceCommand:)],
|
||||
[[FBRoute GET:@"/source"].withoutSession respondWithTarget:self action:@selector(handleGetSourceCommand:)],
|
||||
[[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)],
|
||||
[[FBRoute GET:@"/wda/accessibleSource"].withoutSession respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Commands
|
||||
|
||||
static NSString *const SOURCE_FORMAT_XML = @"xml";
|
||||
static NSString *const SOURCE_FORMAT_JSON = @"json";
|
||||
static NSString *const SOURCE_FORMAT_DESCRIPTION = @"description";
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetSourceCommand:(FBRouteRequest *)request
|
||||
{
|
||||
// This method might be called without session
|
||||
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
NSString *sourceType = request.parameters[@"format"] ?: SOURCE_FORMAT_XML;
|
||||
NSString *sourceScope = request.parameters[@"scope"];
|
||||
id result;
|
||||
if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_XML] == NSOrderedSame) {
|
||||
NSArray<NSString *> *excludedAttributes = nil == request.parameters[@"excluded_attributes"]
|
||||
? nil
|
||||
: [request.parameters[@"excluded_attributes"] componentsSeparatedByString:@","];
|
||||
result = [application fb_xmlRepresentationWithOptions:
|
||||
[[[FBXMLGenerationOptions new]
|
||||
withExcludedAttributes:excludedAttributes]
|
||||
withScope:sourceScope]];
|
||||
} else if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_JSON] == NSOrderedSame) {
|
||||
NSString *excludedAttributesString = request.parameters[@"excluded_attributes"];
|
||||
NSSet<NSString *> *excludedAttributes = (excludedAttributesString == nil)
|
||||
? nil
|
||||
: [NSSet setWithArray:[excludedAttributesString componentsSeparatedByString:@","]];
|
||||
|
||||
result = [application fb_tree:excludedAttributes];
|
||||
} else if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_DESCRIPTION] == NSOrderedSame) {
|
||||
result = application.fb_descriptionRepresentation;
|
||||
} else {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:[NSString stringWithFormat:@"Unknown source format '%@'. Only %@ source formats are supported.",
|
||||
sourceType, @[SOURCE_FORMAT_XML, SOURCE_FORMAT_JSON, SOURCE_FORMAT_DESCRIPTION]] traceback:nil]);
|
||||
}
|
||||
if (nil == result) {
|
||||
return FBResponseWithUnknownErrorFormat(@"Cannot get '%@' source of the current application", sourceType);
|
||||
}
|
||||
return FBResponseWithObject(result);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetAccessibleSourceCommand:(FBRouteRequest *)request
|
||||
{
|
||||
// This method might be called without session
|
||||
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
return FBResponseWithObject(application.fb_accessibilityTree ?: @{});
|
||||
}
|
||||
|
||||
@end
|
||||
19
WebDriverAgentLib/Commands/FBElementCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBElementCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBElementCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
819
WebDriverAgentLib/Commands/FBElementCommands.m
Normal file
819
WebDriverAgentLib/Commands/FBElementCommands.m
Normal file
@@ -0,0 +1,819 @@
|
||||
/**
|
||||
* 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 "FBElementCommands.h"
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBKeyboard.h"
|
||||
#import "FBRoute.h"
|
||||
#import "FBRouteRequest.h"
|
||||
#import "FBRunLoopSpinner.h"
|
||||
#import "FBElementCache.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBSession.h"
|
||||
#import "FBElementUtils.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBMathUtils.h"
|
||||
#import "FBRuntimeUtils.h"
|
||||
#import "NSPredicate+FBFormat.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
#import "XCUICoordinate.h"
|
||||
#import "XCUIDevice.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "XCUIElement+FBPickerWheel.h"
|
||||
#import "XCUIElement+FBScrolling.h"
|
||||
#import "XCUIElement+FBForceTouch.h"
|
||||
#import "XCUIElement+FBSwiping.h"
|
||||
#import "XCUIElement+FBTyping.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
#import "XCUIElement+FBTVFocuse.h"
|
||||
#import "XCUIElement+FBResolve.h"
|
||||
#import "XCUIElement+FBUID.h"
|
||||
#import "FBElementTypeTransformer.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "XCUIElementQuery.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
// 监听网络
|
||||
#import <sys/socket.h>
|
||||
#import <netinet/in.h>
|
||||
#import <arpa/inet.h>
|
||||
#import <fcntl.h>
|
||||
#import <unistd.h>
|
||||
#import <errno.h>
|
||||
|
||||
@interface FBElementCommands ()
|
||||
@end
|
||||
|
||||
@implementation FBElementCommands
|
||||
|
||||
#pragma mark - <FBCommandHandler>
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)],
|
||||
[[FBRoute GET:@"/window/rect"] respondWithTarget:self action:@selector(handleGetWindowRect:)],
|
||||
[[FBRoute GET:@"/window/size"].withoutSession respondWithTarget:self action:@selector(handleGetWindowSize:)],
|
||||
[[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)],
|
||||
[[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)],
|
||||
[[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)],
|
||||
[[FBRoute GET:@"/element/:uuid/text"] respondWithTarget:self action:@selector(handleGetText:)],
|
||||
[[FBRoute GET:@"/element/:uuid/displayed"] respondWithTarget:self action:@selector(handleGetDisplayed:)],
|
||||
[[FBRoute GET:@"/element/:uuid/selected"] respondWithTarget:self action:@selector(handleGetSelected:)],
|
||||
[[FBRoute GET:@"/element/:uuid/name"] respondWithTarget:self action:@selector(handleGetName:)],
|
||||
[[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)],
|
||||
[[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)],
|
||||
[[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)],
|
||||
// W3C element screenshot
|
||||
[[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)],
|
||||
// JSONWP element screenshot
|
||||
[[FBRoute GET:@"/screenshot/:uuid"] respondWithTarget:self action:@selector(handleElementScreenshot:)],
|
||||
[[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)],
|
||||
[[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)],
|
||||
#if TARGET_OS_TV
|
||||
[[FBRoute GET:@"/element/:uuid/attribute/focused"] respondWithTarget:self action:@selector(handleGetFocused:)],
|
||||
[[FBRoute POST:@"/wda/element/:uuid/focuse"] respondWithTarget:self action:@selector(handleFocuse:)],
|
||||
#else
|
||||
[[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)],
|
||||
[[FBRoute POST:@"/wda/swipe"] respondWithTarget:self action:@selector(handleSwipe:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)],
|
||||
[[FBRoute POST:@"/wda/pinch"] respondWithTarget:self action:@selector(handlePinch:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)],
|
||||
[[FBRoute POST:@"/wda/rotate"] respondWithTarget:self action:@selector(handleRotate:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)],
|
||||
[[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)],
|
||||
[[FBRoute POST:@"/wda/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self
|
||||
action:@selector(handleTapWithNumberOfTaps:)],
|
||||
[[FBRoute POST:@"/wda/tapWithNumberOfTaps"] respondWithTarget:self
|
||||
action:@selector(handleTapWithNumberOfTaps:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)],
|
||||
[[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)],
|
||||
[[FBRoute POST:@"/wda/scroll"] respondWithTarget:self action:@selector(handleScroll:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/scrollTo"] respondWithTarget:self action:@selector(handleScrollTo:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)],
|
||||
[[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragWithVelocity:)],
|
||||
[[FBRoute POST:@"/wda/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragCoordinateWithVelocity:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)],
|
||||
[[FBRoute POST:@"/wda/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/element/:uuid/tap"] respondWithTarget:self action:@selector(handleTap:)],
|
||||
[[FBRoute POST:@"/wda/tap"] respondWithTarget:self action:@selector(handleTap:)],
|
||||
|
||||
//添加网络监听方法 张伟 临时添加
|
||||
[[FBRoute GET:@"/wda/netWorkStatus"].withoutSession respondWithTarget:self action:@selector(handleNetWorkStatus:)],
|
||||
[[FBRoute POST:@"/wda/netWorkStatus"].withoutSession respondWithTarget:self action:@selector(handleNetWorkStatus:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)],
|
||||
#endif
|
||||
[[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Commands
|
||||
// 网络监听回调
|
||||
+ (id<FBResponsePayload>)handleNetWorkStatus:(FBRouteRequest *)request
|
||||
{
|
||||
BOOL reachable = FBHasExternalConnectivityViaHTTPS();
|
||||
return FBResponseWithObject(@(reachable));
|
||||
}
|
||||
|
||||
// 检测网络(更稳:更长超时 + 正确处理 wait 超时 + 更清晰的日志)
|
||||
static BOOL FBHasExternalConnectivityViaHTTPS(void) {
|
||||
__block BOOL ok = NO;
|
||||
|
||||
// 仍然保留你的 TikTok 域名探测
|
||||
NSArray<NSString *> *urlStrings = @[
|
||||
@"https://www.tiktok.com/robots.txt",
|
||||
@"https://www.tiktok.com/",
|
||||
@"https://m.tiktok.com/",
|
||||
@"https://www.tiktokv.com/",
|
||||
@"https://api.tiktokv.com/"
|
||||
];
|
||||
|
||||
// ✅ 改:用 default 配置(更接近系统正常网络栈),并把超时拉长
|
||||
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
|
||||
cfg.timeoutIntervalForRequest = 12.0;
|
||||
cfg.timeoutIntervalForResource = 12.0;
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
// ✅ 改:网络刚切换/刚连上时更稳,不会立刻失败
|
||||
cfg.waitsForConnectivity = YES;
|
||||
}
|
||||
|
||||
NSURLSession *s = [NSURLSession sessionWithConfiguration:cfg];
|
||||
|
||||
// ✅ 改:单个 URL 最多等 12 秒(和 timeoutIntervalForRequest 对齐)
|
||||
const NSTimeInterval perURLWaitSeconds = 12.0;
|
||||
|
||||
for (NSString *urlStr in urlStrings) {
|
||||
if (ok) { break; }
|
||||
|
||||
NSURL *url = [NSURL URLWithString:urlStr];
|
||||
if (!url) {
|
||||
NSLog(@"[NetCheck] invalid url: %@", urlStr);
|
||||
continue;
|
||||
}å
|
||||
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
|
||||
// ✅ 改:用 request,便于设置 UA/缓存策略/超时
|
||||
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
|
||||
req.HTTPMethod = @"GET";
|
||||
req.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||
req.timeoutInterval = perURLWaitSeconds;
|
||||
|
||||
// ✅ 可选但推荐:给一个普通 UA,避免被某些 WAF 当成“脚本默认 UA”更严格对待
|
||||
[req setValue:@"Mozilla/5.0 (iPhone; CPU iPhone OS like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile"
|
||||
forHTTPHeaderField:@"User-Agent"];
|
||||
|
||||
__block NSString *localErrDomain = nil;
|
||||
__block NSInteger localErrCode = 0;
|
||||
__block NSInteger localHttpCode = -1;
|
||||
|
||||
[[s dataTaskWithRequest:req completionHandler:^(NSData *d, NSURLResponse *r, NSError *e) {
|
||||
if (e) {
|
||||
localErrDomain = e.domain ?: @"";
|
||||
localErrCode = e.code;
|
||||
NSLog(@"[NetCheck] error (%@): domain=%@ code=%ld desc=%@ userInfo=%@",
|
||||
urlStr, localErrDomain, (long)localErrCode, e.localizedDescription, e.userInfo);
|
||||
} else {
|
||||
if ([r isKindOfClass:NSHTTPURLResponse.class]) {
|
||||
localHttpCode = ((NSHTTPURLResponse *)r).statusCode;
|
||||
NSLog(@"[NetCheck] HTTP (%@) = %ld", urlStr, (long)localHttpCode);
|
||||
|
||||
// ✅ 改:只要拿到 HTTP 响应(哪怕 301/403/404),说明“能连到站点”
|
||||
// 你原来是“无网络层错误就 ok”,这里更显式
|
||||
ok = YES;
|
||||
} else {
|
||||
NSLog(@"[NetCheck] response (%@): %@", urlStr, r);
|
||||
ok = YES;
|
||||
}
|
||||
}
|
||||
dispatch_semaphore_signal(sem);
|
||||
}] resume];
|
||||
|
||||
long waitResult = dispatch_semaphore_wait(
|
||||
sem,
|
||||
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(perURLWaitSeconds * NSEC_PER_SEC))
|
||||
);
|
||||
|
||||
// ✅ 改:如果等超时,明确记录一次(这在旧机上非常关键)
|
||||
if (waitResult != 0) {
|
||||
NSLog(@"[NetCheck] wait timeout (%@) after %.0fs (http=%ld err=%@/%ld)",
|
||||
urlStr, perURLWaitSeconds, (long)localHttpCode,
|
||||
localErrDomain ?: @"", (long)localErrCode);
|
||||
// 这里不把 ok 置为 NO(本来就是 NO),继续试下一个域名
|
||||
}
|
||||
|
||||
// 如果已经 ok,就提前结束
|
||||
if (ok) { break; }
|
||||
}
|
||||
|
||||
[s finishTasksAndInvalidate];
|
||||
NSLog(@"[NetCheck] TikTok reachability via HTTPS: %@", ok ? @"YES" : @"NO");
|
||||
return ok;
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetEnabled:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
return FBResponseWithObject(@(element.isWDEnabled));
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetRect:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
return FBResponseWithObject(element.wdRect);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetAttribute:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
NSString *attributeName = request.parameters[@"name"];
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
id attributeValue = [element fb_valueForWDAttributeName:attributeName];
|
||||
return FBResponseWithObject(attributeValue ?: [NSNull null]);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetText:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
// https://github.com/appium/appium-xcuitest-driver/issues/2552
|
||||
id<FBXCElementSnapshot> snapshot = [element fb_customSnapshot];
|
||||
FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
|
||||
id text = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel);
|
||||
return FBResponseWithObject(text ?: @"");
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetDisplayed:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
return FBResponseWithObject(@(element.isWDVisible));
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetAccessible:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
return FBResponseWithObject(@(element.isWDAccessible));
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetIsAccessibilityContainer:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
return FBResponseWithObject(@(element.isWDAccessibilityContainer));
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetName:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
return FBResponseWithObject(element.wdType);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetSelected:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
return FBResponseWithObject(@(element.wdSelected));
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSetValue:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
|
||||
checkStaleness:YES];
|
||||
id value = request.arguments[@"value"] ?: request.arguments[@"text"];
|
||||
if (!value) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Neither 'value' nor 'text' parameter is provided" traceback:nil]);
|
||||
}
|
||||
NSString *textToType = [value isKindOfClass:NSArray.class]
|
||||
? [value componentsJoinedByString:@""]
|
||||
: value;
|
||||
XCUIElementType elementType = [element elementType];
|
||||
#if !TARGET_OS_TV
|
||||
if (elementType == XCUIElementTypePickerWheel) {
|
||||
[element adjustToPickerWheelValue:textToType];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
#endif
|
||||
if (elementType == XCUIElementTypeSlider) {
|
||||
CGFloat sliderValue = textToType.floatValue;
|
||||
if (sliderValue < 0.0 || sliderValue > 1.0 ) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Value of slider should be in 0..1 range" traceback:nil]);
|
||||
}
|
||||
[element adjustToNormalizedSliderPosition:sliderValue];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
NSUInteger frequency = (NSUInteger)[request.arguments[@"frequency"] longLongValue] ?: [FBConfiguration maxTypingFrequency];
|
||||
NSError *error = nil;
|
||||
if (![element fb_typeText:textToType
|
||||
shouldClear:NO
|
||||
frequency:frequency
|
||||
error:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleClick:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] checkStaleness:YES];
|
||||
#if TARGET_OS_IOS
|
||||
[element tap];
|
||||
#elif TARGET_OS_TV
|
||||
NSError *error = nil;
|
||||
if (![element fb_selectWithError:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
|
||||
}
|
||||
#endif
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleClear:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
NSError *error;
|
||||
if (![element fb_clearTextWithError:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
+ (id<FBResponsePayload>)handleGetFocused:(FBRouteRequest *)request
|
||||
{
|
||||
// `BOOL isFocused = [elementCache elementForUUID:request.parameters[@"uuid"]];`
|
||||
// returns wrong true/false after moving focus by key up/down, for example.
|
||||
// Thus, ensure the focus compares the status with `fb_focusedElement`.
|
||||
BOOL isFocused = NO;
|
||||
XCUIElement *focusedElement = request.session.activeApplication.fb_focusedElement;
|
||||
if (focusedElement != nil) {
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
BOOL useNativeCachingStrategy = request.session.useNativeCachingStrategy;
|
||||
NSString *focusedUUID = [elementCache storeElement:(useNativeCachingStrategy
|
||||
? focusedElement
|
||||
: [focusedElement fb_stableInstanceWithUid:focusedElement.fb_uid])];
|
||||
focusedElement.lastSnapshot = nil;
|
||||
if (focusedUUID && [focusedUUID isEqualToString:(id)request.parameters[@"uuid"]]) {
|
||||
isFocused = YES;
|
||||
}
|
||||
}
|
||||
|
||||
return FBResponseWithObject(@(isFocused));
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleFocuse:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
NSError *error;
|
||||
if (![element fb_setFocusWithError:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
|
||||
}
|
||||
return FBResponseWithStatus([FBCommandStatus okWithValue: FBDictionaryResponseWithElement(element, FBConfiguration.shouldUseCompactResponses)]);
|
||||
}
|
||||
#else
|
||||
+ (id<FBResponsePayload>)handleDoubleTap:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
|
||||
if (nil == target) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
|
||||
traceback:nil]);
|
||||
}
|
||||
[target doubleTap];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleTwoFingerTap:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIElement *element = [self targetFromRequest:request];
|
||||
[element twoFingerTap];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleTapWithNumberOfTaps:(FBRouteRequest *)request
|
||||
{
|
||||
if (nil == request.arguments[@"numberOfTaps"] || nil == request.arguments[@"numberOfTouches"]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both 'numberOfTaps' and 'numberOfTouches' arguments must be provided"
|
||||
traceback:nil]);
|
||||
}
|
||||
XCUIElement *element = [self targetFromRequest:request];
|
||||
[element tapWithNumberOfTaps:[request.arguments[@"numberOfTaps"] integerValue]
|
||||
numberOfTouches:[request.arguments[@"numberOfTouches"] integerValue]];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleTouchAndHold:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
|
||||
if (nil == target) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
|
||||
traceback:nil]);
|
||||
}
|
||||
[target pressForDuration:[request.arguments[@"duration"] doubleValue]];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handlePressAndDragWithVelocity:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [self targetFromRequest:request];
|
||||
[element pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
|
||||
thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"] checkStaleness:YES]
|
||||
withVelocity:[request.arguments[@"velocity"] doubleValue]
|
||||
thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handlePressAndDragCoordinateWithVelocity:(FBRouteRequest *)request
|
||||
{
|
||||
FBSession *session = request.session;
|
||||
CGVector startOffset = CGVectorMake((CGFloat)[request.arguments[@"fromX"] doubleValue],
|
||||
(CGFloat)[request.arguments[@"fromY"] doubleValue]);
|
||||
XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset
|
||||
element:session.activeApplication];
|
||||
CGVector endOffset = CGVectorMake((CGFloat)[request.arguments[@"toX"] doubleValue],
|
||||
(CGFloat)[request.arguments[@"toY"] doubleValue]);
|
||||
XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset
|
||||
element:session.activeApplication];
|
||||
[startCoordinate pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
|
||||
thenDragToCoordinate:endCoordinate
|
||||
withVelocity:[request.arguments[@"velocity"] doubleValue]
|
||||
thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleScroll:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIElement *element = [self targetFromRequest:request];
|
||||
// Using presence of arguments as a way to convey control flow seems like a pretty bad idea but it's
|
||||
// what ios-driver did and sadly, we must copy them.
|
||||
NSString *const name = request.arguments[@"name"];
|
||||
if (name) {
|
||||
XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny]
|
||||
matchingIdentifier:name] allElementsBoundByIndex] lastObject];
|
||||
if (!childElement) {
|
||||
return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' identifier didn't match any elements", name]
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
return [self.class handleScrollElementToVisible:childElement withRequest:request];
|
||||
}
|
||||
|
||||
NSString *const direction = request.arguments[@"direction"];
|
||||
if (direction) {
|
||||
NSString *const distanceString = request.arguments[@"distance"] ?: @"1.0";
|
||||
CGFloat distance = (CGFloat)distanceString.doubleValue;
|
||||
if ([direction isEqualToString:@"up"]) {
|
||||
[element fb_scrollUpByNormalizedDistance:distance];
|
||||
} else if ([direction isEqualToString:@"down"]) {
|
||||
[element fb_scrollDownByNormalizedDistance:distance];
|
||||
} else if ([direction isEqualToString:@"left"]) {
|
||||
[element fb_scrollLeftByNormalizedDistance:distance];
|
||||
} else if ([direction isEqualToString:@"right"]) {
|
||||
[element fb_scrollRightByNormalizedDistance:distance];
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
NSString *const predicateString = request.arguments[@"predicateString"];
|
||||
if (predicateString) {
|
||||
NSPredicate *formattedPredicate = [NSPredicate fb_snapshotBlockPredicateWithPredicate:[NSPredicate
|
||||
predicateWithFormat:predicateString]];
|
||||
XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny]
|
||||
matchingPredicate:formattedPredicate] allElementsBoundByIndex] lastObject];
|
||||
if (!childElement) {
|
||||
return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' predicate didn't match any elements", predicateString]
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
return [self.class handleScrollElementToVisible:childElement withRequest:request];
|
||||
}
|
||||
|
||||
if (request.arguments[@"toVisible"]) {
|
||||
return [self.class handleScrollElementToVisible:element withRequest:request];
|
||||
}
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Unsupported scroll type" traceback:nil]);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleScrollTo:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
NSError *error;
|
||||
return [element fb_nativeScrollToVisibleWithError:&error]
|
||||
? FBResponseWithOK()
|
||||
: FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleDrag:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIElement *target = [self targetFromRequest:request];
|
||||
CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue],
|
||||
[request.arguments[@"fromY"] doubleValue]);
|
||||
XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:target];
|
||||
CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue],
|
||||
[request.arguments[@"toY"] doubleValue]);
|
||||
XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset element:target];
|
||||
NSTimeInterval duration = [request.arguments[@"duration"] doubleValue];
|
||||
[startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSwipe:(FBRouteRequest *)request
|
||||
{
|
||||
NSString *const direction = request.arguments[@"direction"];
|
||||
if (!direction) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'direction' parameter" traceback:nil]);
|
||||
}
|
||||
NSArray<NSString *> *supportedDirections = @[@"up", @"down", @"left", @"right"];
|
||||
if (![supportedDirections containsObject:direction.lowercaseString]) {
|
||||
NSString *message = [NSString stringWithFormat:@"Unsupported swipe direction '%@'. Only the following directions are supported: %@", direction, supportedDirections];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
|
||||
traceback:nil]);
|
||||
}
|
||||
NSError *error;
|
||||
id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
|
||||
if (nil == target) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
|
||||
traceback:nil]);
|
||||
}
|
||||
[target fb_swipeWithDirection:direction velocity:request.arguments[@"velocity"]];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleTap:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
|
||||
if (nil == target) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
|
||||
traceback:nil]);
|
||||
}
|
||||
[target tap];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handlePinch:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIElement *element = [self targetFromRequest:request];
|
||||
CGFloat scale = (CGFloat)[request.arguments[@"scale"] doubleValue];
|
||||
CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue];
|
||||
[element pinchWithScale:scale velocity:velocity];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleRotate:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIElement *element = [self targetFromRequest:request];
|
||||
CGFloat rotation = (CGFloat)[request.arguments[@"rotation"] doubleValue];
|
||||
CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue];
|
||||
[element rotate:rotation withVelocity:velocity];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleForceTouch:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIElement *element = [self targetFromRequest:request];
|
||||
NSNumber *pressure = request.arguments[@"pressure"];
|
||||
NSNumber *duration = request.arguments[@"duration"];
|
||||
NSNumber *x = request.arguments[@"x"];
|
||||
NSNumber *y = request.arguments[@"y"];
|
||||
NSValue *hitPoint = (nil == x || nil == y)
|
||||
? nil
|
||||
: [NSValue valueWithCGPoint:CGPointMake((CGFloat)[x doubleValue], (CGFloat)[y doubleValue])];
|
||||
NSError *error;
|
||||
BOOL didSucceed = [element fb_forceTouchCoordinate:hitPoint
|
||||
pressure:pressure
|
||||
duration:duration
|
||||
error:&error];
|
||||
return didSucceed
|
||||
? FBResponseWithOK()
|
||||
: FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
#endif
|
||||
|
||||
+ (id<FBResponsePayload>)handleKeys:(FBRouteRequest *)request
|
||||
{
|
||||
NSString *textToType = [request.arguments[@"value"] componentsJoinedByString:@""];
|
||||
NSUInteger frequency = [request.arguments[@"frequency"] unsignedIntegerValue] ?: [FBConfiguration maxTypingFrequency];
|
||||
NSError *error;
|
||||
if (!FBTypeText(textToType, frequency, &error)) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetWindowSize:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
|
||||
CGRect frame = app.wdFrame;
|
||||
#if TARGET_OS_TV
|
||||
CGSize screenSize = frame.size;
|
||||
#else
|
||||
CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation);
|
||||
#endif
|
||||
return FBResponseWithObject(@{
|
||||
@"width": @(screenSize.width),
|
||||
@"height": @(screenSize.height),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetWindowRect:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
|
||||
CGRect frame = app.wdFrame;
|
||||
#if TARGET_OS_TV
|
||||
CGSize screenSize = frame.size;
|
||||
#else
|
||||
CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation);
|
||||
#endif
|
||||
return FBResponseWithObject(@{
|
||||
@"x": @(frame.origin.x),
|
||||
@"y": @(frame.origin.y),
|
||||
@"width": @(screenSize.width),
|
||||
@"height": @(screenSize.height),
|
||||
});
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleElementScreenshot:(FBRouteRequest *)request
|
||||
{
|
||||
@autoreleasepool {
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
|
||||
checkStaleness:YES];
|
||||
NSData *screenshotData = nil;
|
||||
@autoreleasepool {
|
||||
screenshotData = [element.screenshot PNGRepresentation];
|
||||
if (nil == screenshotData) {
|
||||
NSString *errMsg = [NSString stringWithFormat:@"Cannot take a screenshot of %@", element.description];
|
||||
return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:errMsg
|
||||
traceback:nil]);
|
||||
}
|
||||
}
|
||||
NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0];
|
||||
screenshotData = nil;
|
||||
return FBResponseWithObject(screenshot);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
static const CGFloat DEFAULT_PICKER_OFFSET = (CGFloat)0.2;
|
||||
static const NSInteger DEFAULT_MAX_PICKER_ATTEMPTS = 25;
|
||||
|
||||
|
||||
+ (id<FBResponsePayload>)handleWheelSelect:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
|
||||
checkStaleness:YES];
|
||||
if ([element elementType] != XCUIElementTypePickerWheel) {
|
||||
NSString *errMsg = [NSString stringWithFormat:@"The element is expected to be a valid Picker Wheel control. '%@' was given instead", element.wdType];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
NSString* order = [request.arguments[@"order"] lowercaseString];
|
||||
CGFloat offset = DEFAULT_PICKER_OFFSET;
|
||||
if (request.arguments[@"offset"]) {
|
||||
offset = (CGFloat)[request.arguments[@"offset"] doubleValue];
|
||||
if (offset <= 0.0 || offset > 0.5) {
|
||||
NSString *errMsg = [NSString stringWithFormat:@"'offset' value is expected to be in range (0.0, 0.5]. '%@' was given instead", request.arguments[@"offset"]];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
}
|
||||
NSNumber *maxAttempts = request.arguments[@"maxAttempts"] ?: @(DEFAULT_MAX_PICKER_ATTEMPTS);
|
||||
NSString *expectedValue = request.arguments[@"value"];
|
||||
NSInteger attempt = 0;
|
||||
while (attempt < [maxAttempts integerValue]) {
|
||||
BOOL isSuccessful = false;
|
||||
NSError *error;
|
||||
if ([order isEqualToString:@"next"]) {
|
||||
isSuccessful = [element fb_selectNextOptionWithOffset:offset error:&error];
|
||||
} else if ([order isEqualToString:@"previous"]) {
|
||||
isSuccessful = [element fb_selectPreviousOptionWithOffset:offset error:&error];
|
||||
} else {
|
||||
NSString *errMsg = [NSString stringWithFormat:@"Only 'previous' and 'next' order values are supported. '%@' was given instead", request.arguments[@"order"]];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
if (!isSuccessful) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
|
||||
}
|
||||
if (nil == expectedValue || [element.wdValue isEqualToString:expectedValue]) {
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
attempt++;
|
||||
}
|
||||
NSString *errMsg = [NSString stringWithFormat:@"Cannot select the expected picker wheel value '%@' after %ld attempts", expectedValue, attempt];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:errMsg traceback:nil]);
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
+ (id<FBResponsePayload>)handleScrollElementToVisible:(XCUIElement *)element withRequest:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
if (!element.exists) {
|
||||
return FBResponseWithStatus([FBCommandStatus elementNotVisibleErrorWithMessage:@"Can't scroll to element that does not exist" traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
if (![element fb_scrollToVisibleWithError:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
/**
|
||||
Returns gesture coordinate for the element based on absolute coordinate
|
||||
|
||||
@param offset absolute screen offset for the given application
|
||||
@param element the element instance to perform the gesture on
|
||||
@return translated gesture coordinates ready to be passed to XCUICoordinate methods
|
||||
*/
|
||||
+ (XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset
|
||||
element:(XCUIElement *)element
|
||||
{
|
||||
return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset];
|
||||
}
|
||||
|
||||
/**
|
||||
Returns either coordinates or the target element for the given request that expects 'x' and 'y' coordannates
|
||||
|
||||
@param request HTTP request object
|
||||
@param error Error instance if any
|
||||
@return Either XCUICoordinate or XCUIElement instance. nil if the input data is invalid
|
||||
*/
|
||||
+ (nullable id)targetWithXyCoordinatesFromRequest:(FBRouteRequest *)request error:(NSError **)error
|
||||
{
|
||||
NSNumber *x = request.arguments[@"x"];
|
||||
NSNumber *y = request.arguments[@"y"];
|
||||
if (nil == x && nil == y) {
|
||||
return [self targetFromRequest:request];
|
||||
}
|
||||
if ((nil == x && nil != y) || (nil != x && nil == y)) {
|
||||
[[[FBErrorBuilder alloc]
|
||||
withDescription:@"Both x and y coordinates must be provided"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
return [self gestureCoordinateWithOffset:CGVectorMake(x.doubleValue, y.doubleValue)
|
||||
element:[self targetFromRequest:request]];
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the target element for the given request
|
||||
|
||||
@param request HTTP request object
|
||||
@return Matching XCUIElement instance
|
||||
*/
|
||||
+ (XCUIElement *)targetFromRequest:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
NSString *elementUuid = (NSString *)request.parameters[@"uuid"];
|
||||
return nil == elementUuid
|
||||
? request.session.activeApplication
|
||||
: [elementCache elementForUUID:elementUuid checkStaleness:YES];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
18
WebDriverAgentLib/Commands/FBFindElementCommands.h
Normal file
18
WebDriverAgentLib/Commands/FBFindElementCommands.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>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBFindElementCommands : NSObject <FBCommandHandler>
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
186
WebDriverAgentLib/Commands/FBFindElementCommands.m
Normal file
186
WebDriverAgentLib/Commands/FBFindElementCommands.m
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 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 "FBFindElementCommands.h"
|
||||
|
||||
#import "FBAlert.h"
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBElementCache.h"
|
||||
#import "FBExceptions.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBRouteRequest.h"
|
||||
#import "FBSession.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIElement+FBClassChain.h"
|
||||
#import "XCUIElement+FBFind.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "XCUIElement+FBUID.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
|
||||
static id<FBResponsePayload> FBNoSuchElementErrorResponseForRequest(FBRouteRequest *request)
|
||||
{
|
||||
return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"unable to find an element using '%@', value '%@'", request.arguments[@"using"], request.arguments[@"value"]]
|
||||
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
|
||||
}
|
||||
|
||||
@implementation FBFindElementCommands
|
||||
|
||||
#pragma mark - <FBCommandHandler>
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute POST:@"/element"] respondWithTarget:self action:@selector(handleFindElement:)],
|
||||
[[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)],
|
||||
[[FBRoute POST:@"/element/:uuid/element"] respondWithTarget:self action:@selector(handleFindSubElement:)],
|
||||
[[FBRoute POST:@"/element/:uuid/elements"] respondWithTarget:self action:@selector(handleFindSubElements:)],
|
||||
[[FBRoute GET:@"/wda/element/:uuid/getVisibleCells"] respondWithTarget:self action:@selector(handleFindVisibleCells:)],
|
||||
#if TARGET_OS_TV
|
||||
[[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetFocusedElement:)],
|
||||
#else
|
||||
[[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetActiveElement:)],
|
||||
#endif
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Commands
|
||||
|
||||
+ (id<FBResponsePayload>)handleFindElement:(FBRouteRequest *)request
|
||||
{
|
||||
FBSession *session = request.session;
|
||||
XCUIElement *element = [self.class elementUsing:request.arguments[@"using"]
|
||||
withValue:request.arguments[@"value"]
|
||||
under:session.activeApplication];
|
||||
if (!element) {
|
||||
return FBNoSuchElementErrorResponseForRequest(request);
|
||||
}
|
||||
return FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleFindElements:(FBRouteRequest *)request
|
||||
{
|
||||
FBSession *session = request.session;
|
||||
NSArray *elements = [self.class elementsUsing:request.arguments[@"using"]
|
||||
withValue:request.arguments[@"value"]
|
||||
under:session.activeApplication
|
||||
shouldReturnAfterFirstMatch:NO];
|
||||
return FBResponseWithCachedElements(elements, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleFindVisibleCells:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
|
||||
id<FBXCElementSnapshot> snapshot = [element fb_customSnapshot];
|
||||
NSArray<id<FBXCElementSnapshot>> *visibleCellSnapshots = [snapshot descendantsByFilteringWithBlock:^BOOL(id<FBXCElementSnapshot> shot) {
|
||||
return shot.elementType == XCUIElementTypeCell
|
||||
&& [FBXCElementSnapshotWrapper ensureWrapped:shot].wdVisible;
|
||||
}];
|
||||
NSArray *cells = [element fb_filterDescendantsWithSnapshots:visibleCellSnapshots
|
||||
onlyChildren:NO];
|
||||
return FBResponseWithCachedElements(cells, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleFindSubElement:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
|
||||
checkStaleness:NO];
|
||||
XCUIElement *foundElement = [self.class elementUsing:request.arguments[@"using"]
|
||||
withValue:request.arguments[@"value"]
|
||||
under:element];
|
||||
if (!foundElement) {
|
||||
return FBNoSuchElementErrorResponseForRequest(request);
|
||||
}
|
||||
return FBResponseWithCachedElement(foundElement, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleFindSubElements:(FBRouteRequest *)request
|
||||
{
|
||||
FBElementCache *elementCache = request.session.elementCache;
|
||||
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
|
||||
checkStaleness:NO];
|
||||
NSArray *foundElements = [self.class elementsUsing:request.arguments[@"using"]
|
||||
withValue:request.arguments[@"value"]
|
||||
under:element
|
||||
shouldReturnAfterFirstMatch:NO];
|
||||
return FBResponseWithCachedElements(foundElements, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetActiveElement:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIElement *element = request.session.activeApplication.fb_activeElement;
|
||||
if (nil == element) {
|
||||
return FBNoSuchElementErrorResponseForRequest(request);
|
||||
}
|
||||
return FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
+ (id<FBResponsePayload>)handleGetFocusedElement:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIElement *element = request.session.activeApplication.fb_focusedElement;
|
||||
return element == nil
|
||||
? FBNoSuchElementErrorResponseForRequest(request)
|
||||
: FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
|
||||
}
|
||||
#endif
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
+ (XCUIElement *)elementUsing:(NSString *)usingText withValue:(NSString *)value under:(XCUIElement *)element
|
||||
{
|
||||
return [[self elementsUsing:usingText
|
||||
withValue:value
|
||||
under:element
|
||||
shouldReturnAfterFirstMatch:YES] firstObject];
|
||||
}
|
||||
|
||||
+ (NSArray *)elementsUsing:(NSString *)usingText
|
||||
withValue:(NSString *)value
|
||||
under:(XCUIElement *)element
|
||||
shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
|
||||
{
|
||||
if ([usingText isEqualToString:@"partial link text"]
|
||||
|| [usingText isEqualToString:@"link text"]) {
|
||||
NSArray *components = [value componentsSeparatedByString:@"="];
|
||||
NSString *propertyValue = components.lastObject;
|
||||
NSString *propertyName = (components.count < 2 ? @"name" : components.firstObject);
|
||||
return [element fb_descendantsMatchingProperty:propertyName
|
||||
value:propertyValue
|
||||
partialSearch:[usingText containsString:@"partial"]];
|
||||
} else if ([usingText isEqualToString:@"class name"]) {
|
||||
return [element fb_descendantsMatchingClassName:value
|
||||
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
|
||||
} else if ([usingText isEqualToString:@"class chain"]) {
|
||||
return [element fb_descendantsMatchingClassChain:value
|
||||
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
|
||||
} else if ([usingText isEqualToString:@"xpath"]) {
|
||||
return [element fb_descendantsMatchingXPathQuery:value
|
||||
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
|
||||
} else if ([usingText isEqualToString:@"predicate string"]) {
|
||||
return [element fb_descendantsMatchingPredicate:[NSPredicate predicateWithFormat:value]
|
||||
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
|
||||
} else if ([usingText isEqualToString:@"name"]
|
||||
|| [usingText isEqualToString:@"id"]
|
||||
|| [usingText isEqualToString:@"accessibility id"]) {
|
||||
return [element fb_descendantsMatchingIdentifier:value
|
||||
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
|
||||
} else {
|
||||
@throw [NSException exceptionWithName:FBElementAttributeUnknownException
|
||||
reason:[NSString stringWithFormat:@"Invalid locator requested: %@", usingText]
|
||||
userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
19
WebDriverAgentLib/Commands/FBOrientationCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBOrientationCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBOrientationCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
185
WebDriverAgentLib/Commands/FBOrientationCommands.m
Normal file
185
WebDriverAgentLib/Commands/FBOrientationCommands.m
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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 "FBOrientationCommands.h"
|
||||
#import "XCUIDevice+FBRotation.h"
|
||||
#import "FBRouteRequest.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBSession.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIDevice.h"
|
||||
|
||||
extern const struct FBWDOrientationValues {
|
||||
FBLiteralString portrait;
|
||||
FBLiteralString landscapeLeft;
|
||||
FBLiteralString landscapeRight;
|
||||
FBLiteralString portraitUpsideDown;
|
||||
} FBWDOrientationValues;
|
||||
|
||||
const struct FBWDOrientationValues FBWDOrientationValues = {
|
||||
.portrait = @"PORTRAIT",
|
||||
.landscapeLeft = @"LANDSCAPE",
|
||||
.landscapeRight = @"UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT",
|
||||
.portraitUpsideDown = @"UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN",
|
||||
};
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
@implementation FBOrientationCommands
|
||||
|
||||
#pragma mark - <FBCommandHandler>
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)],
|
||||
[[FBRoute GET:@"/orientation"].withoutSession respondWithTarget:self action:@selector(handleGetOrientation:)],
|
||||
[[FBRoute POST:@"/orientation"] respondWithTarget:self action:@selector(handleSetOrientation:)],
|
||||
[[FBRoute POST:@"/orientation"].withoutSession respondWithTarget:self action:@selector(handleSetOrientation:)],
|
||||
[[FBRoute GET:@"/rotation"] respondWithTarget:self action:@selector(handleGetRotation:)],
|
||||
[[FBRoute GET:@"/rotation"].withoutSession respondWithTarget:self action:@selector(handleGetRotation:)],
|
||||
[[FBRoute POST:@"/rotation"] respondWithTarget:self action:@selector(handleSetRotation:)],
|
||||
[[FBRoute POST:@"/rotation"].withoutSession respondWithTarget:self action:@selector(handleSetRotation:)],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Commands
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetOrientation:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
NSString *orientation = [self.class interfaceOrientationForApplication:application];
|
||||
return FBResponseWithObject([[self _wdOrientationsMapping] objectForKey:orientation]);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSetOrientation:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
if ([self.class setDeviceOrientation:request.arguments[@"orientation"] forApplication:application]) {
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
return FBResponseWithUnknownErrorFormat(@"Unable To Rotate Device");
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetRotation:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIDevice *device = [XCUIDevice sharedDevice];
|
||||
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
UIInterfaceOrientation orientation = application.interfaceOrientation;
|
||||
return FBResponseWithObject(device.fb_rotationMapping[@(orientation)]);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSetRotation:(FBRouteRequest *)request
|
||||
{
|
||||
if (nil == request.arguments[@"x"] || nil == request.arguments[@"y"] || nil == request.arguments[@"z"]) {
|
||||
NSString *errMessage = [NSString stringWithFormat:@"x, y and z arguments must exist in the request body: %@", request.arguments];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMessage
|
||||
traceback:nil]);
|
||||
}
|
||||
|
||||
NSDictionary* rotation = @{
|
||||
@"x": request.arguments[@"x"] ?: @0,
|
||||
@"y": request.arguments[@"y"] ?: @0,
|
||||
@"z": request.arguments[@"z"] ?: @0,
|
||||
};
|
||||
NSArray<NSDictionary *> *supportedRotations = XCUIDevice.sharedDevice.fb_rotationMapping.allValues;
|
||||
if (![supportedRotations containsObject:rotation]) {
|
||||
NSString *errMessage = [
|
||||
NSString stringWithFormat:@"%@ rotation is not supported. Only the following values are supported: %@",
|
||||
rotation, supportedRotations
|
||||
];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMessage
|
||||
traceback:nil]);
|
||||
}
|
||||
|
||||
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
|
||||
if (![self.class setDeviceRotation:request.arguments forApplication:application]) {
|
||||
NSString *errMessage = [
|
||||
NSString stringWithFormat:@"The current rotation cannot be set to %@. Make sure the %@ application supports it",
|
||||
rotation, application.bundleID
|
||||
];
|
||||
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:errMessage
|
||||
traceback:nil]);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
+ (NSString *)interfaceOrientationForApplication:(XCUIApplication *)application
|
||||
{
|
||||
NSNumber *orientation = @(application.interfaceOrientation);
|
||||
NSSet *keys = [[self _orientationsMapping] keysOfEntriesPassingTest:^BOOL(id key, NSNumber *obj, BOOL *stop) {
|
||||
return [obj isEqualToNumber:orientation];
|
||||
}];
|
||||
if (keys.count == 0) {
|
||||
return @"Unknown orientation";
|
||||
}
|
||||
return keys.anyObject;
|
||||
}
|
||||
|
||||
+ (BOOL)setDeviceRotation:(NSDictionary *)rotationObj forApplication:(XCUIApplication *)application
|
||||
{
|
||||
return [[XCUIDevice sharedDevice] fb_setDeviceRotation:rotationObj];
|
||||
}
|
||||
|
||||
+ (BOOL)setDeviceOrientation:(NSString *)orientation forApplication:(XCUIApplication *)application
|
||||
{
|
||||
NSNumber *orientationValue = [[self _orientationsMapping] objectForKey:[orientation uppercaseString]];
|
||||
if (orientationValue == nil) {
|
||||
return NO;
|
||||
}
|
||||
return [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientationValue.integerValue];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)_orientationsMapping
|
||||
{
|
||||
static NSDictionary *orientationMap;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
orientationMap =
|
||||
@{
|
||||
FBWDOrientationValues.portrait : @(UIDeviceOrientationPortrait),
|
||||
FBWDOrientationValues.portraitUpsideDown : @(UIDeviceOrientationPortraitUpsideDown),
|
||||
FBWDOrientationValues.landscapeLeft : @(UIDeviceOrientationLandscapeLeft),
|
||||
FBWDOrientationValues.landscapeRight : @(UIDeviceOrientationLandscapeRight),
|
||||
};
|
||||
});
|
||||
return orientationMap;
|
||||
}
|
||||
|
||||
/*
|
||||
We already have FBWDOrientationValues as orientation descriptions, however the strings are not valid
|
||||
WebDriver responses. WebDriver can only receive 'portrait' or 'landscape'. So we can pass the keys
|
||||
through this additional filter to ensure we get one of those. It's essentially a mapping from
|
||||
FBWDOrientationValues to the valid subset of itself we can return to the client
|
||||
*/
|
||||
+ (NSDictionary *)_wdOrientationsMapping
|
||||
{
|
||||
static NSDictionary *orientationMap;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
orientationMap =
|
||||
@{
|
||||
FBWDOrientationValues.portrait : FBWDOrientationValues.portrait,
|
||||
FBWDOrientationValues.portraitUpsideDown : FBWDOrientationValues.portrait,
|
||||
FBWDOrientationValues.landscapeLeft : FBWDOrientationValues.landscapeLeft,
|
||||
FBWDOrientationValues.landscapeRight : FBWDOrientationValues.landscapeLeft,
|
||||
};
|
||||
});
|
||||
return orientationMap;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
19
WebDriverAgentLib/Commands/FBScreenshotCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBScreenshotCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBScreenshotCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
40
WebDriverAgentLib/Commands/FBScreenshotCommands.m
Normal file
40
WebDriverAgentLib/Commands/FBScreenshotCommands.m
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 "FBScreenshotCommands.h"
|
||||
|
||||
#import "XCUIDevice+FBHelpers.h"
|
||||
|
||||
@implementation FBScreenshotCommands
|
||||
|
||||
#pragma mark - <FBCommandHandler>
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)],
|
||||
[[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Commands
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetScreenshot:(FBRouteRequest *)request
|
||||
{
|
||||
NSError *error;
|
||||
NSData *screenshotData = [[XCUIDevice sharedDevice] fb_screenshotWithError:&error];
|
||||
if (nil == screenshotData) {
|
||||
return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:error.description traceback:nil]);
|
||||
}
|
||||
NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0];
|
||||
return FBResponseWithObject(screenshot);
|
||||
}
|
||||
|
||||
@end
|
||||
19
WebDriverAgentLib/Commands/FBSessionCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBSessionCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBSessionCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
586
WebDriverAgentLib/Commands/FBSessionCommands.m
Normal file
586
WebDriverAgentLib/Commands/FBSessionCommands.m
Normal file
@@ -0,0 +1,586 @@
|
||||
/**
|
||||
* 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 "FBSessionCommands.h"
|
||||
|
||||
#import "FBCapabilities.h"
|
||||
#import "FBClassChainQueryParser.h"
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBExceptions.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBProtocolHelpers.h"
|
||||
#import "FBRouteRequest.h"
|
||||
#import "FBSession.h"
|
||||
#import "FBSettings.h"
|
||||
#import "FBRuntimeUtils.h"
|
||||
#import "FBActiveAppDetectionPoint.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIApplication+FBQuiescence.h"
|
||||
#import "XCUIDevice.h"
|
||||
#import "XCUIDevice+FBHealthCheck.h"
|
||||
#import "XCUIDevice+FBHelpers.h"
|
||||
#import "XCUIApplicationProcessDelay.h"
|
||||
|
||||
|
||||
@implementation FBSessionCommands
|
||||
|
||||
#pragma mark - <FBCommandHandler>
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute POST:@"/url"] respondWithTarget:self action:@selector(handleOpenURL:)],
|
||||
[[FBRoute POST:@"/session"].withoutSession respondWithTarget:self action:@selector(handleCreateSession:)],
|
||||
[[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)],
|
||||
[[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)],
|
||||
[[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)],
|
||||
[[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)],
|
||||
[[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)],
|
||||
[[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)],
|
||||
[[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)],
|
||||
[[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)],
|
||||
|
||||
// Health check might modify simulator state so it should only be called in-between testing sessions
|
||||
[[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)],
|
||||
|
||||
// Settings endpoints
|
||||
[[FBRoute GET:@"/appium/settings"] respondWithTarget:self action:@selector(handleGetSettings:)],
|
||||
[[FBRoute POST:@"/appium/settings"] respondWithTarget:self action:@selector(handleSetSettings:)],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Commands
|
||||
|
||||
+ (id<FBResponsePayload>)handleOpenURL:(FBRouteRequest *)request
|
||||
{
|
||||
NSString *urlString = request.arguments[@"url"];
|
||||
if (!urlString) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"URL is required" traceback:nil]);
|
||||
}
|
||||
NSString* bundleId = request.arguments[@"bundleId"];
|
||||
NSNumber* idleTimeoutMs = request.arguments[@"idleTimeoutMs"];
|
||||
NSError *error;
|
||||
if (nil == bundleId) {
|
||||
if (![XCUIDevice.sharedDevice fb_openUrl:urlString error:&error]) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
} else {
|
||||
if (![XCUIDevice.sharedDevice fb_openUrl:urlString withApplication:bundleId error:&error]) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
if (idleTimeoutMs.doubleValue > 0) {
|
||||
XCUIApplication *app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleId];
|
||||
[app fb_waitUntilStableWithTimeout:FBMillisToSeconds(idleTimeoutMs.doubleValue)];
|
||||
}
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleCreateSession:(FBRouteRequest *)request
|
||||
{
|
||||
if (nil != FBSession.activeSession) {
|
||||
[FBSession.activeSession kill];
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, id> *capabilities;
|
||||
NSError *error;
|
||||
if (![request.arguments[@"capabilities"] isKindOfClass:NSDictionary.class]) {
|
||||
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:@"'capabilities' is mandatory to create a new session"
|
||||
traceback:nil]);
|
||||
}
|
||||
if (nil == (capabilities = FBParseCapabilities((NSDictionary *)request.arguments[@"capabilities"], &error))) {
|
||||
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:error.localizedDescription traceback:nil]);
|
||||
}
|
||||
|
||||
[FBConfiguration resetSessionSettings];
|
||||
[FBConfiguration setShouldUseTestManagerForVisibilityDetection:[capabilities[FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION] boolValue]];
|
||||
if (capabilities[FB_SETTING_USE_COMPACT_RESPONSES]) {
|
||||
[FBConfiguration setShouldUseCompactResponses:[capabilities[FB_SETTING_USE_COMPACT_RESPONSES] boolValue]];
|
||||
}
|
||||
NSString *elementResponseAttributes = capabilities[FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES];
|
||||
if (elementResponseAttributes) {
|
||||
[FBConfiguration setElementResponseAttributes:elementResponseAttributes];
|
||||
}
|
||||
if (capabilities[FB_CAP_MAX_TYPING_FREQUENCY]) {
|
||||
[FBConfiguration setMaxTypingFrequency:[capabilities[FB_CAP_MAX_TYPING_FREQUENCY] unsignedIntegerValue]];
|
||||
}
|
||||
if (capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER]) {
|
||||
[FBConfiguration setShouldUseSingletonTestManager:[capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER] boolValue]];
|
||||
}
|
||||
if (capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS]) {
|
||||
if ([capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS] boolValue]) {
|
||||
[FBConfiguration disableScreenshots];
|
||||
} else {
|
||||
[FBConfiguration enableScreenshots];
|
||||
}
|
||||
}
|
||||
if (capabilities[FB_CAP_SHOULD_TERMINATE_APP]) {
|
||||
[FBConfiguration setShouldTerminateApp:[capabilities[FB_CAP_SHOULD_TERMINATE_APP] boolValue]];
|
||||
}
|
||||
NSNumber *delay = capabilities[FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC];
|
||||
if ([delay doubleValue] > 0.0) {
|
||||
[XCUIApplicationProcessDelay setEventLoopHasIdledDelay:[delay doubleValue]];
|
||||
} else {
|
||||
[XCUIApplicationProcessDelay disableEventLoopDelay];
|
||||
}
|
||||
|
||||
if (nil != capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) {
|
||||
FBConfiguration.waitForIdleTimeout = [capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue];
|
||||
}
|
||||
|
||||
if (nil == capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] ||
|
||||
[capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] boolValue]) {
|
||||
[FBConfiguration forceSimulatorSoftwareKeyboardPresence];
|
||||
}
|
||||
|
||||
NSString *bundleID = capabilities[FB_CAP_BUNDLE_ID];
|
||||
NSString *initialUrl = capabilities[FB_CAP_INITIAL_URL];
|
||||
XCUIApplication *app = nil;
|
||||
if (bundleID != nil) {
|
||||
app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleID];
|
||||
BOOL forceAppLaunch = YES;
|
||||
if (nil != capabilities[FB_CAP_FORCE_APP_LAUNCH]) {
|
||||
forceAppLaunch = [capabilities[FB_CAP_FORCE_APP_LAUNCH] boolValue];
|
||||
}
|
||||
XCUIApplicationState appState = app.state;
|
||||
BOOL isAppRunning = appState >= XCUIApplicationStateRunningBackground;
|
||||
if (!isAppRunning || (isAppRunning && forceAppLaunch)) {
|
||||
app.fb_shouldWaitForQuiescence = nil == capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE]
|
||||
|| [capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] boolValue];
|
||||
app.launchArguments = (NSArray<NSString *> *)capabilities[FB_CAP_ARGUMENTS] ?: @[];
|
||||
app.launchEnvironment = (NSDictionary <NSString *, NSString *> *)capabilities[FB_CAP_ENVIRNOMENT] ?: @{};
|
||||
if (nil != initialUrl) {
|
||||
if (app.running) {
|
||||
[app terminate];
|
||||
}
|
||||
id<FBResponsePayload> errorResponse = [self openDeepLink:initialUrl
|
||||
withApplication:bundleID
|
||||
timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]];
|
||||
if (nil != errorResponse) {
|
||||
return errorResponse;
|
||||
}
|
||||
} else {
|
||||
NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout();
|
||||
if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) {
|
||||
_XCTSetApplicationStateTimeout([capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC] doubleValue]);
|
||||
}
|
||||
@try {
|
||||
[app launch];
|
||||
} @catch (NSException *e) {
|
||||
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:e.reason traceback:nil]);
|
||||
} @finally {
|
||||
if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) {
|
||||
_XCTSetApplicationStateTimeout(defaultTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!app.running) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Cannot launch %@ application. Make sure the correct bundle identifier has been provided in capabilities and check the device log for possible crash report occurrences", bundleID];
|
||||
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg
|
||||
traceback:nil]);
|
||||
}
|
||||
} else if (appState == XCUIApplicationStateRunningBackground && !forceAppLaunch) {
|
||||
if (nil != initialUrl) {
|
||||
id<FBResponsePayload> errorResponse = [self openDeepLink:initialUrl
|
||||
withApplication:bundleID
|
||||
timeout:nil];
|
||||
if (nil != errorResponse) {
|
||||
return errorResponse;
|
||||
}
|
||||
} else {
|
||||
[app activate];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nil != initialUrl && nil == bundleID) {
|
||||
id<FBResponsePayload> errorResponse = [self openDeepLink:initialUrl
|
||||
withApplication:nil
|
||||
timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]];
|
||||
if (nil != errorResponse) {
|
||||
return errorResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]) {
|
||||
[FBSession initWithApplication:app
|
||||
defaultAlertAction:(id)capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]];
|
||||
} else {
|
||||
[FBSession initWithApplication:app];
|
||||
}
|
||||
|
||||
if (nil != capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY]) {
|
||||
FBSession.activeSession.useNativeCachingStrategy = [capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY] boolValue];
|
||||
}
|
||||
|
||||
return FBResponseWithObject(FBSessionCommands.sessionInformation);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSessionAppLaunch:(FBRouteRequest *)request
|
||||
{
|
||||
[request.session launchApplicationWithBundleId:(id)request.arguments[@"bundleId"]
|
||||
shouldWaitForQuiescence:request.arguments[@"shouldWaitForQuiescence"]
|
||||
arguments:request.arguments[@"arguments"]
|
||||
environment:request.arguments[@"environment"]];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSessionAppActivate:(FBRouteRequest *)request
|
||||
{
|
||||
[request.session activateApplicationWithBundleId:(id)request.arguments[@"bundleId"]];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSessionAppTerminate:(FBRouteRequest *)request
|
||||
{
|
||||
BOOL result = [request.session terminateApplicationWithBundleId:(id)request.arguments[@"bundleId"]];
|
||||
return FBResponseWithObject(@(result));
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleSessionAppState:(FBRouteRequest *)request
|
||||
{
|
||||
NSUInteger state = [request.session applicationStateWithBundleId:(id)request.arguments[@"bundleId"]];
|
||||
return FBResponseWithObject(@(state));
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetActiveAppsList:(FBRouteRequest *)request
|
||||
{
|
||||
return FBResponseWithObject([XCUIApplication fb_activeAppsInfo]);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetActiveSession:(FBRouteRequest *)request
|
||||
{
|
||||
return FBResponseWithObject(FBSessionCommands.sessionInformation);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleDeleteSession:(FBRouteRequest *)request
|
||||
{
|
||||
[request.session kill];
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetStatus:(FBRouteRequest *)request
|
||||
{
|
||||
// For updatedWDABundleId capability by Appium
|
||||
NSString *productBundleIdentifier = @"com.facebook.WebDriverAgentRunner";
|
||||
NSString *envproductBundleIdentifier = NSProcessInfo.processInfo.environment[@"WDA_PRODUCT_BUNDLE_IDENTIFIER"];
|
||||
if (envproductBundleIdentifier && [envproductBundleIdentifier length] != 0) {
|
||||
productBundleIdentifier = NSProcessInfo.processInfo.environment[@"WDA_PRODUCT_BUNDLE_IDENTIFIER"];
|
||||
}
|
||||
|
||||
NSMutableDictionary *buildInfo = [NSMutableDictionary dictionaryWithDictionary:@{
|
||||
@"time" : [self.class buildTimestamp],
|
||||
@"productBundleIdentifier" : productBundleIdentifier,
|
||||
}];
|
||||
NSString *upgradeTimestamp = NSProcessInfo.processInfo.environment[@"UPGRADE_TIMESTAMP"];
|
||||
if (nil != upgradeTimestamp && upgradeTimestamp.length > 0) {
|
||||
[buildInfo setObject:upgradeTimestamp forKey:@"upgradedAt"];
|
||||
}
|
||||
NSDictionary *infoDict = [[NSBundle bundleForClass:self.class] infoDictionary];
|
||||
NSString *version = [infoDict objectForKey:@"CFBundleShortVersionString"];
|
||||
if (nil != version) {
|
||||
[buildInfo setObject:version forKey:@"version"];
|
||||
}
|
||||
|
||||
return FBResponseWithObject(
|
||||
@{
|
||||
@"ready" : @YES,
|
||||
@"message" : @"WebDriverAgent is ready to accept commands",
|
||||
@"state" : @"success",
|
||||
@"os" :
|
||||
@{
|
||||
@"name" : [[UIDevice currentDevice] systemName],
|
||||
@"version" : [[UIDevice currentDevice] systemVersion],
|
||||
@"sdkVersion": FBSDKVersion() ?: @"unknown",
|
||||
@"testmanagerdVersion": @(FBTestmanagerdVersion()),
|
||||
},
|
||||
@"ios" :
|
||||
@{
|
||||
#if TARGET_OS_SIMULATOR
|
||||
@"simulatorVersion" : [[UIDevice currentDevice] systemVersion],
|
||||
#endif
|
||||
@"ip" : [XCUIDevice sharedDevice].fb_wifiIPAddress ?: [NSNull null]
|
||||
},
|
||||
@"build" : buildInfo.copy,
|
||||
@"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetHealthCheck:(FBRouteRequest *)request
|
||||
{
|
||||
if (![[XCUIDevice sharedDevice] fb_healthCheckWithApplication:[XCUIApplication fb_activeApplication]]) {
|
||||
return FBResponseWithUnknownErrorFormat(@"Health check failed");
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetSettings:(FBRouteRequest *)request
|
||||
{
|
||||
return FBResponseWithObject(
|
||||
@{
|
||||
FB_SETTING_USE_COMPACT_RESPONSES: @([FBConfiguration shouldUseCompactResponses]),
|
||||
FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES: [FBConfiguration elementResponseAttributes],
|
||||
FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY: @([FBConfiguration mjpegServerScreenshotQuality]),
|
||||
FB_SETTING_MJPEG_SERVER_FRAMERATE: @([FBConfiguration mjpegServerFramerate]),
|
||||
FB_SETTING_MJPEG_SCALING_FACTOR: @([FBConfiguration mjpegScalingFactor]),
|
||||
FB_SETTING_MJPEG_FIX_ORIENTATION: @([FBConfiguration mjpegShouldFixOrientation]),
|
||||
FB_SETTING_SCREENSHOT_QUALITY: @([FBConfiguration screenshotQuality]),
|
||||
FB_SETTING_KEYBOARD_AUTOCORRECTION: @([FBConfiguration keyboardAutocorrection]),
|
||||
FB_SETTING_KEYBOARD_PREDICTION: @([FBConfiguration keyboardPrediction]),
|
||||
FB_SETTING_SNAPSHOT_MAX_DEPTH: @([FBConfiguration snapshotMaxDepth]),
|
||||
FB_SETTING_USE_FIRST_MATCH: @([FBConfiguration useFirstMatch]),
|
||||
FB_SETTING_WAIT_FOR_IDLE_TIMEOUT: @([FBConfiguration waitForIdleTimeout]),
|
||||
FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT: @([FBConfiguration animationCoolOffTimeout]),
|
||||
FB_SETTING_BOUND_ELEMENTS_BY_INDEX: @([FBConfiguration boundElementsByIndex]),
|
||||
FB_SETTING_REDUCE_MOTION: @([FBConfiguration reduceMotionEnabled]),
|
||||
FB_SETTING_DEFAULT_ACTIVE_APPLICATION: request.session.defaultActiveApplication,
|
||||
FB_SETTING_ACTIVE_APP_DETECTION_POINT: FBActiveAppDetectionPoint.sharedInstance.stringCoordinates,
|
||||
FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS: @([FBConfiguration includeNonModalElements]),
|
||||
FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR: FBConfiguration.acceptAlertButtonSelector,
|
||||
FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR: FBConfiguration.dismissAlertButtonSelector,
|
||||
FB_SETTING_AUTO_CLICK_ALERT_SELECTOR: FBConfiguration.autoClickAlertSelector,
|
||||
FB_SETTING_DEFAULT_ALERT_ACTION: request.session.defaultAlertAction ?: @"",
|
||||
FB_SETTING_MAX_TYPING_FREQUENCY: @([FBConfiguration maxTypingFrequency]),
|
||||
FB_SETTING_RESPECT_SYSTEM_ALERTS: @([FBConfiguration shouldRespectSystemAlerts]),
|
||||
FB_SETTING_USE_CLEAR_TEXT_SHORTCUT: @([FBConfiguration useClearTextShortcut]),
|
||||
FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE: @([FBConfiguration includeHittableInPageSource]),
|
||||
FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE: @([FBConfiguration includeNativeFrameInPageSource]),
|
||||
FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE: @([FBConfiguration includeMinMaxValueInPageSource]),
|
||||
FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE: @([FBConfiguration limitXpathContextScope]),
|
||||
#if !TARGET_OS_TV
|
||||
FB_SETTING_SCREENSHOT_ORIENTATION: [FBConfiguration humanReadableScreenshotOrientation],
|
||||
#endif
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// TODO if we get lots more settings, handling them with a series of if-statements will be unwieldy
|
||||
// and this should be refactored
|
||||
+ (id<FBResponsePayload>)handleSetSettings:(FBRouteRequest *)request
|
||||
{
|
||||
NSDictionary* settings = request.arguments[@"settings"];
|
||||
|
||||
if (nil != [settings objectForKey:FB_SETTING_USE_COMPACT_RESPONSES]) {
|
||||
[FBConfiguration setShouldUseCompactResponses:[[settings objectForKey:FB_SETTING_USE_COMPACT_RESPONSES] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]) {
|
||||
[FBConfiguration setElementResponseAttributes:(NSString *)[settings objectForKey:FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY]) {
|
||||
[FBConfiguration setMjpegServerScreenshotQuality:[[settings objectForKey:FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY] unsignedIntegerValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_MJPEG_SERVER_FRAMERATE]) {
|
||||
[FBConfiguration setMjpegServerFramerate:[[settings objectForKey:FB_SETTING_MJPEG_SERVER_FRAMERATE] unsignedIntegerValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY]) {
|
||||
[FBConfiguration setScreenshotQuality:[[settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY] unsignedIntegerValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR]) {
|
||||
[FBConfiguration setMjpegScalingFactor:[[settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR] floatValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION]) {
|
||||
[FBConfiguration setMjpegShouldFixOrientation:[[settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION]) {
|
||||
[FBConfiguration setKeyboardAutocorrection:[[settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION]) {
|
||||
[FBConfiguration setKeyboardPrediction:[[settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS]) {
|
||||
[FBConfiguration setShouldRespectSystemAlerts:[[settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH]) {
|
||||
[FBConfiguration setSnapshotMaxDepth:[[settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH] intValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_USE_FIRST_MATCH]) {
|
||||
[FBConfiguration setUseFirstMatch:[[settings objectForKey:FB_SETTING_USE_FIRST_MATCH] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_BOUND_ELEMENTS_BY_INDEX]) {
|
||||
[FBConfiguration setBoundElementsByIndex:[[settings objectForKey:FB_SETTING_BOUND_ELEMENTS_BY_INDEX] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_REDUCE_MOTION]) {
|
||||
[FBConfiguration setReduceMotionEnabled:[[settings objectForKey:FB_SETTING_REDUCE_MOTION] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_DEFAULT_ACTIVE_APPLICATION]) {
|
||||
request.session.defaultActiveApplication = (NSString *)[settings objectForKey:FB_SETTING_DEFAULT_ACTIVE_APPLICATION];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_ACTIVE_APP_DETECTION_POINT]) {
|
||||
NSError *error;
|
||||
if (![FBActiveAppDetectionPoint.sharedInstance setCoordinatesWithString:(NSString *)[settings objectForKey:FB_SETTING_ACTIVE_APP_DETECTION_POINT]
|
||||
error:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
|
||||
traceback:nil]);
|
||||
}
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS]) {
|
||||
if ([XCUIElement fb_supportsNonModalElementsInclusion]) {
|
||||
[FBConfiguration setIncludeNonModalElements:[[settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS] boolValue]];
|
||||
} else {
|
||||
[FBLogger logFmt:@"'%@' settings value cannot be assigned, because non modal elements inclusion is not supported by the current iOS SDK", FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS];
|
||||
}
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]) {
|
||||
[FBConfiguration setAcceptAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]) {
|
||||
[FBConfiguration setDismissAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_AUTO_CLICK_ALERT_SELECTOR]) {
|
||||
FBCommandStatus *status = [self.class configureAutoClickAlertWithSelector:settings[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR]
|
||||
forSession:request.session];
|
||||
if (status.hasError) {
|
||||
return FBResponseWithStatus(status);
|
||||
}
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) {
|
||||
[FBConfiguration setWaitForIdleTimeout:[[settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT]) {
|
||||
[FBConfiguration setAnimationCoolOffTimeout:[[settings objectForKey:FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] doubleValue]];
|
||||
}
|
||||
if ([[settings objectForKey:FB_SETTING_DEFAULT_ALERT_ACTION] isKindOfClass:NSString.class]) {
|
||||
request.session.defaultAlertAction = [settings[FB_SETTING_DEFAULT_ALERT_ACTION] lowercaseString];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY]) {
|
||||
[FBConfiguration setMaxTypingFrequency:[[settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY] unsignedIntegerValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT]) {
|
||||
[FBConfiguration setUseClearTextShortcut:[[settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE]) {
|
||||
[FBConfiguration setIncludeHittableInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE]) {
|
||||
[FBConfiguration setIncludeNativeFrameInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE]) {
|
||||
[FBConfiguration setIncludeMinMaxValueInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] boolValue]];
|
||||
}
|
||||
if (nil != [settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE]) {
|
||||
[FBConfiguration setLimitXpathContextScope:[[settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] boolValue]];
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION]) {
|
||||
NSError *error;
|
||||
if (![FBConfiguration setScreenshotOrientation:(NSString *)[settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION]
|
||||
error:&error]) {
|
||||
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
|
||||
traceback:nil]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return [self handleGetSettings:request];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
+ (FBCommandStatus *)configureAutoClickAlertWithSelector:(NSString *)selector
|
||||
forSession:(FBSession *)session
|
||||
{
|
||||
if (0 == [selector length]) {
|
||||
[FBConfiguration setAutoClickAlertSelector:selector];
|
||||
[session disableAlertsMonitor];
|
||||
return [FBCommandStatus ok];
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:selector error:&error];
|
||||
if (nil == parsedChain) {
|
||||
return [FBCommandStatus invalidSelectorErrorWithMessage:error.localizedDescription
|
||||
traceback:nil];
|
||||
}
|
||||
[FBConfiguration setAutoClickAlertSelector:selector];
|
||||
[session enableAlertsMonitor];
|
||||
return [FBCommandStatus ok];
|
||||
}
|
||||
|
||||
+ (NSString *)buildTimestamp
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@ %@",
|
||||
[NSString stringWithUTF8String:__DATE__],
|
||||
[NSString stringWithUTF8String:__TIME__]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
Return current session information.
|
||||
This response does not have any active application information.
|
||||
*/
|
||||
+ (NSDictionary *)sessionInformation
|
||||
{
|
||||
return
|
||||
@{
|
||||
@"sessionId" : [FBSession activeSession].identifier ?: NSNull.null,
|
||||
@"capabilities" : FBSessionCommands.currentCapabilities
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
Return the device kind as lower case
|
||||
*/
|
||||
+ (NSString *)deviceNameByUserInterfaceIdiom:(UIUserInterfaceIdiom) userInterfaceIdiom
|
||||
{
|
||||
if (userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
||||
return @"ipad";
|
||||
} else if (userInterfaceIdiom == UIUserInterfaceIdiomTV) {
|
||||
return @"apple tv";
|
||||
} else if (userInterfaceIdiom == UIUserInterfaceIdiomPhone) {
|
||||
return @"iphone";
|
||||
}
|
||||
// CarPlay, Mac, Vision UI or unknown are possible
|
||||
return @"Unknown";
|
||||
|
||||
}
|
||||
|
||||
+ (NSDictionary *)currentCapabilities
|
||||
{
|
||||
return
|
||||
@{
|
||||
@"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom],
|
||||
@"sdkVersion": [[UIDevice currentDevice] systemVersion]
|
||||
};
|
||||
}
|
||||
|
||||
+(nullable id<FBResponsePayload>)openDeepLink:(NSString *)initialUrl
|
||||
withApplication:(nullable NSString *)bundleID
|
||||
timeout:(nullable NSNumber *)timeout
|
||||
{
|
||||
NSError *openError;
|
||||
NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout();
|
||||
if (nil != timeout) {
|
||||
_XCTSetApplicationStateTimeout([timeout doubleValue]);
|
||||
}
|
||||
@try {
|
||||
BOOL result = nil == bundleID
|
||||
? [XCUIDevice.sharedDevice fb_openUrl:initialUrl
|
||||
error:&openError]
|
||||
: [XCUIDevice.sharedDevice fb_openUrl:initialUrl
|
||||
withApplication:(id)bundleID
|
||||
error:&openError];
|
||||
if (result) {
|
||||
return nil;
|
||||
}
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Cannot open the URL %@ with the %@ application. Original error: %@",
|
||||
initialUrl, bundleID ?: @"default", openError.localizedDescription];
|
||||
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg traceback:nil]);
|
||||
} @finally {
|
||||
if (nil != timeout) {
|
||||
_XCTSetApplicationStateTimeout(defaultTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
19
WebDriverAgentLib/Commands/FBTouchActionCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBTouchActionCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBTouchActionCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
41
WebDriverAgentLib/Commands/FBTouchActionCommands.m
Normal file
41
WebDriverAgentLib/Commands/FBTouchActionCommands.m
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 "FBTouchActionCommands.h"
|
||||
|
||||
#import "FBRoute.h"
|
||||
#import "FBRouteRequest.h"
|
||||
#import "FBSession.h"
|
||||
#import "XCUIApplication+FBTouchAction.h"
|
||||
|
||||
@implementation FBTouchActionCommands
|
||||
|
||||
#pragma mark - <FBCommandHandler>
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute POST:@"/actions"] respondWithTarget:self action:@selector(handlePerformW3CTouchActions:)],
|
||||
];
|
||||
}
|
||||
|
||||
#pragma mark - Commands
|
||||
|
||||
+ (id<FBResponsePayload>)handlePerformW3CTouchActions:(FBRouteRequest *)request
|
||||
{
|
||||
XCUIApplication *application = request.session.activeApplication;
|
||||
NSArray *actions = (NSArray *)request.arguments[@"actions"];
|
||||
NSError *error;
|
||||
if (![application fb_performW3CActions:actions elementCache:request.session.elementCache error:&error]) {
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
@end
|
||||
19
WebDriverAgentLib/Commands/FBTouchIDCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBTouchIDCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBTouchIDCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
30
WebDriverAgentLib/Commands/FBTouchIDCommands.m
Normal file
30
WebDriverAgentLib/Commands/FBTouchIDCommands.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 "FBTouchIDCommands.h"
|
||||
|
||||
#import "FBRouteRequest.h"
|
||||
|
||||
#import "XCUIDevice+FBHelpers.h"
|
||||
|
||||
@implementation FBTouchIDCommands
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return @[
|
||||
[[FBRoute POST:@"/wda/touch_id"] respondWithBlock: ^ id<FBResponsePayload> (FBRouteRequest *request) {
|
||||
BOOL isMatch = [request.arguments[@"match"] boolValue];
|
||||
if (![[XCUIDevice sharedDevice] fb_fingerTouchShouldMatch:isMatch]) {
|
||||
return FBResponseWithUnknownErrorFormat(@"Cannot perform Touch Id %@match", isMatch ? @"" : @"non-");
|
||||
}
|
||||
return FBResponseWithOK();
|
||||
}],
|
||||
];
|
||||
}
|
||||
|
||||
@end
|
||||
19
WebDriverAgentLib/Commands/FBUnknownCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBUnknownCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBUnknownCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
39
WebDriverAgentLib/Commands/FBUnknownCommands.m
Normal file
39
WebDriverAgentLib/Commands/FBUnknownCommands.m
Normal file
@@ -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 "FBUnknownCommands.h"
|
||||
|
||||
#import "FBRouteRequest.h"
|
||||
|
||||
@implementation FBUnknownCommands
|
||||
|
||||
#pragma mark - <FBCommandHandler>
|
||||
|
||||
+ (BOOL)shouldRegisterAutomatically
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute GET:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)],
|
||||
[[FBRoute POST:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)],
|
||||
[[FBRoute PUT:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)],
|
||||
[[FBRoute DELETE:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)]
|
||||
];
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)unhandledHandler:(FBRouteRequest *)request
|
||||
{
|
||||
return FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:[NSString stringWithFormat:@"Unhandled endpoint: %@ with parameters %@", request.URL, request.parameters]
|
||||
traceback:nil]);
|
||||
}
|
||||
|
||||
@end
|
||||
19
WebDriverAgentLib/Commands/FBVideoCommands.h
Normal file
19
WebDriverAgentLib/Commands/FBVideoCommands.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBVideoCommands : NSObject <FBCommandHandler>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
84
WebDriverAgentLib/Commands/FBVideoCommands.m
Normal file
84
WebDriverAgentLib/Commands/FBVideoCommands.m
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 "FBVideoCommands.h"
|
||||
|
||||
#import "FBRouteRequest.h"
|
||||
#import "FBScreenRecordingContainer.h"
|
||||
#import "FBScreenRecordingPromise.h"
|
||||
#import "FBScreenRecordingRequest.h"
|
||||
#import "FBSession.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
|
||||
const NSUInteger DEFAULT_FPS = 24;
|
||||
const NSUInteger DEFAULT_CODEC = 0;
|
||||
|
||||
@implementation FBVideoCommands
|
||||
|
||||
+ (NSArray *)routes
|
||||
{
|
||||
return
|
||||
@[
|
||||
[[FBRoute POST:@"/wda/video/start"] respondWithTarget:self action:@selector(handleStartVideoRecording:)],
|
||||
[[FBRoute POST:@"/wda/video/stop"] respondWithTarget:self action:@selector(handleStopVideoRecording:)],
|
||||
[[FBRoute GET:@"/wda/video"] respondWithTarget:self action:@selector(handleGetVideoRecording:)],
|
||||
|
||||
[[FBRoute POST:@"/wda/video/start"].withoutSession respondWithTarget:self action:@selector(handleStartVideoRecording:)],
|
||||
[[FBRoute POST:@"/wda/video/stop"].withoutSession respondWithTarget:self action:@selector(handleStopVideoRecording:)],
|
||||
[[FBRoute GET:@"/wda/video"].withoutSession respondWithTarget:self action:@selector(handleGetVideoRecording:)],
|
||||
];
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleStartVideoRecording:(FBRouteRequest *)request
|
||||
{
|
||||
FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise;
|
||||
if (nil != activeScreenRecording) {
|
||||
return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary] ?: [NSNull null]);
|
||||
}
|
||||
|
||||
NSNumber *fps = (NSNumber *)request.arguments[@"fps"] ?: @(DEFAULT_FPS);
|
||||
NSNumber *codec = (NSNumber *)request.arguments[@"codec"] ?: @(DEFAULT_CODEC);
|
||||
FBScreenRecordingRequest *recordingRequest = [[FBScreenRecordingRequest alloc] initWithFps:fps.integerValue
|
||||
codec:codec.longLongValue];
|
||||
NSError *error;
|
||||
FBScreenRecordingPromise* promise = [FBXCTestDaemonsProxy startScreenRecordingWithRequest:recordingRequest
|
||||
error:&error];
|
||||
if (nil == promise) {
|
||||
[FBScreenRecordingContainer.sharedInstance reset];
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
[FBScreenRecordingContainer.sharedInstance storeScreenRecordingPromise:promise
|
||||
fps:fps.integerValue
|
||||
codec:codec.longLongValue];
|
||||
return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary]);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleStopVideoRecording:(FBRouteRequest *)request
|
||||
{
|
||||
FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise;
|
||||
if (nil == activeScreenRecording) {
|
||||
return FBResponseWithOK();
|
||||
}
|
||||
|
||||
NSUUID *recordingId = activeScreenRecording.identifier;
|
||||
NSDictionary *response = [FBScreenRecordingContainer.sharedInstance toDictionary];
|
||||
NSError *error;
|
||||
if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:recordingId error:&error]) {
|
||||
[FBScreenRecordingContainer.sharedInstance reset];
|
||||
return FBResponseWithUnknownError(error);
|
||||
}
|
||||
[FBScreenRecordingContainer.sharedInstance reset];
|
||||
return FBResponseWithObject(response);
|
||||
}
|
||||
|
||||
+ (id<FBResponsePayload>)handleGetVideoRecording:(FBRouteRequest *)request
|
||||
{
|
||||
return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary] ?: [NSNull null]);
|
||||
}
|
||||
|
||||
@end
|
||||
91
WebDriverAgentLib/FBAlert.h
Normal file
91
WebDriverAgentLib/FBAlert.h
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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>
|
||||
|
||||
@class XCUIApplication;
|
||||
@class XCUIElement;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Alert helper class that abstracts alert handling
|
||||
*/
|
||||
@interface FBAlert : NSObject
|
||||
|
||||
/**
|
||||
Creates alert helper for given application
|
||||
|
||||
@param application The application that contains the alert
|
||||
*/
|
||||
+ (instancetype)alertWithApplication:(XCUIApplication *)application;
|
||||
|
||||
/**
|
||||
Creates alert helper for given application
|
||||
|
||||
@param element The element which represents the alert
|
||||
*/
|
||||
+ (instancetype)alertWithElement:(XCUIElement *)element;
|
||||
|
||||
/**
|
||||
Determines whether alert is present
|
||||
*/
|
||||
- (BOOL)isPresent;
|
||||
|
||||
/**
|
||||
Gets the labels of the buttons visible in the alert
|
||||
*/
|
||||
- (nullable NSArray *)buttonLabels;
|
||||
|
||||
/**
|
||||
Returns alert's title and description separated by new lines
|
||||
*/
|
||||
- (nullable NSString *)text;
|
||||
|
||||
/**
|
||||
Accepts alert, if present
|
||||
|
||||
@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)acceptWithError:(NSError **)error;
|
||||
|
||||
/**
|
||||
Dismisses alert, if present
|
||||
|
||||
@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)dismissWithError:(NSError **)error;
|
||||
|
||||
/**
|
||||
Clicks on an alert button, if present
|
||||
|
||||
@param label The label of the button on which to click.
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the operation suceeds, otherwise NO.
|
||||
*/
|
||||
- (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error;
|
||||
|
||||
/**
|
||||
XCUElement that represents alert
|
||||
*/
|
||||
- (nullable XCUIElement *)alertElement;
|
||||
|
||||
/**
|
||||
Types a text into an input inside the alert container, if it is present
|
||||
|
||||
@param text the text to type
|
||||
@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)typeText:(NSString *)text error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
274
WebDriverAgentLib/FBAlert.m
Normal file
274
WebDriverAgentLib/FBAlert.m
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 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 "FBAlert.h"
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIApplication+FBAlert.h"
|
||||
#import "XCUIElement+FBClassChain.h"
|
||||
#import "XCUIElement+FBTyping.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
|
||||
|
||||
@interface FBAlert ()
|
||||
@property (nonatomic, strong) XCUIApplication *application;
|
||||
@property (nonatomic, strong, nullable) XCUIElement *element;
|
||||
@end
|
||||
|
||||
@implementation FBAlert
|
||||
|
||||
+ (instancetype)alertWithApplication:(XCUIApplication *)application
|
||||
{
|
||||
FBAlert *alert = [FBAlert new];
|
||||
alert.application = application;
|
||||
return alert;
|
||||
}
|
||||
|
||||
+ (instancetype)alertWithElement:(XCUIElement *)element
|
||||
{
|
||||
FBAlert *alert = [FBAlert new];
|
||||
alert.element = element;
|
||||
alert.application = element.application;
|
||||
return alert;
|
||||
}
|
||||
|
||||
- (BOOL)isPresent
|
||||
{
|
||||
@try {
|
||||
if (nil == self.alertElement) {
|
||||
return NO;
|
||||
}
|
||||
[self.alertElement fb_customSnapshot];
|
||||
return YES;
|
||||
} @catch (NSException *) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)notPresentWithError:(NSError **)error
|
||||
{
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"No alert is open"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
+ (BOOL)isSafariWebAlertWithSnapshot:(id<FBXCElementSnapshot>)snapshot
|
||||
{
|
||||
if (snapshot.elementType != XCUIElementTypeOther) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
FBXCElementSnapshotWrapper *snapshotWrapper = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
|
||||
id<FBXCElementSnapshot> application = [snapshotWrapper fb_parentMatchingType:XCUIElementTypeApplication];
|
||||
return nil != application && [application.label isEqualToString:FB_SAFARI_APP_NAME];
|
||||
}
|
||||
|
||||
- (NSString *)text
|
||||
{
|
||||
if (!self.isPresent) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<NSString *> *resultText = [NSMutableArray array];
|
||||
id<FBXCElementSnapshot> snapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
|
||||
BOOL isSafariAlert = [self.class isSafariWebAlertWithSnapshot:snapshot];
|
||||
[snapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
|
||||
XCUIElementType elementType = descendant.elementType;
|
||||
if (!(elementType == XCUIElementTypeTextView || elementType == XCUIElementTypeStaticText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
FBXCElementSnapshotWrapper *descendantWrapper = [FBXCElementSnapshotWrapper ensureWrapped:descendant];
|
||||
if (elementType == XCUIElementTypeStaticText
|
||||
&& nil != [descendantWrapper fb_parentMatchingType:XCUIElementTypeButton]) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *text = descendantWrapper.wdLabel ?: descendantWrapper.wdValue;
|
||||
if (isSafariAlert && nil != descendant.parent) {
|
||||
FBXCElementSnapshotWrapper *descendantParentWrapper = [FBXCElementSnapshotWrapper ensureWrapped:descendant.parent];
|
||||
NSString *parentText = descendantParentWrapper.wdLabel ?: descendantParentWrapper.wdValue;
|
||||
if ([parentText isEqualToString:text]) {
|
||||
// Avoid duplicated texts on Safari alerts
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (nil != text) {
|
||||
[resultText addObject:[NSString stringWithFormat:@"%@", text]];
|
||||
}
|
||||
}];
|
||||
return [resultText componentsJoinedByString:@"\n"];
|
||||
}
|
||||
|
||||
- (BOOL)typeText:(NSString *)text error:(NSError **)error
|
||||
{
|
||||
if (!self.isPresent) {
|
||||
return [self notPresentWithError:error];
|
||||
}
|
||||
|
||||
NSPredicate *textCollectorPredicate = [NSPredicate predicateWithFormat:@"elementType IN {%lu,%lu}",
|
||||
XCUIElementTypeTextField, XCUIElementTypeSecureTextField];
|
||||
NSArray<XCUIElement *> *dstFields = [[self.alertElement descendantsMatchingType:XCUIElementTypeAny]
|
||||
matchingPredicate:textCollectorPredicate].allElementsBoundByIndex;
|
||||
if (dstFields.count > 1) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The alert contains more than one input field"]
|
||||
buildError:error];
|
||||
}
|
||||
if (0 == dstFields.count) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The alert contains no input fields"]
|
||||
buildError:error];
|
||||
}
|
||||
return [dstFields.firstObject fb_typeText:text
|
||||
shouldClear:YES
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (NSArray *)buttonLabels
|
||||
{
|
||||
if (!self.isPresent) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<NSString *> *labels = [NSMutableArray array];
|
||||
id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
|
||||
[alertSnapshot enumerateDescendantsUsingBlock:^(id<FBXCElementSnapshot> descendant) {
|
||||
if (descendant.elementType != XCUIElementTypeButton) {
|
||||
return;
|
||||
}
|
||||
NSString *label = [FBXCElementSnapshotWrapper ensureWrapped:descendant].wdLabel;
|
||||
if (nil != label) {
|
||||
[labels addObject:[NSString stringWithFormat:@"%@", label]];
|
||||
}
|
||||
}];
|
||||
return labels.copy;
|
||||
}
|
||||
|
||||
- (BOOL)acceptWithError:(NSError **)error
|
||||
{
|
||||
if (!self.isPresent) {
|
||||
return [self notPresentWithError:error];
|
||||
}
|
||||
|
||||
id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
|
||||
XCUIElement *acceptButton = nil;
|
||||
if (FBConfiguration.acceptAlertButtonSelector.length) {
|
||||
NSString *errorReason = nil;
|
||||
@try {
|
||||
acceptButton = [[self.alertElement fb_descendantsMatchingClassChain:FBConfiguration.acceptAlertButtonSelector
|
||||
shouldReturnAfterFirstMatch:YES] firstObject];
|
||||
} @catch (NSException *ex) {
|
||||
errorReason = ex.reason;
|
||||
}
|
||||
if (nil == acceptButton) {
|
||||
[FBLogger logFmt:@"Cannot find any match for Accept alert button using the class chain selector '%@'",
|
||||
FBConfiguration.acceptAlertButtonSelector];
|
||||
if (nil != errorReason) {
|
||||
[FBLogger logFmt:@"Original error: %@", errorReason];
|
||||
}
|
||||
[FBLogger log:@"Will fallback to the default button location algorithm"];
|
||||
}
|
||||
}
|
||||
if (nil == acceptButton) {
|
||||
NSArray<XCUIElement *> *buttons = [self.alertElement.fb_query
|
||||
descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex;
|
||||
acceptButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
|
||||
? buttons.lastObject
|
||||
: buttons.firstObject;
|
||||
}
|
||||
if (nil == acceptButton) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Failed to find accept button for alert: %@", self.alertElement]
|
||||
buildError:error];
|
||||
}
|
||||
[acceptButton tap];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)dismissWithError:(NSError **)error
|
||||
{
|
||||
if (!self.isPresent) {
|
||||
return [self notPresentWithError:error];
|
||||
}
|
||||
|
||||
id<FBXCElementSnapshot> alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot];
|
||||
XCUIElement *dismissButton = nil;
|
||||
if (FBConfiguration.dismissAlertButtonSelector.length) {
|
||||
NSString *errorReason = nil;
|
||||
@try {
|
||||
dismissButton = [[self.alertElement fb_descendantsMatchingClassChain:FBConfiguration.dismissAlertButtonSelector
|
||||
shouldReturnAfterFirstMatch:YES] firstObject];
|
||||
} @catch (NSException *ex) {
|
||||
errorReason = ex.reason;
|
||||
}
|
||||
if (nil == dismissButton) {
|
||||
[FBLogger logFmt:@"Cannot find any match for Dismiss alert button using the class chain selector '%@'",
|
||||
FBConfiguration.dismissAlertButtonSelector];
|
||||
if (nil != errorReason) {
|
||||
[FBLogger logFmt:@"Original error: %@", errorReason];
|
||||
}
|
||||
[FBLogger log:@"Will fallback to the default button location algorithm"];
|
||||
}
|
||||
}
|
||||
if (nil == dismissButton) {
|
||||
NSArray<XCUIElement *> *buttons = [self.alertElement.fb_query
|
||||
descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex;
|
||||
dismissButton = (alertSnapshot.elementType == XCUIElementTypeAlert || [self.class isSafariWebAlertWithSnapshot:alertSnapshot])
|
||||
? buttons.firstObject
|
||||
: buttons.lastObject;
|
||||
}
|
||||
|
||||
if (nil == dismissButton) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Failed to find dismiss button for alert: %@", self.alertElement]
|
||||
buildError:error];
|
||||
}
|
||||
[dismissButton tap];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error
|
||||
{
|
||||
if (!self.isPresent) {
|
||||
return [self notPresentWithError:error];
|
||||
}
|
||||
|
||||
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label == %@", label];
|
||||
XCUIElement *requestedButton = [[self.alertElement descendantsMatchingType:XCUIElementTypeButton]
|
||||
matchingPredicate:predicate].allElementsBoundByIndex.firstObject;
|
||||
if (!requestedButton) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Failed to find button with label '%@' for alert: %@", label, self.alertElement]
|
||||
buildError:error];
|
||||
}
|
||||
[requestedButton tap];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (XCUIElement *)alertElement
|
||||
{
|
||||
if (nil == self.element) {
|
||||
XCUIApplication *systemApp = XCUIApplication.fb_systemApplication;
|
||||
if ([systemApp fb_isSameAppAs:self.application]) {
|
||||
self.element = systemApp.fb_alertElement;
|
||||
} else {
|
||||
self.element = systemApp.fb_alertElement ?: self.application.fb_alertElement;
|
||||
}
|
||||
}
|
||||
return self.element;
|
||||
}
|
||||
|
||||
@end
|
||||
26
WebDriverAgentLib/Info.plist
Normal file
26
WebDriverAgentLib/Info.plist
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>10.1.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>10.1.2</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string/>
|
||||
</dict>
|
||||
</plist>
|
||||
38
WebDriverAgentLib/Routing/FBCommandHandler.h
Normal file
38
WebDriverAgentLib/Routing/FBCommandHandler.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 <Foundation/Foundation.h>
|
||||
|
||||
#import <WebDriverAgentLib/FBCommandStatus.h>
|
||||
#import <WebDriverAgentLib/FBResponsePayload.h>
|
||||
#import <WebDriverAgentLib/FBRoute.h>
|
||||
#import <WebDriverAgentLib/FBResponsePayload.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Protocol for Classes to declare intent to implement responses to commands
|
||||
*/
|
||||
@protocol FBCommandHandler <NSObject>
|
||||
|
||||
/**
|
||||
* Should return map of FBRouteCommandHandler block with keys as supported routes
|
||||
*
|
||||
* @return map an NSArray<FBRoute *> of routes.
|
||||
*/
|
||||
+ (NSArray *)routes;
|
||||
|
||||
@optional
|
||||
/**
|
||||
* @return BOOL deciding if class should be added to route handlers automatically, default (if not implemented) is YES
|
||||
*/
|
||||
+ (BOOL)shouldRegisterAutomatically;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
80
WebDriverAgentLib/Routing/FBCommandStatus.h
Normal file
80
WebDriverAgentLib/Routing/FBCommandStatus.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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>
|
||||
#import <WebDriverAgentLib/FBHTTPStatusCodes.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBCommandStatus : NSObject
|
||||
|
||||
@property (nonatomic, nullable, readonly) id value;
|
||||
@property (nonatomic, nullable, readonly) NSString* error;
|
||||
@property (nonatomic, nullable, readonly) NSString* message;
|
||||
@property (nonatomic, nullable, readonly) NSString* traceback;
|
||||
@property (nonatomic, readonly) HTTPStatusCode statusCode;
|
||||
@property (nonatomic, readonly) BOOL hasError;
|
||||
|
||||
+ (instancetype)ok;
|
||||
|
||||
+ (instancetype)okWithValue:(nullable id)value;
|
||||
|
||||
+ (instancetype)unknownErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)unsupportedOperationErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)unableToCaptureScreenErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)noSuchElementErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)invalidElementStateErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)invalidArgumentErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)staleElementReferenceErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)invalidSelectorErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)noAlertOpenErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)unexpectedAlertOpenErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)notImplementedErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)sessionNotCreatedError:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)invalidCoordinatesErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)unknownCommandErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)timeoutErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)elementNotVisibleErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
+ (instancetype)noSuchDriverErrorWithMessage:(nullable NSString *)message
|
||||
traceback:(nullable NSString *)traceback;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
279
WebDriverAgentLib/Routing/FBCommandStatus.m
Normal file
279
WebDriverAgentLib/Routing/FBCommandStatus.m
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* 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 "FBCommandStatus.h"
|
||||
|
||||
static NSString *const FB_UNKNOWN_ERROR = @"unknown error";
|
||||
static const HTTPStatusCode FB_UNKNOWN_ERROR_CODE = kHTTPStatusCodeInternalServerError;
|
||||
static NSString *const FB_UNKNOWN_ERROR_MSG = @"An unknown server-side error occurred while processing the command";
|
||||
|
||||
static NSString *const FB_UNABLE_TO_CAPTURE_ERROR = @"unable to capture screen";
|
||||
static const HTTPStatusCode FB_UNABLE_TO_CAPTURE_ERROR_CODE = kHTTPStatusCodeInternalServerError;
|
||||
static NSString *const FB_UNABLE_TO_CAPTURE_MSG = @"A screen capture was made impossible";
|
||||
|
||||
static NSString *const FB_NO_SUCH_ELEMENT_ERROR = @"no such element";
|
||||
static const HTTPStatusCode FB_NO_SUCH_ELEMENT_ERROR_CODE = kHTTPStatusCodeNotFound;
|
||||
static NSString *const FB_NO_SUCH_ELEMENT_MSG = @"An element could not be located on the page using the given search parameters";
|
||||
|
||||
static NSString *const FB_INVALID_ELEMENT_STATE_ERROR = @"invalid element state";
|
||||
static const HTTPStatusCode FB_INVALID_ELEMENT_STATE_ERROR_CODE = kHTTPStatusCodeBadRequest;
|
||||
static NSString *const FB_INVALID_ELEMENT_STATE_MSG = @"An element command could not be completed because the element is in an invalid state (e.g. attempting to click a disabled element)";
|
||||
|
||||
static NSString *const FB_INVALID_ARGUMENT_ERROR = @"invalid argument";
|
||||
static const HTTPStatusCode FB_INVALID_ARGUMENT_ERROR_CODE = kHTTPStatusCodeBadRequest;
|
||||
static NSString *const FB_INVALID_ARGUMENT_MSG = @"The arguments passed to the command are either invalid or malformed";
|
||||
|
||||
static NSString *const FB_STALE_ELEMENT_REF_ERROR = @"stale element reference";
|
||||
static const HTTPStatusCode FB_STALE_ELEMENT_REF_ERROR_CODE = kHTTPStatusCodeNotFound;
|
||||
static NSString *const FB_STALE_ELEMENT_REF_MSG = @"An element command failed because the referenced element is no longer attached to the DOM";
|
||||
|
||||
static NSString *const FB_INVALID_SELECTOR_ERROR = @"invalid selector";
|
||||
static const HTTPStatusCode FB_INVALID_SELECTOR_ERROR_CODE = kHTTPStatusCodeBadRequest;
|
||||
static NSString *const FB_INVALID_SELECTOR_MSG = @"Argument was an invalid selector (e.g. XPath/Class Chain)";
|
||||
|
||||
static NSString *const FB_NO_ALERT_OPEN_ERROR = @"no such alert";
|
||||
static const HTTPStatusCode FB_NO_ALERT_OPEN_ERROR_CODE = kHTTPStatusCodeNotFound;
|
||||
static NSString *const FB_NO_ALERT_OPEN_MSG = @"An attempt was made to operate on a modal dialog when one was not open";
|
||||
|
||||
static NSString *const FB_UNEXPECTED_ALERT_OPEN_ERROR = @"unexpected alert open";
|
||||
static const HTTPStatusCode FB_UNEXPECTED_ALERT_OPEN_ERROR_CODE = kHTTPStatusCodeInternalServerError;
|
||||
static NSString *const FB_UNEXPECTED_ALERT_OPEN_MSG = @"A modal dialog was open, blocking this operation";
|
||||
|
||||
static NSString *const FB_NOT_IMPLEMENTED_ERROR = @"unknown method";
|
||||
static const HTTPStatusCode FB_NOT_IMPLEMENTED_ERROR_CODE = kHTTPStatusCodeMethodNotAllowed;
|
||||
static NSString *const FB_NOT_IMPLEMENTED_MSG = @"Method is not implemented";
|
||||
|
||||
static NSString *const FB_SESSION_NOT_CREATED_ERROR = @"session not created";
|
||||
static const HTTPStatusCode FB_SESSION_NOT_CREATED_ERROR_CODE = kHTTPStatusCodeInternalServerError;
|
||||
static NSString *const FB_SESSION_NOT_CREATED_MSG = @"A new session could not be created";
|
||||
|
||||
static NSString *const FB_INVALID_COORDINATES_ERROR = @"invalid coordinates";
|
||||
static const HTTPStatusCode FB_INVALID_COORDINATES_ERROR_CODE = kHTTPStatusCodeBadRequest;
|
||||
static NSString *const FB_INVALID_COORDINATES_MSG = @"The coordinates provided to an interactions operation are invalid";
|
||||
|
||||
static NSString *const FB_UNSUPPORTED_OPERATION_ERROR = @"unsupported operation";
|
||||
static const HTTPStatusCode FB_UNSUPPORTED_OPERATION_ERROR_CODE = kHTTPStatusCodeInternalServerError;
|
||||
static NSString *const FB_UNSUPPORTED_OPERATION_ERROR_MSG = @"The requested operation is not supported";
|
||||
|
||||
static NSString *const FB_UNKNOWN_COMMAND_ERROR = @"unknown command";
|
||||
static const HTTPStatusCode FB_UNKNOWN_COMMAND_ERROR_CODE = kHTTPStatusCodeNotFound;
|
||||
static NSString *const FB_UNKNOWN_COMMAND_MSG = @"The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource";
|
||||
|
||||
static NSString *const FB_TIMEOUT_ERROR = @"timeout";
|
||||
static const HTTPStatusCode FB_TIMEOUT_ERROR_CODE = kHTTPStatusCodeRequestTimeout;
|
||||
static NSString *const FB_TIMEOUT_MSG = @"An operation did not complete before its timeout expired";
|
||||
|
||||
static NSString *const FB_ELEMENT_NOT_VISIBLE_ERROR = @"element not visible";
|
||||
static const HTTPStatusCode FB_ELEMENT_NOT_VISIBLE_ERROR_CODE = kHTTPStatusCodeBadRequest;
|
||||
static NSString *const FB_ELEMENT_NOT_VISIBLE_MSG = @"An element command could not be completed because the element is not visible on the page";
|
||||
|
||||
static NSString *const FB_NO_SUCH_DRIVER_ERROR = @"invalid session id";
|
||||
static const HTTPStatusCode FB_NO_SUCH_DRIVER_ERROR_CODE = kHTTPStatusCodeNotFound;
|
||||
static NSString *const FB_NO_SUCH_DRIVER_MSG = @"A session is either terminated or not started";
|
||||
|
||||
|
||||
@implementation FBCommandStatus
|
||||
|
||||
- (instancetype)initWithValue:(nullable id)value
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_value = value;
|
||||
_message = nil;
|
||||
_error = nil;
|
||||
_traceback = nil;
|
||||
_statusCode = kHTTPStatusCodeOK;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithError:(NSString *)error
|
||||
statusCode:(HTTPStatusCode)statusCode
|
||||
message:(NSString *)message
|
||||
traceback:(nullable NSString *)traceback
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_error = error;
|
||||
_statusCode = statusCode;
|
||||
_message = message;
|
||||
_traceback = traceback;
|
||||
_value = nil;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)hasError
|
||||
{
|
||||
return self.statusCode != kHTTPStatusCodeOK;
|
||||
}
|
||||
|
||||
+ (instancetype)ok
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithValue:nil];
|
||||
}
|
||||
|
||||
+ (instancetype)okWithValue:(id)value
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithValue:value];
|
||||
}
|
||||
|
||||
+ (instancetype)unknownErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_UNKNOWN_ERROR
|
||||
statusCode:FB_UNKNOWN_ERROR_CODE
|
||||
message:message ?: FB_UNKNOWN_ERROR_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)unsupportedOperationErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_UNSUPPORTED_OPERATION_ERROR
|
||||
statusCode:FB_UNSUPPORTED_OPERATION_ERROR_CODE
|
||||
message:message ?: FB_UNSUPPORTED_OPERATION_ERROR_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)unableToCaptureScreenErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_UNABLE_TO_CAPTURE_ERROR
|
||||
statusCode:FB_UNABLE_TO_CAPTURE_ERROR_CODE
|
||||
message:message ?: FB_UNABLE_TO_CAPTURE_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)noSuchElementErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_NO_SUCH_ELEMENT_ERROR
|
||||
statusCode:FB_NO_SUCH_ELEMENT_ERROR_CODE
|
||||
message:message ?: FB_NO_SUCH_ELEMENT_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)invalidElementStateErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_INVALID_ELEMENT_STATE_ERROR
|
||||
statusCode:FB_INVALID_ELEMENT_STATE_ERROR_CODE
|
||||
message:message ?: FB_INVALID_ELEMENT_STATE_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)invalidArgumentErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_INVALID_ARGUMENT_ERROR
|
||||
statusCode:FB_INVALID_ARGUMENT_ERROR_CODE
|
||||
message:message ?: FB_INVALID_ARGUMENT_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)staleElementReferenceErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_STALE_ELEMENT_REF_ERROR
|
||||
statusCode:FB_STALE_ELEMENT_REF_ERROR_CODE
|
||||
message:message ?: FB_STALE_ELEMENT_REF_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)invalidSelectorErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_INVALID_SELECTOR_ERROR
|
||||
statusCode:FB_INVALID_SELECTOR_ERROR_CODE
|
||||
message:message ?: FB_INVALID_SELECTOR_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)noAlertOpenErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_NO_ALERT_OPEN_ERROR
|
||||
statusCode:FB_NO_ALERT_OPEN_ERROR_CODE
|
||||
message:message ?: FB_NO_ALERT_OPEN_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)unexpectedAlertOpenErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_UNEXPECTED_ALERT_OPEN_ERROR
|
||||
statusCode:FB_UNEXPECTED_ALERT_OPEN_ERROR_CODE
|
||||
message:message ?: FB_UNEXPECTED_ALERT_OPEN_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)notImplementedErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_NOT_IMPLEMENTED_ERROR
|
||||
statusCode:FB_NOT_IMPLEMENTED_ERROR_CODE
|
||||
message:message ?: FB_NOT_IMPLEMENTED_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)sessionNotCreatedError:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_SESSION_NOT_CREATED_ERROR
|
||||
statusCode:FB_SESSION_NOT_CREATED_ERROR_CODE
|
||||
message:message ?: FB_SESSION_NOT_CREATED_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)invalidCoordinatesErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_INVALID_COORDINATES_ERROR
|
||||
statusCode:FB_INVALID_COORDINATES_ERROR_CODE
|
||||
message:message ?: FB_INVALID_COORDINATES_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)unknownCommandErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_UNKNOWN_COMMAND_ERROR
|
||||
statusCode:FB_UNKNOWN_COMMAND_ERROR_CODE
|
||||
message:message ?: FB_UNKNOWN_COMMAND_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)timeoutErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_TIMEOUT_ERROR
|
||||
statusCode:FB_TIMEOUT_ERROR_CODE
|
||||
message:message ?: FB_TIMEOUT_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)elementNotVisibleErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_ELEMENT_NOT_VISIBLE_ERROR
|
||||
statusCode:FB_ELEMENT_NOT_VISIBLE_ERROR_CODE
|
||||
message:message ?: FB_ELEMENT_NOT_VISIBLE_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
+ (instancetype)noSuchDriverErrorWithMessage:(NSString *)message
|
||||
traceback:(NSString *)traceback
|
||||
{
|
||||
return [[FBCommandStatus alloc] initWithError:FB_NO_SUCH_DRIVER_ERROR
|
||||
statusCode:FB_NO_SUCH_DRIVER_ERROR_CODE
|
||||
message:message ?: FB_NO_SUCH_DRIVER_MSG
|
||||
traceback:traceback];
|
||||
}
|
||||
|
||||
@end
|
||||
92
WebDriverAgentLib/Routing/FBElement.h
Normal file
92
WebDriverAgentLib/Routing/FBElement.h
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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 <CoreGraphics/CoreGraphics.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Protocol that should be implemented by class that can return element properties defined in WebDriver Spec
|
||||
*/
|
||||
@protocol FBElement <NSObject>
|
||||
|
||||
/*! Element's frame in normalized (rounded dimensions without Infinity values) CGRect format */
|
||||
@property (nonatomic, readonly, assign) CGRect wdFrame;
|
||||
|
||||
/*! Represents the element's frame as a CGRect, preserving the actual values. */
|
||||
@property (nonatomic, readonly, assign) CGRect wdNativeFrame;
|
||||
|
||||
/*! Element's wsFrame in NSDictionary format */
|
||||
@property (nonatomic, readonly, copy) NSDictionary *wdRect;
|
||||
|
||||
/*! Element's name */
|
||||
@property (nonatomic, readonly, copy, nullable) NSString *wdName;
|
||||
|
||||
/*! Element's label */
|
||||
@property (nonatomic, readonly, copy, nullable) NSString *wdLabel;
|
||||
|
||||
/*! Element's selected state */
|
||||
@property (nonatomic, readonly, getter = isWDSelected) BOOL wdSelected;
|
||||
|
||||
/*! Element's type */
|
||||
@property (nonatomic, readonly, copy) NSString *wdType;
|
||||
|
||||
/*! Element's accessibility traits as a comma-separated string */
|
||||
@property (nonatomic, readonly, copy) NSString *wdTraits;
|
||||
|
||||
/*! Element's value */
|
||||
@property (nonatomic, readonly, strong, nullable) NSString *wdValue;
|
||||
|
||||
/*! Element's unique identifier */
|
||||
@property (nonatomic, readonly, copy, nullable) NSString *wdUID;
|
||||
|
||||
/*! Whether element is enabled */
|
||||
@property (nonatomic, readonly, getter = isWDEnabled) BOOL wdEnabled;
|
||||
|
||||
/*! Whether element is visible */
|
||||
@property (nonatomic, readonly, getter = isWDVisible) BOOL wdVisible;
|
||||
|
||||
/*! Whether element is accessible */
|
||||
@property (nonatomic, readonly, getter = isWDAccessible) BOOL wdAccessible;
|
||||
|
||||
/*! Whether element is an accessibility container (contains children of any depth that are accessible) */
|
||||
@property (nonatomic, readonly, getter = isWDAccessibilityContainer) BOOL wdAccessibilityContainer;
|
||||
|
||||
/*! Whether element is focused */
|
||||
@property (nonatomic, readonly, getter = isWDFocused) BOOL wdFocused;
|
||||
|
||||
/*! Whether element is hittable */
|
||||
@property (nonatomic, readonly, getter = isWDHittable) BOOL wdHittable;
|
||||
|
||||
/*! Element's index relatively to its parent. Starts from zero */
|
||||
@property (nonatomic, readonly) NSUInteger wdIndex;
|
||||
|
||||
/*! Element's placeholder value */
|
||||
@property (nonatomic, readonly, copy, nullable) NSString *wdPlaceholderValue;
|
||||
|
||||
/*! Element's minimum value */
|
||||
@property (nonatomic, readonly, strong, nullable) NSNumber *wdMinValue;
|
||||
|
||||
/*! Element's maximum value */
|
||||
@property (nonatomic, readonly, strong, nullable) NSNumber *wdMaxValue;
|
||||
|
||||
/**
|
||||
Returns value of given property specified in WebDriver Spec
|
||||
Check the FBElement protocol to get list of supported attributes.
|
||||
This method also supports shortcuts, like wdName == name, wdValue == value.
|
||||
|
||||
@param name WebDriver Spec property name
|
||||
@return the corresponding property value
|
||||
@throws FBUnknownAttributeException if there is no matching attribute defined in FBElement protocol
|
||||
*/
|
||||
- (nullable id)fb_valueForWDAttributeName:(NSString *__nullable)name;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user