初始化提交
This commit is contained in:
20
WebDriverAgentLib/Utilities/FBAccessibilityTraits.h
Normal file
20
WebDriverAgentLib/Utilities/FBAccessibilityTraits.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
Converts accessibility traits bitmask to an array of string representations
|
||||
@param traits The accessibility traits bitmask
|
||||
@return Array of strings representing the accessibility traits
|
||||
*/
|
||||
NSArray<NSString *> *FBAccessibilityTraitsToStringsArray(unsigned long long traits);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
61
WebDriverAgentLib/Utilities/FBAccessibilityTraits.m
Normal file
61
WebDriverAgentLib/Utilities/FBAccessibilityTraits.m
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 "FBAccessibilityTraits.h"
|
||||
|
||||
NSArray<NSString *> *FBAccessibilityTraitsToStringsArray(unsigned long long traits) {
|
||||
NSMutableArray<NSString *> *traitStringsArray;
|
||||
NSNumber *key;
|
||||
|
||||
static NSDictionary<NSNumber *, NSString *> *traitsMapping;
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSMutableDictionary<NSNumber *, NSString *> *mapping = [@{
|
||||
@(UIAccessibilityTraitNone): @"None",
|
||||
@(UIAccessibilityTraitButton): @"Button",
|
||||
@(UIAccessibilityTraitLink): @"Link",
|
||||
@(UIAccessibilityTraitHeader): @"Header",
|
||||
@(UIAccessibilityTraitSearchField): @"SearchField",
|
||||
@(UIAccessibilityTraitImage): @"Image",
|
||||
@(UIAccessibilityTraitSelected): @"Selected",
|
||||
@(UIAccessibilityTraitPlaysSound): @"PlaysSound",
|
||||
@(UIAccessibilityTraitKeyboardKey): @"KeyboardKey",
|
||||
@(UIAccessibilityTraitStaticText): @"StaticText",
|
||||
@(UIAccessibilityTraitSummaryElement): @"SummaryElement",
|
||||
@(UIAccessibilityTraitNotEnabled): @"NotEnabled",
|
||||
@(UIAccessibilityTraitUpdatesFrequently): @"UpdatesFrequently",
|
||||
@(UIAccessibilityTraitStartsMediaSession): @"StartsMediaSession",
|
||||
@(UIAccessibilityTraitAdjustable): @"Adjustable",
|
||||
@(UIAccessibilityTraitAllowsDirectInteraction): @"AllowsDirectInteraction",
|
||||
@(UIAccessibilityTraitCausesPageTurn): @"CausesPageTurn",
|
||||
@(UIAccessibilityTraitTabBar): @"TabBar"
|
||||
} mutableCopy];
|
||||
|
||||
#if __clang_major__ >= 16
|
||||
// Add iOS 17.0 specific traits if available
|
||||
if (@available(iOS 17.0, *)) {
|
||||
[mapping addEntriesFromDictionary:@{
|
||||
@(UIAccessibilityTraitToggleButton): @"ToggleButton",
|
||||
@(UIAccessibilityTraitSupportsZoom): @"SupportsZoom"
|
||||
}];
|
||||
}
|
||||
#endif
|
||||
|
||||
traitsMapping = [mapping copy];
|
||||
});
|
||||
|
||||
traitStringsArray = [NSMutableArray array];
|
||||
for (key in traitsMapping) {
|
||||
if (traits & [key unsignedLongLongValue] && nil != traitsMapping[key]) {
|
||||
[traitStringsArray addObject:(id)traitsMapping[key]];
|
||||
}
|
||||
}
|
||||
|
||||
return [traitStringsArray copy];
|
||||
}
|
||||
56
WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h
Normal file
56
WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h
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 <XCTest/XCTest.h>
|
||||
@protocol FBXCAccessibilityElement;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBActiveAppDetectionPoint : NSObject
|
||||
|
||||
@property (nonatomic) CGPoint coordinates;
|
||||
|
||||
/**
|
||||
* Retrieves singleton representation of the current class
|
||||
*/
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
/**
|
||||
* Calculates the accessbility element which is located at the given screen coordinates
|
||||
*
|
||||
* @param point The screen coordinates
|
||||
* @returns The retrieved accessbility element or nil if it cannot be detected
|
||||
*/
|
||||
+ (nullable id<FBXCAccessibilityElement>)axElementWithPoint:(CGPoint)point;
|
||||
|
||||
/**
|
||||
* Retrieves the accessbility element for the current screen point
|
||||
*
|
||||
* @returns The retrieved accessbility element or nil if it cannot be detected
|
||||
*/
|
||||
- (nullable id<FBXCAccessibilityElement>)axElement;
|
||||
|
||||
/**
|
||||
* Sets the coordinates for the current screen point
|
||||
*
|
||||
* @param coordinatesStr The coordinates string in `x,y` format. x and y can be any float numbers
|
||||
* @param error Is assigned to the actual error object if coordinates cannot be set
|
||||
* @returns YES if the coordinates were successfully set
|
||||
*/
|
||||
- (BOOL)setCoordinatesWithString:(NSString *)coordinatesStr error:(NSError **)error;
|
||||
|
||||
/**
|
||||
* Retrieves the coordinates of the current screen point in string representation
|
||||
*
|
||||
* @returns Point coordinates as `x,y` string
|
||||
*/
|
||||
- (NSString *)stringCoordinates;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
87
WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m
Normal file
87
WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m
Normal file
@@ -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 "FBActiveAppDetectionPoint.h"
|
||||
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "XCTestManager_ManagerInterface-Protocol.h"
|
||||
|
||||
@implementation FBActiveAppDetectionPoint
|
||||
|
||||
- (instancetype)init {
|
||||
if ((self = [super init])) {
|
||||
CGSize screenSize = [UIScreen mainScreen].bounds.size;
|
||||
// Consider the element, which is located close to the top left corner of the screen the on-screen one.
|
||||
CGFloat pointDistance = MIN(screenSize.width, screenSize.height) * (CGFloat) 0.2;
|
||||
_coordinates = CGPointMake(pointDistance, pointDistance);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static FBActiveAppDetectionPoint *instance;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
+ (id<FBXCAccessibilityElement>)axElementWithPoint:(CGPoint)point
|
||||
{
|
||||
__block id<FBXCAccessibilityElement> onScreenElement = nil;
|
||||
id<XCTestManager_ManagerInterface> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
[proxy _XCT_requestElementAtPoint:point
|
||||
reply:^(id element, NSError *error) {
|
||||
if (nil == error) {
|
||||
onScreenElement = element;
|
||||
} else {
|
||||
[FBLogger logFmt:@"Cannot request the screen point at %@", NSStringFromCGPoint(point)];
|
||||
}
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)));
|
||||
return onScreenElement;
|
||||
}
|
||||
|
||||
- (id<FBXCAccessibilityElement>)axElement
|
||||
{
|
||||
return [self.class axElementWithPoint:self.coordinates];
|
||||
}
|
||||
|
||||
- (BOOL)setCoordinatesWithString:(NSString *)coordinatesStr error:(NSError **)error
|
||||
{
|
||||
NSArray<NSString *> *screenPointCoords = [coordinatesStr componentsSeparatedByString:@","];
|
||||
if (screenPointCoords.count != 2) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The screen point coordinates should be separated by a single comma character. Got '%@' instead", coordinatesStr]
|
||||
buildError:error];
|
||||
}
|
||||
NSString *strX = [screenPointCoords.firstObject stringByTrimmingCharactersInSet:
|
||||
NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
NSString *strY = [screenPointCoords.lastObject stringByTrimmingCharactersInSet:
|
||||
NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
if (0 == strX.length || 0 == strY.length) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Both screen point coordinates should be valid numbers. Got '%@' instead", coordinatesStr]
|
||||
buildError:error];
|
||||
}
|
||||
self.coordinates = CGPointMake((CGFloat) strX.doubleValue, (CGFloat) strY.doubleValue);
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSString *)stringCoordinates
|
||||
{
|
||||
return [NSString stringWithFormat:@"%.2f,%.2f", self.coordinates.x, self.coordinates.y];
|
||||
}
|
||||
|
||||
@end
|
||||
51
WebDriverAgentLib/Utilities/FBAlertsMonitor.h
Normal file
51
WebDriverAgentLib/Utilities/FBAlertsMonitor.h
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 <XCTest/XCTest.h>
|
||||
|
||||
@class FBAlert, XCUIApplication;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol FBAlertsMonitorDelegate
|
||||
|
||||
/**
|
||||
The callback which is invoked when an unexpected on-screen alert is shown
|
||||
|
||||
@param alert The instance of the current alert
|
||||
*/
|
||||
- (void)didDetectAlert:(FBAlert *)alert;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBAlertsMonitor : NSObject
|
||||
|
||||
/*! The delegate which decides on what to do when an alert is detected */
|
||||
@property (nonatomic, nullable, weak) id<FBAlertsMonitorDelegate> delegate;
|
||||
|
||||
/**
|
||||
Creates an instance of alerts monitor.
|
||||
The monitoring is done on the main thread and is disabled unless `enable` is called.
|
||||
|
||||
@return Alerts monitor instance
|
||||
*/
|
||||
- (instancetype)init;
|
||||
|
||||
/**
|
||||
Enables alerts monitoring
|
||||
*/
|
||||
- (void)enable;
|
||||
|
||||
/**
|
||||
Disables alerts monitoring
|
||||
*/
|
||||
- (void)disable;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
94
WebDriverAgentLib/Utilities/FBAlertsMonitor.m
Normal file
94
WebDriverAgentLib/Utilities/FBAlertsMonitor.m
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 "FBAlertsMonitor.h"
|
||||
|
||||
#import "FBAlert.h"
|
||||
#import "FBLogger.h"
|
||||
#import "XCUIApplication+FBAlert.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
|
||||
static const NSTimeInterval FB_MONTORING_INTERVAL = 2.0;
|
||||
|
||||
@interface FBAlertsMonitor()
|
||||
|
||||
@property (atomic) BOOL isMonitoring;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBAlertsMonitor
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
_isMonitoring = NO;
|
||||
_delegate = nil;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)scheduleNextTick
|
||||
{
|
||||
if (!self.isMonitoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_time_t delta = (int64_t)(FB_MONTORING_INTERVAL * NSEC_PER_SEC);
|
||||
|
||||
if (nil == self.delegate) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delta), dispatch_get_main_queue(), ^{
|
||||
[self scheduleNextTick];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSArray<XCUIApplication *> *activeApps = XCUIApplication.fb_activeApplications;
|
||||
for (XCUIApplication *activeApp in activeApps) {
|
||||
XCUIElement *alertElement = nil;
|
||||
@try {
|
||||
alertElement = activeApp.fb_alertElement;
|
||||
if (nil != alertElement) {
|
||||
[self.delegate didDetectAlert:[FBAlert alertWithElement:alertElement]];
|
||||
}
|
||||
} @catch (NSException *e) {
|
||||
[FBLogger logFmt:@"Got an unexpected exception while monitoring alerts: %@\n%@", e.reason, e.callStackSymbols];
|
||||
}
|
||||
if (nil != alertElement) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.isMonitoring) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delta), dispatch_get_main_queue(), ^{
|
||||
[self scheduleNextTick];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)enable
|
||||
{
|
||||
if (self.isMonitoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.isMonitoring = YES;
|
||||
[self scheduleNextTick];
|
||||
}
|
||||
|
||||
- (void)disable
|
||||
{
|
||||
if (!self.isMonitoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.isMonitoring = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
131
WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h
Normal file
131
WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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 "FBElementCache.h"
|
||||
#import "FBXCElementSnapshot.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCSynthesizedEventRecord.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@interface FBBaseActionItem : NSObject
|
||||
|
||||
/*! Raw JSON representation of the corresponding action item */
|
||||
@property (nonatomic) NSDictionary<NSString *, id> *actionItem;
|
||||
/*! Current application instance */
|
||||
@property (nonatomic) XCUIApplication *application;
|
||||
/*! Action offset in the chain in milliseconds */
|
||||
@property (nonatomic) double offset;
|
||||
|
||||
/**
|
||||
Get the name of the corresponding raw action item. This method is expected to be overriden in subclasses.
|
||||
|
||||
@return The corresponding action item key in object's raw JSON reprsentation
|
||||
*/
|
||||
+ (NSString *)actionName;
|
||||
|
||||
/**
|
||||
Add the current gesture to XCPointerEventPath instance. This method is expected to be overriden in subclasses.
|
||||
|
||||
@param eventPath The destination XCPointerEventPath instance. If nil value is passed then a new XCPointerEventPath instance is going to be created
|
||||
@param allItems The existing actions chain to be transformed into event path
|
||||
@param currentItemIndex The index of the current item in allItems array
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem
|
||||
@return the constructed XCPointerEventPath instance or nil in case of failure
|
||||
*/
|
||||
- (nullable NSArray<XCPointerEventPath *> *)addToEventPath:(nullable XCPointerEventPath *)eventPath
|
||||
allItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBBaseGestureItem : FBBaseActionItem
|
||||
|
||||
/*! Absolute position on the screen where the gesure should be performed */
|
||||
@property (nonatomic) XCUICoordinate *atPosition;
|
||||
/*! Gesture duration in milliseconds */
|
||||
@property (nonatomic) double duration;
|
||||
|
||||
/**
|
||||
Calculate absolute gesture position on the screen based on provided element and positionOffset values.
|
||||
|
||||
@param element The element instance to perform the gesture on. If element equals to nil then positionOffset is considered as absolute coordinates
|
||||
@param positionOffset The actual coordinate offset. If this calue equals to nil then element's hitpoint is taken as gesture position. If element is not nil then this offset is calculated relatively to the top-left cordner of the element's position
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem
|
||||
@return Adbsolute gesture position on the screen or nil if the calculation fails (for example, the element is invisible)
|
||||
*/
|
||||
- (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element
|
||||
positionOffset:(nullable NSValue *)positionOffset
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBBaseActionItemsChain : NSObject
|
||||
|
||||
/*! All gesture items collected in the chain */
|
||||
@property (readonly, nonatomic) NSMutableArray *items;
|
||||
/*! Total length of all the gestures in the chain in milliseconds */
|
||||
@property (nonatomic) double durationOffset;
|
||||
|
||||
/**
|
||||
Add a new gesture item to the current chain. The method is expected to be overriden in subclasses.
|
||||
|
||||
@param item The actual gesture instance to be added
|
||||
*/
|
||||
- (void)addItem:(FBBaseActionItem *)item;
|
||||
|
||||
/**
|
||||
Represents the chain as XCPointerEventPath instance.
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem
|
||||
@return The constructed array of XCPointerEventPath instances or nil if there was a failure
|
||||
*/
|
||||
- (nullable NSArray<XCPointerEventPath *> *)asEventPathsWithError:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBBaseActionsSynthesizer : NSObject
|
||||
|
||||
/*! Raw actions chain received from request's JSON */
|
||||
@property (readonly, nonatomic) NSArray *actions;
|
||||
/*! Current application instance */
|
||||
@property (readonly, nonatomic) XCUIApplication *application;
|
||||
/*! Current elements cache */
|
||||
@property (readonly, nonatomic, nullable) FBElementCache *elementCache;
|
||||
|
||||
/**
|
||||
Initializes actions synthesizer. This initializer should be used only by subclasses.
|
||||
|
||||
@param actions The raw actions chain received from request's JSON. The format of this chain is defined by the standard, implemented in the correspoding subclass.
|
||||
@param application Current application instance
|
||||
@param elementCache Elements cache, which is used to replace elements references in the chain with their instances. We assume the chain already contains element instances if this parameter is set to nil
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem
|
||||
@return The corresponding synthesizer instance or nil in case of failure (for example if `actions` is nil or empty)
|
||||
*/
|
||||
- (nullable instancetype)initWithActions:(NSArray *)actions
|
||||
forApplication:(XCUIApplication *)application
|
||||
elementCache:(nullable FBElementCache *)elementCache
|
||||
error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Synthesizes XCTest-compatible event record to be performed in the UI. This method is supposed to be overriden by subclasses.
|
||||
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem
|
||||
@return The generated event record or nil in case of failure
|
||||
*/
|
||||
- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error;
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
165
WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m
Normal file
165
WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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 "FBBaseActionsSynthesizer.h"
|
||||
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBMathUtils.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCPointerEventPath.h"
|
||||
#import "XCSynthesizedEventRecord.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@implementation FBBaseActionItem
|
||||
|
||||
+ (NSString *)actionName
|
||||
{
|
||||
@throw [[FBErrorBuilder.builder withDescription:@"Override this method in subclasses"] build];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
|
||||
allItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
error:(NSError **)error
|
||||
{
|
||||
@throw [[FBErrorBuilder.builder withDescription:@"Override this method in subclasses"] build];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBBaseGestureItem
|
||||
|
||||
- (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element
|
||||
positionOffset:(nullable NSValue *)positionOffset
|
||||
error:(NSError **)error
|
||||
{
|
||||
if (nil == element) {
|
||||
CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y);
|
||||
// Only absolute offset is defined
|
||||
return [[self.application coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset];
|
||||
}
|
||||
|
||||
// The offset relative to the element is defined
|
||||
if (nil == positionOffset) {
|
||||
if (element.hittable) {
|
||||
// short circuit element hitpoint
|
||||
return element.hitPointCoordinate;
|
||||
}
|
||||
[FBLogger logFmt:@"Will use the frame of '%@' for hit point calculation instead", element.debugDescription];
|
||||
}
|
||||
if (CGRectIsEmpty(element.frame)) {
|
||||
[FBLogger log:self.application.fb_descriptionRepresentation];
|
||||
NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable",
|
||||
element.description];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
if (nil == positionOffset) {
|
||||
return [element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)];
|
||||
}
|
||||
|
||||
CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y);
|
||||
// TODO: Shall we throw an exception if hitPoint is out of the element frame?
|
||||
return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBBaseActionItemsChain
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_items = [NSMutableArray array];
|
||||
_durationOffset = 0.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)addItem:(FBBaseActionItem *)item __attribute__((noreturn))
|
||||
{
|
||||
@throw [[FBErrorBuilder.builder withDescription:@"Override this method in subclasses"] build];
|
||||
}
|
||||
|
||||
- (nullable NSArray<XCPointerEventPath *> *)asEventPathsWithError:(NSError **)error
|
||||
{
|
||||
if (0 == self.items.count) {
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:@"Action items list cannot be empty"] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<XCPointerEventPath *> *result = [NSMutableArray array];
|
||||
XCPointerEventPath *previousEventPath = nil;
|
||||
XCPointerEventPath *currentEventPath = nil;
|
||||
NSUInteger index = 0;
|
||||
for (FBBaseActionItem *item in self.items.copy) {
|
||||
NSArray<XCPointerEventPath *> *currentEventPaths = [item addToEventPath:currentEventPath
|
||||
allItems:self.items.copy
|
||||
currentItemIndex:index++
|
||||
error:error];
|
||||
if (currentEventPaths == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
currentEventPath = currentEventPaths.lastObject;
|
||||
if (nil == currentEventPath) {
|
||||
currentEventPath = previousEventPath;
|
||||
} else if (currentEventPath != previousEventPath) {
|
||||
[result addObjectsFromArray:currentEventPaths];
|
||||
previousEventPath = currentEventPath;
|
||||
}
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBBaseActionsSynthesizer
|
||||
|
||||
- (instancetype)initWithActions:(NSArray *)actions
|
||||
forApplication:(XCUIApplication *)application
|
||||
elementCache:(nullable FBElementCache *)elementCache
|
||||
error:(NSError **)error
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
if ((nil == actions || 0 == actions.count) && error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:@"Actions list cannot be empty"] build];
|
||||
return nil;
|
||||
}
|
||||
_actions = actions;
|
||||
_application = application;
|
||||
_elementCache = elementCache;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error
|
||||
{
|
||||
@throw [[FBErrorBuilder.builder withDescription:@"Override synthesizeWithError method in subclasses"] build];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
45
WebDriverAgentLib/Utilities/FBCapabilities.h
Normal file
45
WebDriverAgentLib/Utilities/FBCapabilities.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 <Foundation/Foundation.h>
|
||||
|
||||
/** Whether to use alternative elements visivility detection method */
|
||||
extern NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION;
|
||||
/** Set the maximum amount of characters that could be typed within a minute (60 by default) */
|
||||
extern NSString* const FB_CAP_MAX_TYPING_FREQUENCY;
|
||||
/** this setting was needed for some legacy stuff */
|
||||
extern NSString* const FB_CAP_USE_SINGLETON_TEST_MANAGER;
|
||||
/** Whether to disable screneshots that XCTest automaticallly creates after each step */
|
||||
extern NSString* const FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS;
|
||||
/** Whether to terminate the application under test after the session ends */
|
||||
extern NSString* const FB_CAP_SHOULD_TERMINATE_APP;
|
||||
/** The maximum amount of seconds to wait for the event loop to become idle */
|
||||
extern NSString* const FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC;
|
||||
/** Bundle identifier of the application to run the test for */
|
||||
extern NSString* const FB_CAP_BUNDLE_ID;
|
||||
/**
|
||||
Usually an URL used as initial link to run Mobile Safari, but could be any other deep link.
|
||||
This might also work together with `FB_CAP_BUNLDE_ID`, which tells XCTest to open
|
||||
the given deep link in the particular app.
|
||||
Only works since iOS 16.4
|
||||
*/
|
||||
extern NSString* const FB_CAP_INITIAL_URL;
|
||||
/** Whether to enforrce (re)start of the application under test on session startup */
|
||||
extern NSString* const FB_CAP_FORCE_APP_LAUNCH;
|
||||
/** Whether to wait for quiescence before starting interaction with apps laucnhes in scope of the test session */
|
||||
extern NSString* const FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE;
|
||||
/** Array of command line arguments to be passed to the application under test */
|
||||
extern NSString* const FB_CAP_ARGUMENTS;
|
||||
/** Dictionary of environment variables to be passed to the application under test */
|
||||
extern NSString* const FB_CAP_ENVIRNOMENT;
|
||||
/** Whether to use native XCTest caching strategy */
|
||||
extern NSString* const FB_CAP_USE_NATIVE_CACHING_STRATEGY;
|
||||
/** Whether to enforce software keyboard presence on simulator */
|
||||
extern NSString* const FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE;
|
||||
/** Sets the application state change timeout for the initial app startup */
|
||||
extern NSString* const FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC;
|
||||
25
WebDriverAgentLib/Utilities/FBCapabilities.m
Normal file
25
WebDriverAgentLib/Utilities/FBCapabilities.m
Normal file
@@ -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 "FBCapabilities.h"
|
||||
|
||||
NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION = @"shouldUseTestManagerForVisibilityDetection";
|
||||
NSString* const FB_CAP_MAX_TYPING_FREQUENCY = @"maxTypingFrequency";
|
||||
NSString* const FB_CAP_USE_SINGLETON_TEST_MANAGER = @"shouldUseSingletonTestManager";
|
||||
NSString* const FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS = @"disableAutomaticScreenshots";
|
||||
NSString* const FB_CAP_SHOULD_TERMINATE_APP = @"shouldTerminateApp";
|
||||
NSString* const FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC = @"eventloopIdleDelaySec";
|
||||
NSString* const FB_CAP_BUNDLE_ID = @"bundleId";
|
||||
NSString* const FB_CAP_INITIAL_URL = @"initialUrl";
|
||||
NSString* const FB_CAP_FORCE_APP_LAUNCH = @"forceAppLaunch";
|
||||
NSString* const FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE = @"shouldWaitForQuiescence";
|
||||
NSString* const FB_CAP_ARGUMENTS = @"arguments";
|
||||
NSString* const FB_CAP_ENVIRNOMENT = @"environment";
|
||||
NSString* const FB_CAP_USE_NATIVE_CACHING_STRATEGY = @"useNativeCachingStrategy";
|
||||
NSString* const FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE = @"forceSimulatorSoftwareKeyboardPresence";
|
||||
NSString* const FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC = @"appLaunchStateTimeoutSec";
|
||||
99
WebDriverAgentLib/Utilities/FBClassChainQueryParser.h
Normal file
99
WebDriverAgentLib/Utilities/FBClassChainQueryParser.h
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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 FBAbstractPredicateItem : NSObject
|
||||
|
||||
/*! The actual predicate value of an item */
|
||||
@property (nonatomic, readonly) NSPredicate *value;
|
||||
|
||||
/**
|
||||
Instance constructor, which allows to set item value on instance creation
|
||||
|
||||
@param value the actual predicate value
|
||||
@return FBAbstractPredicateItem instance
|
||||
*/
|
||||
- (instancetype)initWithValue:(NSPredicate *)value;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBSelfPredicateItem : FBAbstractPredicateItem
|
||||
|
||||
@end
|
||||
|
||||
@interface FBDescendantPredicateItem : FBAbstractPredicateItem
|
||||
|
||||
@end
|
||||
|
||||
@interface FBClassChainItem : NSObject
|
||||
|
||||
/*! Element's position */
|
||||
@property (readonly, nonatomic, nullable) NSNumber *position;
|
||||
/*! Element's type */
|
||||
@property (readonly, nonatomic) XCUIElementType type;
|
||||
/*! Whether an element is a descendant of the previos element */
|
||||
@property (readonly, nonatomic) BOOL isDescendant;
|
||||
/*! The ordered list of matching predicates for the current element */
|
||||
@property (readonly, nonatomic) NSArray<FBAbstractPredicateItem *> *predicates;
|
||||
|
||||
/**
|
||||
Instance constructor, which allows to set element type and position
|
||||
|
||||
@param type on of supoported element types declared in XCUIElementType enum
|
||||
@param position element position relative to its sibling element. Numeration
|
||||
starts with 1. Zero value means that all sibling element should be selected.
|
||||
Negative value means that numeration starts from the last element, for example
|
||||
-1 is the last child element and -2 is the second last element.
|
||||
nil value means that no element position has been set explicitly.
|
||||
@param predicates the list of matching descendant/self predicates
|
||||
@param isDescendant equals to YES if the element is a descendantt element of
|
||||
the previous element in the chain. NO value means the element is the direct
|
||||
child of the previous element
|
||||
@return FBClassChainElement instance
|
||||
*/
|
||||
- (instancetype)initWithType:(XCUIElementType)type position:(nullable NSNumber *)position predicates:(NSArray<FBAbstractPredicateItem *> *)predicates isDescendant:(BOOL)isDescendant;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBClassChain : NSObject
|
||||
|
||||
/*! Array of parsed chain items */
|
||||
@property (readonly, nonatomic, copy) NSArray<FBClassChainItem *> *elements;
|
||||
|
||||
/**
|
||||
Instance constructor for parsed class chain instance
|
||||
|
||||
@param elements an array of parsed chains elements
|
||||
@return FBClassChain instance
|
||||
*/
|
||||
- (instancetype)initWithElements:(NSArray<FBClassChainItem *> *)elements;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBClassChainQueryParser : NSObject
|
||||
|
||||
/**
|
||||
Method used to interpret class chain queries
|
||||
|
||||
@param classChainQuery class chain query as string. See the documentation of
|
||||
XCUIElement+FBClassChain category for more details about the expected query format
|
||||
@param error standard NSError object, which is going to be initializaed if
|
||||
there are query parsing errors
|
||||
@return list of parsed primitives packed to FBClassChainElement class or nil in case
|
||||
there was parsing error (the parameter will be initialized with detailed error description in such case)
|
||||
@throws FBUnknownAttributeException if any of predicates in the chain contains unknown attribute
|
||||
*/
|
||||
+ (nullable FBClassChain*)parseQuery:(NSString*)classChainQuery error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
666
WebDriverAgentLib/Utilities/FBClassChainQueryParser.m
Normal file
666
WebDriverAgentLib/Utilities/FBClassChainQueryParser.m
Normal file
@@ -0,0 +1,666 @@
|
||||
/**
|
||||
* 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 "FBClassChainQueryParser.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBElementTypeTransformer.h"
|
||||
#import "FBExceptions.h"
|
||||
#import "NSPredicate+FBFormat.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBBaseClassChainToken : NSObject
|
||||
|
||||
@property (nonatomic) NSString *asString;
|
||||
@property (nonatomic) NSUInteger previousItemsCountToOverride;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBClassNameToken : FBBaseClassChainToken
|
||||
|
||||
@end
|
||||
|
||||
@interface FBStarToken : FBBaseClassChainToken
|
||||
|
||||
@end
|
||||
|
||||
@interface FBDescendantMarkerToken : FBBaseClassChainToken
|
||||
|
||||
@end
|
||||
|
||||
@interface FBSplitterToken : FBBaseClassChainToken
|
||||
|
||||
@end
|
||||
|
||||
@interface FBOpeningBracketToken : FBBaseClassChainToken
|
||||
|
||||
@end
|
||||
|
||||
@interface FBClosingBracketToken : FBBaseClassChainToken
|
||||
|
||||
@end
|
||||
|
||||
@interface FBNumberToken : FBBaseClassChainToken
|
||||
|
||||
@end
|
||||
|
||||
@interface FBAbstractPredicateToken : FBBaseClassChainToken
|
||||
|
||||
@property (nonatomic) BOOL isParsingCompleted;
|
||||
|
||||
+ (NSString *)enclosingMarker;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBSelfPredicateToken : FBAbstractPredicateToken
|
||||
|
||||
@end
|
||||
|
||||
@interface FBDescendantPredicateToken : FBAbstractPredicateToken
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
|
||||
@implementation FBBaseClassChainToken
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_asString = @"";
|
||||
_previousItemsCountToOverride = 0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithStringValue:(NSString *)stringValue
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_asString = stringValue;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSCharacterSet *)allowedCharacters
|
||||
{
|
||||
// This method is expected to be overriden by subclasses
|
||||
return [NSCharacterSet characterSetWithCharactersInString:@""];
|
||||
}
|
||||
|
||||
+ (NSUInteger)maxLength
|
||||
{
|
||||
// This method is expected to be overriden by subclasses
|
||||
return ULONG_MAX;
|
||||
}
|
||||
|
||||
- (NSArray<Class> *)followingTokens
|
||||
{
|
||||
// This method is expected to be overriden by subclasses
|
||||
return @[];
|
||||
}
|
||||
|
||||
+ (BOOL)canConsumeCharacter:(unichar)character
|
||||
{
|
||||
return [self.allowedCharacters characterIsMember:character];
|
||||
}
|
||||
|
||||
- (void)appendChar:(unichar)character
|
||||
{
|
||||
NSMutableString *value = [NSMutableString stringWithString:self.asString];
|
||||
[value appendFormat:@"%C", character];
|
||||
self.asString = value.copy;;
|
||||
}
|
||||
|
||||
- (nullable FBBaseClassChainToken*)followingTokenBasedOn:(unichar)character
|
||||
{
|
||||
for (Class matchingTokenClass in self.followingTokens) {
|
||||
if ([matchingTokenClass canConsumeCharacter:character]) {
|
||||
return [[[matchingTokenClass alloc] init] nextTokenWithCharacter:character];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character
|
||||
{
|
||||
if ([self.class canConsumeCharacter:character] && self.asString.length < [self.class maxLength]) {
|
||||
[self appendChar:character];
|
||||
return self;
|
||||
}
|
||||
return [self followingTokenBasedOn:character];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBClassNameToken
|
||||
|
||||
+ (NSCharacterSet *)allowedCharacters
|
||||
{
|
||||
return [NSCharacterSet letterCharacterSet];
|
||||
}
|
||||
|
||||
- (NSArray<Class> *)followingTokens
|
||||
{
|
||||
return @[FBSplitterToken.class, FBOpeningBracketToken.class];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NSString *const STAR_TOKEN = @"*";
|
||||
@implementation FBStarToken
|
||||
|
||||
+ (NSCharacterSet *)allowedCharacters
|
||||
{
|
||||
return [NSCharacterSet characterSetWithCharactersInString:STAR_TOKEN];
|
||||
}
|
||||
|
||||
- (NSArray<Class> *)followingTokens
|
||||
{
|
||||
return @[FBSplitterToken.class, FBOpeningBracketToken.class];
|
||||
}
|
||||
|
||||
- (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character
|
||||
{
|
||||
if ([self.class.allowedCharacters characterIsMember:character]) {
|
||||
if (self.asString.length >= 1) {
|
||||
FBDescendantMarkerToken *nextToken = [[FBDescendantMarkerToken alloc] initWithStringValue:[NSString stringWithFormat:@"%@%@", STAR_TOKEN, STAR_TOKEN]];
|
||||
nextToken.previousItemsCountToOverride = 1;
|
||||
return nextToken;
|
||||
}
|
||||
[self appendChar:character];
|
||||
return self;
|
||||
}
|
||||
return [self followingTokenBasedOn:character];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
static NSString *const DESCENDANT_MARKER = @"**/";
|
||||
@implementation FBDescendantMarkerToken
|
||||
|
||||
+ (NSCharacterSet *)allowedCharacters
|
||||
{
|
||||
return [NSCharacterSet characterSetWithCharactersInString:@"*/"];
|
||||
}
|
||||
|
||||
- (NSArray<Class> *)followingTokens
|
||||
{
|
||||
return @[FBClassNameToken.class, FBStarToken.class];
|
||||
}
|
||||
|
||||
+ (NSUInteger)maxLength
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
- (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character
|
||||
{
|
||||
if ([self.class.allowedCharacters characterIsMember:character] && self.asString.length <= self.class.maxLength) {
|
||||
if (self.asString.length > 0 && ![DESCENDANT_MARKER hasPrefix:self.asString]) {
|
||||
return nil;
|
||||
}
|
||||
if (self.asString.length < self.class.maxLength) {
|
||||
[self appendChar:character];
|
||||
return self;
|
||||
}
|
||||
}
|
||||
return [self followingTokenBasedOn:character];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBSplitterToken
|
||||
|
||||
+ (NSCharacterSet *)allowedCharacters
|
||||
{
|
||||
return [NSCharacterSet characterSetWithCharactersInString:@"/"];
|
||||
}
|
||||
|
||||
- (NSArray<Class> *)followingTokens
|
||||
{
|
||||
return @[FBStarToken.class, FBClassNameToken.class];
|
||||
}
|
||||
|
||||
+ (NSUInteger)maxLength
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBOpeningBracketToken
|
||||
|
||||
+ (NSCharacterSet *)allowedCharacters
|
||||
{
|
||||
return [NSCharacterSet characterSetWithCharactersInString:@"["];
|
||||
}
|
||||
|
||||
- (NSArray<Class> *)followingTokens
|
||||
{
|
||||
return @[FBNumberToken.class, FBSelfPredicateToken.class, FBDescendantPredicateToken.class];
|
||||
}
|
||||
|
||||
+ (NSUInteger)maxLength
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBNumberToken
|
||||
|
||||
+ (NSCharacterSet *)allowedCharacters
|
||||
{
|
||||
NSMutableCharacterSet *result = [NSMutableCharacterSet new];
|
||||
[result formUnionWithCharacterSet:[NSCharacterSet decimalDigitCharacterSet]];
|
||||
[result addCharactersInString:@"-"];
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
- (NSArray<Class> *)followingTokens
|
||||
{
|
||||
return @[FBClosingBracketToken.class];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBClosingBracketToken
|
||||
|
||||
+ (NSCharacterSet *)allowedCharacters
|
||||
{
|
||||
return [NSCharacterSet characterSetWithCharactersInString:@"]"];
|
||||
}
|
||||
|
||||
- (NSArray<Class> *)followingTokens
|
||||
{
|
||||
return @[FBSplitterToken.class, FBOpeningBracketToken.class];
|
||||
}
|
||||
|
||||
+ (NSUInteger)maxLength
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NSString *const FBAbstractMethodInvocationException = @"FBAbstractMethodInvocationException";
|
||||
|
||||
@implementation FBAbstractPredicateToken
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_isParsingCompleted = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSString *)enclosingMarker
|
||||
{
|
||||
NSString *errMsg = [NSString stringWithFormat:@"The + (NSString *)enclosingMarker method is expected to be overriden by %@ class", NSStringFromClass(self.class)];
|
||||
@throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
|
||||
}
|
||||
|
||||
+ (NSCharacterSet *)allowedCharacters
|
||||
{
|
||||
return [NSCharacterSet illegalCharacterSet].invertedSet;
|
||||
}
|
||||
|
||||
- (NSArray<Class> *)followingTokens
|
||||
{
|
||||
return @[FBClosingBracketToken.class];
|
||||
}
|
||||
|
||||
+ (BOOL)canConsumeCharacter:(unichar)character
|
||||
{
|
||||
return [[NSCharacterSet characterSetWithCharactersInString:self.class.enclosingMarker] characterIsMember:character];
|
||||
}
|
||||
|
||||
- (void)stripLastChar
|
||||
{
|
||||
if (self.asString.length > 0) {
|
||||
self.asString = [self.asString substringToIndex:self.asString.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable FBBaseClassChainToken*)nextTokenWithCharacter:(unichar)character
|
||||
{
|
||||
NSString *currentChar = [NSString stringWithFormat:@"%C", character];
|
||||
if (!self.isParsingCompleted && [self.class.allowedCharacters characterIsMember:character]) {
|
||||
if (0 == self.asString.length) {
|
||||
if ([self.class.enclosingMarker isEqualToString:currentChar]) {
|
||||
// Do not include enclosing character
|
||||
return self;
|
||||
}
|
||||
} else if ([self.class.enclosingMarker isEqualToString:currentChar]) {
|
||||
[self appendChar:character];
|
||||
self.isParsingCompleted = YES;
|
||||
return self;
|
||||
}
|
||||
[self appendChar:character];
|
||||
return self;
|
||||
}
|
||||
if (self.isParsingCompleted) {
|
||||
if ([currentChar isEqualToString:self.class.enclosingMarker]) {
|
||||
// Escaped enclosing character has been detected. Do not finish parsing
|
||||
self.isParsingCompleted = NO;
|
||||
return self;
|
||||
} else {
|
||||
// Do not include enclosing character
|
||||
[self stripLastChar];
|
||||
}
|
||||
}
|
||||
return [self followingTokenBasedOn:character];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBSelfPredicateToken
|
||||
|
||||
+ (NSString *)enclosingMarker
|
||||
{
|
||||
return @"`";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBDescendantPredicateToken
|
||||
|
||||
+ (NSString *)enclosingMarker
|
||||
{
|
||||
return @"$";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBClassChainItem
|
||||
|
||||
- (instancetype)initWithType:(XCUIElementType)type position:(NSNumber *)position predicates:(NSArray<FBAbstractPredicateItem *> *)predicates isDescendant:(BOOL)isDescendant
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_type = type;
|
||||
_position = position;
|
||||
_predicates = predicates;
|
||||
_isDescendant = isDescendant;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBClassChain
|
||||
|
||||
- (instancetype)initWithElements:(NSArray<FBClassChainItem *> *)elements
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_elements = elements;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBClassChainQueryParser
|
||||
|
||||
static NSNumberFormatter *numberFormatter = nil;
|
||||
|
||||
+ (void)initialize {
|
||||
if (nil == numberFormatter) {
|
||||
numberFormatter = [[NSNumberFormatter alloc] init];
|
||||
numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSError *)tokenizationErrorWithIndex:(NSUInteger)index originalQuery:(NSString *)originalQuery
|
||||
{
|
||||
NSString *description = [NSString stringWithFormat:@"Cannot parse class chain query '%@'. Unexpected character detected at position %@:\n%@ <----", originalQuery, @(index + 1), [originalQuery substringToIndex:index + 1]];
|
||||
return [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
|
||||
+ (nullable NSArray<FBBaseClassChainToken *> *)tokenizedQueryWithQuery:(NSString*)classChainQuery error:(NSError **)error
|
||||
{
|
||||
NSUInteger queryStringLength = classChainQuery.length;
|
||||
FBBaseClassChainToken *token;
|
||||
unichar firstCharacter = [classChainQuery characterAtIndex:0];
|
||||
if ([classChainQuery hasPrefix:DESCENDANT_MARKER]) {
|
||||
token = [[FBDescendantMarkerToken alloc] initWithStringValue:DESCENDANT_MARKER];
|
||||
} else if ([FBClassNameToken canConsumeCharacter:firstCharacter]) {
|
||||
token = [[FBClassNameToken alloc] initWithStringValue:[NSString stringWithFormat:@"%C", firstCharacter]];
|
||||
} else if ([FBStarToken canConsumeCharacter:firstCharacter]) {
|
||||
token = [[FBStarToken alloc] initWithStringValue:[NSString stringWithFormat:@"%C", firstCharacter]];
|
||||
} else {
|
||||
if (error) {
|
||||
*error = [self.class tokenizationErrorWithIndex:0 originalQuery:classChainQuery];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
FBBaseClassChainToken *nextToken = token;
|
||||
for (NSUInteger charIdx = token.asString.length; charIdx < queryStringLength; ++charIdx) {
|
||||
nextToken = [token nextTokenWithCharacter:[classChainQuery characterAtIndex:charIdx]];
|
||||
if (nil == nextToken) {
|
||||
if (error) {
|
||||
*error = [self.class tokenizationErrorWithIndex:charIdx originalQuery:classChainQuery];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
if (nextToken != token) {
|
||||
[result addObject:token];
|
||||
if (nextToken.previousItemsCountToOverride > 0 && result.count > 0) {
|
||||
NSUInteger itemsCountToOverride = nextToken.previousItemsCountToOverride <= result.count ? nextToken.previousItemsCountToOverride : result.count;
|
||||
[result removeObjectsInRange:NSMakeRange(result.count - itemsCountToOverride, itemsCountToOverride)];
|
||||
}
|
||||
token = nextToken;
|
||||
}
|
||||
}
|
||||
if (nextToken) {
|
||||
if (nextToken.previousItemsCountToOverride > 0 && result.count > 0) {
|
||||
NSUInteger itemsCountToOverride = nextToken.previousItemsCountToOverride <= result.count ? nextToken.previousItemsCountToOverride : result.count;
|
||||
[result removeObjectsInRange:NSMakeRange(result.count - itemsCountToOverride, itemsCountToOverride)];
|
||||
}
|
||||
[result addObject:nextToken];
|
||||
}
|
||||
|
||||
FBBaseClassChainToken *lastToken = [result lastObject];
|
||||
if (!([lastToken isKindOfClass:FBClosingBracketToken.class] ||
|
||||
[lastToken isKindOfClass:FBClassNameToken.class] ||
|
||||
[lastToken isKindOfClass:FBStarToken.class])) {
|
||||
if (error) {
|
||||
*error = [self.class tokenizationErrorWithIndex:queryStringLength - 1 originalQuery:classChainQuery];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
+ (NSError *)compilationErrorWithQuery:(NSString *)originalQuery description:(NSString *)description
|
||||
{
|
||||
NSString *fullDescription = [NSString stringWithFormat:@"Cannot parse class chain query '%@'. %@", originalQuery, description];
|
||||
return [[FBErrorBuilder.builder withDescription:fullDescription] build];
|
||||
}
|
||||
|
||||
+ (nullable FBClassChain*)compiledQueryWithTokenizedQuery:(NSArray<FBBaseClassChainToken *> *)tokenizedQuery
|
||||
originalQuery:(NSString *)originalQuery
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
XCUIElementType chainElementType = XCUIElementTypeAny;
|
||||
NSNumber *chainElementPosition = nil;
|
||||
BOOL isTypeSet = NO;
|
||||
BOOL isPositionSet = NO;
|
||||
BOOL isDescendantSet = NO;
|
||||
NSMutableArray<FBAbstractPredicateItem *> *predicates = [NSMutableArray array];
|
||||
for (FBBaseClassChainToken *token in tokenizedQuery) {
|
||||
if ([token isKindOfClass:FBClassNameToken.class]) {
|
||||
if (isTypeSet) {
|
||||
if (error) {
|
||||
NSString *description = [NSString stringWithFormat:@"Unexpected token '%@'. The type name can be set only once.", token.asString];
|
||||
*error = [self.class compilationErrorWithQuery:originalQuery description:description];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
@try {
|
||||
chainElementType = [FBElementTypeTransformer elementTypeWithTypeName:token.asString];
|
||||
isTypeSet = YES;
|
||||
} @catch (NSException *e) {
|
||||
if ([e.name isEqualToString:FBInvalidArgumentException]) {
|
||||
if (error) {
|
||||
NSString *description = [NSString stringWithFormat:@"'%@' class name is unknown to WDA", token.asString];
|
||||
*error = [self.class compilationErrorWithQuery:originalQuery description:description];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
@throw e;
|
||||
}
|
||||
} else if ([token isKindOfClass:FBStarToken.class]) {
|
||||
if (isTypeSet) {
|
||||
if (error) {
|
||||
NSString *description = [NSString stringWithFormat:@"Unexpected token '%@'. The type name can be set only once.", token.asString];
|
||||
*error = [self.class compilationErrorWithQuery:originalQuery description:description];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
chainElementType = XCUIElementTypeAny;
|
||||
isTypeSet = YES;
|
||||
} else if ([token isKindOfClass:FBDescendantMarkerToken.class]) {
|
||||
if (isDescendantSet) {
|
||||
NSString *description = [NSString stringWithFormat:@"Unexpected token '%@'. Descendant markers cannot be duplicated.", token.asString];
|
||||
if (error) {
|
||||
*error = [self.class compilationErrorWithQuery:originalQuery description:description];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
isTypeSet = NO;
|
||||
isPositionSet = NO;
|
||||
[predicates removeAllObjects];
|
||||
isDescendantSet = YES;
|
||||
} else if ([token isKindOfClass:FBAbstractPredicateToken.class]) {
|
||||
if (isPositionSet) {
|
||||
NSString *description = [NSString stringWithFormat:@"Predicate value '%@' must be set before position value.", token.asString];
|
||||
if (error) {
|
||||
*error = [self.class compilationErrorWithQuery:originalQuery description:description];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
if (!((FBAbstractPredicateToken *)token).isParsingCompleted) {
|
||||
NSString *description = [NSString stringWithFormat:@"Cannot find the end of '%@' predicate value.", token.asString];
|
||||
if (error) {
|
||||
*error = [self.class compilationErrorWithQuery:originalQuery description:description];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
NSPredicate *value = [NSPredicate fb_snapshotBlockPredicateWithPredicate:[NSPredicate predicateWithFormat:token.asString]];
|
||||
if ([token isKindOfClass:FBSelfPredicateToken.class]) {
|
||||
[predicates addObject:[[FBSelfPredicateItem alloc] initWithValue:value]];
|
||||
} else if ([token isKindOfClass:FBDescendantPredicateToken.class]) {
|
||||
[predicates addObject:[[FBDescendantPredicateItem alloc] initWithValue:value]];
|
||||
}
|
||||
} else if ([token isKindOfClass:FBNumberToken.class]) {
|
||||
if (isPositionSet) {
|
||||
NSString *description = [NSString stringWithFormat:@"Position value '%@' is expected to be set only once.", token.asString];
|
||||
if (error) {
|
||||
*error = [self.class compilationErrorWithQuery:originalQuery description:description];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
NSNumber *position = [numberFormatter numberFromString:token.asString];
|
||||
if (nil == position || 0 == position.intValue) {
|
||||
NSString *description = [NSString stringWithFormat:@"Position value '%@' is expected to be a valid integer number not equal to zero.", token.asString];
|
||||
if (error) {
|
||||
*error = [self.class compilationErrorWithQuery:originalQuery description:description];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
chainElementPosition = position;
|
||||
isPositionSet = YES;
|
||||
} else if ([token isKindOfClass:FBSplitterToken.class]) {
|
||||
if (!isPositionSet) {
|
||||
chainElementPosition = nil;
|
||||
}
|
||||
if (isDescendantSet) {
|
||||
if (isTypeSet) {
|
||||
[result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:YES]];
|
||||
isDescendantSet = NO;
|
||||
}
|
||||
} else {
|
||||
[result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:NO]];
|
||||
}
|
||||
isTypeSet = NO;
|
||||
isPositionSet = NO;
|
||||
[predicates removeAllObjects];
|
||||
}
|
||||
}
|
||||
if (!isPositionSet) {
|
||||
chainElementPosition = nil;
|
||||
}
|
||||
if (isDescendantSet) {
|
||||
if (isTypeSet) {
|
||||
[result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:YES]];
|
||||
} else {
|
||||
if (error) {
|
||||
NSString *description = @"Descendants lookup modifier '**/' should be followed with the actual element type";
|
||||
*error = [self.class compilationErrorWithQuery:originalQuery description:description];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
} else {
|
||||
[result addObject:[[FBClassChainItem alloc] initWithType:chainElementType position:chainElementPosition predicates:predicates.copy isDescendant:NO]];
|
||||
}
|
||||
return [[FBClassChain alloc] initWithElements:result.copy];
|
||||
}
|
||||
|
||||
+ (FBClassChain *)parseQuery:(NSString*)classChainQuery error:(NSError **)error
|
||||
{
|
||||
NSAssert(classChainQuery.length > 0, @"Query length should be greater than zero", nil);
|
||||
NSArray *tokenizedQuery = [self.class tokenizedQueryWithQuery:classChainQuery error:error];
|
||||
if (nil == tokenizedQuery) {
|
||||
return nil;
|
||||
}
|
||||
return [self.class compiledQueryWithTokenizedQuery:tokenizedQuery originalQuery:classChainQuery error:error];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBAbstractPredicateItem
|
||||
|
||||
- (instancetype)initWithValue:(NSPredicate *)value
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_value = value;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBSelfPredicateItem
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBDescendantPredicateItem
|
||||
|
||||
@end
|
||||
375
WebDriverAgentLib/Utilities/FBConfiguration.h
Normal file
375
WebDriverAgentLib/Utilities/FBConfiguration.h
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
extern NSString *const FBSnapshotMaxDepthKey;
|
||||
|
||||
/**
|
||||
Accessors for Global Constants.
|
||||
*/
|
||||
@interface FBConfiguration : NSObject
|
||||
|
||||
/*! If set to YES will ask TestManagerDaemon for element visibility */
|
||||
+ (void)setShouldUseTestManagerForVisibilityDetection:(BOOL)value;
|
||||
+ (BOOL)shouldUseTestManagerForVisibilityDetection;
|
||||
|
||||
/*! If set to YES will use compact (standards-compliant) & faster responses */
|
||||
+ (void)setShouldUseCompactResponses:(BOOL)value;
|
||||
+ (BOOL)shouldUseCompactResponses;
|
||||
|
||||
/*! If set to YES (which is the default), the app will be terminated at the end of the session, if a bundleId was specified */
|
||||
+ (void)setShouldTerminateApp:(BOOL)value;
|
||||
+ (BOOL)shouldTerminateApp;
|
||||
|
||||
/*! If shouldUseCompactResponses == NO, is the comma-separated list of fields to return with each element. Defaults to "type,label". */
|
||||
+ (void)setElementResponseAttributes:(NSString *)value;
|
||||
+ (NSString *)elementResponseAttributes;
|
||||
|
||||
/*! Disables remote query evaluation making Xcode 9.x tests behave same as Xcode 8.x test */
|
||||
+ (void)disableRemoteQueryEvaluation;
|
||||
|
||||
/*! Enables the extended XCTest debug logging. Useful for developemnt purposes */
|
||||
+ (void)enableXcTestDebugLogs;
|
||||
|
||||
/*! Disables attribute key path analysis, which will cause XCTest on Xcode 9.x to ignore some elements */
|
||||
+ (void)disableAttributeKeyPathAnalysis;
|
||||
|
||||
/*! Disables XCTest automated screenshots taking */
|
||||
+ (void)disableScreenshots;
|
||||
/*! Enables XCTest automated screenshots taking */
|
||||
+ (void)enableScreenshots;
|
||||
|
||||
/*! Disables XCTest automated videos taking (iOS 17+) */
|
||||
+ (void)disableScreenRecordings;
|
||||
/*! Enables XCTest automated videos taking (iOS 17+) */
|
||||
+ (void)enableScreenRecordings;
|
||||
|
||||
/* The maximum typing frequency for all typing activities */
|
||||
+ (void)setMaxTypingFrequency:(NSUInteger)value;
|
||||
+ (NSUInteger)maxTypingFrequency;
|
||||
+ (NSUInteger)defaultTypingFrequency;
|
||||
|
||||
/* Use singleton test manager proxy */
|
||||
+ (void)setShouldUseSingletonTestManager:(BOOL)value;
|
||||
+ (BOOL)shouldUseSingletonTestManager;
|
||||
|
||||
/* Enforces WDA to verify the presense of system alerts while checking for an active app */
|
||||
+ (void)setShouldRespectSystemAlerts:(BOOL)value;
|
||||
+ (BOOL)shouldRespectSystemAlerts;
|
||||
|
||||
/**
|
||||
* Extract switch value from arguments
|
||||
*
|
||||
* @param arguments Array of strings with the command-line arguments, e.g. @[@"--port", @"12345"].
|
||||
* @param key Switch to look up value for, e.g. @"--port".
|
||||
*
|
||||
* @return Switch value or nil if the switch is not present in arguments.
|
||||
*/
|
||||
+ (NSString* _Nullable)valueFromArguments: (NSArray<NSString *> *)arguments forKey: (NSString*)key;
|
||||
|
||||
/**
|
||||
The quality of the screenshots generated by the screenshots broadcaster, expressed
|
||||
as a value from 0 to 100. The value 0 represents the maximum compression
|
||||
(or lowest quality) while the value 100 represents the least compression (or best
|
||||
quality). The default value is 25.
|
||||
*/
|
||||
+ (NSUInteger)mjpegServerScreenshotQuality;
|
||||
+ (void)setMjpegServerScreenshotQuality:(NSUInteger)quality;
|
||||
|
||||
/**
|
||||
Whether to apply orientation fixes to the streamed JPEG images.
|
||||
This is an expensive operation and it is disabled by default, so screenshots
|
||||
are returned in portrait, but their actual orientation value could still be found in the EXIF
|
||||
metadata.
|
||||
! Enablement of this setting may lead to WDA process termination because of an excessive CPU usage.
|
||||
*/
|
||||
+ (BOOL)mjpegShouldFixOrientation;
|
||||
+ (void)setMjpegShouldFixOrientation:(BOOL)enabled;
|
||||
|
||||
/**
|
||||
The framerate at which the background screenshots broadcaster should broadcast
|
||||
screenshots in range 1..60. The default value is 10 (Frames Per Second).
|
||||
Setting zero value will cause the framerate to be at its maximum possible value.
|
||||
*/
|
||||
+ (NSUInteger)mjpegServerFramerate;
|
||||
+ (void)setMjpegServerFramerate:(NSUInteger)framerate;
|
||||
|
||||
/**
|
||||
Whether to limit the XPath scope to descendant items only while performing a lookup
|
||||
in an element context. Enabled by default. Being disabled, allows to use XPath locators
|
||||
like ".." in order to match parent items of the current context root.
|
||||
*/
|
||||
+ (BOOL)limitXpathContextScope;
|
||||
+ (void)setLimitXpathContextScope:(BOOL)enabled;
|
||||
|
||||
/**
|
||||
The quality of display screenshots. The higher quality you set is the bigger screenshot size is.
|
||||
The highest quality value is 0 (lossless PNG) or 3 (lossless HEIC). The lowest quality is 2 (highly compressed JPEG).
|
||||
The default quality value is 3 (lossless HEIC).
|
||||
See https://developer.apple.com/documentation/xctest/xctimagequality?language=objc
|
||||
*/
|
||||
+ (NSUInteger)screenshotQuality;
|
||||
+ (void)setScreenshotQuality:(NSUInteger)quality;
|
||||
|
||||
/**
|
||||
The range of ports that the HTTP Server should attempt to bind on launch
|
||||
*/
|
||||
+ (NSRange)bindingPortRange;
|
||||
|
||||
/**
|
||||
The port number where the background screenshots broadcaster is supposed to run
|
||||
*/
|
||||
+ (NSInteger)mjpegServerPort;
|
||||
|
||||
/**
|
||||
The scaling factor for frames of the mjpeg stream. The default (and maximum) value is 100,
|
||||
which does not perform any scaling. The minimum value must be greater than zero.
|
||||
! Setting this to a value less than 100, especially together with orientation fixing enabled
|
||||
! may lead to WDA process termination because of an excessive CPU usage.
|
||||
*/
|
||||
+ (CGFloat)mjpegScalingFactor;
|
||||
+ (void)setMjpegScalingFactor:(CGFloat)scalingFactor;
|
||||
|
||||
/**
|
||||
YES if verbose logging is enabled. NO otherwise.
|
||||
*/
|
||||
+ (BOOL)verboseLoggingEnabled;
|
||||
|
||||
/**
|
||||
Disables automatic handling of XCTest UI interruptions.
|
||||
*/
|
||||
+ (void)disableApplicationUIInterruptionsHandling;
|
||||
|
||||
/**
|
||||
* Configure keyboards preference to make test running stable
|
||||
*/
|
||||
+ (void)configureDefaultKeyboardPreferences;
|
||||
|
||||
|
||||
/**
|
||||
* Turn on softwar keyboard forcefully for simulator.
|
||||
*/
|
||||
+ (void)forceSimulatorSoftwareKeyboardPresence;
|
||||
|
||||
/**
|
||||
Defines keyboard preference enabled status
|
||||
*/
|
||||
typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) {
|
||||
FBConfigurationKeyboardPreferenceDisabled = 0,
|
||||
FBConfigurationKeyboardPreferenceEnabled = 1,
|
||||
FBConfigurationKeyboardPreferenceNotSupported = 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify keyboard configuration of 'auto-correction'.
|
||||
*
|
||||
* @param isEnabled Turn the configuration on if the value is YES
|
||||
*/
|
||||
+ (void)setKeyboardAutocorrection:(BOOL)isEnabled;
|
||||
+ (FBConfigurationKeyboardPreference)keyboardAutocorrection;
|
||||
|
||||
/**
|
||||
* Modify keyboard configuration of 'predictive'
|
||||
*
|
||||
* @param isEnabled Turn the configuration on if the value is YES
|
||||
*/
|
||||
+ (void)setKeyboardPrediction:(BOOL)isEnabled;
|
||||
+ (FBConfigurationKeyboardPreference)keyboardPrediction;
|
||||
|
||||
/**
|
||||
Sets maximum depth for traversing elements tree from parents to children while requesting XCElementSnapshot.
|
||||
Used to set maxDepth value in a dictionary provided by XCAXClient_iOS's method defaultParams.
|
||||
The original XCAXClient_iOS maxDepth value is set to INT_MAX, which is too big for some queries
|
||||
(for example: searching elements inside a WebView).
|
||||
Reasonable values are from 15 to 100 (larger numbers make queries slower).
|
||||
|
||||
@param maxDepth The number of maximum depth for traversing elements tree
|
||||
*/
|
||||
+ (void)setSnapshotMaxDepth:(int)maxDepth;
|
||||
|
||||
/**
|
||||
@return The number of maximum depth for traversing elements tree
|
||||
*/
|
||||
+ (int)snapshotMaxDepth;
|
||||
|
||||
/**
|
||||
* Whether to use fast search result matching while searching for elements.
|
||||
* By default this is disabled due to https://github.com/appium/appium/issues/10101
|
||||
* but it still makes sense to enable it for views containing large counts of elements
|
||||
*
|
||||
* @param enabled Either YES or NO
|
||||
*/
|
||||
+ (void)setUseFirstMatch:(BOOL)enabled;
|
||||
+ (BOOL)useFirstMatch;
|
||||
|
||||
/**
|
||||
* Whether to bound the lookup results by index.
|
||||
* By default this is disabled and bounding by accessibility is used.
|
||||
* Read https://stackoverflow.com/questions/49307513/meaning-of-allelementsboundbyaccessibilityelement
|
||||
* for more details on these two bounding methods.
|
||||
*
|
||||
* @param enabled Either YES or NO
|
||||
*/
|
||||
+ (void)setBoundElementsByIndex:(BOOL)enabled;
|
||||
+ (BOOL)boundElementsByIndex;
|
||||
|
||||
/**
|
||||
* Modify reduce motion configuration in accessibility.
|
||||
* It works only for Simulator since Real device has security model which allows chnaging preferences
|
||||
* only from settings app.
|
||||
*
|
||||
* @param isEnabled Turn the configuration on if the value is YES
|
||||
*/
|
||||
+ (void)setReduceMotionEnabled:(BOOL)isEnabled;
|
||||
+ (BOOL)reduceMotionEnabled;
|
||||
|
||||
/**
|
||||
* Set the idling timeout. If the timeout expires then WDA
|
||||
* tries to interact with the application even if it is not idling.
|
||||
* Setting it to zero disables idling checks.
|
||||
* The default timeout is set to 10 seconds.
|
||||
*
|
||||
* @param timeout The actual timeout value in float seconds
|
||||
*/
|
||||
+ (void)setWaitForIdleTimeout:(NSTimeInterval)timeout;
|
||||
+ (NSTimeInterval)waitForIdleTimeout;
|
||||
|
||||
/**
|
||||
* Set the idling timeout for different actions, for example events synthesis, rotation change,
|
||||
* etc. If the timeout expires then WDA tries to interact with the application even if it is not idling.
|
||||
* Setting it to zero disables idling checks.
|
||||
* The default timeout is set to 2 seconds.
|
||||
*
|
||||
* @param timeout The actual timeout value in float seconds
|
||||
*/
|
||||
+ (void)setAnimationCoolOffTimeout:(NSTimeInterval)timeout;
|
||||
+ (NSTimeInterval)animationCoolOffTimeout;
|
||||
|
||||
/**
|
||||
Enforces the page hierarchy to include non modal elements,
|
||||
like Contacts. By default such elements are not present there.
|
||||
See https://github.com/appium/appium/issues/13227
|
||||
|
||||
@param isEnabled Set to YES in order to enable non modal elements inclusion.
|
||||
Setting this value to YES will have no effect if the current iOS SDK does not support such feature.
|
||||
*/
|
||||
+ (void)setIncludeNonModalElements:(BOOL)isEnabled;
|
||||
+ (BOOL)includeNonModalElements;
|
||||
|
||||
/**
|
||||
Sets custom class chain locators for accept/dismiss alert buttons location.
|
||||
This might be useful if the default buttons detection algorithm fails to determine alert buttons properly
|
||||
when defaultAlertAction is set.
|
||||
|
||||
@param classChainSelector Valid class chain locator, which determines accept/reject button
|
||||
on the alert. The search root is the alert element itself.
|
||||
Setting this value to nil or an empty string (the default
|
||||
value) will enforce WDA to apply the default algorithm for alert buttons location.
|
||||
If an invalid/non-parseable locator is set then the lookup will fallback to the default algorithm and print a
|
||||
warning into the log.
|
||||
Example: ** /XCUIElementTypeButton[`label CONTAINS[c] 'accept'`]
|
||||
*/
|
||||
+ (void)setAcceptAlertButtonSelector:(NSString *)classChainSelector;
|
||||
+ (NSString *)acceptAlertButtonSelector;
|
||||
+ (void)setDismissAlertButtonSelector:(NSString *)classChainSelector;
|
||||
+ (NSString *)dismissAlertButtonSelector;
|
||||
|
||||
/**
|
||||
Sets class chain selector to apply for an automated alert click
|
||||
*/
|
||||
+ (void)setAutoClickAlertSelector:(NSString *)classChainSelector;
|
||||
+ (NSString *)autoClickAlertSelector;
|
||||
|
||||
/**
|
||||
* Whether to use HIDEvent for text clear.
|
||||
* By default this is enabled and HIDEvent is used for text clear.
|
||||
*
|
||||
* @param enabled Either YES or NO
|
||||
*/
|
||||
+ (void)setUseClearTextShortcut:(BOOL)enabled;
|
||||
+ (BOOL)useClearTextShortcut;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
/**
|
||||
Set the screenshot orientation for iOS
|
||||
|
||||
It helps to fix the screenshot orientation when the device under test's orientation changes.
|
||||
For example, when a device changes to the landscape, the screenshot orientation could be wrong.
|
||||
Then, this setting can force change the screenshot orientation.
|
||||
Xcode versions, OS versions or device models and simulator or real device could influence it.
|
||||
|
||||
@param orientation Set the orientation to adjust the screenshot.
|
||||
Case insensitive "portrait", "portraitUpsideDown", "landscapeRight" and "landscapeLeft" are available
|
||||
to force the coodinate adjust. Other words are handled as "auto", which handles
|
||||
the adjustment automatically. Defaults to "auto".
|
||||
@param error If no availale orientation strategy was given, it returns an NSError object that describes the problem.
|
||||
*/
|
||||
+ (BOOL)setScreenshotOrientation:(NSString *)orientation error:(NSError **)error;
|
||||
|
||||
/**
|
||||
@return The value of UIInterfaceOrientation
|
||||
*/
|
||||
+ (NSInteger)screenshotOrientation;
|
||||
|
||||
/**
|
||||
@return The orientation as String for human read
|
||||
*/
|
||||
+ (NSString *)humanReadableScreenshotOrientation;
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
Resets all session-specific settings to their default values
|
||||
*/
|
||||
+ (void)resetSessionSettings;
|
||||
|
||||
/**
|
||||
* Whether to calculate `hittable` attribute using native APIs
|
||||
* instead of legacy heuristics.
|
||||
* This flag improves accuracy, but may affect performance.
|
||||
* Disabled by default.
|
||||
*
|
||||
* @param enabled Either YES or NO
|
||||
*/
|
||||
+ (void)setIncludeHittableInPageSource:(BOOL)enabled;
|
||||
+ (BOOL)includeHittableInPageSource;
|
||||
|
||||
/**
|
||||
* Whether to include `nativeFrame` attribute in the XML page source.
|
||||
*
|
||||
* When enabled, the XML representation will contain the precise rendered
|
||||
* frame of the UI element.
|
||||
*
|
||||
* This value is more accurate than the legacy `wdFrame`, which applies rounding
|
||||
* and may introduce inconsistencies in size and position calculations.
|
||||
*
|
||||
* The value is disabled by default to avoid potential performance overhead.
|
||||
*
|
||||
* @param enabled Either YES or NO
|
||||
*/
|
||||
+ (void)setIncludeNativeFrameInPageSource:(BOOL)enabled;
|
||||
+ (BOOL)includeNativeFrameInPageSource;
|
||||
|
||||
/**
|
||||
* Whether to include `minValue`/`maxValue` attributes in the page source.
|
||||
* These attributes are retrieved from native element snapshots and represent
|
||||
* value boundaries for elements like sliders or progress indicators.
|
||||
* This may affect performance if used on many elements.
|
||||
* Disabled by default.
|
||||
*
|
||||
* @param enabled Either YES or NO
|
||||
*/
|
||||
+ (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled;
|
||||
+ (BOOL)includeMinMaxValueInPageSource;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
677
WebDriverAgentLib/Utilities/FBConfiguration.m
Normal file
677
WebDriverAgentLib/Utilities/FBConfiguration.m
Normal file
@@ -0,0 +1,677 @@
|
||||
/**
|
||||
* 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 "FBConfiguration.h"
|
||||
|
||||
#import "AXSettings.h"
|
||||
#import "UIKeyboardImpl.h"
|
||||
#import "TIPreferencesController.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#include "TargetConditionals.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCAXClient_iOS+FBSnapshotReqParams.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
#import "XCTestConfiguration.h"
|
||||
#import "XCUIApplication+FBUIInterruptions.h"
|
||||
|
||||
static NSUInteger const DefaultStartingPort = 8567;
|
||||
static NSUInteger const DefaultMjpegServerPort = 9567;
|
||||
static NSUInteger const DefaultPortRange = 100;
|
||||
|
||||
static char const *const controllerPrefBundlePath = "/System/Library/PrivateFrameworks/TextInput.framework/TextInput";
|
||||
static NSString *const controllerClassName = @"TIPreferencesController";
|
||||
static NSString *const FBKeyboardAutocorrectionKey = @"KeyboardAutocorrection";
|
||||
static NSString *const FBKeyboardPredictionKey = @"KeyboardPrediction";
|
||||
static NSString *const axSettingsClassName = @"AXSettings";
|
||||
|
||||
static BOOL FBShouldUseTestManagerForVisibilityDetection = NO;
|
||||
static BOOL FBShouldUseSingletonTestManager = YES;
|
||||
static BOOL FBShouldRespectSystemAlerts = NO;
|
||||
|
||||
static CGFloat FBMjpegScalingFactor = 100.0;
|
||||
static BOOL FBMjpegShouldFixOrientation = NO;
|
||||
static NSUInteger FBMjpegServerScreenshotQuality = 25;
|
||||
static NSUInteger FBMjpegServerFramerate = 10;
|
||||
|
||||
// Session-specific settings
|
||||
static BOOL FBShouldTerminateApp;
|
||||
static NSNumber* FBMaxTypingFrequency;
|
||||
static NSUInteger FBScreenshotQuality;
|
||||
static BOOL FBShouldUseFirstMatch;
|
||||
static BOOL FBShouldBoundElementsByIndex;
|
||||
static BOOL FBIncludeNonModalElements;
|
||||
static NSString *FBAcceptAlertButtonSelector;
|
||||
static NSString *FBDismissAlertButtonSelector;
|
||||
static NSString *FBAutoClickAlertSelector;
|
||||
static NSTimeInterval FBWaitForIdleTimeout;
|
||||
static NSTimeInterval FBAnimationCoolOffTimeout;
|
||||
static BOOL FBShouldUseCompactResponses;
|
||||
static NSString *FBElementResponseAttributes;
|
||||
static BOOL FBUseClearTextShortcut;
|
||||
static BOOL FBLimitXpathContextScope = YES;
|
||||
#if !TARGET_OS_TV
|
||||
static UIInterfaceOrientation FBScreenshotOrientation;
|
||||
#endif
|
||||
static BOOL FBShouldIncludeHittableInPageSource = NO;
|
||||
static BOOL FBShouldIncludeNativeFrameInPageSource = NO;
|
||||
static BOOL FBShouldIncludeMinMaxValueInPageSource = NO;
|
||||
|
||||
@implementation FBConfiguration
|
||||
|
||||
+ (NSUInteger)defaultTypingFrequency
|
||||
{
|
||||
NSInteger defaultFreq = [[NSUserDefaults standardUserDefaults]
|
||||
integerForKey:@"com.apple.xctest.iOSMaximumTypingFrequency"];
|
||||
return defaultFreq > 0 ? defaultFreq : 60;
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
[FBConfiguration resetSessionSettings];
|
||||
}
|
||||
|
||||
#pragma mark Public
|
||||
|
||||
+ (void)disableRemoteQueryEvaluation
|
||||
{
|
||||
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"XCTDisableRemoteQueryEvaluation"];
|
||||
}
|
||||
|
||||
+ (void)disableApplicationUIInterruptionsHandling
|
||||
{
|
||||
[XCUIApplication fb_disableUIInterruptionsHandling];
|
||||
}
|
||||
|
||||
+ (void)enableXcTestDebugLogs
|
||||
{
|
||||
((XCTestConfiguration *)XCTestConfiguration.activeTestConfiguration).emitOSLogs = YES;
|
||||
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"XCTEmitOSLogs"];
|
||||
}
|
||||
|
||||
+ (void)disableAttributeKeyPathAnalysis
|
||||
{
|
||||
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"XCTDisableAttributeKeyPathAnalysis"];
|
||||
}
|
||||
|
||||
+ (void)disableScreenshots
|
||||
{
|
||||
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"DisableScreenshots"];
|
||||
}
|
||||
|
||||
+ (void)enableScreenshots
|
||||
{
|
||||
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"DisableScreenshots"];
|
||||
}
|
||||
|
||||
+ (void)disableScreenRecordings
|
||||
{
|
||||
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"DisableDiagnosticScreenRecordings"];
|
||||
}
|
||||
|
||||
+ (void)enableScreenRecordings
|
||||
{
|
||||
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"DisableDiagnosticScreenRecordings"];
|
||||
}
|
||||
|
||||
+ (NSRange)bindingPortRange
|
||||
{
|
||||
// 'WebDriverAgent --port 8080' can be passed via the arguments to the process
|
||||
if (self.bindingPortRangeFromArguments.location != NSNotFound) {
|
||||
return self.bindingPortRangeFromArguments;
|
||||
}
|
||||
|
||||
// Existence of USE_PORT in the environment implies the port range is managed by the launching process.
|
||||
if (NSProcessInfo.processInfo.environment[@"USE_PORT"] &&
|
||||
[NSProcessInfo.processInfo.environment[@"USE_PORT"] length] > 0) {
|
||||
return NSMakeRange([NSProcessInfo.processInfo.environment[@"USE_PORT"] integerValue] , 1);
|
||||
}
|
||||
|
||||
return NSMakeRange(DefaultStartingPort, DefaultPortRange);
|
||||
}
|
||||
|
||||
+ (NSInteger)mjpegServerPort
|
||||
{
|
||||
if (self.mjpegServerPortFromArguments != NSNotFound) {
|
||||
return self.mjpegServerPortFromArguments;
|
||||
}
|
||||
|
||||
if (NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] &&
|
||||
[NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] length] > 0) {
|
||||
return [NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] integerValue];
|
||||
}
|
||||
|
||||
return DefaultMjpegServerPort;
|
||||
}
|
||||
|
||||
+ (CGFloat)mjpegScalingFactor
|
||||
{
|
||||
return FBMjpegScalingFactor;
|
||||
}
|
||||
|
||||
+ (void)setMjpegScalingFactor:(CGFloat)scalingFactor {
|
||||
FBMjpegScalingFactor = scalingFactor;
|
||||
}
|
||||
|
||||
+ (BOOL)mjpegShouldFixOrientation
|
||||
{
|
||||
return FBMjpegShouldFixOrientation;
|
||||
}
|
||||
|
||||
+ (void)setMjpegShouldFixOrientation:(BOOL)enabled {
|
||||
FBMjpegShouldFixOrientation = enabled;
|
||||
}
|
||||
|
||||
+ (BOOL)verboseLoggingEnabled
|
||||
{
|
||||
return [NSProcessInfo.processInfo.environment[@"VERBOSE_LOGGING"] boolValue];
|
||||
}
|
||||
|
||||
+ (void)setShouldUseTestManagerForVisibilityDetection:(BOOL)value
|
||||
{
|
||||
FBShouldUseTestManagerForVisibilityDetection = value;
|
||||
}
|
||||
|
||||
+ (BOOL)shouldUseTestManagerForVisibilityDetection
|
||||
{
|
||||
return FBShouldUseTestManagerForVisibilityDetection;
|
||||
}
|
||||
|
||||
+ (void)setShouldUseCompactResponses:(BOOL)value
|
||||
{
|
||||
FBShouldUseCompactResponses = value;
|
||||
}
|
||||
|
||||
+ (BOOL)shouldUseCompactResponses
|
||||
{
|
||||
return FBShouldUseCompactResponses;
|
||||
}
|
||||
|
||||
+ (void)setShouldTerminateApp:(BOOL)value
|
||||
{
|
||||
FBShouldTerminateApp = value;
|
||||
}
|
||||
|
||||
+ (BOOL)shouldTerminateApp
|
||||
{
|
||||
return FBShouldTerminateApp;
|
||||
}
|
||||
|
||||
+ (void)setElementResponseAttributes:(NSString *)value
|
||||
{
|
||||
FBElementResponseAttributes = value;
|
||||
}
|
||||
|
||||
+ (NSString *)elementResponseAttributes
|
||||
{
|
||||
return FBElementResponseAttributes;
|
||||
}
|
||||
|
||||
+ (void)setMaxTypingFrequency:(NSUInteger)value
|
||||
{
|
||||
FBMaxTypingFrequency = @(value);
|
||||
}
|
||||
|
||||
+ (NSUInteger)maxTypingFrequency
|
||||
{
|
||||
if (nil == FBMaxTypingFrequency) {
|
||||
return [self defaultTypingFrequency];
|
||||
}
|
||||
return FBMaxTypingFrequency.integerValue <= 0
|
||||
? [self defaultTypingFrequency]
|
||||
: FBMaxTypingFrequency.integerValue;
|
||||
}
|
||||
|
||||
+ (void)setShouldUseSingletonTestManager:(BOOL)value
|
||||
{
|
||||
FBShouldUseSingletonTestManager = value;
|
||||
}
|
||||
|
||||
+ (BOOL)shouldUseSingletonTestManager
|
||||
{
|
||||
return FBShouldUseSingletonTestManager;
|
||||
}
|
||||
|
||||
+ (NSUInteger)mjpegServerFramerate
|
||||
{
|
||||
return FBMjpegServerFramerate;
|
||||
}
|
||||
|
||||
+ (void)setMjpegServerFramerate:(NSUInteger)framerate
|
||||
{
|
||||
FBMjpegServerFramerate = framerate;
|
||||
}
|
||||
|
||||
+ (NSUInteger)mjpegServerScreenshotQuality
|
||||
{
|
||||
return FBMjpegServerScreenshotQuality;
|
||||
}
|
||||
|
||||
+ (void)setMjpegServerScreenshotQuality:(NSUInteger)quality
|
||||
{
|
||||
FBMjpegServerScreenshotQuality = quality;
|
||||
}
|
||||
|
||||
+ (NSUInteger)screenshotQuality
|
||||
{
|
||||
return FBScreenshotQuality;
|
||||
}
|
||||
|
||||
+ (void)setScreenshotQuality:(NSUInteger)quality
|
||||
{
|
||||
FBScreenshotQuality = quality;
|
||||
}
|
||||
|
||||
+ (NSTimeInterval)waitForIdleTimeout
|
||||
{
|
||||
return FBWaitForIdleTimeout;
|
||||
}
|
||||
|
||||
+ (void)setWaitForIdleTimeout:(NSTimeInterval)timeout
|
||||
{
|
||||
FBWaitForIdleTimeout = timeout;
|
||||
}
|
||||
|
||||
+ (NSTimeInterval)animationCoolOffTimeout
|
||||
{
|
||||
return FBAnimationCoolOffTimeout;
|
||||
}
|
||||
|
||||
+ (void)setAnimationCoolOffTimeout:(NSTimeInterval)timeout
|
||||
{
|
||||
FBAnimationCoolOffTimeout = timeout;
|
||||
}
|
||||
|
||||
// Works for Simulator and Real devices
|
||||
+ (void)configureDefaultKeyboardPreferences
|
||||
{
|
||||
void *handle = dlopen(controllerPrefBundlePath, RTLD_LAZY);
|
||||
|
||||
Class controllerClass = NSClassFromString(controllerClassName);
|
||||
|
||||
TIPreferencesController *controller = [controllerClass sharedPreferencesController];
|
||||
// Auto-Correction in Keyboards
|
||||
// 'setAutocorrectionEnabled' Was in TextInput.framework/TIKeyboardState.h over iOS 10.3
|
||||
if ([controller respondsToSelector:@selector(setAutocorrectionEnabled:)]) {
|
||||
// Under iOS 10.2
|
||||
controller.autocorrectionEnabled = NO;
|
||||
} else if ([controller respondsToSelector:@selector(setValue:forPreferenceKey:)]) {
|
||||
// Over iOS 10.3
|
||||
[controller setValue:@NO forPreferenceKey:FBKeyboardAutocorrectionKey];
|
||||
}
|
||||
|
||||
// Predictive in Keyboards
|
||||
if ([controller respondsToSelector:@selector(setPredictionEnabled:)]) {
|
||||
controller.predictionEnabled = NO;
|
||||
} else if ([controller respondsToSelector:@selector(setValue:forPreferenceKey:)]) {
|
||||
[controller setValue:@NO forPreferenceKey:FBKeyboardPredictionKey];
|
||||
}
|
||||
|
||||
// To dismiss keyboard tutorial on iOS 11+ (iPad)
|
||||
if ([controller respondsToSelector:@selector(setValue:forPreferenceKey:)]) {
|
||||
[controller setValue:@YES forPreferenceKey:@"DidShowGestureKeyboardIntroduction"];
|
||||
if (isSDKVersionGreaterThanOrEqualTo(@"13.0")) {
|
||||
[controller setValue:@YES forPreferenceKey:@"DidShowContinuousPathIntroduction"];
|
||||
}
|
||||
[controller synchronizePreferences];
|
||||
}
|
||||
|
||||
dlclose(handle);
|
||||
}
|
||||
|
||||
+ (void)forceSimulatorSoftwareKeyboardPresence
|
||||
{
|
||||
#if TARGET_OS_SIMULATOR
|
||||
// Force toggle software keyboard on.
|
||||
// This can avoid 'Keyboard is not present' error which can happen
|
||||
// when send_keys are called by client
|
||||
[[UIKeyboardImpl sharedInstance] setAutomaticMinimizationEnabled:NO];
|
||||
|
||||
if ([(NSObject *)[UIKeyboardImpl sharedInstance]
|
||||
respondsToSelector:@selector(setSoftwareKeyboardShownByTouch:)]) {
|
||||
// Xcode 13 no longer has this method
|
||||
[[UIKeyboardImpl sharedInstance] setSoftwareKeyboardShownByTouch:YES];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (FBConfigurationKeyboardPreference)keyboardAutocorrection
|
||||
{
|
||||
return [self keyboardsPreference:FBKeyboardAutocorrectionKey];
|
||||
}
|
||||
|
||||
+ (void)setKeyboardAutocorrection:(BOOL)isEnabled
|
||||
{
|
||||
[self configureKeyboardsPreference:isEnabled forPreferenceKey:FBKeyboardAutocorrectionKey];
|
||||
}
|
||||
|
||||
+ (FBConfigurationKeyboardPreference)keyboardPrediction
|
||||
{
|
||||
return [self keyboardsPreference:FBKeyboardPredictionKey];
|
||||
}
|
||||
|
||||
+ (void)setKeyboardPrediction:(BOOL)isEnabled
|
||||
{
|
||||
[self configureKeyboardsPreference:isEnabled forPreferenceKey:FBKeyboardPredictionKey];
|
||||
}
|
||||
|
||||
+ (void)setSnapshotMaxDepth:(int)maxDepth
|
||||
{
|
||||
FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @(maxDepth));
|
||||
}
|
||||
|
||||
+ (int)snapshotMaxDepth
|
||||
{
|
||||
return [FBGetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey) intValue];
|
||||
}
|
||||
|
||||
+ (void)setShouldRespectSystemAlerts:(BOOL)value
|
||||
{
|
||||
FBShouldRespectSystemAlerts = value;
|
||||
}
|
||||
|
||||
+ (BOOL)shouldRespectSystemAlerts
|
||||
{
|
||||
return FBShouldRespectSystemAlerts;
|
||||
}
|
||||
|
||||
+ (void)setUseFirstMatch:(BOOL)enabled
|
||||
{
|
||||
FBShouldUseFirstMatch = enabled;
|
||||
}
|
||||
|
||||
+ (BOOL)useFirstMatch
|
||||
{
|
||||
return FBShouldUseFirstMatch;
|
||||
}
|
||||
|
||||
+ (void)setBoundElementsByIndex:(BOOL)enabled
|
||||
{
|
||||
FBShouldBoundElementsByIndex = enabled;
|
||||
}
|
||||
|
||||
+ (BOOL)boundElementsByIndex
|
||||
{
|
||||
return FBShouldBoundElementsByIndex;
|
||||
}
|
||||
|
||||
+ (void)setIncludeNonModalElements:(BOOL)isEnabled
|
||||
{
|
||||
FBIncludeNonModalElements = isEnabled;
|
||||
}
|
||||
|
||||
+ (BOOL)includeNonModalElements
|
||||
{
|
||||
return FBIncludeNonModalElements;
|
||||
}
|
||||
|
||||
+ (void)setAcceptAlertButtonSelector:(NSString *)classChainSelector
|
||||
{
|
||||
FBAcceptAlertButtonSelector = classChainSelector;
|
||||
}
|
||||
|
||||
+ (NSString *)acceptAlertButtonSelector
|
||||
{
|
||||
return FBAcceptAlertButtonSelector;
|
||||
}
|
||||
|
||||
+ (void)setDismissAlertButtonSelector:(NSString *)classChainSelector
|
||||
{
|
||||
FBDismissAlertButtonSelector = classChainSelector;
|
||||
}
|
||||
|
||||
+ (NSString *)dismissAlertButtonSelector
|
||||
{
|
||||
return FBDismissAlertButtonSelector;
|
||||
}
|
||||
|
||||
+ (void)setAutoClickAlertSelector:(NSString *)classChainSelector
|
||||
{
|
||||
FBAutoClickAlertSelector = classChainSelector;
|
||||
}
|
||||
|
||||
+ (NSString *)autoClickAlertSelector
|
||||
{
|
||||
return FBAutoClickAlertSelector;
|
||||
}
|
||||
|
||||
+ (void)setUseClearTextShortcut:(BOOL)enabled
|
||||
{
|
||||
FBUseClearTextShortcut = enabled;
|
||||
}
|
||||
|
||||
+ (BOOL)useClearTextShortcut
|
||||
{
|
||||
return FBUseClearTextShortcut;
|
||||
}
|
||||
|
||||
+ (BOOL)limitXpathContextScope
|
||||
{
|
||||
return FBLimitXpathContextScope;
|
||||
}
|
||||
|
||||
+ (void)setLimitXpathContextScope:(BOOL)enabled
|
||||
{
|
||||
FBLimitXpathContextScope = enabled;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
+ (BOOL)setScreenshotOrientation:(NSString *)orientation error:(NSError **)error
|
||||
{
|
||||
// Only UIInterfaceOrientationUnknown is over iOS 8. Others are over iOS 2.
|
||||
// https://developer.apple.com/documentation/uikit/uiinterfaceorientation/uiinterfaceorientationunknown
|
||||
if ([orientation.lowercaseString isEqualToString:@"portrait"]) {
|
||||
FBScreenshotOrientation = UIInterfaceOrientationPortrait;
|
||||
} else if ([orientation.lowercaseString isEqualToString:@"portraitupsidedown"]) {
|
||||
FBScreenshotOrientation = UIInterfaceOrientationPortraitUpsideDown;
|
||||
} else if ([orientation.lowercaseString isEqualToString:@"landscaperight"]) {
|
||||
FBScreenshotOrientation = UIInterfaceOrientationLandscapeRight;
|
||||
} else if ([orientation.lowercaseString isEqualToString:@"landscapeleft"]) {
|
||||
FBScreenshotOrientation = UIInterfaceOrientationLandscapeLeft;
|
||||
} else if ([orientation.lowercaseString isEqualToString:@"auto"]) {
|
||||
FBScreenshotOrientation = UIInterfaceOrientationUnknown;
|
||||
} else {
|
||||
return [[FBErrorBuilder.builder withDescriptionFormat:
|
||||
@"The orientation value '%@' is not known. Only the following orientation values are supported: " \
|
||||
"'auto', 'portrait', 'portraitUpsideDown', 'landscapeRight' and 'landscapeLeft'", orientation]
|
||||
buildError:error];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (NSInteger)screenshotOrientation
|
||||
{
|
||||
return FBScreenshotOrientation;
|
||||
}
|
||||
|
||||
+ (NSString *)humanReadableScreenshotOrientation
|
||||
{
|
||||
switch (FBScreenshotOrientation) {
|
||||
case UIInterfaceOrientationPortrait:
|
||||
return @"portrait";
|
||||
case UIInterfaceOrientationPortraitUpsideDown:
|
||||
return @"portraitUpsideDown";
|
||||
case UIInterfaceOrientationLandscapeRight:
|
||||
return @"landscapeRight";
|
||||
case UIInterfaceOrientationLandscapeLeft:
|
||||
return @"landscapeLeft";
|
||||
case UIInterfaceOrientationUnknown:
|
||||
return @"auto";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
+ (void)resetSessionSettings
|
||||
{
|
||||
FBShouldTerminateApp = YES;
|
||||
FBShouldUseCompactResponses = YES;
|
||||
FBElementResponseAttributes = @"type,label";
|
||||
FBMaxTypingFrequency = @([self defaultTypingFrequency]);
|
||||
FBScreenshotQuality = 3;
|
||||
FBShouldUseFirstMatch = NO;
|
||||
FBShouldBoundElementsByIndex = NO;
|
||||
// This is diabled by default because enabling it prevents the accessbility snapshot to be taken
|
||||
// (it always errors with kxIllegalArgument error)
|
||||
FBIncludeNonModalElements = NO;
|
||||
FBAcceptAlertButtonSelector = @"";
|
||||
FBDismissAlertButtonSelector = @"";
|
||||
FBAutoClickAlertSelector = @"";
|
||||
FBWaitForIdleTimeout = 10.;
|
||||
FBAnimationCoolOffTimeout = 2.;
|
||||
// 50 should be enough for the majority of the cases. The performance is acceptable for values up to 100.
|
||||
FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @50);
|
||||
FBUseClearTextShortcut = YES;
|
||||
FBLimitXpathContextScope = YES;
|
||||
#if !TARGET_OS_TV
|
||||
FBScreenshotOrientation = UIInterfaceOrientationUnknown;
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
+ (FBConfigurationKeyboardPreference)keyboardsPreference:(nonnull NSString *)key
|
||||
{
|
||||
Class controllerClass = NSClassFromString(controllerClassName);
|
||||
TIPreferencesController *controller = [controllerClass sharedPreferencesController];
|
||||
if ([key isEqualToString:FBKeyboardAutocorrectionKey]) {
|
||||
if ([controller respondsToSelector:@selector(boolForPreferenceKey:)]) {
|
||||
return [controller boolForPreferenceKey:FBKeyboardAutocorrectionKey]
|
||||
? FBConfigurationKeyboardPreferenceEnabled
|
||||
: FBConfigurationKeyboardPreferenceDisabled;
|
||||
} else {
|
||||
[FBLogger log:@"Updating keyboard autocorrection preference is not supported"];
|
||||
return FBConfigurationKeyboardPreferenceNotSupported;
|
||||
}
|
||||
} else if ([key isEqualToString:FBKeyboardPredictionKey]) {
|
||||
if ([controller respondsToSelector:@selector(boolForPreferenceKey:)]) {
|
||||
return [controller boolForPreferenceKey:FBKeyboardPredictionKey]
|
||||
? FBConfigurationKeyboardPreferenceEnabled
|
||||
: FBConfigurationKeyboardPreferenceDisabled;
|
||||
} else {
|
||||
[FBLogger log:@"Updating keyboard prediction preference is not supported"];
|
||||
return FBConfigurationKeyboardPreferenceNotSupported;
|
||||
}
|
||||
}
|
||||
@throw [[FBErrorBuilder.builder withDescriptionFormat:@"No available keyboardsPreferenceKey: '%@'", key] build];
|
||||
}
|
||||
|
||||
+ (void)configureKeyboardsPreference:(BOOL)enable forPreferenceKey:(nonnull NSString *)key
|
||||
{
|
||||
void *handle = dlopen(controllerPrefBundlePath, RTLD_LAZY);
|
||||
Class controllerClass = NSClassFromString(controllerClassName);
|
||||
|
||||
TIPreferencesController *controller = [controllerClass sharedPreferencesController];
|
||||
|
||||
if ([key isEqualToString:FBKeyboardAutocorrectionKey]) {
|
||||
// Auto-Correction in Keyboards
|
||||
if ([controller respondsToSelector:@selector(setAutocorrectionEnabled:)]) {
|
||||
controller.autocorrectionEnabled = enable;
|
||||
} else {
|
||||
[controller setValue:@(enable) forPreferenceKey:FBKeyboardAutocorrectionKey];
|
||||
}
|
||||
} else if ([key isEqualToString:FBKeyboardPredictionKey]) {
|
||||
// Predictive in Keyboards
|
||||
if ([controller respondsToSelector:@selector(setPredictionEnabled:)]) {
|
||||
controller.predictionEnabled = enable;
|
||||
} else {
|
||||
[controller setValue:@(enable) forPreferenceKey:FBKeyboardPredictionKey];
|
||||
}
|
||||
}
|
||||
|
||||
[controller synchronizePreferences];
|
||||
dlclose(handle);
|
||||
}
|
||||
|
||||
+ (NSString*)valueFromArguments: (NSArray<NSString *> *)arguments forKey: (NSString*)key
|
||||
{
|
||||
NSUInteger index = [arguments indexOfObject:key];
|
||||
if (index == NSNotFound || index == arguments.count - 1) {
|
||||
return nil;
|
||||
}
|
||||
return arguments[index + 1];
|
||||
}
|
||||
|
||||
+ (NSUInteger)mjpegServerPortFromArguments
|
||||
{
|
||||
NSString *portNumberString = [self valueFromArguments: NSProcessInfo.processInfo.arguments
|
||||
forKey: @"--mjpeg-server-port"];
|
||||
NSUInteger port = (NSUInteger)[portNumberString integerValue];
|
||||
if (port == 0) {
|
||||
return NSNotFound;
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
+ (NSRange)bindingPortRangeFromArguments
|
||||
{
|
||||
NSString *portNumberString = [self valueFromArguments:NSProcessInfo.processInfo.arguments
|
||||
forKey: @"--port"];
|
||||
NSUInteger port = (NSUInteger)[portNumberString integerValue];
|
||||
if (port == 0) {
|
||||
return NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
return NSMakeRange(port, 1);
|
||||
}
|
||||
|
||||
+ (void)setReduceMotionEnabled:(BOOL)isEnabled
|
||||
{
|
||||
Class settingsClass = NSClassFromString(axSettingsClassName);
|
||||
AXSettings *settings = [settingsClass sharedInstance];
|
||||
|
||||
// Below does not work on real devices because of iOS security model
|
||||
// (lldb) po settings.reduceMotionEnabled = isEnabled
|
||||
// 2019-08-21 22:58:19.776165+0900 WebDriverAgentRunner-Runner[322:13361] [User Defaults] Couldn't write value for key ReduceMotionEnabled in CFPrefsPlistSource<0x28111a700> (Domain: com.apple.Accessibility, User: kCFPreferencesCurrentUser, ByHost: No, Container: (null), Contents Need Refresh: No): setting preferences outside an application's container requires user-preference-write or file-write-data sandbox access
|
||||
if ([settings respondsToSelector:@selector(setReduceMotionEnabled:)]) {
|
||||
[settings setReduceMotionEnabled:isEnabled];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)reduceMotionEnabled
|
||||
{
|
||||
Class settingsClass = NSClassFromString(axSettingsClassName);
|
||||
AXSettings *settings = [settingsClass sharedInstance];
|
||||
|
||||
if ([settings respondsToSelector:@selector(reduceMotionEnabled)]) {
|
||||
return settings.reduceMotionEnabled;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (void)setIncludeHittableInPageSource:(BOOL)enabled
|
||||
{
|
||||
FBShouldIncludeHittableInPageSource = enabled;
|
||||
}
|
||||
|
||||
+ (BOOL)includeHittableInPageSource
|
||||
{
|
||||
return FBShouldIncludeHittableInPageSource;
|
||||
}
|
||||
|
||||
+ (void)setIncludeNativeFrameInPageSource:(BOOL)enabled
|
||||
{
|
||||
FBShouldIncludeNativeFrameInPageSource = enabled;
|
||||
}
|
||||
|
||||
+ (BOOL)includeNativeFrameInPageSource
|
||||
{
|
||||
return FBShouldIncludeNativeFrameInPageSource;
|
||||
}
|
||||
|
||||
+ (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled
|
||||
{
|
||||
FBShouldIncludeMinMaxValueInPageSource = enabled;
|
||||
}
|
||||
|
||||
+ (BOOL)includeMinMaxValueInPageSource
|
||||
{
|
||||
return FBShouldIncludeMinMaxValueInPageSource;
|
||||
}
|
||||
|
||||
@end
|
||||
26
WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h
Normal file
26
WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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/XCDebugLogDelegate-Protocol.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
XCTestLogger decorator that will print all debug information to console
|
||||
*/
|
||||
@interface FBDebugLogDelegateDecorator : NSObject <XCDebugLogDelegate>
|
||||
|
||||
/**
|
||||
Decorates XCTestLogger by also printing debug message to console
|
||||
*/
|
||||
+ (void)decorateXCTestLogger;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
49
WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m
Normal file
49
WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 "FBDebugLogDelegateDecorator.h"
|
||||
|
||||
#import "FBLogger.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
|
||||
@interface FBDebugLogDelegateDecorator ()
|
||||
@property (nonatomic, strong) id<XCDebugLogDelegate> debugLogger;
|
||||
@end
|
||||
|
||||
@implementation FBDebugLogDelegateDecorator
|
||||
|
||||
+ (void)decorateXCTestLogger
|
||||
{
|
||||
FBDebugLogDelegateDecorator *decorator = [FBDebugLogDelegateDecorator new];
|
||||
id<XCDebugLogDelegate> debugLogger = XCDebugLogger();
|
||||
if ([debugLogger isKindOfClass:FBDebugLogDelegateDecorator.class]) {
|
||||
// Already decorated
|
||||
return;
|
||||
}
|
||||
decorator.debugLogger = debugLogger;
|
||||
XCSetDebugLogger(decorator);
|
||||
}
|
||||
|
||||
- (void)logDebugMessage:(NSString *)logEntry
|
||||
{
|
||||
NSString *debugLogEntry = logEntry;
|
||||
static NSString *processName;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
processName = [NSProcessInfo processInfo].processName;
|
||||
});
|
||||
if ([logEntry rangeOfString:[NSString stringWithFormat:@" %@[", processName]].location != NSNotFound) {
|
||||
// Ignoring "13:37:07.638 TestingApp[56374:10997466] " from log entry
|
||||
NSUInteger ignoreCharCount = [logEntry rangeOfString:@"]"].location + 2;
|
||||
debugLogEntry = [logEntry substringWithRange:NSMakeRange(ignoreCharCount, logEntry.length - ignoreCharCount)];
|
||||
}
|
||||
[FBLogger verboseLog:debugLogEntry];
|
||||
[self.debugLogger logDebugMessage:logEntry];
|
||||
}
|
||||
|
||||
@end
|
||||
29
WebDriverAgentLib/Utilities/FBElementHelpers.h
Normal file
29
WebDriverAgentLib/Utilities/FBElementHelpers.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>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Checks if the element is a text field
|
||||
|
||||
@param elementType XCTest element type
|
||||
@return YES if the element is a text field
|
||||
*/
|
||||
BOOL FBDoesElementSupportInnerText(XCUIElementType elementType);
|
||||
|
||||
/**
|
||||
Checks if the element supports min/max value attributes
|
||||
|
||||
@param elementType XCTest element type
|
||||
@return YES if the element type supports min/max value attributes
|
||||
*/
|
||||
BOOL FBDoesElementSupportMinMaxValue(XCUIElementType elementType);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
21
WebDriverAgentLib/Utilities/FBElementHelpers.m
Normal file
21
WebDriverAgentLib/Utilities/FBElementHelpers.m
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 "FBElementHelpers.h"
|
||||
|
||||
BOOL FBDoesElementSupportInnerText(XCUIElementType elementType) {
|
||||
return elementType == XCUIElementTypeTextView
|
||||
|| elementType == XCUIElementTypeTextField
|
||||
|| elementType == XCUIElementTypeSearchField
|
||||
|| elementType == XCUIElementTypeSecureTextField;
|
||||
}
|
||||
|
||||
BOOL FBDoesElementSupportMinMaxValue(XCUIElementType elementType) {
|
||||
return elementType == XCUIElementTypeSlider
|
||||
|| elementType == XCUIElementTypeStepper;
|
||||
}
|
||||
45
WebDriverAgentLib/Utilities/FBElementTypeTransformer.h
Normal file
45
WebDriverAgentLib/Utilities/FBElementTypeTransformer.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 <Foundation/Foundation.h>
|
||||
#import <XCTest/XCUIElementTypes.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Class used to translate between XCUIElementType and string name
|
||||
*/
|
||||
@interface FBElementTypeTransformer : NSObject
|
||||
|
||||
/**
|
||||
Converts string to XCUIElementType
|
||||
|
||||
@param typeName converted string to XCUIElementType
|
||||
@return Proper XCUIElementType or XCUIElementTypeAny if typeName is nil or unrecognised
|
||||
*/
|
||||
+ (XCUIElementType)elementTypeWithTypeName:(NSString *__nullable)typeName;
|
||||
|
||||
/**
|
||||
Converts XCUIElementType to string
|
||||
|
||||
@param type converted XCUIElementType to string
|
||||
@return XCUIElementType as NSString
|
||||
*/
|
||||
+ (NSString *)stringWithElementType:(XCUIElementType)type;
|
||||
|
||||
/**
|
||||
Converts XCUIElementType to short string by striping `XCUIElementType` from it
|
||||
|
||||
@param type converted XCUIElementType to string
|
||||
@return XCUIElementType as NSString with stripped `XCUIElementType`
|
||||
*/
|
||||
+ (NSString *)shortStringWithElementType:(XCUIElementType)type;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
149
WebDriverAgentLib/Utilities/FBElementTypeTransformer.m
Normal file
149
WebDriverAgentLib/Utilities/FBElementTypeTransformer.m
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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 "FBElementTypeTransformer.h"
|
||||
|
||||
#import "FBExceptions.h"
|
||||
|
||||
@implementation FBElementTypeTransformer
|
||||
|
||||
static NSDictionary *ElementTypeToStringMapping;
|
||||
static NSDictionary *StringToElementTypeMapping;
|
||||
|
||||
static NSString const *FB_ELEMENT_TYPE_PREFIX = @"XCUIElementType";
|
||||
|
||||
+ (void)createMapping
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
ElementTypeToStringMapping =
|
||||
@{
|
||||
@0 : @"XCUIElementTypeAny",
|
||||
@1 : @"XCUIElementTypeOther",
|
||||
@2 : @"XCUIElementTypeApplication",
|
||||
@3 : @"XCUIElementTypeGroup",
|
||||
@4 : @"XCUIElementTypeWindow",
|
||||
@5 : @"XCUIElementTypeSheet",
|
||||
@6 : @"XCUIElementTypeDrawer",
|
||||
@7 : @"XCUIElementTypeAlert",
|
||||
@8 : @"XCUIElementTypeDialog",
|
||||
@9 : @"XCUIElementTypeButton",
|
||||
@10 : @"XCUIElementTypeRadioButton",
|
||||
@11 : @"XCUIElementTypeRadioGroup",
|
||||
@12 : @"XCUIElementTypeCheckBox",
|
||||
@13 : @"XCUIElementTypeDisclosureTriangle",
|
||||
@14 : @"XCUIElementTypePopUpButton",
|
||||
@15 : @"XCUIElementTypeComboBox",
|
||||
@16 : @"XCUIElementTypeMenuButton",
|
||||
@17 : @"XCUIElementTypeToolbarButton",
|
||||
@18 : @"XCUIElementTypePopover",
|
||||
@19 : @"XCUIElementTypeKeyboard",
|
||||
@20 : @"XCUIElementTypeKey",
|
||||
@21 : @"XCUIElementTypeNavigationBar",
|
||||
@22 : @"XCUIElementTypeTabBar",
|
||||
@23 : @"XCUIElementTypeTabGroup",
|
||||
@24 : @"XCUIElementTypeToolbar",
|
||||
@25 : @"XCUIElementTypeStatusBar",
|
||||
@26 : @"XCUIElementTypeTable",
|
||||
@27 : @"XCUIElementTypeTableRow",
|
||||
@28 : @"XCUIElementTypeTableColumn",
|
||||
@29 : @"XCUIElementTypeOutline",
|
||||
@30 : @"XCUIElementTypeOutlineRow",
|
||||
@31 : @"XCUIElementTypeBrowser",
|
||||
@32 : @"XCUIElementTypeCollectionView",
|
||||
@33 : @"XCUIElementTypeSlider",
|
||||
@34 : @"XCUIElementTypePageIndicator",
|
||||
@35 : @"XCUIElementTypeProgressIndicator",
|
||||
@36 : @"XCUIElementTypeActivityIndicator",
|
||||
@37 : @"XCUIElementTypeSegmentedControl",
|
||||
@38 : @"XCUIElementTypePicker",
|
||||
@39 : @"XCUIElementTypePickerWheel",
|
||||
@40 : @"XCUIElementTypeSwitch",
|
||||
@41 : @"XCUIElementTypeToggle",
|
||||
@42 : @"XCUIElementTypeLink",
|
||||
@43 : @"XCUIElementTypeImage",
|
||||
@44 : @"XCUIElementTypeIcon",
|
||||
@45 : @"XCUIElementTypeSearchField",
|
||||
@46 : @"XCUIElementTypeScrollView",
|
||||
@47 : @"XCUIElementTypeScrollBar",
|
||||
@48 : @"XCUIElementTypeStaticText",
|
||||
@49 : @"XCUIElementTypeTextField",
|
||||
@50 : @"XCUIElementTypeSecureTextField",
|
||||
@51 : @"XCUIElementTypeDatePicker",
|
||||
@52 : @"XCUIElementTypeTextView",
|
||||
@53 : @"XCUIElementTypeMenu",
|
||||
@54 : @"XCUIElementTypeMenuItem",
|
||||
@55 : @"XCUIElementTypeMenuBar",
|
||||
@56 : @"XCUIElementTypeMenuBarItem",
|
||||
@57 : @"XCUIElementTypeMap",
|
||||
@58 : @"XCUIElementTypeWebView",
|
||||
@59 : @"XCUIElementTypeIncrementArrow",
|
||||
@60 : @"XCUIElementTypeDecrementArrow",
|
||||
@61 : @"XCUIElementTypeTimeline",
|
||||
@62 : @"XCUIElementTypeRatingIndicator",
|
||||
@63 : @"XCUIElementTypeValueIndicator",
|
||||
@64 : @"XCUIElementTypeSplitGroup",
|
||||
@65 : @"XCUIElementTypeSplitter",
|
||||
@66 : @"XCUIElementTypeRelevanceIndicator",
|
||||
@67 : @"XCUIElementTypeColorWell",
|
||||
@68 : @"XCUIElementTypeHelpTag",
|
||||
@69 : @"XCUIElementTypeMatte",
|
||||
@70 : @"XCUIElementTypeDockItem",
|
||||
@71 : @"XCUIElementTypeRuler",
|
||||
@72 : @"XCUIElementTypeRulerMarker",
|
||||
@73 : @"XCUIElementTypeGrid",
|
||||
@74 : @"XCUIElementTypeLevelIndicator",
|
||||
@75 : @"XCUIElementTypeCell",
|
||||
@76 : @"XCUIElementTypeLayoutArea",
|
||||
@77 : @"XCUIElementTypeLayoutItem",
|
||||
@78 : @"XCUIElementTypeHandle",
|
||||
@79 : @"XCUIElementTypeStepper",
|
||||
@80 : @"XCUIElementTypeTab",
|
||||
@81 : @"XCUIElementTypeTouchBar",
|
||||
@82 : @"XCUIElementTypeStatusItem",
|
||||
// !!! This mapping should be updated if there are changes after each new XCTest release
|
||||
};
|
||||
NSMutableDictionary *swappedMapping = [NSMutableDictionary dictionary];
|
||||
[ElementTypeToStringMapping enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
swappedMapping[obj] = key;
|
||||
}];
|
||||
StringToElementTypeMapping = swappedMapping.copy;
|
||||
});
|
||||
}
|
||||
|
||||
+ (XCUIElementType)elementTypeWithTypeName:(NSString *)typeName
|
||||
{
|
||||
[self createMapping];
|
||||
NSNumber *type = StringToElementTypeMapping[typeName];
|
||||
if (nil == type) {
|
||||
if ([typeName hasPrefix:(NSString *)FB_ELEMENT_TYPE_PREFIX] && typeName.length > FB_ELEMENT_TYPE_PREFIX.length) {
|
||||
// Consider the element type is something new and has to be added into ElementTypeToStringMapping
|
||||
return XCUIElementTypeOther;
|
||||
}
|
||||
NSString *reason = [NSString stringWithFormat:@"Invalid argument for class used '%@'. Did you mean %@%@?", typeName, FB_ELEMENT_TYPE_PREFIX, typeName];
|
||||
@throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:@{}];
|
||||
}
|
||||
return (XCUIElementType) type.unsignedIntegerValue;
|
||||
}
|
||||
|
||||
+ (NSString *)stringWithElementType:(XCUIElementType)type
|
||||
{
|
||||
[self createMapping];
|
||||
NSString *typeName = ElementTypeToStringMapping[@(type)];
|
||||
return nil == typeName
|
||||
// Consider the type name is something new and has to be added into ElementTypeToStringMapping
|
||||
? [NSString stringWithFormat:@"%@Other", FB_ELEMENT_TYPE_PREFIX]
|
||||
: typeName;
|
||||
}
|
||||
|
||||
+ (NSString *)shortStringWithElementType:(XCUIElementType)type
|
||||
{
|
||||
return [[self stringWithElementType:type] stringByReplacingOccurrencesOfString:(NSString *)FB_ELEMENT_TYPE_PREFIX withString:@""];
|
||||
}
|
||||
|
||||
@end
|
||||
64
WebDriverAgentLib/Utilities/FBErrorBuilder.h
Normal file
64
WebDriverAgentLib/Utilities/FBErrorBuilder.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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Builder used create error raised by WebDriverAgent
|
||||
*/
|
||||
@interface FBErrorBuilder : NSObject
|
||||
|
||||
/**
|
||||
Default constructor
|
||||
*/
|
||||
+ (instancetype)builder;
|
||||
|
||||
/**
|
||||
Configures description set as NSLocalizedDescriptionKey
|
||||
|
||||
@param description set as NSLocalizedDescriptionKey
|
||||
@return builder instance
|
||||
*/
|
||||
- (instancetype)withDescription:(NSString *)description;
|
||||
|
||||
/**
|
||||
Configures description set as NSLocalizedDescriptionKey with convenient format
|
||||
|
||||
@param format of description set as NSLocalizedDescriptionKey
|
||||
@return builder instance
|
||||
*/
|
||||
- (instancetype)withDescriptionFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
|
||||
/**
|
||||
Configures error set as NSUnderlyingErrorKey
|
||||
|
||||
@param innerError used to set NSUnderlyingErrorKey
|
||||
@return builder instance
|
||||
*/
|
||||
- (instancetype)withInnerError:(NSError *)innerError;
|
||||
|
||||
/**
|
||||
Builder used create error raised by WebDriverAgent
|
||||
|
||||
@return built error
|
||||
*/
|
||||
- (NSError *)build;
|
||||
|
||||
/**
|
||||
Builder used create error raised by WebDriverAgent
|
||||
|
||||
@param error pointer used to return built error
|
||||
@return fixed NO to apply to Apple's coding conventions
|
||||
*/
|
||||
- (BOOL)buildError:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
75
WebDriverAgentLib/Utilities/FBErrorBuilder.m
Normal file
75
WebDriverAgentLib/Utilities/FBErrorBuilder.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 "FBErrorBuilder.h"
|
||||
|
||||
static NSString *const FBWebServerErrorDomain = @"com.facebook.WebDriverAgent";
|
||||
|
||||
@interface FBErrorBuilder ()
|
||||
@property (nonatomic, copy) NSString *errorDescription;
|
||||
@property (nonatomic, strong) NSError *innerError;
|
||||
@end
|
||||
|
||||
@implementation FBErrorBuilder
|
||||
|
||||
+ (instancetype)builder
|
||||
{
|
||||
return [FBErrorBuilder new];
|
||||
}
|
||||
|
||||
- (instancetype)withDescription:(NSString *)description
|
||||
{
|
||||
self.errorDescription = description;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)withDescriptionFormat:(NSString *)format, ...
|
||||
{
|
||||
va_list argList;
|
||||
va_start(argList, format);
|
||||
self.errorDescription = [[NSString alloc] initWithFormat:format arguments:argList];
|
||||
va_end(argList);
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)withInnerError:(NSError *)error
|
||||
{
|
||||
self.innerError = error;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)buildError:(NSError **)errorOut
|
||||
{
|
||||
if (errorOut) {
|
||||
*errorOut = [self build];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSError *)build
|
||||
{
|
||||
return
|
||||
[NSError errorWithDomain:FBWebServerErrorDomain
|
||||
code:1
|
||||
userInfo:[self buildUserInfo]
|
||||
];
|
||||
}
|
||||
|
||||
- (NSDictionary *)buildUserInfo
|
||||
{
|
||||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||||
if (self.errorDescription) {
|
||||
userInfo[NSLocalizedDescriptionKey] = self.errorDescription;
|
||||
}
|
||||
if (self.innerError) {
|
||||
userInfo[NSUnderlyingErrorKey] = self.innerError;
|
||||
}
|
||||
return userInfo.copy;
|
||||
}
|
||||
|
||||
@end
|
||||
19
WebDriverAgentLib/Utilities/FBFailureProofTestCase.h
Normal file
19
WebDriverAgentLib/Utilities/FBFailureProofTestCase.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 <WebDriverAgentLib/XCTestCase.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
Test Case that will never fail or stop from running in case of failure
|
||||
*/
|
||||
@interface FBFailureProofTestCase : XCTestCase
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
68
WebDriverAgentLib/Utilities/FBFailureProofTestCase.m
Normal file
68
WebDriverAgentLib/Utilities/FBFailureProofTestCase.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 "FBFailureProofTestCase.h"
|
||||
|
||||
#import "FBLogger.h"
|
||||
|
||||
@implementation FBFailureProofTestCase
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[super setUp];
|
||||
self.continueAfterFailure = YES;
|
||||
// https://github.com/appium/appium/issues/13949
|
||||
self.shouldSetShouldHaltWhenReceivesControl = NO;
|
||||
self.shouldHaltWhenReceivesControl = NO;
|
||||
}
|
||||
|
||||
- (void)_recordIssue:(XCTIssue *)issue
|
||||
{
|
||||
NSString *description = [NSString stringWithFormat:@"%@ (%@)", issue.compactDescription, issue.associatedError.description];
|
||||
[FBLogger logFmt:@"Issue type: %ld", issue.type];
|
||||
[self _enqueueFailureWithDescription:description
|
||||
inFile:issue.sourceCodeContext.location.fileURL.path
|
||||
atLine:issue.sourceCodeContext.location.lineNumber
|
||||
// 5 == XCTIssueTypeUnmatchedExpectedFailure
|
||||
expected:issue.type == 5];
|
||||
}
|
||||
|
||||
- (void)_recordIssue:(XCTIssue *)issue forCaughtError:(id)error
|
||||
{
|
||||
[self _recordIssue:issue];
|
||||
}
|
||||
|
||||
- (void)recordIssue:(XCTIssue *)issue
|
||||
{
|
||||
[self _recordIssue:issue];
|
||||
}
|
||||
|
||||
/**
|
||||
Override 'recordFailureWithDescription' to not stop by failures.
|
||||
*/
|
||||
- (void)recordFailureWithDescription:(NSString *)description
|
||||
inFile:(NSString *)filePath
|
||||
atLine:(NSUInteger)lineNumber
|
||||
expected:(BOOL)expected
|
||||
{
|
||||
[self _enqueueFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected];
|
||||
}
|
||||
|
||||
/**
|
||||
Private XCTestCase method used to block and tunnel failure messages
|
||||
*/
|
||||
- (void)_enqueueFailureWithDescription:(NSString *)description
|
||||
inFile:(NSString *)filePath
|
||||
atLine:(NSUInteger)lineNumber
|
||||
expected:(BOOL)expected
|
||||
{
|
||||
[FBLogger logFmt:@"Enqueue Failure: %@ %@ %lu %d", description, filePath, (unsigned long)lineNumber, expected];
|
||||
// TODO: Figure out which error types we want to escalate
|
||||
}
|
||||
|
||||
@end
|
||||
55
WebDriverAgentLib/Utilities/FBImageProcessor.h
Normal file
55
WebDriverAgentLib/Utilities/FBImageProcessor.h
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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 <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
@class UTType;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Those values define the allowed ranges for the scaling factor and compression quality settings
|
||||
extern const CGFloat FBMinScalingFactor;
|
||||
extern const CGFloat FBMaxScalingFactor;
|
||||
extern const CGFloat FBMinCompressionQuality;
|
||||
extern const CGFloat FBMaxCompressionQuality;
|
||||
|
||||
@interface FBImageProcessor : NSObject
|
||||
|
||||
/**
|
||||
Puts the passed image on the queue and dispatches a scaling operation. If there is already a image on the
|
||||
queue it will be replaced with the new one
|
||||
|
||||
@param image The image to scale down
|
||||
@param completionHandler called after successfully scaling down an image
|
||||
@param scalingFactor the scaling factor in range 0.01..1.0. A value of 1.0 won't perform scaling at all
|
||||
*/
|
||||
- (void)submitImageData:(NSData *)image
|
||||
scalingFactor:(CGFloat)scalingFactor
|
||||
completionHandler:(void (^)(NSData *))completionHandler;
|
||||
|
||||
/**
|
||||
Scales and crops the source image
|
||||
|
||||
@param image The source image data
|
||||
@param uti Either UTTypePNG or UTTypeJPEG
|
||||
@param scalingFactor Scaling factor in range 0.01..1.0. A value of 1.0 won't perform scaling at all
|
||||
@param compressionQuality the compression quality in range 0.0..1.0 (0.0 for max. compression and 1.0 for lossless compression).
|
||||
Only works if UTI is set to kUTTypeJPEG
|
||||
@param error The actual error instance if the returned result is nil
|
||||
@returns Processed image data compressed according to the given UTI or nil in case of a failure
|
||||
*/
|
||||
- (nullable NSData *)scaledImageWithData:(NSData *)image
|
||||
uti:(UTType *)uti
|
||||
scalingFactor:(CGFloat)scalingFactor
|
||||
compressionQuality:(CGFloat)compressionQuality
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
170
WebDriverAgentLib/Utilities/FBImageProcessor.m
Normal file
170
WebDriverAgentLib/Utilities/FBImageProcessor.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 "FBImageProcessor.h"
|
||||
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
@import UniformTypeIdentifiers;
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBImageUtils.h"
|
||||
#import "FBLogger.h"
|
||||
|
||||
const CGFloat FBMinScalingFactor = 0.01f;
|
||||
const CGFloat FBMaxScalingFactor = 1.0f;
|
||||
const CGFloat FBMinCompressionQuality = 0.0f;
|
||||
const CGFloat FBMaxCompressionQuality = 1.0f;
|
||||
|
||||
@interface FBImageProcessor ()
|
||||
|
||||
@property (nonatomic) NSData *nextImage;
|
||||
@property (nonatomic, readonly) NSLock *nextImageLock;
|
||||
@property (nonatomic, readonly) dispatch_queue_t scalingQueue;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBImageProcessor
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_nextImageLock = [[NSLock alloc] init];
|
||||
_scalingQueue = dispatch_queue_create("image.scaling.queue", NULL);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)submitImageData:(NSData *)image
|
||||
scalingFactor:(CGFloat)scalingFactor
|
||||
completionHandler:(void (^)(NSData *))completionHandler
|
||||
{
|
||||
[self.nextImageLock lock];
|
||||
if (self.nextImage != nil) {
|
||||
[FBLogger verboseLog:@"Discarding screenshot"];
|
||||
}
|
||||
self.nextImage = image;
|
||||
[self.nextImageLock unlock];
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wcompletion-handler"
|
||||
dispatch_async(self.scalingQueue, ^{
|
||||
[self.nextImageLock lock];
|
||||
NSData *nextImageData = self.nextImage;
|
||||
self.nextImage = nil;
|
||||
[self.nextImageLock unlock];
|
||||
if (nextImageData == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We do not want this value to be too high because then we get images larger in size than original ones
|
||||
// Although, we also don't want to lose too much of the quality on recompression
|
||||
CGFloat recompressionQuality = MAX(0.9,
|
||||
MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0));
|
||||
NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData
|
||||
scalingFactor:scalingFactor
|
||||
uti:UTTypeJPEG
|
||||
compressionQuality:recompressionQuality
|
||||
// iOS always returns screnshots in portrait orientation, but puts the real value into the metadata
|
||||
// Use it with care. See https://github.com/appium/WebDriverAgent/pull/812
|
||||
fixOrientation:FBConfiguration.mjpegShouldFixOrientation
|
||||
desiredOrientation:nil];
|
||||
completionHandler(thumbnailData ?: nextImageData);
|
||||
});
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
+ (nullable NSData *)fixedImageDataWithImageData:(NSData *)imageData
|
||||
scalingFactor:(CGFloat)scalingFactor
|
||||
uti:(UTType *)uti
|
||||
compressionQuality:(CGFloat)compressionQuality
|
||||
fixOrientation:(BOOL)fixOrientation
|
||||
desiredOrientation:(nullable NSNumber *)orientation
|
||||
{
|
||||
scalingFactor = MAX(FBMinScalingFactor, MIN(FBMaxScalingFactor, scalingFactor));
|
||||
BOOL usesScaling = scalingFactor > 0.0 && scalingFactor < FBMaxScalingFactor;
|
||||
@autoreleasepool {
|
||||
if (!usesScaling && !fixOrientation) {
|
||||
return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality);
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithData:imageData];
|
||||
if (nil == image
|
||||
|| ((image.imageOrientation == UIImageOrientationUp || !fixOrientation) && !usesScaling)) {
|
||||
return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality);
|
||||
}
|
||||
|
||||
CGSize scaledSize = CGSizeMake(image.size.width * scalingFactor, image.size.height * scalingFactor);
|
||||
if (!fixOrientation && usesScaling) {
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
__block UIImage *result = nil;
|
||||
[image prepareThumbnailOfSize:scaledSize
|
||||
completionHandler:^(UIImage * _Nullable thumbnail) {
|
||||
result = thumbnail;
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}];
|
||||
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
||||
if (nil == result) {
|
||||
return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality);
|
||||
}
|
||||
return [uti conformsToType:UTTypePNG]
|
||||
? UIImagePNGRepresentation(result)
|
||||
: UIImageJPEGRepresentation(result, compressionQuality);
|
||||
}
|
||||
|
||||
UIGraphicsImageRendererFormat *format = [[UIGraphicsImageRendererFormat alloc] init];
|
||||
format.scale = scalingFactor;
|
||||
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize
|
||||
format:format];
|
||||
UIImageOrientation desiredOrientation = orientation == nil
|
||||
? image.imageOrientation
|
||||
: (UIImageOrientation)orientation.integerValue;
|
||||
UIImage *uiImage = [UIImage imageWithCGImage:(CGImageRef)image.CGImage
|
||||
scale:image.scale
|
||||
orientation:desiredOrientation];
|
||||
return [uti conformsToType:UTTypePNG]
|
||||
? [renderer PNGDataWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
|
||||
[uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
|
||||
}]
|
||||
: [renderer JPEGDataWithCompressionQuality:compressionQuality
|
||||
actions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
|
||||
[uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSData *)scaledImageWithData:(NSData *)imageData
|
||||
uti:(UTType *)uti
|
||||
scalingFactor:(CGFloat)scalingFactor
|
||||
compressionQuality:(CGFloat)compressionQuality
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSNumber *orientation = nil;
|
||||
#if !TARGET_OS_TV
|
||||
if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortrait) {
|
||||
orientation = @(UIImageOrientationUp);
|
||||
} else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortraitUpsideDown) {
|
||||
orientation = @(UIImageOrientationDown);
|
||||
} else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeLeft) {
|
||||
orientation = @(UIImageOrientationRight);
|
||||
} else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeRight) {
|
||||
orientation = @(UIImageOrientationLeft);
|
||||
}
|
||||
#endif
|
||||
NSData *resultData = [self.class fixedImageDataWithImageData:imageData
|
||||
scalingFactor:scalingFactor
|
||||
uti:uti
|
||||
compressionQuality:compressionQuality
|
||||
fixOrientation:YES
|
||||
desiredOrientation:orientation];
|
||||
return resultData ?: imageData;
|
||||
}
|
||||
|
||||
@end
|
||||
25
WebDriverAgentLib/Utilities/FBImageUtils.h
Normal file
25
WebDriverAgentLib/Utilities/FBImageUtils.h
Normal file
@@ -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>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/*! Returns YES if the data contains a PNG image */
|
||||
BOOL FBIsPngImage(NSData *imageData);
|
||||
|
||||
/*! Converts the given image data to a PNG representation if necessary */
|
||||
NSData *_Nullable FBToPngData(NSData *imageData);
|
||||
|
||||
/*! Returns YES if the data contains a JPG image */
|
||||
BOOL FBIsJpegImage(NSData *imageData);
|
||||
|
||||
/*! Converts the given image data to a JPG representation if necessary */
|
||||
NSData *_Nullable FBToJpegData(NSData *imageData, CGFloat compressionQuality);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
80
WebDriverAgentLib/Utilities/FBImageUtils.m
Normal file
80
WebDriverAgentLib/Utilities/FBImageUtils.m
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 "FBImageUtils.h"
|
||||
|
||||
#import "FBMacros.h"
|
||||
#import "FBConfiguration.h"
|
||||
|
||||
// https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||
static uint8_t PNG_MAGIC[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
static const NSUInteger PNG_MAGIC_LEN = 8;
|
||||
static uint8_t JPG_MAGIC[] = { 0xff, 0xd8, 0xff };
|
||||
static const NSUInteger JPG_MAGIC_LEN = 3;
|
||||
|
||||
BOOL FBIsPngImage(NSData *imageData)
|
||||
{
|
||||
if (nil == imageData || [imageData length] < PNG_MAGIC_LEN) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
static NSData* pngMagicStartData = nil;
|
||||
static dispatch_once_t oncePngToken;
|
||||
dispatch_once(&oncePngToken, ^{
|
||||
pngMagicStartData = [NSData dataWithBytesNoCopy:(void*)PNG_MAGIC length:PNG_MAGIC_LEN freeWhenDone:NO];
|
||||
});
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wassign-enum"
|
||||
NSRange range = [imageData rangeOfData:pngMagicStartData options:kNilOptions range:NSMakeRange(0, PNG_MAGIC_LEN)];
|
||||
#pragma clang diagnostic pop
|
||||
return range.location != NSNotFound;
|
||||
}
|
||||
|
||||
NSData *FBToPngData(NSData *imageData) {
|
||||
if (nil == imageData || [imageData length] < PNG_MAGIC_LEN) {
|
||||
return nil;
|
||||
}
|
||||
if (FBIsPngImage(imageData)) {
|
||||
return imageData;
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithData:imageData];
|
||||
return nil == image ? nil : (NSData *)UIImagePNGRepresentation(image);
|
||||
}
|
||||
|
||||
BOOL FBIsJpegImage(NSData *imageData)
|
||||
{
|
||||
if (nil == imageData || [imageData length] < JPG_MAGIC_LEN) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
static NSData* jpgMagicStartData = nil;
|
||||
static dispatch_once_t onceJpgToken;
|
||||
dispatch_once(&onceJpgToken, ^{
|
||||
jpgMagicStartData = [NSData dataWithBytesNoCopy:(void*)JPG_MAGIC length:JPG_MAGIC_LEN freeWhenDone:NO];
|
||||
});
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wassign-enum"
|
||||
NSRange range = [imageData rangeOfData:jpgMagicStartData options:kNilOptions range:NSMakeRange(0, JPG_MAGIC_LEN)];
|
||||
#pragma clang diagnostic pop
|
||||
return range.location != NSNotFound;
|
||||
}
|
||||
|
||||
NSData *FBToJpegData(NSData *imageData, CGFloat compressionQuality) {
|
||||
if (nil == imageData || [imageData length] < JPG_MAGIC_LEN) {
|
||||
return nil;
|
||||
}
|
||||
if (FBIsJpegImage(imageData)) {
|
||||
return imageData;
|
||||
}
|
||||
|
||||
UIImage *image = [UIImage imageWithData:imageData];
|
||||
return nil == image ? nil : (NSData *)UIImageJPEGRepresentation(image, compressionQuality);
|
||||
}
|
||||
37
WebDriverAgentLib/Utilities/FBKeyboard.h
Normal file
37
WebDriverAgentLib/Utilities/FBKeyboard.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 FBKeyboard : NSObject
|
||||
|
||||
#if (!TARGET_OS_TV && __clang_major__ >= 15)
|
||||
/**
|
||||
Transforms key name to its string representation, which could be used with XCTest
|
||||
|
||||
@param name one of available keyboard key names defined in https://developer.apple.com/documentation/xctest/xcuikeyboardkey?language=objc
|
||||
@return Either the key value or nil if no matches have been found
|
||||
*/
|
||||
+ (nullable NSString *)keyValueForName:(NSString *)name;
|
||||
#endif
|
||||
|
||||
/**
|
||||
Waits until the keyboard is visible on the screen or a timeout happens
|
||||
|
||||
@param app that should be typed
|
||||
@param timeout the maximum duration in seconds to wait until the keyboard is visible. If the timeout value is equal or less than zero then immediate visibility verification is going to be performed.
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return YES if the keyboard is visible after the timeout, otherwise NO.
|
||||
*/
|
||||
+ (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app timeout:(NSTimeInterval)timeout error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
123
WebDriverAgentLib/Utilities/FBKeyboard.m
Normal file
123
WebDriverAgentLib/Utilities/FBKeyboard.m
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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 "FBKeyboard.h"
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBRunLoopSpinner.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "XCTestDriver.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBConfiguration.h"
|
||||
|
||||
@implementation FBKeyboard
|
||||
|
||||
+ (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app
|
||||
timeout:(NSTimeInterval)timeout
|
||||
error:(NSError **)error
|
||||
{
|
||||
BOOL (^isKeyboardVisible)(void) = ^BOOL(void) {
|
||||
XCUIElement *keyboard = app.keyboards.fb_firstMatch;
|
||||
if (nil == keyboard) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSPredicate *keySearchPredicate = [NSPredicate predicateWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot,
|
||||
NSDictionary *bindings) {
|
||||
return snapshot.label.length > 0;
|
||||
}];
|
||||
XCUIElement *firstKey = [[keyboard descendantsMatchingType:XCUIElementTypeKey]
|
||||
matchingPredicate:keySearchPredicate].allElementsBoundByIndex.firstObject;
|
||||
return firstKey.exists && firstKey.hittable;
|
||||
};
|
||||
NSString* errMessage = @"The on-screen keyboard must be present to send keys";
|
||||
if (timeout <= 0) {
|
||||
if (!isKeyboardVisible()) {
|
||||
return [[[FBErrorBuilder builder] withDescription:errMessage] buildError:error];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
return
|
||||
[[[[FBRunLoopSpinner new]
|
||||
timeout:timeout]
|
||||
timeoutErrorMessage:errMessage]
|
||||
spinUntilTrue:isKeyboardVisible
|
||||
error:error];
|
||||
}
|
||||
|
||||
#if (!TARGET_OS_TV && __clang_major__ >= 15)
|
||||
|
||||
+ (NSString *)keyValueForName:(NSString *)name
|
||||
{
|
||||
static dispatch_once_t onceKeys;
|
||||
static NSDictionary<NSString *, NSString *> *keysMapping;
|
||||
dispatch_once(&onceKeys, ^{
|
||||
keysMapping = @{
|
||||
@"XCUIKeyboardKeyDelete": XCUIKeyboardKeyDelete,
|
||||
@"XCUIKeyboardKeyReturn": XCUIKeyboardKeyReturn,
|
||||
@"XCUIKeyboardKeyEnter": XCUIKeyboardKeyEnter,
|
||||
@"XCUIKeyboardKeyTab": XCUIKeyboardKeyTab,
|
||||
@"XCUIKeyboardKeySpace": XCUIKeyboardKeySpace,
|
||||
@"XCUIKeyboardKeyEscape": XCUIKeyboardKeyEscape,
|
||||
|
||||
@"XCUIKeyboardKeyUpArrow": XCUIKeyboardKeyUpArrow,
|
||||
@"XCUIKeyboardKeyDownArrow": XCUIKeyboardKeyDownArrow,
|
||||
@"XCUIKeyboardKeyLeftArrow": XCUIKeyboardKeyLeftArrow,
|
||||
@"XCUIKeyboardKeyRightArrow": XCUIKeyboardKeyRightArrow,
|
||||
|
||||
@"XCUIKeyboardKeyF1": XCUIKeyboardKeyF1,
|
||||
@"XCUIKeyboardKeyF2": XCUIKeyboardKeyF2,
|
||||
@"XCUIKeyboardKeyF3": XCUIKeyboardKeyF3,
|
||||
@"XCUIKeyboardKeyF4": XCUIKeyboardKeyF4,
|
||||
@"XCUIKeyboardKeyF5": XCUIKeyboardKeyF5,
|
||||
@"XCUIKeyboardKeyF6": XCUIKeyboardKeyF6,
|
||||
@"XCUIKeyboardKeyF7": XCUIKeyboardKeyF7,
|
||||
@"XCUIKeyboardKeyF8": XCUIKeyboardKeyF8,
|
||||
@"XCUIKeyboardKeyF9": XCUIKeyboardKeyF9,
|
||||
@"XCUIKeyboardKeyF10": XCUIKeyboardKeyF10,
|
||||
@"XCUIKeyboardKeyF11": XCUIKeyboardKeyF11,
|
||||
@"XCUIKeyboardKeyF12": XCUIKeyboardKeyF12,
|
||||
@"XCUIKeyboardKeyF13": XCUIKeyboardKeyF13,
|
||||
@"XCUIKeyboardKeyF14": XCUIKeyboardKeyF14,
|
||||
@"XCUIKeyboardKeyF15": XCUIKeyboardKeyF15,
|
||||
@"XCUIKeyboardKeyF16": XCUIKeyboardKeyF16,
|
||||
@"XCUIKeyboardKeyF17": XCUIKeyboardKeyF17,
|
||||
@"XCUIKeyboardKeyF18": XCUIKeyboardKeyF18,
|
||||
@"XCUIKeyboardKeyF19": XCUIKeyboardKeyF19,
|
||||
|
||||
@"XCUIKeyboardKeyForwardDelete": XCUIKeyboardKeyForwardDelete,
|
||||
@"XCUIKeyboardKeyHome": XCUIKeyboardKeyHome,
|
||||
@"XCUIKeyboardKeyEnd": XCUIKeyboardKeyEnd,
|
||||
@"XCUIKeyboardKeyPageUp": XCUIKeyboardKeyPageUp,
|
||||
@"XCUIKeyboardKeyPageDown": XCUIKeyboardKeyPageDown,
|
||||
@"XCUIKeyboardKeyClear": XCUIKeyboardKeyClear,
|
||||
@"XCUIKeyboardKeyHelp": XCUIKeyboardKeyHelp,
|
||||
|
||||
@"XCUIKeyboardKeyCapsLock": XCUIKeyboardKeyCapsLock,
|
||||
@"XCUIKeyboardKeyShift": XCUIKeyboardKeyShift,
|
||||
@"XCUIKeyboardKeyControl": XCUIKeyboardKeyControl,
|
||||
@"XCUIKeyboardKeyOption": XCUIKeyboardKeyOption,
|
||||
@"XCUIKeyboardKeyCommand": XCUIKeyboardKeyCommand,
|
||||
@"XCUIKeyboardKeyRightShift": XCUIKeyboardKeyRightShift,
|
||||
@"XCUIKeyboardKeyRightControl": XCUIKeyboardKeyRightControl,
|
||||
@"XCUIKeyboardKeyRightOption": XCUIKeyboardKeyRightOption,
|
||||
@"XCUIKeyboardKeyRightCommand": XCUIKeyboardKeyRightCommand,
|
||||
@"XCUIKeyboardKeySecondaryFn": XCUIKeyboardKeySecondaryFn
|
||||
};
|
||||
});
|
||||
return keysMapping[name];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
32
WebDriverAgentLib/Utilities/FBLogger.h
Normal file
32
WebDriverAgentLib/Utilities/FBLogger.h
Normal file
@@ -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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
A Global Logger object that understands log levels
|
||||
*/
|
||||
@interface FBLogger : NSObject
|
||||
|
||||
/**
|
||||
Log to stdout.
|
||||
*/
|
||||
+ (void)log:(NSString *)message;
|
||||
+ (void)logFmt:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
|
||||
/**
|
||||
Log to stdout, only if WDA is Verbose
|
||||
*/
|
||||
+ (void)verboseLog:(NSString *)message;
|
||||
+ (void)verboseLogFmt:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
47
WebDriverAgentLib/Utilities/FBLogger.m
Normal file
47
WebDriverAgentLib/Utilities/FBLogger.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 "FBLogger.h"
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
|
||||
@implementation FBLogger
|
||||
|
||||
+ (void)log:(NSString *)message
|
||||
{
|
||||
NSLog(@"%@", message);
|
||||
}
|
||||
|
||||
+ (void)logFmt:(NSString *)format, ...
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
NSLogv(format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
+ (void)verboseLog:(NSString *)message
|
||||
{
|
||||
if (!FBConfiguration.verboseLoggingEnabled) {
|
||||
return;
|
||||
}
|
||||
[self log:message];
|
||||
}
|
||||
|
||||
+ (void)verboseLogFmt:(NSString *)format, ...
|
||||
{
|
||||
if (!FBConfiguration.verboseLoggingEnabled) {
|
||||
return;
|
||||
}
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
NSLogv(format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
@end
|
||||
57
WebDriverAgentLib/Utilities/FBMacros.h
Normal file
57
WebDriverAgentLib/Utilities/FBMacros.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.
|
||||
*/
|
||||
|
||||
// Typedef to help with storing constant strings for enums.
|
||||
#if __has_feature(objc_arc)
|
||||
typedef __unsafe_unretained NSString* FBLiteralString;
|
||||
#else
|
||||
typedef NSString* FBLiteralString;
|
||||
#endif
|
||||
|
||||
/*! Returns 'value' or nil if 'value' is an empty string */
|
||||
#define FBTransferEmptyStringToNil(value) ([value isEqual:@""] ? nil : value)
|
||||
|
||||
/*! Returns 'value1' or 'value2' if 'value1' is an empty string */
|
||||
#define FBFirstNonEmptyValue(value1, value2) ^{ \
|
||||
id value1computed = value1; \
|
||||
return (value1computed == nil || [value1computed isEqual:@""] ? value2 : value1computed); \
|
||||
}()
|
||||
|
||||
/*! Returns 'value' or NSNull if 'value' is nil */
|
||||
#define FBValueOrNull(value) ((value) ?: [NSNull null])
|
||||
|
||||
/*!
|
||||
Returns name of class property as a string
|
||||
previously used [class new] errors out on certain classes because new will be declared unavailable
|
||||
Instead we are casting into a class to get compiler support with property name.
|
||||
*/
|
||||
#define FBStringify(class, property) ({if(NO){[((class *)nil) property];} @#property;})
|
||||
|
||||
/*! Creates weak type for given 'arg' */
|
||||
#define FBWeakify(arg) typeof(arg) __weak wda_weak_##arg = arg
|
||||
|
||||
/*! Creates strong type for FBWeakify-ed 'arg' */
|
||||
#define FBStrongify(arg) \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
|
||||
typeof(arg) arg = wda_weak_##arg \
|
||||
_Pragma("clang diagnostic pop")
|
||||
|
||||
/*! Returns YES if current system version satisfies the given codition */
|
||||
#define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
|
||||
#define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
|
||||
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
|
||||
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
|
||||
#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)
|
||||
|
||||
/*! Converts the given number of milliseconds into seconds */
|
||||
#define FBMillisToSeconds(ms) ((ms) / 1000.0)
|
||||
|
||||
/*! Converts boolean value to its string representation */
|
||||
#define FBBoolToString(b) ((b) ? @"true" : @"false")
|
||||
|
||||
36
WebDriverAgentLib/Utilities/FBMathUtils.h
Normal file
36
WebDriverAgentLib/Utilities/FBMathUtils.h
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 <UIKit/UIKit.h>
|
||||
|
||||
@class XCUIApplication;
|
||||
|
||||
extern CGFloat FBDefaultFrameFuzzyThreshold;
|
||||
|
||||
/*! Returns center point of given rect */
|
||||
CGPoint FBRectGetCenter(CGRect rect);
|
||||
|
||||
/*! Returns whether floatss are equal within given threshold */
|
||||
BOOL FBFloatFuzzyEqualToFloat(CGFloat float1, CGFloat float2, CGFloat threshold);
|
||||
|
||||
/*! Returns whether points are equal within given threshold */
|
||||
BOOL FBPointFuzzyEqualToPoint(CGPoint point1, CGPoint point2, CGFloat threshold);
|
||||
|
||||
/*! Returns whether vectors are equal within given threshold */
|
||||
BOOL FBVectorFuzzyEqualToVector(CGVector a, CGVector b, CGFloat threshold);
|
||||
|
||||
/*! Returns whether size are equal within given threshold */
|
||||
BOOL FBSizeFuzzyEqualToSize(CGSize size1, CGSize size2, CGFloat threshold);
|
||||
|
||||
/*! Returns whether rect are equal within given threshold */
|
||||
BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold);
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
/*! Inverts size if necessary to match current screen orientation */
|
||||
CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation);
|
||||
#endif
|
||||
63
WebDriverAgentLib/Utilities/FBMathUtils.m
Normal file
63
WebDriverAgentLib/Utilities/FBMathUtils.m
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 "FBMathUtils.h"
|
||||
|
||||
#import "FBMacros.h"
|
||||
|
||||
CGFloat FBDefaultFrameFuzzyThreshold = 2.0;
|
||||
|
||||
CGPoint FBRectGetCenter(CGRect rect)
|
||||
{
|
||||
return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
|
||||
}
|
||||
|
||||
BOOL FBFloatFuzzyEqualToFloat(CGFloat float1, CGFloat float2, CGFloat threshold)
|
||||
{
|
||||
return (fabs(float1 - float2) <= threshold);
|
||||
}
|
||||
|
||||
BOOL FBVectorFuzzyEqualToVector(CGVector a, CGVector b, CGFloat threshold)
|
||||
{
|
||||
return FBFloatFuzzyEqualToFloat(a.dx, b.dx, threshold) && FBFloatFuzzyEqualToFloat(a.dy, b.dy, threshold);
|
||||
}
|
||||
|
||||
BOOL FBPointFuzzyEqualToPoint(CGPoint point1, CGPoint point2, CGFloat threshold)
|
||||
{
|
||||
return FBFloatFuzzyEqualToFloat(point1.x, point2.x, threshold) && FBFloatFuzzyEqualToFloat(point1.y, point2.y, threshold);
|
||||
}
|
||||
|
||||
BOOL FBSizeFuzzyEqualToSize(CGSize size1, CGSize size2, CGFloat threshold)
|
||||
{
|
||||
return FBFloatFuzzyEqualToFloat(size1.width, size2.width, threshold) && FBFloatFuzzyEqualToFloat(size1.height, size2.height, threshold);
|
||||
}
|
||||
|
||||
BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold)
|
||||
{
|
||||
return
|
||||
FBPointFuzzyEqualToPoint(FBRectGetCenter(rect1), FBRectGetCenter(rect2), threshold) &&
|
||||
FBSizeFuzzyEqualToSize(rect1.size, rect2.size, threshold);
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation)
|
||||
{
|
||||
if (orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight) {
|
||||
/*
|
||||
There is an XCTest bug that application.frame property returns exchanged dimensions for landscape mode.
|
||||
This verification is just to make sure the bug is still there (since height is never greater than width in landscape)
|
||||
and to make it still working properly after XCTest itself starts to respect landscape mode.
|
||||
*/
|
||||
if (actualSize.height > actualSize.width) {
|
||||
return CGSizeMake(actualSize.height, actualSize.width);
|
||||
}
|
||||
}
|
||||
return actualSize;
|
||||
}
|
||||
#endif
|
||||
24
WebDriverAgentLib/Utilities/FBMjpegServer.h
Normal file
24
WebDriverAgentLib/Utilities/FBMjpegServer.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 "FBTCPSocket.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBMjpegServer : NSObject <FBTCPSocketDelegate>
|
||||
|
||||
/**
|
||||
The default constructor for the screenshot bradcaster service.
|
||||
This service sends low resolution screenshots 10 times per seconds
|
||||
to all connected clients.
|
||||
*/
|
||||
- (instancetype)init;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
151
WebDriverAgentLib/Utilities/FBMjpegServer.m
Normal file
151
WebDriverAgentLib/Utilities/FBMjpegServer.m
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 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 "FBMjpegServer.h"
|
||||
|
||||
#import <mach/mach_time.h>
|
||||
@import UniformTypeIdentifiers;
|
||||
|
||||
#import "GCDAsyncSocket.h"
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBScreenshot.h"
|
||||
#import "FBImageProcessor.h"
|
||||
#import "FBImageUtils.h"
|
||||
#import "XCUIScreen.h"
|
||||
|
||||
static const NSUInteger MAX_FPS = 60;
|
||||
static const NSTimeInterval FRAME_TIMEOUT = 1.;
|
||||
|
||||
static NSString *const SERVER_NAME = @"WDA MJPEG Server";
|
||||
static const char *QUEUE_NAME = "JPEG Screenshots Provider Queue";
|
||||
|
||||
|
||||
@interface FBMjpegServer()
|
||||
|
||||
@property (nonatomic, readonly) dispatch_queue_t backgroundQueue;
|
||||
@property (nonatomic, readonly) NSMutableArray<GCDAsyncSocket *> *listeningClients;
|
||||
@property (nonatomic, readonly) FBImageProcessor *imageProcessor;
|
||||
@property (nonatomic, readonly) long long mainScreenID;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBMjpegServer
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
_listeningClients = [NSMutableArray array];
|
||||
dispatch_queue_attr_t queueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
|
||||
_backgroundQueue = dispatch_queue_create(QUEUE_NAME, queueAttributes);
|
||||
dispatch_async(_backgroundQueue, ^{
|
||||
[self streamScreenshot];
|
||||
});
|
||||
_imageProcessor = [[FBImageProcessor alloc] init];
|
||||
_mainScreenID = [XCUIScreen.mainScreen displayID];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:(uint64_t)timeStarted
|
||||
{
|
||||
uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted;
|
||||
int64_t nextTickDelta = timerInterval - timeElapsed;
|
||||
if (nextTickDelta > 0) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, nextTickDelta), self.backgroundQueue, ^{
|
||||
[self streamScreenshot];
|
||||
});
|
||||
} else {
|
||||
// Try to do our best to keep the FPS at a decent level
|
||||
dispatch_async(self.backgroundQueue, ^{
|
||||
[self streamScreenshot];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)streamScreenshot
|
||||
{
|
||||
NSUInteger framerate = FBConfiguration.mjpegServerFramerate;
|
||||
uint64_t timerInterval = (uint64_t)(1.0 / ((0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate) * NSEC_PER_SEC);
|
||||
uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
|
||||
@synchronized (self.listeningClients) {
|
||||
if (0 == self.listeningClients.count) {
|
||||
[self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
CGFloat compressionQuality = MAX(FBMinCompressionQuality,
|
||||
MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0));
|
||||
NSData *screenshotData = [FBScreenshot takeInOriginalResolutionWithScreenID:self.mainScreenID
|
||||
compressionQuality:compressionQuality
|
||||
uti:UTTypeJPEG
|
||||
timeout:FRAME_TIMEOUT
|
||||
error:&error];
|
||||
if (nil == screenshotData) {
|
||||
[FBLogger logFmt:@"%@", error.description];
|
||||
[self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted];
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat scalingFactor = FBConfiguration.mjpegScalingFactor / 100.0;
|
||||
[self.imageProcessor submitImageData:screenshotData
|
||||
scalingFactor:scalingFactor
|
||||
completionHandler:^(NSData * _Nonnull scaled) {
|
||||
[self sendScreenshot:scaled];
|
||||
}];
|
||||
|
||||
[self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted];
|
||||
}
|
||||
|
||||
- (void)sendScreenshot:(NSData *)screenshotData {
|
||||
NSString *chunkHeader = [NSString stringWithFormat:@"--BoundaryString\r\nContent-type: image/jpeg\r\nContent-Length: %@\r\n\r\n", @(screenshotData.length)];
|
||||
NSMutableData *chunk = [[chunkHeader dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
|
||||
[chunk appendData:screenshotData];
|
||||
[chunk appendData:(id)[@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
@synchronized (self.listeningClients) {
|
||||
for (GCDAsyncSocket *client in self.listeningClients) {
|
||||
[client writeData:chunk withTimeout:-1 tag:0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didClientConnect:(GCDAsyncSocket *)newClient
|
||||
{
|
||||
[FBLogger logFmt:@"Got screenshots broadcast client connection at %@:%d", newClient.connectedHost, newClient.connectedPort];
|
||||
// Start broadcast only after there is any data from the client
|
||||
[newClient readDataWithTimeout:-1 tag:0];
|
||||
}
|
||||
|
||||
- (void)didClientSendData:(GCDAsyncSocket *)client
|
||||
{
|
||||
@synchronized (self.listeningClients) {
|
||||
if ([self.listeningClients containsObject:client]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[FBLogger logFmt:@"Starting screenshots broadcast for the client at %@:%d", client.connectedHost, client.connectedPort];
|
||||
NSString *streamHeader = [NSString stringWithFormat:@"HTTP/1.0 200 OK\r\nServer: %@\r\nConnection: close\r\nMax-Age: 0\r\nExpires: 0\r\nCache-Control: no-cache, private\r\nPragma: no-cache\r\nContent-Type: multipart/x-mixed-replace; boundary=--BoundaryString\r\n\r\n", SERVER_NAME];
|
||||
[client writeData:(id)[streamHeader dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
|
||||
@synchronized (self.listeningClients) {
|
||||
[self.listeningClients addObject:client];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didClientDisconnect:(GCDAsyncSocket *)client
|
||||
{
|
||||
@synchronized (self.listeningClients) {
|
||||
[self.listeningClients removeObject:client];
|
||||
}
|
||||
[FBLogger log:@"Disconnected a client from screenshots broadcast"];
|
||||
}
|
||||
|
||||
@end
|
||||
37
WebDriverAgentLib/Utilities/FBNotificationsHelper.h
Normal file
37
WebDriverAgentLib/Utilities/FBNotificationsHelper.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 FBNotificationsHelper : NSObject
|
||||
|
||||
/**
|
||||
Creates an expectation that is fulfilled when an expected NSNotification is received
|
||||
|
||||
@param name The name of the awaited notification
|
||||
@param timeout The maximum amount of float seconds to wait for the expectation
|
||||
@return The appropriate waiter result
|
||||
*/
|
||||
+ (XCTWaiterResult)waitForNotificationWithName:(NSNotificationName)name
|
||||
timeout:(NSTimeInterval)timeout;
|
||||
|
||||
/**
|
||||
Creates an expectation that is fulfilled when an expected Darwin notification is received
|
||||
|
||||
@param name The name of the awaited notification
|
||||
@param timeout The maximum amount of float seconds to wait for the expectation
|
||||
@return The appropriate waiter result
|
||||
*/
|
||||
+ (XCTWaiterResult)waitForDarwinNotificationWithName:(NSString *)name
|
||||
timeout:(NSTimeInterval)timeout;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
29
WebDriverAgentLib/Utilities/FBNotificationsHelper.m
Normal file
29
WebDriverAgentLib/Utilities/FBNotificationsHelper.m
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 "FBNotificationsHelper.h"
|
||||
|
||||
@implementation FBNotificationsHelper
|
||||
|
||||
+ (XCTWaiterResult)waitForNotificationWithName:(NSNotificationName)name
|
||||
timeout:(NSTimeInterval)timeout
|
||||
{
|
||||
XCTNSNotificationExpectation *expectation = [[XCTNSNotificationExpectation alloc]
|
||||
initWithName:name];
|
||||
return [XCTWaiter waitForExpectations:@[expectation] timeout:timeout];
|
||||
}
|
||||
|
||||
+ (XCTWaiterResult)waitForDarwinNotificationWithName:(NSString *)name
|
||||
timeout:(NSTimeInterval)timeout
|
||||
{
|
||||
XCTDarwinNotificationExpectation *expectation = [[XCTDarwinNotificationExpectation alloc]
|
||||
initWithNotificationName:name];
|
||||
return [XCTWaiter waitForExpectations:@[expectation] timeout:timeout];
|
||||
}
|
||||
|
||||
@end
|
||||
40
WebDriverAgentLib/Utilities/FBPasteboard.h
Normal file
40
WebDriverAgentLib/Utilities/FBPasteboard.h
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 <XCTest/XCTest.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@interface FBPasteboard : NSObject
|
||||
|
||||
/**
|
||||
Sets data to the general pasteboard
|
||||
|
||||
@param data base64-encoded string containing the data chunk which is going to be written to the pasteboard
|
||||
@param type one of the possible data types to set: plaintext, url, image
|
||||
@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)setData:(NSData *)data forType:(NSString *)type error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Gets the data contained in the general pasteboard
|
||||
|
||||
@param type one of the possible data types to get: plaintext, url, image
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem
|
||||
@return NSData object, containing the pasteboard content or an empty string if the pasteboard is empty.
|
||||
nil is returned if there was an error while getting the data from the pasteboard
|
||||
*/
|
||||
+ (nullable NSData *)dataForType:(NSString *)type error:(NSError **)error;
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
172
WebDriverAgentLib/Utilities/FBPasteboard.m
Normal file
172
WebDriverAgentLib/Utilities/FBPasteboard.m
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 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 "FBPasteboard.h"
|
||||
|
||||
#import <mach/mach_time.h>
|
||||
#import "FBAlert.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBMacros.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIApplication+FBAlert.h"
|
||||
|
||||
#define ALERT_TIMEOUT_SEC 30
|
||||
// Must not be less than FB_MONTORING_INTERVAL in FBAlertsMonitor
|
||||
#define ALERT_CHECK_INTERVAL_SEC 2
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@implementation FBPasteboard
|
||||
|
||||
+ (BOOL)setData:(NSData *)data forType:(NSString *)type error:(NSError **)error
|
||||
{
|
||||
UIPasteboard *pb = UIPasteboard.generalPasteboard;
|
||||
if ([type.lowercaseString isEqualToString:@"plaintext"]) {
|
||||
pb.string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
} else if ([type.lowercaseString isEqualToString:@"image"]) {
|
||||
UIImage *image = [UIImage imageWithData:data];
|
||||
if (nil == image) {
|
||||
NSString *description = @"No image can be parsed from the given pasteboard data";
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
pb.image = image;
|
||||
} else if ([type.lowercaseString isEqualToString:@"url"]) {
|
||||
NSString *urlString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
NSURL *url = [[NSURL alloc] initWithString:urlString];
|
||||
if (nil == url) {
|
||||
NSString *description = @"No URL can be parsed from the given pasteboard data";
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
pb.URL = url;
|
||||
} else {
|
||||
NSString *description = [NSString stringWithFormat:@"Unsupported content type: %@", type];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (nullable id)pasteboardContentForItem:(NSString *)item
|
||||
instance:(UIPasteboard *)pbInstance
|
||||
timeout:(NSTimeInterval)timeout
|
||||
error:(NSError **)error
|
||||
{
|
||||
SEL selector = NSSelectorFromString(item);
|
||||
NSMethodSignature *methodSignature = [pbInstance methodSignatureForSelector:selector];
|
||||
if (nil == methodSignature) {
|
||||
NSString *description = [NSString stringWithFormat:@"Cannot retrieve '%@' from a UIPasteboard instance", item];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
|
||||
[invocation setSelector:selector];
|
||||
[invocation setTarget:pbInstance];
|
||||
if (SYSTEM_VERSION_LESS_THAN(@"16.0")) {
|
||||
[invocation invoke];
|
||||
id __unsafe_unretained result;
|
||||
[invocation getReturnValue:&result];
|
||||
return result;
|
||||
}
|
||||
|
||||
// https://github.com/appium/appium/issues/17392
|
||||
__block id pasteboardContent;
|
||||
dispatch_queue_t backgroundQueue = dispatch_queue_create("GetPasteboard", NULL);
|
||||
__block BOOL didFinishGetPasteboard = NO;
|
||||
dispatch_async(backgroundQueue, ^{
|
||||
[invocation invoke];
|
||||
id __unsafe_unretained result;
|
||||
[invocation getReturnValue:&result];
|
||||
pasteboardContent = result;
|
||||
didFinishGetPasteboard = YES;
|
||||
});
|
||||
uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
|
||||
while (!didFinishGetPasteboard) {
|
||||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:ALERT_CHECK_INTERVAL_SEC]];
|
||||
if (didFinishGetPasteboard) {
|
||||
break;
|
||||
}
|
||||
|
||||
XCUIElement *alertElement = XCUIApplication.fb_systemApplication.fb_alertElement;
|
||||
if (nil != alertElement) {
|
||||
FBAlert *alert = [FBAlert alertWithElement:alertElement];
|
||||
[alert acceptWithError:nil];
|
||||
}
|
||||
uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted;
|
||||
if (timeElapsed / NSEC_PER_SEC > timeout) {
|
||||
NSString *description = [NSString stringWithFormat:@"Cannot handle pasteboard alert within %@s timeout", @(timeout)];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
return pasteboardContent;
|
||||
}
|
||||
|
||||
+ (NSData *)dataForType:(NSString *)type error:(NSError **)error
|
||||
{
|
||||
UIPasteboard *pb = UIPasteboard.generalPasteboard;
|
||||
if ([type.lowercaseString isEqualToString:@"plaintext"]) {
|
||||
if (pb.hasStrings) {
|
||||
id result = [self.class pasteboardContentForItem:@"strings"
|
||||
instance:pb
|
||||
timeout:ALERT_TIMEOUT_SEC
|
||||
error:error
|
||||
];
|
||||
return nil == result
|
||||
? nil
|
||||
: [[(NSArray *)result componentsJoinedByString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
} else if ([type.lowercaseString isEqualToString:@"image"]) {
|
||||
if (pb.hasImages) {
|
||||
id result = [self.class pasteboardContentForItem:@"image"
|
||||
instance:pb
|
||||
timeout:ALERT_TIMEOUT_SEC
|
||||
error:error
|
||||
];
|
||||
return nil == result ? nil : UIImagePNGRepresentation((UIImage *)result);
|
||||
}
|
||||
} else if ([type.lowercaseString isEqualToString:@"url"]) {
|
||||
if (pb.hasURLs) {
|
||||
id result = [self.class pasteboardContentForItem:@"URLs"
|
||||
instance:pb
|
||||
timeout:ALERT_TIMEOUT_SEC
|
||||
error:error
|
||||
];
|
||||
if (nil == result) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray<NSString *> *urls = [NSMutableArray array];
|
||||
for (NSURL *url in (NSArray *)result) {
|
||||
if (nil != url.absoluteString) {
|
||||
[urls addObject:(id)url.absoluteString];
|
||||
}
|
||||
}
|
||||
return [[urls componentsJoinedByString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
} else {
|
||||
NSString *description = [NSString stringWithFormat:@"Unsupported content type: %@", type];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
return [@"" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
46
WebDriverAgentLib/Utilities/FBProtocolHelpers.h
Normal file
46
WebDriverAgentLib/Utilities/FBProtocolHelpers.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
Prepares an element dictionary, which could be then used in hybrid W3C/JWP responses
|
||||
|
||||
@param element Either element identifier or element object itself
|
||||
@returns The resulting dictionary
|
||||
*/
|
||||
NSDictionary<NSString *, id> *FBToElementDict(id element);
|
||||
|
||||
/**
|
||||
Extracts element uuid from dictionary
|
||||
|
||||
@param src The source dictionary
|
||||
@returns The resulting element or nil if no element keys are found
|
||||
*/
|
||||
id _Nullable FBExtractElement(NSDictionary *src);
|
||||
|
||||
/**
|
||||
Cleanup items having element keys from the dictionary
|
||||
|
||||
@param src The source dictionary
|
||||
@returns The resulting dictionary
|
||||
*/
|
||||
NSDictionary *FBCleanupElements(NSDictionary *src);
|
||||
|
||||
/**
|
||||
Parses key/value pairs of valid W3C capabilities
|
||||
|
||||
@param caps The source capabilitites dictionary
|
||||
@param error Is set if there was an error while parsing the source capabilities
|
||||
@returns Parsed capabilitites mapping or nil in case of failure
|
||||
*/
|
||||
NSDictionary<NSString *, id> *_Nullable FBParseCapabilities(NSDictionary<NSString *, id> *caps, NSError **error);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
144
WebDriverAgentLib/Utilities/FBProtocolHelpers.m
Normal file
144
WebDriverAgentLib/Utilities/FBProtocolHelpers.m
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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 "FBProtocolHelpers.h"
|
||||
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBLogger.h"
|
||||
|
||||
static NSString *const W3C_ELEMENT_KEY = @"element-6066-11e4-a52e-4f735466cecf";
|
||||
static NSString *const JSONWP_ELEMENT_KEY = @"ELEMENT";
|
||||
|
||||
static NSString *const APPIUM_PREFIX = @"appium";
|
||||
static NSString *const ALWAYS_MATCH_KEY = @"alwaysMatch";
|
||||
static NSString *const FIRST_MATCH_KEY = @"firstMatch";
|
||||
|
||||
|
||||
NSDictionary<NSString *, id> *FBToElementDict(id element)
|
||||
{
|
||||
return @{
|
||||
W3C_ELEMENT_KEY: element,
|
||||
JSONWP_ELEMENT_KEY: element
|
||||
};
|
||||
}
|
||||
|
||||
id FBExtractElement(NSDictionary *src)
|
||||
{
|
||||
for (NSString* key in src) {
|
||||
if ([key.lowercaseString isEqualToString:W3C_ELEMENT_KEY.lowercaseString]
|
||||
|| [key.lowercaseString isEqualToString:JSONWP_ELEMENT_KEY.lowercaseString]) {
|
||||
return src[key];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *FBCleanupElements(NSDictionary *src)
|
||||
{
|
||||
NSMutableDictionary *result = src.mutableCopy;
|
||||
for (NSString* key in src) {
|
||||
if ([key.lowercaseString isEqualToString:W3C_ELEMENT_KEY.lowercaseString]
|
||||
|| [key.lowercaseString isEqualToString:JSONWP_ELEMENT_KEY.lowercaseString]) {
|
||||
[result removeObjectForKey:key];
|
||||
}
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
NSArray<NSString *> *standardCapabilities(void)
|
||||
{
|
||||
static NSArray<NSString *> *standardCaps;
|
||||
static dispatch_once_t onceStandardCaps;
|
||||
dispatch_once(&onceStandardCaps, ^{
|
||||
standardCaps = @[
|
||||
@"browserName",
|
||||
@"browserVersion",
|
||||
@"platformName",
|
||||
@"acceptInsecureCerts",
|
||||
@"pageLoadStrategy",
|
||||
@"proxy",
|
||||
@"setWindowRect",
|
||||
@"timeouts",
|
||||
@"unhandledPromptBehavior"
|
||||
];
|
||||
});
|
||||
return standardCaps;
|
||||
}
|
||||
|
||||
BOOL isStandardCap(NSString *capName)
|
||||
{
|
||||
return [standardCapabilities() containsObject:capName];
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, id> *_Nullable mergeCaps(NSDictionary<NSString *, id> *primary, NSDictionary<NSString *, id> *secondary, NSError **error)
|
||||
{
|
||||
NSMutableDictionary<NSString *, id> *result = primary.mutableCopy;
|
||||
for (NSString *capName in secondary) {
|
||||
if (nil != result[capName]) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Property '%@' should not exist on both primary (%@) and secondary (%@) objects", capName, primary, secondary]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
[result setObject:(id) secondary[capName] forKey:capName];
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, id> *_Nullable stripPrefixes(NSDictionary<NSString *, id> *caps, NSError **error)
|
||||
{
|
||||
NSString* prefix = [NSString stringWithFormat:@"%@:", APPIUM_PREFIX];
|
||||
NSMutableDictionary<NSString *, id> *filteredCaps = [NSMutableDictionary dictionary];
|
||||
NSMutableArray<NSString *> *badPrefixedCaps = [NSMutableArray array];
|
||||
for (NSString *capName in caps) {
|
||||
if (![capName hasPrefix:prefix]) {
|
||||
[filteredCaps setObject:(id) caps[capName] forKey:capName];
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *strippedName = [capName substringFromIndex:prefix.length];
|
||||
[filteredCaps setObject:(id) caps[capName] forKey:strippedName];
|
||||
if (isStandardCap(strippedName)) {
|
||||
[badPrefixedCaps addObject:strippedName];
|
||||
}
|
||||
}
|
||||
if (badPrefixedCaps.count > 0) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The capabilities %@ are standard and should not have the '%@' prefix", badPrefixedCaps, prefix]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
return filteredCaps.copy;
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, id> *FBParseCapabilities(NSDictionary<NSString *, id> *caps, NSError **error)
|
||||
{
|
||||
NSDictionary<NSString *, id> *alwaysMatch = caps[ALWAYS_MATCH_KEY] ?: @{};
|
||||
NSArray<NSDictionary<NSString *, id> *> *firstMatch = caps[FIRST_MATCH_KEY] ?: @[];
|
||||
NSArray<NSDictionary<NSString *, id> *> *allFirstMatchCaps = firstMatch.count == 0 ? @[@{}] : firstMatch;
|
||||
NSDictionary<NSString *, id> *requiredCaps;
|
||||
if (nil == (requiredCaps = stripPrefixes(alwaysMatch, error))) {
|
||||
return nil;
|
||||
}
|
||||
for (NSDictionary<NSString *, id> *fmc in allFirstMatchCaps) {
|
||||
NSDictionary<NSString *, id> *strippedCaps;
|
||||
if (nil == (strippedCaps = stripPrefixes(fmc, error))) {
|
||||
return nil;
|
||||
}
|
||||
NSDictionary<NSString *, id> *mergedCaps;
|
||||
if (nil == (mergedCaps = mergeCaps(requiredCaps, strippedCaps, error))) {
|
||||
[FBLogger logFmt:@"%@", (*error).description];
|
||||
continue;
|
||||
}
|
||||
return mergedCaps;
|
||||
}
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Could not find matching capabilities from %@", caps]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
23
WebDriverAgentLib/Utilities/FBReflectionUtils.h
Normal file
23
WebDriverAgentLib/Utilities/FBReflectionUtils.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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* Swizzles the implemntation of originalSelector with the swizzledSelector for the given class.
|
||||
* Both methods must belong to this class.
|
||||
*
|
||||
* @param cls The class where to swizzle
|
||||
* @param originalSelector original method selector
|
||||
* @paramswizzledSelector swizzled method selector
|
||||
*/
|
||||
void FBReplaceMethod(Class cls, SEL originalSelector, SEL swizzledSelector);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
31
WebDriverAgentLib/Utilities/FBReflectionUtils.m
Normal file
31
WebDriverAgentLib/Utilities/FBReflectionUtils.m
Normal file
@@ -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 "FBReflectionUtils.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
void FBReplaceMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
|
||||
Method originalMethod = class_getInstanceMethod(class, originalSelector);
|
||||
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
|
||||
|
||||
BOOL didAddMethod =
|
||||
class_addMethod(class,
|
||||
originalSelector,
|
||||
method_getImplementation(swizzledMethod),
|
||||
method_getTypeEncoding(swizzledMethod));
|
||||
|
||||
if (didAddMethod) {
|
||||
class_replaceMethod(class,
|
||||
swizzledSelector,
|
||||
method_getImplementation(originalMethod),
|
||||
method_getTypeEncoding(originalMethod));
|
||||
} else {
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod);
|
||||
}
|
||||
}
|
||||
77
WebDriverAgentLib/Utilities/FBRunLoopSpinner.h
Normal file
77
WebDriverAgentLib/Utilities/FBRunLoopSpinner.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
typedef BOOL (^FBRunLoopSpinnerBlock)(void);
|
||||
typedef __nullable id (^FBRunLoopSpinnerObjectBlock)(void);
|
||||
|
||||
@interface FBRunLoopSpinner : NSObject
|
||||
|
||||
/**
|
||||
Dispatches block and spins the run loop until `completion` block is called.
|
||||
|
||||
@param block the block to wait for to finish.
|
||||
*/
|
||||
+ (void)spinUntilCompletion:(void (^)(void(^completion)(void)))block;
|
||||
|
||||
/**
|
||||
Updates the error message to print in the event of a timeout.
|
||||
|
||||
@param timeoutErrorMessage the Error Message to print.
|
||||
@return the receiver, for chaining.
|
||||
*/
|
||||
- (instancetype)timeoutErrorMessage:(NSString *)timeoutErrorMessage;
|
||||
|
||||
/**
|
||||
Updates the timeout of the receiver.
|
||||
|
||||
@param timeout the amount of time to wait before timing out.
|
||||
@return the receiver, for chaining.
|
||||
*/
|
||||
- (instancetype)timeout:(NSTimeInterval)timeout;
|
||||
|
||||
/**
|
||||
Updates the interval of the receiver.
|
||||
|
||||
@param interval the amount of time to wait before checking condition again.
|
||||
@return the receiver, for chaining.
|
||||
*/
|
||||
- (instancetype)interval:(NSTimeInterval)interval;
|
||||
|
||||
/**
|
||||
Spins the Run Loop until `untilTrue` returns YES or a timeout is reached.
|
||||
|
||||
@param untilTrue the condition to meet.
|
||||
@return YES if the condition was met, NO if the timeout was reached first.
|
||||
*/
|
||||
- (BOOL)spinUntilTrue:(FBRunLoopSpinnerBlock)untilTrue;
|
||||
|
||||
/**
|
||||
Spins the Run Loop until `untilTrue` returns YES or a timeout is reached.
|
||||
|
||||
@param untilTrue the condition to meet.
|
||||
@param error to fill in case of timeout.
|
||||
@return YES if the condition was met, NO if the timeout was reached first.
|
||||
*/
|
||||
- (BOOL)spinUntilTrue:(FBRunLoopSpinnerBlock)untilTrue error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Spins the Run Loop until `untilNotNil` returns non nil value or a timeout is reached.
|
||||
|
||||
@param untilNotNil the condition to meet.
|
||||
@param error to fill in case of timeout.
|
||||
@return YES if the condition was met, NO if the timeout was reached first.
|
||||
*/
|
||||
- (nullable id)spinUntilNotNil:(FBRunLoopSpinnerObjectBlock)untilNotNil error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
94
WebDriverAgentLib/Utilities/FBRunLoopSpinner.m
Normal file
94
WebDriverAgentLib/Utilities/FBRunLoopSpinner.m
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 "FBRunLoopSpinner.h"
|
||||
|
||||
#import <stdatomic.h>
|
||||
|
||||
#import "FBErrorBuilder.h"
|
||||
|
||||
static const NSTimeInterval FBWaitInterval = 0.1;
|
||||
|
||||
@interface FBRunLoopSpinner ()
|
||||
@property (nonatomic, copy) NSString *timeoutErrorMessage;
|
||||
@property (nonatomic, assign) NSTimeInterval timeout;
|
||||
@property (nonatomic, assign) NSTimeInterval interval;
|
||||
@end
|
||||
|
||||
@implementation FBRunLoopSpinner
|
||||
|
||||
+ (void)spinUntilCompletion:(void (^)(void(^completion)(void)))block
|
||||
{
|
||||
__block volatile atomic_bool didFinish = false;
|
||||
block(^{
|
||||
atomic_fetch_or(&didFinish, true);
|
||||
});
|
||||
while (!atomic_fetch_and(&didFinish, false)) {
|
||||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBWaitInterval]];
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_interval = FBWaitInterval;
|
||||
_timeout = 60;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)timeoutErrorMessage:(NSString *)timeoutErrorMessage
|
||||
{
|
||||
self.timeoutErrorMessage = timeoutErrorMessage;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)timeout:(NSTimeInterval)timeout
|
||||
{
|
||||
self.timeout = timeout;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)interval:(NSTimeInterval)interval
|
||||
{
|
||||
self.interval = interval;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)spinUntilTrue:(FBRunLoopSpinnerBlock)untilTrue
|
||||
{
|
||||
return [self spinUntilTrue:untilTrue error:nil];
|
||||
}
|
||||
|
||||
- (BOOL)spinUntilTrue:(FBRunLoopSpinnerBlock)untilTrue error:(NSError **)error
|
||||
{
|
||||
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:self.timeout];
|
||||
while (!untilTrue()) {
|
||||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:self.interval]];
|
||||
if (timeoutDate.timeIntervalSinceNow < 0) {
|
||||
return
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescription:(self.timeoutErrorMessage ?: @"FBRunLoopSpinner timeout")]
|
||||
buildError:error];
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (id)spinUntilNotNil:(FBRunLoopSpinnerObjectBlock)untilNotNil error:(NSError **)error
|
||||
{
|
||||
__block id object;
|
||||
[self spinUntilTrue:^BOOL{
|
||||
object = untilNotNil();
|
||||
return object != nil;
|
||||
} error:error];
|
||||
return object;
|
||||
}
|
||||
|
||||
@end
|
||||
79
WebDriverAgentLib/Utilities/FBRuntimeUtils.h
Normal file
79
WebDriverAgentLib/Utilities/FBRuntimeUtils.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
Returns array of classes that conforms to given protocol
|
||||
*/
|
||||
NSArray<Class> *FBClassesThatConformsToProtocol(Protocol *protocol);
|
||||
|
||||
/**
|
||||
Method used to retrieve pointer for given symbol 'name' from given 'binary'
|
||||
|
||||
@param binary path to binary we want to retrieve symbols pointer from
|
||||
@param name name of the symbol
|
||||
@return pointer to symbol
|
||||
*/
|
||||
void *FBRetrieveSymbolFromBinary(const char *binary, const char *name);
|
||||
|
||||
/**
|
||||
Get the compiler SDK version as string.
|
||||
|
||||
@return SDK version as string, for example "10.0" or nil if it cannot be received
|
||||
*/
|
||||
NSString * _Nullable FBSDKVersion(void);
|
||||
|
||||
/**
|
||||
Check if the compiler SDK version is less than the given version.
|
||||
The current iOS version is taken instead if SDK version cannot be retrieved.
|
||||
|
||||
@param expected the expected version to compare with, for example '10.3'
|
||||
@return YES if the given version is less than the SDK version used for WDA compilation
|
||||
*/
|
||||
BOOL isSDKVersionLessThan(NSString *expected);
|
||||
|
||||
/**
|
||||
Check if the compiler SDK version is less or equal to the given version.
|
||||
The current iOS version is taken instead if SDK version cannot be retrieved.
|
||||
|
||||
@param expected the expected version to compare with, for example '10.3'
|
||||
@return YES if the given version is less or equal to the SDK version used for WDA compilation
|
||||
*/
|
||||
BOOL isSDKVersionLessThanOrEqualTo(NSString *expected);
|
||||
|
||||
/**
|
||||
Check if the compiler SDK version is equal to the given version.
|
||||
The current iOS version is taken instead if SDK version cannot be retrieved.
|
||||
|
||||
@param expected the expected version to compare with, for example '10.3'
|
||||
@return YES if the given version is equal to the SDK version used for WDA compilation
|
||||
*/
|
||||
BOOL isSDKVersionEqualTo(NSString *expected);
|
||||
|
||||
/**
|
||||
Check if the compiler SDK version is greater or equal to the given version.
|
||||
The current iOS version is taken instead if SDK version cannot be retrieved.
|
||||
|
||||
@param expected the expected version to compare with, for example '10.3'
|
||||
@return YES if the given version is greater or equal to the SDK version used for WDA compilation
|
||||
*/
|
||||
BOOL isSDKVersionGreaterThanOrEqualTo(NSString *expected);
|
||||
|
||||
/**
|
||||
Check if the compiler SDK version is greater than the given version.
|
||||
The current iOS version is taken instead if SDK version cannot be retrieved.
|
||||
|
||||
@param expected the expected version to compare with, for example '10.3'
|
||||
@return YES if the given version is greater than the SDK version used for WDA compilation
|
||||
*/
|
||||
BOOL isSDKVersionGreaterThan(NSString *expected);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
112
WebDriverAgentLib/Utilities/FBRuntimeUtils.m
Normal file
112
WebDriverAgentLib/Utilities/FBRuntimeUtils.m
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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 "FBRuntimeUtils.h"
|
||||
|
||||
#import "FBMacros.h"
|
||||
#import "XCUIDevice.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
NSArray<Class> *FBClassesThatConformsToProtocol(Protocol *protocol)
|
||||
{
|
||||
Class *classes = NULL;
|
||||
NSMutableArray *collection = [NSMutableArray array];
|
||||
int numClasses = objc_getClassList(NULL, 0);
|
||||
if (numClasses == 0 ) {
|
||||
return @[];
|
||||
}
|
||||
|
||||
classes = (__unsafe_unretained Class*)malloc(sizeof(Class) * numClasses);
|
||||
numClasses = objc_getClassList(classes, numClasses);
|
||||
for (int index = 0; index < numClasses; index++) {
|
||||
Class aClass = classes[index];
|
||||
if (class_conformsToProtocol(aClass, protocol)) {
|
||||
[collection addObject:aClass];
|
||||
}
|
||||
}
|
||||
free(classes);
|
||||
return collection.copy;
|
||||
}
|
||||
|
||||
void *FBRetrieveSymbolFromBinary(const char *binary, const char *name)
|
||||
{
|
||||
void *handle = dlopen(binary, RTLD_LAZY);
|
||||
NSCAssert(handle, @"%s could not be opened", binary);
|
||||
void *pointer = dlsym(handle, name);
|
||||
NSCAssert(pointer, @"%s could not be located", name);
|
||||
return pointer;
|
||||
}
|
||||
|
||||
static NSString *sdkVersion = nil;
|
||||
static dispatch_once_t onceSdkVersionToken;
|
||||
NSString * _Nullable FBSDKVersion(void)
|
||||
{
|
||||
dispatch_once(&onceSdkVersionToken, ^{
|
||||
NSString *sdkName = [[NSBundle mainBundle] infoDictionary][@"DTSDKName"];
|
||||
if (sdkName) {
|
||||
// the value of DTSDKName looks like 'iphoneos9.2'
|
||||
NSRange versionRange = [sdkName rangeOfString:@"\\d+\\.\\d+" options:NSRegularExpressionSearch];
|
||||
if (versionRange.location != NSNotFound) {
|
||||
sdkVersion = [sdkName substringWithRange:versionRange];
|
||||
}
|
||||
}
|
||||
});
|
||||
return sdkVersion;
|
||||
}
|
||||
|
||||
BOOL isSDKVersionLessThan(NSString *expected)
|
||||
{
|
||||
NSString *version = FBSDKVersion();
|
||||
if (nil == version) {
|
||||
return SYSTEM_VERSION_LESS_THAN(expected);
|
||||
}
|
||||
NSComparisonResult result = [version compare:expected options:NSNumericSearch];
|
||||
return result == NSOrderedAscending;
|
||||
}
|
||||
|
||||
BOOL isSDKVersionLessThanOrEqualTo(NSString *expected)
|
||||
{
|
||||
NSString *version = FBSDKVersion();
|
||||
if (nil == version) {
|
||||
return SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(expected);
|
||||
}
|
||||
NSComparisonResult result = [version compare:expected options:NSNumericSearch];
|
||||
return result == NSOrderedAscending || result == NSOrderedSame;
|
||||
}
|
||||
|
||||
BOOL isSDKVersionEqualTo(NSString *expected)
|
||||
{
|
||||
NSString *version = FBSDKVersion();
|
||||
if (nil == version) {
|
||||
return SYSTEM_VERSION_EQUAL_TO(expected);
|
||||
}
|
||||
NSComparisonResult result = [version compare:expected options:NSNumericSearch];
|
||||
return result == NSOrderedSame;
|
||||
}
|
||||
|
||||
BOOL isSDKVersionGreaterThanOrEqualTo(NSString *expected)
|
||||
{
|
||||
NSString *version = FBSDKVersion();
|
||||
if (nil == version) {
|
||||
return SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(expected);
|
||||
}
|
||||
NSComparisonResult result = [version compare:expected options:NSNumericSearch];
|
||||
return result == NSOrderedDescending || result == NSOrderedSame;
|
||||
}
|
||||
|
||||
BOOL isSDKVersionGreaterThan(NSString *expected)
|
||||
{
|
||||
NSString *version = FBSDKVersion();
|
||||
if (nil == version) {
|
||||
return SYSTEM_VERSION_GREATER_THAN(expected);
|
||||
}
|
||||
NSComparisonResult result = [version compare:expected options:NSNumericSearch];
|
||||
return result == NSOrderedDescending;
|
||||
}
|
||||
22
WebDriverAgentLib/Utilities/FBScreen.h
Normal file
22
WebDriverAgentLib/Utilities/FBScreen.h
Normal file
@@ -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 FBScreen : NSObject
|
||||
|
||||
/**
|
||||
The scale factor of the main device's screen
|
||||
*/
|
||||
+ (double)scale;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
21
WebDriverAgentLib/Utilities/FBScreen.m
Normal file
21
WebDriverAgentLib/Utilities/FBScreen.m
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 "FBScreen.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "XCUIScreen.h"
|
||||
|
||||
@implementation FBScreen
|
||||
|
||||
+ (double)scale
|
||||
{
|
||||
return [XCUIScreen.mainScreen scale];
|
||||
}
|
||||
|
||||
@end
|
||||
44
WebDriverAgentLib/Utilities/FBScreenshot.h
Normal file
44
WebDriverAgentLib/Utilities/FBScreenshot.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 UTType;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBScreenshot : NSObject
|
||||
|
||||
/**
|
||||
Retrieves non-scaled screenshot of the whole screen
|
||||
|
||||
@param quality The number in range 0-3, where 0 is PNG (lossless), 3 is HEIC (lossless), 1- low quality JPEG and 2 - high quality JPEG
|
||||
@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 *)takeInOriginalResolutionWithQuality:(NSUInteger)quality
|
||||
error:(NSError **)error;
|
||||
|
||||
/**
|
||||
Retrieves non-scaled screenshot of the whole screen
|
||||
|
||||
@param screenID The screen identifier to take the screenshot from
|
||||
@param compressionQuality Normalized screenshot quality value in range 0..1, where 1 is the best quality
|
||||
@param uti UTType... constant, which defines the type of the returned screenshot image
|
||||
@param timeout how much time to allow for the screenshot to be taken
|
||||
@param error If there is an error, upon return contains an NSError object that describes the problem.
|
||||
@return Device screenshot as PNG-, HEIC- or JPG-encoded data or nil in case of failure
|
||||
*/
|
||||
+ (nullable NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID
|
||||
compressionQuality:(CGFloat)compressionQuality
|
||||
uti:(UTType *)uti
|
||||
timeout:(NSTimeInterval)timeout
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
227
WebDriverAgentLib/Utilities/FBScreenshot.m
Normal file
227
WebDriverAgentLib/Utilities/FBScreenshot.m
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 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 "FBScreenshot.h"
|
||||
|
||||
@import UniformTypeIdentifiers;
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBImageProcessor.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "XCTestManager_ManagerInterface-Protocol.h"
|
||||
#import "XCUIScreen.h"
|
||||
|
||||
static const NSTimeInterval SCREENSHOT_TIMEOUT = 20.;
|
||||
static const CGFloat SCREENSHOT_SCALE = 1.0; // Screenshot API should keep the original screen scale
|
||||
static const CGFloat HIGH_QUALITY = 0.8;
|
||||
static const CGFloat LOW_QUALITY = 0.25;
|
||||
|
||||
NSString *formatTimeInterval(NSTimeInterval interval) {
|
||||
NSUInteger milliseconds = (NSUInteger)(interval * 1000);
|
||||
return [NSString stringWithFormat:@"%lu ms", milliseconds];
|
||||
}
|
||||
|
||||
@implementation FBScreenshot
|
||||
|
||||
+ (CGFloat)compressionQualityWithQuality:(NSUInteger)quality
|
||||
{
|
||||
switch (quality) {
|
||||
case 1:
|
||||
return HIGH_QUALITY;
|
||||
case 2:
|
||||
return LOW_QUALITY;
|
||||
default:
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
+ (UTType *)imageUtiWithQuality:(NSUInteger)quality
|
||||
{
|
||||
switch (quality) {
|
||||
case 1:
|
||||
case 2:
|
||||
return UTTypeJPEG;
|
||||
case 3:
|
||||
return UTTypeHEIC;
|
||||
default:
|
||||
return UTTypePNG;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSData *)takeInOriginalResolutionWithQuality:(NSUInteger)quality
|
||||
error:(NSError **)error
|
||||
{
|
||||
XCUIScreen *mainScreen = XCUIScreen.mainScreen;
|
||||
return [self.class takeWithScreenID:mainScreen.displayID
|
||||
scale:SCREENSHOT_SCALE
|
||||
compressionQuality:[self.class compressionQualityWithQuality:quality]
|
||||
sourceUTI:[self.class imageUtiWithQuality:quality]
|
||||
error:error];
|
||||
}
|
||||
|
||||
+ (NSData *)takeWithScreenID:(long long)screenID
|
||||
scale:(CGFloat)scale
|
||||
compressionQuality:(CGFloat)compressionQuality
|
||||
sourceUTI:(UTType *)uti
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSData *screenshotData = [self.class takeInOriginalResolutionWithScreenID:screenID
|
||||
compressionQuality:compressionQuality
|
||||
uti:uti
|
||||
timeout:SCREENSHOT_TIMEOUT
|
||||
error:error];
|
||||
if (nil == screenshotData) {
|
||||
return nil;
|
||||
}
|
||||
return [[[FBImageProcessor alloc] init] scaledImageWithData:screenshotData
|
||||
uti:UTTypePNG
|
||||
scalingFactor:1.0 / scale
|
||||
compressionQuality:FBMaxCompressionQuality
|
||||
error:error];
|
||||
}
|
||||
|
||||
+ (NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID
|
||||
compressionQuality:(CGFloat)compressionQuality
|
||||
uti:(UTType *)uti
|
||||
timeout:(NSTimeInterval)timeout
|
||||
error:(NSError **)error
|
||||
{
|
||||
id<XCTestManager_ManagerInterface> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
|
||||
__block NSData *screenshotData = nil;
|
||||
__block NSError *innerError = nil;
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
id screnshotRequest = [self.class screenshotRequestWithScreenID:screenID
|
||||
rect:CGRectNull
|
||||
uti:uti
|
||||
compressionQuality:compressionQuality
|
||||
error:error];
|
||||
if (nil == screnshotRequest) {
|
||||
return nil;
|
||||
}
|
||||
[proxy _XCT_requestScreenshot:screnshotRequest
|
||||
withReply:^(id image, NSError *err) {
|
||||
if (nil != err) {
|
||||
innerError = err;
|
||||
} else {
|
||||
screenshotData = [image data];
|
||||
}
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC);
|
||||
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
|
||||
NSString *timeoutMsg = [NSString stringWithFormat:@"Cannot take a screenshot within %@ timeout", formatTimeInterval(SCREENSHOT_TIMEOUT)];
|
||||
if (nil == error) {
|
||||
[FBLogger log:timeoutMsg];
|
||||
} else if (nil == innerError) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescription:timeoutMsg]
|
||||
buildError:error];
|
||||
}
|
||||
};
|
||||
if (nil != error && nil != innerError) {
|
||||
*error = innerError;
|
||||
}
|
||||
return screenshotData;
|
||||
}
|
||||
|
||||
+ (nullable id)imageEncodingWithUniformTypeIdentifier:(UTType *)uti
|
||||
compressionQuality:(CGFloat)compressionQuality
|
||||
error:(NSError **)error
|
||||
{
|
||||
Class imageEncodingClass = NSClassFromString(@"XCTImageEncoding");
|
||||
if (nil == imageEncodingClass) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescription:@"Cannot find XCTImageEncoding class"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ([uti conformsToType:UTTypeHEIC]) {
|
||||
static BOOL isHeicSuppported = NO;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
SEL selector = NSSelectorFromString(@"supportsHEICImageEncoding");
|
||||
NSMethodSignature *signature = [imageEncodingClass methodSignatureForSelector:selector];
|
||||
if (nil != signature) {
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
|
||||
[invocation setSelector:selector];
|
||||
[invocation invokeWithTarget:imageEncodingClass];
|
||||
[invocation getReturnValue:&isHeicSuppported];
|
||||
}
|
||||
});
|
||||
if (!isHeicSuppported) {
|
||||
[FBLogger logFmt:@"The device under test does not support HEIC image encoding. Falling back to PNG"];
|
||||
uti = UTTypePNG;
|
||||
}
|
||||
}
|
||||
|
||||
id imageEncodingAllocated = [imageEncodingClass alloc];
|
||||
SEL imageEncodingConstructorSelector = NSSelectorFromString(@"initWithUniformTypeIdentifier:compressionQuality:");
|
||||
if (![imageEncodingAllocated respondsToSelector:imageEncodingConstructorSelector]) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescription:@"'initWithUniformTypeIdentifier:compressionQuality:' contructor is not found on XCTImageEncoding class"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
NSMethodSignature *imageEncodingContructorSignature = [imageEncodingAllocated methodSignatureForSelector:imageEncodingConstructorSelector];
|
||||
NSInvocation *imageEncodingInitInvocation = [NSInvocation invocationWithMethodSignature:imageEncodingContructorSignature];
|
||||
[imageEncodingInitInvocation setSelector:imageEncodingConstructorSelector];
|
||||
NSString *utiIdentifier = uti.identifier;
|
||||
[imageEncodingInitInvocation setArgument:&utiIdentifier atIndex:2];
|
||||
[imageEncodingInitInvocation setArgument:&compressionQuality atIndex:3];
|
||||
[imageEncodingInitInvocation invokeWithTarget:imageEncodingAllocated];
|
||||
id __unsafe_unretained imageEncoding;
|
||||
[imageEncodingInitInvocation getReturnValue:&imageEncoding];
|
||||
return imageEncoding;
|
||||
}
|
||||
|
||||
+ (nullable id)screenshotRequestWithScreenID:(long long)screenID
|
||||
rect:(struct CGRect)rect
|
||||
uti:(UTType *)uti
|
||||
compressionQuality:(CGFloat)compressionQuality
|
||||
error:(NSError **)error
|
||||
{
|
||||
id imageEncoding = [self.class imageEncodingWithUniformTypeIdentifier:uti
|
||||
compressionQuality:compressionQuality
|
||||
error:error];
|
||||
if (nil == imageEncoding) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
Class screenshotRequestClass = NSClassFromString(@"XCTScreenshotRequest");
|
||||
if (nil == screenshotRequestClass) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescription:@"Cannot find XCTScreenshotRequest class"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
id screenshotRequestAllocated = [screenshotRequestClass alloc];
|
||||
SEL screenshotRequestConstructorSelector = NSSelectorFromString(@"initWithScreenID:rect:encoding:");
|
||||
if (![screenshotRequestAllocated respondsToSelector:screenshotRequestConstructorSelector]) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescription:@"'initWithScreenID:rect:encoding:' contructor is not found on XCTScreenshotRequest class"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
NSMethodSignature *screenshotRequestContructorSignature = [screenshotRequestAllocated methodSignatureForSelector:screenshotRequestConstructorSelector];
|
||||
NSInvocation *screenshotRequestInitInvocation = [NSInvocation invocationWithMethodSignature:screenshotRequestContructorSignature];
|
||||
[screenshotRequestInitInvocation setSelector:screenshotRequestConstructorSelector];
|
||||
[screenshotRequestInitInvocation setArgument:&screenID atIndex:2];
|
||||
[screenshotRequestInitInvocation setArgument:&rect atIndex:3];
|
||||
[screenshotRequestInitInvocation setArgument:&imageEncoding atIndex:4];
|
||||
[screenshotRequestInitInvocation invokeWithTarget:screenshotRequestAllocated];
|
||||
id __unsafe_unretained screenshotRequest;
|
||||
[screenshotRequestInitInvocation getReturnValue:&screenshotRequest];
|
||||
return screenshotRequest;
|
||||
}
|
||||
|
||||
@end
|
||||
46
WebDriverAgentLib/Utilities/FBSettings.h
Normal file
46
WebDriverAgentLib/Utilities/FBSettings.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
// See FBConfiguration.h for more details on the meaning of each setting
|
||||
|
||||
extern NSString* const FB_SETTING_USE_COMPACT_RESPONSES;
|
||||
extern NSString* const FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES;
|
||||
extern NSString* const FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY;
|
||||
extern NSString* const FB_SETTING_MJPEG_SERVER_FRAMERATE;
|
||||
extern NSString* const FB_SETTING_MJPEG_FIX_ORIENTATION;
|
||||
extern NSString* const FB_SETTING_MJPEG_SCALING_FACTOR;
|
||||
extern NSString* const FB_SETTING_SCREENSHOT_QUALITY;
|
||||
extern NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION;
|
||||
extern NSString* const FB_SETTING_KEYBOARD_PREDICTION;
|
||||
extern NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH;
|
||||
extern NSString* const FB_SETTING_USE_FIRST_MATCH;
|
||||
extern NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX;
|
||||
extern NSString* const FB_SETTING_REDUCE_MOTION;
|
||||
extern NSString* const FB_SETTING_DEFAULT_ACTIVE_APPLICATION;
|
||||
extern NSString* const FB_SETTING_ACTIVE_APP_DETECTION_POINT;
|
||||
extern NSString* const FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS;
|
||||
extern NSString* const FB_SETTING_DEFAULT_ALERT_ACTION;
|
||||
extern NSString* const FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR;
|
||||
extern NSString* const FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR;
|
||||
extern NSString* const FB_SETTING_SCREENSHOT_ORIENTATION;
|
||||
extern NSString* const FB_SETTING_WAIT_FOR_IDLE_TIMEOUT;
|
||||
extern NSString* const FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT;
|
||||
extern NSString* const FB_SETTING_MAX_TYPING_FREQUENCY;
|
||||
extern NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS;
|
||||
extern NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT;
|
||||
extern NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE;
|
||||
extern NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR;
|
||||
extern NSString *const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE;
|
||||
extern NSString *const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE;
|
||||
extern NSString *const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE;
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
40
WebDriverAgentLib/Utilities/FBSettings.m
Normal file
40
WebDriverAgentLib/Utilities/FBSettings.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 "FBSettings.h"
|
||||
|
||||
NSString* const FB_SETTING_USE_COMPACT_RESPONSES = @"shouldUseCompactResponses";
|
||||
NSString* const FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES = @"elementResponseAttributes";
|
||||
NSString* const FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY = @"mjpegServerScreenshotQuality";
|
||||
NSString* const FB_SETTING_MJPEG_SERVER_FRAMERATE = @"mjpegServerFramerate";
|
||||
NSString* const FB_SETTING_MJPEG_SCALING_FACTOR = @"mjpegScalingFactor";
|
||||
NSString* const FB_SETTING_MJPEG_FIX_ORIENTATION = @"mjpegFixOrientation";
|
||||
NSString* const FB_SETTING_SCREENSHOT_QUALITY = @"screenshotQuality";
|
||||
NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION = @"keyboardAutocorrection";
|
||||
NSString* const FB_SETTING_KEYBOARD_PREDICTION = @"keyboardPrediction";
|
||||
NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH = @"snapshotMaxDepth";
|
||||
NSString* const FB_SETTING_USE_FIRST_MATCH = @"useFirstMatch";
|
||||
NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX = @"boundElementsByIndex";
|
||||
NSString* const FB_SETTING_REDUCE_MOTION = @"reduceMotion";
|
||||
NSString* const FB_SETTING_DEFAULT_ACTIVE_APPLICATION = @"defaultActiveApplication";
|
||||
NSString* const FB_SETTING_ACTIVE_APP_DETECTION_POINT = @"activeAppDetectionPoint";
|
||||
NSString* const FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS = @"includeNonModalElements";
|
||||
NSString* const FB_SETTING_DEFAULT_ALERT_ACTION = @"defaultAlertAction";
|
||||
NSString* const FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR = @"acceptAlertButtonSelector";
|
||||
NSString* const FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR = @"dismissAlertButtonSelector";
|
||||
NSString* const FB_SETTING_SCREENSHOT_ORIENTATION = @"screenshotOrientation";
|
||||
NSString* const FB_SETTING_WAIT_FOR_IDLE_TIMEOUT = @"waitForIdleTimeout";
|
||||
NSString* const FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT = @"animationCoolOffTimeout";
|
||||
NSString* const FB_SETTING_MAX_TYPING_FREQUENCY = @"maxTypingFrequency";
|
||||
NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS = @"respectSystemAlerts";
|
||||
NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT = @"useClearTextShortcut";
|
||||
NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE = @"limitXPathContextScope";
|
||||
NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR = @"autoClickAlertSelector";
|
||||
NSString* const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE = @"includeHittableInPageSource";
|
||||
NSString* const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE = @"includeNativeFrameInPageSource";
|
||||
NSString* const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE = @"includeMinMaxValueInPageSource";
|
||||
27
WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h
Normal file
27
WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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 <Foundation/Foundation.h>
|
||||
|
||||
#if TARGET_OS_TV
|
||||
|
||||
@interface FBTVNavigationItem ()
|
||||
@property (nonatomic, readonly) NSString *uid;
|
||||
@property (nonatomic, readonly) NSMutableSet<NSNumber *>* directions;
|
||||
|
||||
+ (instancetype)itemWithUid:(NSString *) uid;
|
||||
@end
|
||||
|
||||
|
||||
@interface FBTVNavigationTracker ()
|
||||
|
||||
- (FBTVDirection)horizontalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta;
|
||||
- (FBTVDirection)verticalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta;
|
||||
@end
|
||||
|
||||
#endif
|
||||
52
WebDriverAgentLib/Utilities/FBTVNavigationTracker.h
Normal file
52
WebDriverAgentLib/Utilities/FBTVNavigationTracker.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 <Foundation/Foundation.h>
|
||||
#import <XCTest/XCUIElement.h>
|
||||
|
||||
#if TARGET_OS_TV
|
||||
|
||||
/**
|
||||
Defines directions to move focuse to.
|
||||
*/
|
||||
typedef NS_ENUM(NSUInteger, FBTVDirection) {
|
||||
FBTVDirectionUp = 0,
|
||||
FBTVDirectionDown = 1,
|
||||
FBTVDirectionLeft = 2,
|
||||
FBTVDirectionRight = 3,
|
||||
FBTVDirectionNone = 4
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBTVNavigationItem : NSObject
|
||||
@end
|
||||
|
||||
@interface FBTVNavigationTracker : NSObject
|
||||
|
||||
/**
|
||||
Track the target element's point
|
||||
|
||||
@param targetElement A target element which will track
|
||||
@return An instancce of FBTVNavigationTracker
|
||||
*/
|
||||
+ (instancetype)trackerWithTargetElement: (XCUIElement *) targetElement;
|
||||
|
||||
/**
|
||||
Determine the correct direction to move the focus to the tracked target
|
||||
element from the currently focused one
|
||||
|
||||
@return FBTVDirection to move the focus to
|
||||
*/
|
||||
- (FBTVDirection)directionToFocusedElement;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
#endif
|
||||
142
WebDriverAgentLib/Utilities/FBTVNavigationTracker.m
Normal file
142
WebDriverAgentLib/Utilities/FBTVNavigationTracker.m
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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 "FBTVNavigationTracker.h"
|
||||
#import "FBTVNavigationTracker-Private.h"
|
||||
|
||||
#import "FBMathUtils.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
|
||||
#if TARGET_OS_TV
|
||||
|
||||
@implementation FBTVNavigationItem
|
||||
|
||||
+ (instancetype)itemWithUid:(NSString *) uid
|
||||
{
|
||||
return [[FBTVNavigationItem alloc] initWithUid:uid];
|
||||
}
|
||||
|
||||
- (instancetype)initWithUid:(NSString *) uid
|
||||
{
|
||||
self = [super init];
|
||||
if(self) {
|
||||
_uid = uid;
|
||||
_directions = [NSMutableSet set];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface FBTVNavigationTracker ()
|
||||
@property (nonatomic, strong) XCUIElement *targetElement;
|
||||
@property (nonatomic, assign) CGPoint targetCenter;
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, FBTVNavigationItem* >* navigationItems;
|
||||
@end
|
||||
|
||||
@implementation FBTVNavigationTracker
|
||||
|
||||
+ (instancetype)trackerWithTargetElement:(XCUIElement *)targetElement
|
||||
{
|
||||
FBTVNavigationTracker *tracker = [[FBTVNavigationTracker alloc] initWithTargetElement:targetElement];
|
||||
tracker.targetElement = targetElement;
|
||||
return tracker;
|
||||
}
|
||||
|
||||
- (instancetype)initWithTargetElement:(XCUIElement *)targetElement
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_targetElement = targetElement;
|
||||
CGRect frame = targetElement.wdFrame;
|
||||
_targetCenter = FBRectGetCenter(frame);
|
||||
_navigationItems = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (FBTVDirection)directionToFocusedElement
|
||||
{
|
||||
XCUIElement *focused = XCUIApplication.fb_activeApplication.fb_focusedElement;
|
||||
|
||||
CGPoint focusedCenter = FBRectGetCenter(focused.wdFrame);
|
||||
FBTVNavigationItem *item = [self navigationItemWithElement:focused];
|
||||
CGFloat yDelta = self.targetCenter.y - focusedCenter.y;
|
||||
CGFloat xDelta = self.targetCenter.x - focusedCenter.x;
|
||||
FBTVDirection direction;
|
||||
if (fabs(yDelta) > fabs(xDelta)) {
|
||||
direction = [self verticalDirectionWithItem:item andDelta:yDelta];
|
||||
if (direction == FBTVDirectionNone) {
|
||||
direction = [self horizontalDirectionWithItem:item andDelta:xDelta];
|
||||
}
|
||||
} else {
|
||||
direction = [self horizontalDirectionWithItem:item andDelta:xDelta];
|
||||
if (direction == FBTVDirectionNone) {
|
||||
direction = [self verticalDirectionWithItem:item andDelta:yDelta];
|
||||
}
|
||||
}
|
||||
|
||||
return direction;
|
||||
}
|
||||
|
||||
#pragma mark - Utilities
|
||||
- (FBTVNavigationItem*)navigationItemWithElement:(id<FBElement>)element
|
||||
{
|
||||
NSString *uid = element.wdUID;
|
||||
if (nil == uid) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
FBTVNavigationItem* item = [self.navigationItems objectForKey:uid];
|
||||
if (nil != item) {
|
||||
return item;
|
||||
}
|
||||
|
||||
item = [FBTVNavigationItem itemWithUid:uid];
|
||||
[self.navigationItems setObject:item forKey:uid];
|
||||
return item;
|
||||
}
|
||||
|
||||
- (FBTVDirection)horizontalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta
|
||||
{
|
||||
// GCFloat is double in 64bit. tvOS is only for arm64
|
||||
if (delta > DBL_EPSILON &&
|
||||
![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionRight]]) {
|
||||
[item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionRight]];
|
||||
return FBTVDirectionRight;
|
||||
}
|
||||
if (delta < -DBL_EPSILON &&
|
||||
![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionLeft]]) {
|
||||
[item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionLeft]];
|
||||
return FBTVDirectionLeft;
|
||||
}
|
||||
return FBTVDirectionNone;
|
||||
}
|
||||
|
||||
- (FBTVDirection)verticalDirectionWithItem:(FBTVNavigationItem *)item andDelta:(CGFloat)delta
|
||||
{
|
||||
// GCFloat is double in 64bit. tvOS is only for arm64
|
||||
if (delta > DBL_EPSILON &&
|
||||
![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionDown]]) {
|
||||
[item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionDown]];
|
||||
return FBTVDirectionDown;
|
||||
}
|
||||
if (delta < -DBL_EPSILON &&
|
||||
![item.directions containsObject: [NSNumber numberWithInteger: FBTVDirectionUp]]) {
|
||||
[item.directions addObject: [NSNumber numberWithInteger: FBTVDirectionUp]];
|
||||
return FBTVDirectionUp;
|
||||
}
|
||||
return FBTVDirectionNone;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
26
WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h
Normal file
26
WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
Launches apps without attaching them to an XCUITest or a WDA session, allowing them to remain open
|
||||
when WDA closes.
|
||||
*/
|
||||
@interface FBUnattachedAppLauncher : NSObject
|
||||
|
||||
/**
|
||||
Launch the app with the specified bundle ID. Return YES if successful, NO otherwise.
|
||||
*/
|
||||
+ (BOOL)launchAppWithBundleId:(NSString *)bundleId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
21
WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m
Normal file
21
WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 "FBUnattachedAppLauncher.h"
|
||||
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
|
||||
#import "LSApplicationWorkspace.h"
|
||||
|
||||
@implementation FBUnattachedAppLauncher
|
||||
|
||||
+ (BOOL)launchAppWithBundleId:(NSString *)bundleId {
|
||||
return [[LSApplicationWorkspace defaultWorkspace] openApplicationWithBundleID:bundleId];
|
||||
}
|
||||
|
||||
@end
|
||||
42
WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h
Normal file
42
WebDriverAgentLib/Utilities/FBW3CActionsHelpers.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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* Extracts value property for a key action
|
||||
*
|
||||
* @param actionItem Action item dictionary
|
||||
* @param error Contains the acttual error in case of failure
|
||||
* @returns Either the extracted value or nil in case of failure
|
||||
*/
|
||||
NSString *_Nullable FBRequireValue(NSDictionary<NSString *, id> *actionItem, NSError **error);
|
||||
|
||||
/**
|
||||
* Extracts duration property for an action
|
||||
*
|
||||
* @param actionItem Action item dictionary
|
||||
* @param defaultValue The default duration value if it is not present. If nil then the error will be set
|
||||
* @param error Contains the acttual error in case of failure
|
||||
* @returns Either the extracted value or nil in case of failure
|
||||
*/
|
||||
NSNumber *_Nullable FBOptDuration(NSDictionary<NSString *, id> *actionItem, NSNumber *_Nullable defaultValue, NSError **error);
|
||||
|
||||
/**
|
||||
* Maps W3C meta modifier to XCUITest compatible-one
|
||||
* See https://w3c.github.io/webdriver/#keyboard-actions
|
||||
*
|
||||
* @param value key action value
|
||||
* @returns the mapped modifier value or the same input character
|
||||
* if no mapped value could be found for it.
|
||||
*/
|
||||
NSString * FBMapIfSpecialCharacter(NSString *value);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
119
WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m
Normal file
119
WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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 "FBW3CActionsHelpers.h"
|
||||
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "FBLogger.h"
|
||||
|
||||
static NSString *const FB_ACTION_ITEM_KEY_VALUE = @"value";
|
||||
static NSString *const FB_ACTION_ITEM_KEY_DURATION = @"duration";
|
||||
|
||||
NSString *FBRequireValue(NSDictionary<NSString *, id> *actionItem, NSError **error)
|
||||
{
|
||||
id value = [actionItem objectForKey:FB_ACTION_ITEM_KEY_VALUE];
|
||||
if (![value isKindOfClass:NSString.class] || [value length] == 0) {
|
||||
NSString *description = [NSString stringWithFormat:@"Key value must be present and should be a valid non-empty string for '%@'", actionItem];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
NSRange r = [(NSString *)value rangeOfComposedCharacterSequenceAtIndex:0];
|
||||
return [(NSString *)value substringWithRange:r];
|
||||
}
|
||||
|
||||
NSNumber *_Nullable FBOptDuration(NSDictionary<NSString *, id> *actionItem, NSNumber *defaultValue, NSError **error)
|
||||
{
|
||||
NSNumber *durationObj = [actionItem objectForKey:FB_ACTION_ITEM_KEY_DURATION];
|
||||
if (nil == durationObj) {
|
||||
if (nil == defaultValue) {
|
||||
NSString *description = [NSString stringWithFormat:@"Duration must be present for '%@' action item", actionItem];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
if ([durationObj doubleValue] < 0.0) {
|
||||
NSString *description = [NSString stringWithFormat:@"Duration must be a valid positive number for '%@' action item", actionItem];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
return durationObj;
|
||||
}
|
||||
|
||||
NSString *FBMapIfSpecialCharacter(NSString *value)
|
||||
{
|
||||
if (0 == [value length]) {
|
||||
return value;
|
||||
}
|
||||
|
||||
unichar charCode = [value characterAtIndex:0];
|
||||
switch (charCode) {
|
||||
case 0xE000:
|
||||
return @"";
|
||||
case 0xE003:
|
||||
return [NSString stringWithFormat:@"%C", 0x0008];
|
||||
case 0xE004:
|
||||
return [NSString stringWithFormat:@"%C", 0x0009];
|
||||
case 0xE006:
|
||||
return [NSString stringWithFormat:@"%C", 0x000D];
|
||||
case 0xE007:
|
||||
return [NSString stringWithFormat:@"%C", 0x000A];
|
||||
case 0xE00C:
|
||||
return [NSString stringWithFormat:@"%C", 0x001B];
|
||||
case 0xE00D:
|
||||
case 0xE05D:
|
||||
return @" ";
|
||||
case 0xE017:
|
||||
return [NSString stringWithFormat:@"%C", 0x007F];
|
||||
case 0xE018:
|
||||
return @";";
|
||||
case 0xE019:
|
||||
return @"=";
|
||||
case 0xE01A:
|
||||
return @"0";
|
||||
case 0xE01B:
|
||||
return @"1";
|
||||
case 0xE01C:
|
||||
return @"2";
|
||||
case 0xE01D:
|
||||
return @"3";
|
||||
case 0xE01E:
|
||||
return @"4";
|
||||
case 0xE01F:
|
||||
return @"5";
|
||||
case 0xE020:
|
||||
return @"6";
|
||||
case 0xE021:
|
||||
return @"7";
|
||||
case 0xE022:
|
||||
return @"8";
|
||||
case 0xE023:
|
||||
return @"9";
|
||||
case 0xE024:
|
||||
return @"*";
|
||||
case 0xE025:
|
||||
return @"+";
|
||||
case 0xE026:
|
||||
return @",";
|
||||
case 0xE027:
|
||||
return @"-";
|
||||
case 0xE028:
|
||||
return @".";
|
||||
case 0xE029:
|
||||
return @"/";
|
||||
default:
|
||||
return charCode >= 0xE000 && charCode <= 0xE05D ? @"" : value;
|
||||
}
|
||||
}
|
||||
19
WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h
Normal file
19
WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.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 "FBBaseActionsSynthesizer.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@interface FBW3CActionsSynthesizer : FBBaseActionsSynthesizer
|
||||
|
||||
@end
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
885
WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m
Normal file
885
WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m
Normal file
@@ -0,0 +1,885 @@
|
||||
/**
|
||||
* 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 "FBW3CActionsSynthesizer.h"
|
||||
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBElementCache.h"
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBMathUtils.h"
|
||||
#import "FBProtocolHelpers.h"
|
||||
#import "FBW3CActionsHelpers.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIDevice.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCUIElement+FBIsVisible.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "XCSynthesizedEventRecord.h"
|
||||
#import "XCPointerEventPath.h"
|
||||
#import "XCPointerEvent.h"
|
||||
|
||||
|
||||
static NSString *const FB_KEY_TYPE = @"type";
|
||||
static NSString *const FB_ACTION_TYPE_POINTER = @"pointer";
|
||||
static NSString *const FB_ACTION_TYPE_KEY = @"key";
|
||||
static NSString *const FB_ACTION_TYPE_NONE = @"none";
|
||||
|
||||
static NSString *const FB_PARAMETERS_KEY_POINTER_TYPE = @"pointerType";
|
||||
static NSString *const FB_POINTER_TYPE_MOUSE = @"mouse";
|
||||
static NSString *const FB_POINTER_TYPE_PEN = @"pen";
|
||||
static NSString *const FB_POINTER_TYPE_TOUCH = @"touch";
|
||||
|
||||
static NSString *const FB_ACTION_ITEM_KEY_ORIGIN = @"origin";
|
||||
static NSString *const FB_ORIGIN_TYPE_VIEWPORT = @"viewport";
|
||||
static NSString *const FB_ORIGIN_TYPE_POINTER = @"pointer";
|
||||
|
||||
static NSString *const FB_ACTION_ITEM_KEY_TYPE = @"type";
|
||||
static NSString *const FB_ACTION_ITEM_TYPE_POINTER_MOVE = @"pointerMove";
|
||||
static NSString *const FB_ACTION_ITEM_TYPE_POINTER_DOWN = @"pointerDown";
|
||||
static NSString *const FB_ACTION_ITEM_TYPE_POINTER_UP = @"pointerUp";
|
||||
static NSString *const FB_ACTION_ITEM_TYPE_POINTER_CANCEL = @"pointerCancel";
|
||||
static NSString *const FB_ACTION_ITEM_TYPE_PAUSE = @"pause";
|
||||
static NSString *const FB_ACTION_ITEM_TYPE_KEY_UP = @"keyUp";
|
||||
static NSString *const FB_ACTION_ITEM_TYPE_KEY_DOWN = @"keyDown";
|
||||
|
||||
static NSString *const FB_ACTION_ITEM_KEY_X = @"x";
|
||||
static NSString *const FB_ACTION_ITEM_KEY_Y = @"y";
|
||||
static NSString *const FB_ACTION_ITEM_KEY_BUTTON = @"button";
|
||||
static NSString *const FB_ACTION_ITEM_KEY_PRESSURE = @"pressure";
|
||||
|
||||
static NSString *const FB_KEY_ID = @"id";
|
||||
static NSString *const FB_KEY_PARAMETERS = @"parameters";
|
||||
static NSString *const FB_KEY_ACTIONS = @"actions";
|
||||
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@interface FBW3CGestureItem : FBBaseGestureItem
|
||||
|
||||
@property (nullable, readonly, nonatomic) FBBaseGestureItem *previousItem;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBPointerDownItem : FBW3CGestureItem
|
||||
@property (nullable, readonly, nonatomic) NSNumber *pressure;
|
||||
@end
|
||||
|
||||
@interface FBPointerMoveItem : FBW3CGestureItem
|
||||
|
||||
@end
|
||||
|
||||
@interface FBPointerUpItem : FBW3CGestureItem
|
||||
|
||||
@end
|
||||
|
||||
@interface FBPointerPauseItem : FBW3CGestureItem
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBW3CKeyItem : FBBaseActionItem
|
||||
|
||||
@property (nullable, readonly, nonatomic) FBW3CKeyItem *previousItem;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBKeyUpItem : FBW3CKeyItem
|
||||
|
||||
@property (readonly, nonatomic) NSString *value;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBKeyDownItem : FBW3CKeyItem
|
||||
|
||||
@property (readonly, nonatomic) NSString *value;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBKeyPauseItem : FBW3CKeyItem
|
||||
|
||||
@property (readonly, nonatomic) double duration;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation FBW3CGestureItem
|
||||
|
||||
- (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
|
||||
application:(XCUIApplication *)application
|
||||
previousItem:(nullable FBBaseGestureItem *)previousItem
|
||||
offset:(double)offset
|
||||
error:(NSError **)error
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.actionItem = actionItem;
|
||||
self.application = application;
|
||||
self.offset = offset;
|
||||
_previousItem = previousItem;
|
||||
NSNumber *durationObj = FBOptDuration(actionItem, @0, error);
|
||||
if (nil == durationObj) {
|
||||
return nil;
|
||||
}
|
||||
self.duration = durationObj.doubleValue;
|
||||
XCUICoordinate *position = [self positionWithError:error];
|
||||
if (nil == position) {
|
||||
return nil;
|
||||
}
|
||||
self.atPosition = position;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable XCUICoordinate *)positionWithError:(NSError **)error
|
||||
{
|
||||
if (nil == self.previousItem) {
|
||||
NSString *errorDescription = [NSString stringWithFormat:@"The '%@' action item must be preceded by %@ item", self.actionItem, FB_ACTION_ITEM_TYPE_POINTER_MOVE];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
return self.previousItem.atPosition;
|
||||
}
|
||||
|
||||
- (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element
|
||||
positionOffset:(nullable NSValue *)positionOffset
|
||||
error:(NSError **)error
|
||||
{
|
||||
if (nil == element || nil == positionOffset) {
|
||||
return [super hitpointWithElement:element positionOffset:positionOffset error:error];
|
||||
}
|
||||
|
||||
// An offset relative to the element is defined
|
||||
if (CGRectIsEmpty(element.frame)) {
|
||||
[FBLogger log:self.application.fb_descriptionRepresentation];
|
||||
NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable",
|
||||
element.description];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// W3C standard requires that relative element coordinates start at the center of the element's rectangle
|
||||
CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y);
|
||||
// TODO: Shall we throw an exception if hitPoint is out of the element frame?
|
||||
return [[element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)] coordinateWithOffset:offset];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBPointerDownItem
|
||||
|
||||
- (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
|
||||
application:(XCUIApplication *)application
|
||||
previousItem:(nullable FBW3CGestureItem *)previousItem
|
||||
offset:(double)offset
|
||||
error:(NSError **)error
|
||||
{
|
||||
self = [super initWithActionItem:actionItem application:application previousItem:previousItem offset:offset error:error];
|
||||
if (self) {
|
||||
_pressure = [actionItem objectForKey:FB_ACTION_ITEM_KEY_PRESSURE];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSString *)actionName
|
||||
{
|
||||
return FB_ACTION_ITEM_TYPE_POINTER_DOWN;
|
||||
}
|
||||
|
||||
- (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
|
||||
allItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
error:(NSError **)error
|
||||
{
|
||||
if (nil != eventPath && currentItemIndex == 1) {
|
||||
FBW3CGestureItem *preceedingItem = [allItems objectAtIndex:currentItemIndex - 1];
|
||||
if ([preceedingItem isKindOfClass:FBPointerMoveItem.class]) {
|
||||
return @[];
|
||||
}
|
||||
}
|
||||
if (nil == self.pressure) {
|
||||
XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint
|
||||
offset:FBMillisToSeconds(self.offset)];
|
||||
return @[result];
|
||||
}
|
||||
|
||||
if (nil == eventPath) {
|
||||
NSString *description = [NSString stringWithFormat:@"'%@' action with pressure must be preceeded with at least one '%@' action without this option", self.class.actionName, self.class.actionName];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
if (![XCUIDevice sharedDevice].supportsPressureInteraction) {
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:@"This device does not support force press interactions"] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
[eventPath pressDownWithPressure:self.pressure.doubleValue
|
||||
atOffset:FBMillisToSeconds(self.offset)];
|
||||
return @[];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBPointerMoveItem
|
||||
|
||||
- (nullable XCUICoordinate *)positionWithError:(NSError **)error
|
||||
{
|
||||
static NSArray<NSString *> *supportedOriginTypes;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
supportedOriginTypes = @[FB_ORIGIN_TYPE_POINTER, FB_ORIGIN_TYPE_VIEWPORT];
|
||||
});
|
||||
id origin = [self.actionItem objectForKey:FB_ACTION_ITEM_KEY_ORIGIN] ?: FB_ORIGIN_TYPE_VIEWPORT;
|
||||
BOOL isOriginAnElement = [origin isKindOfClass:XCUIElement.class] && [(XCUIElement *)origin exists];
|
||||
if (!isOriginAnElement && ![supportedOriginTypes containsObject:origin]) {
|
||||
NSString *description = [NSString stringWithFormat:@"Unsupported %@ type '%@' is set for '%@' action item. Supported origin types: %@ or an element instance", FB_ACTION_ITEM_KEY_ORIGIN, origin, self.actionItem, supportedOriginTypes];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
XCUIElement *element = isOriginAnElement ? (XCUIElement *)origin : nil;
|
||||
NSNumber *x = [self.actionItem objectForKey:FB_ACTION_ITEM_KEY_X];
|
||||
NSNumber *y = [self.actionItem objectForKey:FB_ACTION_ITEM_KEY_Y];
|
||||
if ((nil != x && nil == y) || (nil != y && nil == x) ||
|
||||
([origin isKindOfClass:NSString.class] && [origin isEqualToString:FB_ORIGIN_TYPE_VIEWPORT] && (nil == x || nil == y))) {
|
||||
NSString *errorDescription = [NSString stringWithFormat:@"Both 'x' and 'y' options should be set for '%@' action item", self.actionItem];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (nil != element) {
|
||||
if (nil == x && nil == y) {
|
||||
return [self hitpointWithElement:element positionOffset:nil error:error];
|
||||
}
|
||||
return [self hitpointWithElement:element positionOffset:[NSValue valueWithCGPoint:CGPointMake(x.floatValue, y.floatValue)] error:error];
|
||||
}
|
||||
|
||||
if ([origin isKindOfClass:NSString.class] && [origin isEqualToString:FB_ORIGIN_TYPE_VIEWPORT]) {
|
||||
return [self hitpointWithElement:nil positionOffset:[NSValue valueWithCGPoint:CGPointMake(x.floatValue, y.floatValue)] error:error];
|
||||
}
|
||||
|
||||
// origin == FB_ORIGIN_TYPE_POINTER
|
||||
if (nil == self.previousItem) {
|
||||
NSString *errorDescription = [NSString stringWithFormat:@"There is no previous item for '%@' action item, however %@ is set to '%@'", self.actionItem, FB_ACTION_ITEM_KEY_ORIGIN, FB_ORIGIN_TYPE_POINTER];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:errorDescription] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
XCUICoordinate *recentPosition = self.previousItem.atPosition;
|
||||
CGVector offsetRelativeToRecentPosition = (nil == x && nil == y) ? CGVectorMake(0, 0) : CGVectorMake(x.floatValue, y.floatValue);
|
||||
return [recentPosition coordinateWithOffset:offsetRelativeToRecentPosition];
|
||||
}
|
||||
|
||||
+ (NSString *)actionName
|
||||
{
|
||||
return FB_ACTION_ITEM_TYPE_POINTER_MOVE;
|
||||
}
|
||||
|
||||
- (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
|
||||
allItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
error:(NSError **)error
|
||||
{
|
||||
if (nil == eventPath) {
|
||||
return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint
|
||||
offset:FBMillisToSeconds(self.offset + self.duration)]];
|
||||
}
|
||||
[eventPath moveToPoint:self.atPosition.screenPoint
|
||||
atOffset:FBMillisToSeconds(self.offset + self.duration)];
|
||||
return @[];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBPointerPauseItem
|
||||
|
||||
+ (NSString *)actionName
|
||||
{
|
||||
return FB_ACTION_ITEM_TYPE_PAUSE;
|
||||
}
|
||||
|
||||
- (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
|
||||
allItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
error:(NSError **)error
|
||||
{
|
||||
return @[];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBPointerUpItem
|
||||
|
||||
+ (NSString *)actionName
|
||||
{
|
||||
return FB_ACTION_ITEM_TYPE_POINTER_UP;
|
||||
}
|
||||
|
||||
- (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
|
||||
allItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
error:(NSError **)error
|
||||
{
|
||||
if (nil == eventPath) {
|
||||
NSString *description = [NSString stringWithFormat:@"Pointer Up must not be the first action in '%@'", self.actionItem];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
[eventPath liftUpAtOffset:FBMillisToSeconds(self.offset)];
|
||||
return @[];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBW3CKeyItem
|
||||
|
||||
- (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
|
||||
application:(XCUIApplication *)application
|
||||
previousItem:(nullable FBW3CKeyItem *)previousItem
|
||||
offset:(double)offset
|
||||
error:(NSError **)error
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.actionItem = actionItem;
|
||||
self.application = application;
|
||||
self.offset = offset;
|
||||
_previousItem = previousItem;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBKeyUpItem : FBW3CKeyItem
|
||||
|
||||
- (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
|
||||
application:(XCUIApplication *)application
|
||||
previousItem:(nullable FBW3CKeyItem *)previousItem
|
||||
offset:(double)offset
|
||||
error:(NSError **)error
|
||||
{
|
||||
self = [super initWithActionItem:actionItem
|
||||
application:application
|
||||
previousItem:previousItem
|
||||
offset:offset
|
||||
error:error];
|
||||
if (self) {
|
||||
NSString *value = FBRequireValue(actionItem, error);
|
||||
if (nil == value) {
|
||||
return nil;
|
||||
}
|
||||
_value = value;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSString *)actionName
|
||||
{
|
||||
return FB_ACTION_ITEM_TYPE_KEY_UP;
|
||||
}
|
||||
|
||||
- (BOOL)hasDownPairInItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
{
|
||||
NSInteger balance = 1;
|
||||
for (NSInteger index = currentItemIndex - 1; index >= 0; index--) {
|
||||
FBW3CKeyItem *item = [allItems objectAtIndex:index];
|
||||
BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
|
||||
BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
|
||||
if (!isKeyUp && !isKeyDown) {
|
||||
break;
|
||||
}
|
||||
|
||||
NSString *value = [item performSelector:@selector(value)];
|
||||
if (isKeyDown && [value isEqualToString:self.value]) {
|
||||
balance--;
|
||||
}
|
||||
if (isKeyUp && [value isEqualToString:self.value]) {
|
||||
balance++;
|
||||
}
|
||||
}
|
||||
return 0 == balance;
|
||||
}
|
||||
|
||||
- (NSString *)collectTextWithItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
{
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
for (NSInteger index = currentItemIndex; index >= 0; index--) {
|
||||
FBW3CKeyItem *item = [allItems objectAtIndex:index];
|
||||
BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
|
||||
BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
|
||||
if (!isKeyUp && !isKeyDown) {
|
||||
break;
|
||||
}
|
||||
|
||||
NSString *value = [item performSelector:@selector(value)];
|
||||
if (isKeyUp) {
|
||||
[result addObject:FBMapIfSpecialCharacter(value)];
|
||||
}
|
||||
}
|
||||
return [result.reverseObjectEnumerator.allObjects componentsJoinedByString:@""];
|
||||
}
|
||||
|
||||
- (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
|
||||
allItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
error:(NSError **)error
|
||||
{
|
||||
if (![self hasDownPairInItems:allItems currentItemIndex:currentItemIndex]) {
|
||||
NSString *description = [NSString stringWithFormat:@"Key Up action '%@' is not balanced with a preceding Key Down one in '%@'", self.value, self.actionItem];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL isLastKeyUpInGroup = currentItemIndex == allItems.count - 1
|
||||
|| [[allItems objectAtIndex:currentItemIndex + 1] isKindOfClass:FBKeyPauseItem.class];
|
||||
if (!isLastKeyUpInGroup) {
|
||||
return @[];
|
||||
}
|
||||
|
||||
NSString *text = [self collectTextWithItems:allItems currentItemIndex:currentItemIndex];
|
||||
NSTimeInterval offset = FBMillisToSeconds(self.offset);
|
||||
XCPointerEventPath *resultPath = [[XCPointerEventPath alloc] initForTextInput];
|
||||
[resultPath typeText:text
|
||||
atOffset:offset
|
||||
typingSpeed:FBConfiguration.maxTypingFrequency
|
||||
shouldRedact:YES];
|
||||
return @[resultPath];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBKeyDownItem : FBW3CKeyItem
|
||||
|
||||
- (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
|
||||
application:(XCUIApplication *)application
|
||||
previousItem:(nullable FBW3CKeyItem *)previousItem
|
||||
offset:(double)offset
|
||||
error:(NSError **)error
|
||||
{
|
||||
self = [super initWithActionItem:actionItem
|
||||
application:application
|
||||
previousItem:previousItem
|
||||
offset:offset
|
||||
error:error];
|
||||
if (self) {
|
||||
NSString *value = FBRequireValue(actionItem, error);
|
||||
if (nil == value) {
|
||||
return nil;
|
||||
}
|
||||
_value = value;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSString *)actionName
|
||||
{
|
||||
return FB_ACTION_ITEM_TYPE_KEY_DOWN;
|
||||
}
|
||||
|
||||
- (BOOL)hasUpPairInItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
{
|
||||
NSInteger balance = 1;
|
||||
for (NSUInteger index = currentItemIndex + 1; index < allItems.count; index++) {
|
||||
FBW3CKeyItem *item = [allItems objectAtIndex:index];
|
||||
BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class];
|
||||
BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class];
|
||||
if (!isKeyUp && !isKeyDown) {
|
||||
break;
|
||||
}
|
||||
|
||||
NSString *value = [item performSelector:@selector(value)];
|
||||
if (isKeyUp && [value isEqualToString:self.value]) {
|
||||
balance--;
|
||||
}
|
||||
if (isKeyDown && [value isEqualToString:self.value]) {
|
||||
balance++;
|
||||
}
|
||||
}
|
||||
return 0 == balance;
|
||||
}
|
||||
|
||||
- (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
|
||||
allItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
error:(NSError **)error
|
||||
{
|
||||
if (![self hasUpPairInItems:allItems currentItemIndex:currentItemIndex]) {
|
||||
NSString *description = [NSString stringWithFormat:@"Key Down action '%@' must have a closing Key Up successor in '%@'", self.value, self.actionItem];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return @[];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBKeyPauseItem
|
||||
|
||||
- (nullable instancetype)initWithActionItem:(NSDictionary<NSString *, id> *)actionItem
|
||||
application:(XCUIApplication *)application
|
||||
previousItem:(nullable FBW3CKeyItem *)previousItem
|
||||
offset:(double)offset
|
||||
error:(NSError **)error
|
||||
{
|
||||
self = [super initWithActionItem:actionItem
|
||||
application:application
|
||||
previousItem:previousItem
|
||||
offset:offset
|
||||
error:error];
|
||||
if (self) {
|
||||
NSNumber *duration = FBOptDuration(actionItem, nil, error);
|
||||
if (nil == duration) {
|
||||
return nil;
|
||||
}
|
||||
_duration = [duration doubleValue];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSString *)actionName
|
||||
{
|
||||
return FB_ACTION_ITEM_TYPE_PAUSE;
|
||||
}
|
||||
|
||||
- (NSArray<XCPointerEventPath *> *)addToEventPath:(XCPointerEventPath *)eventPath
|
||||
allItems:(NSArray *)allItems
|
||||
currentItemIndex:(NSUInteger)currentItemIndex
|
||||
error:(NSError **)error
|
||||
{
|
||||
return @[];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBW3CGestureItemsChain : FBBaseActionItemsChain
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBW3CGestureItemsChain
|
||||
|
||||
- (void)addItem:(FBBaseActionItem *)item
|
||||
{
|
||||
self.durationOffset += ((FBBaseGestureItem *)item).duration;
|
||||
[self.items addObject:item];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FBW3CKeyItemsChain : FBBaseActionItemsChain
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBW3CKeyItemsChain
|
||||
|
||||
- (void)addItem:(FBBaseActionItem *)item
|
||||
{
|
||||
if ([item isKindOfClass:FBKeyPauseItem.class]) {
|
||||
self.durationOffset += ((FBKeyPauseItem *)item).duration;
|
||||
}
|
||||
[self.items addObject:item];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FBW3CActionsSynthesizer
|
||||
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)preprocessedActionItemsWith:(NSArray<NSDictionary<NSString *, id> *> *)actionItems
|
||||
{
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
|
||||
BOOL shouldCancelNextItem = NO;
|
||||
for (NSDictionary<NSString *, id> *actionItem in [actionItems reverseObjectEnumerator]) {
|
||||
if (shouldCancelNextItem) {
|
||||
shouldCancelNextItem = NO;
|
||||
continue;
|
||||
}
|
||||
NSString *actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE];
|
||||
if (actionItemType != nil && [actionItemType isEqualToString:FB_ACTION_ITEM_TYPE_POINTER_CANCEL]) {
|
||||
shouldCancelNextItem = YES;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nil == self.elementCache) {
|
||||
[result addObject:actionItem];
|
||||
continue;
|
||||
}
|
||||
id origin = [actionItem objectForKey:FB_ACTION_ITEM_KEY_ORIGIN];
|
||||
if (nil == origin || [@[FB_ORIGIN_TYPE_POINTER, FB_ORIGIN_TYPE_VIEWPORT] containsObject:origin]) {
|
||||
[result addObject:actionItem];
|
||||
continue;
|
||||
}
|
||||
// Selenium Python client passes 'origin' element in the following format:
|
||||
//
|
||||
// if isinstance(origin, WebElement):
|
||||
// action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
|
||||
if ([origin isKindOfClass:NSDictionary.class]) {
|
||||
id element = FBExtractElement(origin);
|
||||
if (nil != element) {
|
||||
origin = element;
|
||||
}
|
||||
}
|
||||
|
||||
XCUIElement *instance;
|
||||
if ([origin isKindOfClass:XCUIElement.class]) {
|
||||
instance = origin;
|
||||
} else if ([origin isKindOfClass:NSString.class]) {
|
||||
instance = [self.elementCache elementForUUID:(NSString *)origin checkStaleness:YES];
|
||||
} else {
|
||||
[result addObject:actionItem];
|
||||
continue;
|
||||
}
|
||||
NSMutableDictionary<NSString *, id> *processedItem = actionItem.mutableCopy;
|
||||
[processedItem setObject:instance forKey:FB_ACTION_ITEM_KEY_ORIGIN];
|
||||
[result addObject:processedItem.copy];
|
||||
}
|
||||
return [[result reverseObjectEnumerator] allObjects];
|
||||
}
|
||||
|
||||
- (nullable NSArray<XCPointerEventPath *> *)eventPathsWithKeyAction:(NSDictionary<NSString *, id> *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error
|
||||
{
|
||||
static NSDictionary<NSString *, Class> *keyItemsMapping;
|
||||
static NSArray<NSString *> *supportedActionItemTypes;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSMutableDictionary<NSString *, Class> *itemsMapping = [NSMutableDictionary dictionary];
|
||||
for (Class cls in @[FBKeyDownItem.class,
|
||||
FBKeyPauseItem.class,
|
||||
FBKeyUpItem.class]) {
|
||||
[itemsMapping setObject:cls forKey:[cls actionName]];
|
||||
}
|
||||
keyItemsMapping = itemsMapping.copy;
|
||||
supportedActionItemTypes = @[FB_ACTION_ITEM_TYPE_PAUSE,
|
||||
FB_ACTION_ITEM_TYPE_KEY_UP,
|
||||
FB_ACTION_ITEM_TYPE_KEY_DOWN];
|
||||
});
|
||||
|
||||
NSArray<NSDictionary<NSString *, id> *> *actionItems = [actionDescription objectForKey:FB_KEY_ACTIONS];
|
||||
if (nil == actionItems || 0 == actionItems.count) {
|
||||
NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one item defined for each action. Action with id '%@' contains none", actionId];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
FBW3CKeyItemsChain *chain = [[FBW3CKeyItemsChain alloc] init];
|
||||
NSArray<NSDictionary<NSString *, id> *> *processedItems = [self preprocessedActionItemsWith:actionItems];
|
||||
for (NSDictionary<NSString *, id> *actionItem in processedItems) {
|
||||
id actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE];
|
||||
if (![actionItemType isKindOfClass:NSString.class]) {
|
||||
NSString *description = [NSString stringWithFormat:@"The %@ property is mandatory to set for '%@' action item", FB_ACTION_ITEM_KEY_TYPE, actionItem];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
Class keyItemClass = [keyItemsMapping objectForKey:actionItemType];
|
||||
if (nil == keyItemClass) {
|
||||
NSString *description = [NSString stringWithFormat:@"'%@' action item type '%@' is not supported. Only the following action item types are supported: %@", actionId, actionItemType, supportedActionItemTypes];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
FBW3CKeyItem *keyItem = [[keyItemClass alloc] initWithActionItem:actionItem
|
||||
application:self.application
|
||||
previousItem:[chain.items lastObject]
|
||||
offset:chain.durationOffset
|
||||
error:error];
|
||||
if (nil == keyItem) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
[chain addItem:keyItem];
|
||||
}
|
||||
|
||||
return [chain asEventPathsWithError:error];
|
||||
}
|
||||
|
||||
- (nullable NSArray<XCPointerEventPath *> *)eventPathsWithGestureAction:(NSDictionary<NSString *, id> *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error
|
||||
{
|
||||
static NSDictionary<NSString *, Class> *gestureItemsMapping;
|
||||
static NSArray<NSString *> *supportedActionItemTypes;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSMutableDictionary<NSString *, Class> *itemsMapping = [NSMutableDictionary dictionary];
|
||||
for (Class cls in @[FBPointerDownItem.class,
|
||||
FBPointerMoveItem.class,
|
||||
FBPointerPauseItem.class,
|
||||
FBPointerUpItem.class]) {
|
||||
[itemsMapping setObject:cls forKey:[cls actionName]];
|
||||
}
|
||||
gestureItemsMapping = itemsMapping.copy;
|
||||
supportedActionItemTypes = @[FB_ACTION_ITEM_TYPE_PAUSE,
|
||||
FB_ACTION_ITEM_TYPE_POINTER_UP,
|
||||
FB_ACTION_ITEM_TYPE_POINTER_DOWN,
|
||||
FB_ACTION_ITEM_TYPE_POINTER_MOVE];
|
||||
});
|
||||
|
||||
id parameters = [actionDescription objectForKey:FB_KEY_PARAMETERS];
|
||||
id pointerType = FB_POINTER_TYPE_MOUSE;
|
||||
if ([parameters isKindOfClass:NSDictionary.class]) {
|
||||
pointerType = [parameters objectForKey:FB_PARAMETERS_KEY_POINTER_TYPE] ?: FB_POINTER_TYPE_MOUSE;
|
||||
}
|
||||
if (![pointerType isKindOfClass:NSString.class] || ![pointerType isEqualToString:FB_POINTER_TYPE_TOUCH]) {
|
||||
NSString *description = [NSString stringWithFormat:@"Only pointer type '%@' is supported. '%@' is given instead for action with id '%@'", FB_POINTER_TYPE_TOUCH, pointerType, actionId];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSArray<NSDictionary<NSString *, id> *> *actionItems = [actionDescription objectForKey:FB_KEY_ACTIONS];
|
||||
if (nil == actionItems || 0 == actionItems.count) {
|
||||
NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one gesture item defined for each action. Action with id '%@' contains none", actionId];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
FBW3CGestureItemsChain *chain = [[FBW3CGestureItemsChain alloc] init];
|
||||
NSArray<NSDictionary<NSString *, id> *> *processedItems = [self preprocessedActionItemsWith:actionItems];
|
||||
for (NSDictionary<NSString *, id> *actionItem in processedItems) {
|
||||
id actionItemType = [actionItem objectForKey:FB_ACTION_ITEM_KEY_TYPE];
|
||||
if (![actionItemType isKindOfClass:NSString.class]) {
|
||||
NSString *description = [NSString stringWithFormat:@"The %@ property is mandatory to set for '%@' action item", FB_ACTION_ITEM_KEY_TYPE, actionItem];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
Class gestureItemClass = [gestureItemsMapping objectForKey:actionItemType];
|
||||
if (nil == gestureItemClass) {
|
||||
NSString *description = [NSString stringWithFormat:@"'%@' action item type '%@' is not supported. Only the following action item types are supported: %@", actionId, actionItemType, supportedActionItemTypes];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
FBW3CGestureItem *gestureItem = [[gestureItemClass alloc] initWithActionItem:actionItem application:self.application previousItem:[chain.items lastObject] offset:chain.durationOffset error:error];
|
||||
if (nil == gestureItem) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
[chain addItem:gestureItem];
|
||||
}
|
||||
|
||||
return [chain asEventPathsWithError:error];
|
||||
}
|
||||
|
||||
- (nullable NSArray<XCPointerEventPath *> *)eventPathsWithActionDescription:(NSDictionary<NSString *, id> *)actionDescription forActionId:(NSString *)actionId error:(NSError **)error
|
||||
{
|
||||
id actionType = [actionDescription objectForKey:FB_KEY_TYPE];
|
||||
if (![actionType isKindOfClass:NSString.class] ||
|
||||
!([actionType isEqualToString:FB_ACTION_TYPE_POINTER]
|
||||
|| ([XCPointerEvent.class fb_areKeyEventsSupported] && [actionType isEqualToString:FB_ACTION_TYPE_KEY]))) {
|
||||
NSString *description = [NSString stringWithFormat:@"Only actions of '%@' types are supported. '%@' is given instead for action with id '%@'", @[FB_ACTION_TYPE_POINTER, FB_ACTION_TYPE_KEY], actionType, actionId];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ([actionType isEqualToString:FB_ACTION_TYPE_POINTER]) {
|
||||
return [self eventPathsWithGestureAction:actionDescription forActionId:actionId error:error];
|
||||
}
|
||||
|
||||
return [self eventPathsWithKeyAction:actionDescription forActionId:actionId error:error];
|
||||
}
|
||||
|
||||
- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error
|
||||
{
|
||||
XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc]
|
||||
initWithName:@"W3C Touch Action"
|
||||
interfaceOrientation:self.application.interfaceOrientation];
|
||||
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *actionsMapping = [NSMutableDictionary new];
|
||||
NSMutableArray<NSString *> *actionIds = [NSMutableArray new];
|
||||
for (NSDictionary<NSString *, id> *action in self.actions) {
|
||||
id actionId = [action objectForKey:FB_KEY_ID];
|
||||
if (![actionId isKindOfClass:NSString.class] || 0 == [actionId length]) {
|
||||
if (error) {
|
||||
NSString *description = [NSString stringWithFormat:@"The mandatory action %@ field is missing or empty for '%@'", FB_KEY_ID, action];
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
if (nil != [actionsMapping objectForKey:actionId]) {
|
||||
if (error) {
|
||||
NSString *description = [NSString stringWithFormat:@"Action %@ '%@' is not unique for '%@'", FB_KEY_ID, actionId, action];
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
NSArray<NSDictionary<NSString *, id> *> *actionItems = [action objectForKey:FB_KEY_ACTIONS];
|
||||
if (nil == actionItems) {
|
||||
NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one item defined for each action. Action with id '%@' contains none", actionId];
|
||||
if (error) {
|
||||
*error = [[FBErrorBuilder.builder withDescription:description] build];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
if (0 == actionItems.count) {
|
||||
[FBLogger logFmt:@"Action items in the action id '%@' had an empty array. Skipping the action.", actionId];
|
||||
continue;
|
||||
}
|
||||
|
||||
[actionIds addObject:actionId];
|
||||
[actionsMapping setObject:action forKey:actionId];
|
||||
}
|
||||
for (NSString *actionId in actionIds.copy) {
|
||||
NSDictionary<NSString *, id> *actionDescription = [actionsMapping objectForKey:actionId];
|
||||
NSArray<XCPointerEventPath *> *eventPaths = [self eventPathsWithActionDescription:actionDescription forActionId:actionId error:error];
|
||||
if (nil == eventPaths) {
|
||||
return nil;
|
||||
}
|
||||
for (XCPointerEventPath *eventPath in eventPaths) {
|
||||
[eventRecord addPointerEventPath:eventPath];
|
||||
}
|
||||
}
|
||||
return eventRecord;
|
||||
}
|
||||
|
||||
@end
|
||||
#endif
|
||||
22
WebDriverAgentLib/Utilities/FBWebServerParams.h
Normal file
22
WebDriverAgentLib/Utilities/FBWebServerParams.h
Normal file
@@ -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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBWebServerParams : NSObject
|
||||
|
||||
/** The local port number WDA server is running on */
|
||||
@property (nonatomic, nullable) NSNumber *port;
|
||||
|
||||
+ (id)sharedInstance;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
23
WebDriverAgentLib/Utilities/FBWebServerParams.m
Normal file
23
WebDriverAgentLib/Utilities/FBWebServerParams.m
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 "FBWebServerParams.h"
|
||||
|
||||
@implementation FBWebServerParams
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static FBWebServerParams *instance;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
@end
|
||||
49
WebDriverAgentLib/Utilities/FBXCAXClientProxy.h
Normal file
49
WebDriverAgentLib/Utilities/FBXCAXClientProxy.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 "FBXCElementSnapshot.h"
|
||||
|
||||
@protocol FBXCAccessibilityElement;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
This class acts as a proxy between WDA and XCAXClient_iOS.
|
||||
Other classes are obliged to use its methods instead of directly accessing XCAXClient_iOS,
|
||||
since Apple resticted the interface of XCAXClient_iOS class since Xcode10.2
|
||||
*/
|
||||
@interface FBXCAXClientProxy : NSObject
|
||||
|
||||
+ (instancetype)sharedClient;
|
||||
|
||||
- (BOOL)setAXTimeout:(NSTimeInterval)timeout error:(NSError **)error;
|
||||
|
||||
- (nullable id<FBXCElementSnapshot>)snapshotForElement:(id<FBXCAccessibilityElement>)element
|
||||
attributes:(nullable NSArray<NSString *> *)attributes
|
||||
inDepth:(BOOL)inDepth
|
||||
error:(NSError **)error;
|
||||
|
||||
- (NSArray<id<FBXCAccessibilityElement>> *)activeApplications;
|
||||
|
||||
- (id<FBXCAccessibilityElement>)systemApplication;
|
||||
|
||||
- (NSDictionary *)defaultParameters;
|
||||
|
||||
- (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)application
|
||||
reply:(void (^)(void))reply;
|
||||
|
||||
- (nullable NSDictionary *)attributesForElement:(id<FBXCAccessibilityElement>)element
|
||||
attributes:(NSArray *)attributes
|
||||
error:(NSError**)error;
|
||||
|
||||
- (nullable XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
119
WebDriverAgentLib/Utilities/FBXCAXClientProxy.m
Normal file
119
WebDriverAgentLib/Utilities/FBXCAXClientProxy.m
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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 "FBXCAXClientProxy.h"
|
||||
|
||||
#import "FBXCAccessibilityElement.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBMacros.h"
|
||||
#import "XCAXClient_iOS+FBSnapshotReqParams.h"
|
||||
#import "XCUIDevice.h"
|
||||
#import "XCUIApplication.h"
|
||||
|
||||
static id FBAXClient = nil;
|
||||
|
||||
@interface FBXCAXClientProxy ()
|
||||
|
||||
@property (nonatomic) NSMutableDictionary<NSNumber *, XCUIApplication *> *appsCache;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBXCAXClientProxy
|
||||
|
||||
+ (instancetype)sharedClient
|
||||
{
|
||||
static FBXCAXClientProxy *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] init];
|
||||
instance.appsCache = [NSMutableDictionary dictionary];
|
||||
FBAXClient = [XCUIDevice.sharedDevice accessibilityInterface];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (BOOL)setAXTimeout:(NSTimeInterval)timeout error:(NSError **)error
|
||||
{
|
||||
return [FBAXClient _setAXTimeout:timeout error:error];
|
||||
}
|
||||
|
||||
- (id<FBXCElementSnapshot>)snapshotForElement:(id<FBXCAccessibilityElement>)element
|
||||
attributes:(NSArray<NSString *> *)attributes
|
||||
inDepth:(BOOL)inDepth
|
||||
error:(NSError **)error
|
||||
{
|
||||
NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:self.defaultParameters];
|
||||
if (!inDepth) {
|
||||
parameters[FBSnapshotMaxDepthKey] = @1;
|
||||
}
|
||||
|
||||
id result = [FBAXClient requestSnapshotForElement:element
|
||||
attributes:attributes
|
||||
parameters:[parameters copy]
|
||||
error:error];
|
||||
id<FBXCElementSnapshot> snapshot = [result valueForKey:@"_rootElementSnapshot"];
|
||||
return nil == snapshot ? result : snapshot;
|
||||
}
|
||||
|
||||
- (NSArray<id<FBXCAccessibilityElement>> *)activeApplications
|
||||
{
|
||||
return [FBAXClient activeApplications];
|
||||
}
|
||||
|
||||
- (id<FBXCAccessibilityElement>)systemApplication
|
||||
{
|
||||
return [FBAXClient systemApplication];
|
||||
}
|
||||
|
||||
- (NSDictionary *)defaultParameters
|
||||
{
|
||||
return [FBAXClient defaultParameters];
|
||||
}
|
||||
|
||||
- (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)application
|
||||
reply:(void (^)(void))reply
|
||||
{
|
||||
[FBAXClient notifyWhenNoAnimationsAreActiveForApplication:application reply:reply];
|
||||
}
|
||||
|
||||
- (NSDictionary *)attributesForElement:(id<FBXCAccessibilityElement>)element
|
||||
attributes:(NSArray *)attributes
|
||||
error:(NSError**)error;
|
||||
{
|
||||
return [FBAXClient attributesForElement:element
|
||||
attributes:attributes
|
||||
error:error];
|
||||
}
|
||||
|
||||
- (XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid
|
||||
{
|
||||
NSMutableSet *terminatedAppIds = [NSMutableSet set];
|
||||
for (NSNumber *appPid in self.appsCache) {
|
||||
if (![self.appsCache[appPid] running]) {
|
||||
[terminatedAppIds addObject:appPid];
|
||||
}
|
||||
}
|
||||
for (NSNumber *appPid in terminatedAppIds) {
|
||||
[self.appsCache removeObjectForKey:appPid];
|
||||
}
|
||||
|
||||
XCUIApplication *result = [self.appsCache objectForKey:@(pid)];
|
||||
if (nil != result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
XCUIApplication *app = [[FBAXClient applicationProcessTracker]
|
||||
monitoredApplicationWithProcessIdentifier:pid];
|
||||
if (nil == app) {
|
||||
return nil;
|
||||
}
|
||||
[self.appsCache setObject:app forKey:@(pid)];
|
||||
return app;
|
||||
}
|
||||
|
||||
@end
|
||||
45
WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h
Normal file
45
WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.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 <XCTest/XCTest.h>
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
#endif
|
||||
|
||||
#import "XCSynthesizedEventRecord.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol XCTestManager_ManagerInterface;
|
||||
@class FBScreenRecordingRequest, FBScreenRecordingPromise;
|
||||
|
||||
@interface FBXCTestDaemonsProxy : NSObject
|
||||
|
||||
+ (id<XCTestManager_ManagerInterface>)testRunnerProxy;
|
||||
|
||||
+ (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record
|
||||
error:(NSError *__autoreleasing*)error;
|
||||
|
||||
+ (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError **)error;
|
||||
+ (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError **)error;
|
||||
|
||||
+ (nullable FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecordingRequest *)request
|
||||
error:(NSError **)error;
|
||||
+ (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid
|
||||
error:(NSError **)error;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
+ (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError **)error;
|
||||
+ (nullable CLLocation *)getSimulatedLocation:(NSError **)error;
|
||||
+ (BOOL)clearSimulatedLocation:(NSError **)error;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
359
WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m
Normal file
359
WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 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 "FBXCTestDaemonsProxy.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBExceptions.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBRunLoopSpinner.h"
|
||||
#import "FBScreenRecordingPromise.h"
|
||||
#import "FBScreenRecordingRequest.h"
|
||||
#import "XCTestDriver.h"
|
||||
#import "XCTRunnerDaemonSession.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIDevice.h"
|
||||
|
||||
#define LAUNCH_APP_TIMEOUT_SEC 300
|
||||
|
||||
static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *));
|
||||
|
||||
static void swizzledLaunchApp(id self, SEL _cmd, NSString *path, NSString *bundleID,
|
||||
NSArray *arguments, NSDictionary *environment,
|
||||
void (^reply)(_Bool, NSError *))
|
||||
{
|
||||
__block BOOL isSuccessful;
|
||||
__block NSError *error;
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
originalLaunchAppMethod(self, _cmd, path, bundleID, arguments, environment, ^(BOOL passed, NSError *innerError) {
|
||||
isSuccessful = passed;
|
||||
error = innerError;
|
||||
dispatch_semaphore_signal(sem);
|
||||
});
|
||||
int64_t timeoutNs = (int64_t)(LAUNCH_APP_TIMEOUT_SEC * NSEC_PER_SEC);
|
||||
if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) {
|
||||
NSString *message = [NSString stringWithFormat:@"The application '%@' cannot be launched within %d seconds timeout",
|
||||
bundleID ?: path, LAUNCH_APP_TIMEOUT_SEC];
|
||||
@throw [NSException exceptionWithName:FBTimeoutException reason:message userInfo:nil];
|
||||
}
|
||||
if (!isSuccessful || nil != error) {
|
||||
[FBLogger logFmt:@"%@", error.description];
|
||||
NSString *message = error.description ?: [NSString stringWithFormat:@"The application '%@' is not installed on the device under test",
|
||||
bundleID ?: path];
|
||||
@throw [NSException exceptionWithName:FBApplicationMissingException reason:message userInfo:nil];
|
||||
}
|
||||
reply(isSuccessful, error);
|
||||
}
|
||||
|
||||
@implementation FBXCTestDaemonsProxy
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-load-method"
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
[self.class swizzleLaunchApp];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
+ (void)swizzleLaunchApp {
|
||||
Method original = class_getInstanceMethod([XCTRunnerDaemonSession class],
|
||||
@selector(launchApplicationWithPath:bundleID:arguments:environment:completion:));
|
||||
if (original == nil) {
|
||||
[FBLogger log:@"Could not find method -[XCTRunnerDaemonSession launchApplicationWithPath:]"];
|
||||
return;
|
||||
}
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wcast-function-type-strict"
|
||||
// Workaround for https://github.com/appium/WebDriverAgent/issues/702
|
||||
originalLaunchAppMethod = (void(*)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *))) method_getImplementation(original);
|
||||
method_setImplementation(original, (IMP)swizzledLaunchApp);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
+ (id<XCTestManager_ManagerInterface>)testRunnerProxy
|
||||
{
|
||||
static id<XCTestManager_ManagerInterface> proxy = nil;
|
||||
if ([FBConfiguration shouldUseSingletonTestManager]) {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
[FBLogger logFmt:@"Using singleton test manager"];
|
||||
proxy = [self.class retrieveTestRunnerProxy];
|
||||
});
|
||||
} else {
|
||||
[FBLogger logFmt:@"Using general test manager"];
|
||||
proxy = [self.class retrieveTestRunnerProxy];
|
||||
}
|
||||
NSAssert(proxy != NULL, @"Could not determine testRunnerProxy", proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
+ (id<XCTestManager_ManagerInterface>)retrieveTestRunnerProxy
|
||||
{
|
||||
return ((XCTRunnerDaemonSession *)[XCTRunnerDaemonSession sharedSession]).daemonProxy;
|
||||
}
|
||||
|
||||
+ (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error
|
||||
{
|
||||
__block NSError *innerError = nil;
|
||||
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
|
||||
void (^errorHandler)(NSError *) = ^(NSError *invokeError) {
|
||||
if (nil != invokeError) {
|
||||
innerError = invokeError;
|
||||
}
|
||||
completion();
|
||||
};
|
||||
|
||||
XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) {
|
||||
errorHandler(invokeError);
|
||||
};
|
||||
[[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) {
|
||||
handlerBlock(record, invokeError);
|
||||
}];
|
||||
}];
|
||||
if (nil != innerError) {
|
||||
if (error) {
|
||||
*error = innerError;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError *__autoreleasing*)error
|
||||
{
|
||||
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
|
||||
if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs with given application"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
__block NSError *innerError = nil;
|
||||
__block BOOL didSucceed = NO;
|
||||
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
|
||||
[session openURL:url usingApplication:bundleId completion:^(bool result, NSError *invokeError) {
|
||||
if (nil != invokeError) {
|
||||
innerError = invokeError;
|
||||
} else {
|
||||
didSucceed = result;
|
||||
}
|
||||
completion();
|
||||
}];
|
||||
}];
|
||||
if (nil != innerError && error) {
|
||||
*error = innerError;
|
||||
}
|
||||
return didSucceed;
|
||||
}
|
||||
|
||||
+ (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasing*)error
|
||||
{
|
||||
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
|
||||
if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
__block NSError *innerError = nil;
|
||||
__block BOOL didSucceed = NO;
|
||||
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
|
||||
[session openDefaultApplicationForURL:url completion:^(bool result, NSError *invokeError) {
|
||||
if (nil != invokeError) {
|
||||
innerError = invokeError;
|
||||
} else {
|
||||
didSucceed = result;
|
||||
}
|
||||
completion();
|
||||
}];
|
||||
}];
|
||||
if (nil != innerError && error) {
|
||||
*error = innerError;
|
||||
}
|
||||
return didSucceed;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
+ (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleasing*)error
|
||||
{
|
||||
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
|
||||
if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
|
||||
buildError:error];
|
||||
}
|
||||
if (![session supportsLocationSimulation]) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Your device does not support location simulation"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
__block NSError *innerError = nil;
|
||||
__block BOOL didSucceed = NO;
|
||||
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
|
||||
[session setSimulatedLocation:location completion:^(bool result, NSError *invokeError) {
|
||||
if (nil != invokeError) {
|
||||
innerError = invokeError;
|
||||
} else {
|
||||
didSucceed = result;
|
||||
}
|
||||
completion();
|
||||
}];
|
||||
}];
|
||||
if (nil != innerError && error) {
|
||||
*error = innerError;
|
||||
}
|
||||
return didSucceed;
|
||||
}
|
||||
|
||||
+ (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error;
|
||||
{
|
||||
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
|
||||
if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
if (![session supportsLocationSimulation]) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Your device does not support location simulation"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
|
||||
__block NSError *innerError = nil;
|
||||
__block CLLocation *location = nil;
|
||||
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
|
||||
[session getSimulatedLocationWithReply:^(CLLocation *reply, NSError *invokeError) {
|
||||
if (nil != invokeError) {
|
||||
innerError = invokeError;
|
||||
} else {
|
||||
location = reply;
|
||||
}
|
||||
completion();
|
||||
}];
|
||||
}];
|
||||
if (nil != innerError && error) {
|
||||
*error = innerError;
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
+ (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error
|
||||
{
|
||||
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
|
||||
if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
|
||||
buildError:error];
|
||||
}
|
||||
if (![session supportsLocationSimulation]) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Your device does not support location simulation"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
__block NSError *innerError = nil;
|
||||
__block BOOL didSucceed = NO;
|
||||
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
|
||||
[session clearSimulatedLocationWithReply:^(bool result, NSError *invokeError) {
|
||||
if (nil != invokeError) {
|
||||
innerError = invokeError;
|
||||
} else {
|
||||
didSucceed = result;
|
||||
}
|
||||
completion();
|
||||
}];
|
||||
}];
|
||||
if (nil != innerError && error) {
|
||||
*error = innerError;
|
||||
}
|
||||
return didSucceed;
|
||||
}
|
||||
#endif
|
||||
|
||||
+ (FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecordingRequest *)request
|
||||
error:(NSError *__autoreleasing*)error
|
||||
{
|
||||
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
|
||||
if (![session respondsToSelector:@selector(startScreenRecordingWithRequest:withReply:)]) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
if (![session supportsScreenRecording]) {
|
||||
[[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Your device does not support screen recording"]
|
||||
buildError:error];
|
||||
return nil;
|
||||
}
|
||||
|
||||
id nativeRequest = [request toNativeRequestWithError:error];
|
||||
if (nil == nativeRequest) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
__block id futureMetadata = nil;
|
||||
__block NSError *innerError = nil;
|
||||
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
|
||||
[session startScreenRecordingWithRequest:nativeRequest withReply:^(id reply, NSError *invokeError) {
|
||||
if (nil == invokeError) {
|
||||
futureMetadata = reply;
|
||||
} else {
|
||||
innerError = invokeError;
|
||||
}
|
||||
completion();
|
||||
}];
|
||||
}];
|
||||
if (nil != innerError) {
|
||||
if (error) {
|
||||
*error = innerError;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
return [[FBScreenRecordingPromise alloc] initWithNativePromise:futureMetadata];
|
||||
}
|
||||
|
||||
+ (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid error:(NSError *__autoreleasing*)error
|
||||
{
|
||||
XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession];
|
||||
if (![session respondsToSelector:@selector(stopScreenRecordingWithUUID:withReply:)]) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"]
|
||||
buildError:error];
|
||||
|
||||
}
|
||||
if (![session supportsScreenRecording]) {
|
||||
return [[[FBErrorBuilder builder]
|
||||
withDescriptionFormat:@"Your device does not support screen recording"]
|
||||
buildError:error];
|
||||
}
|
||||
|
||||
__block NSError *innerError = nil;
|
||||
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
|
||||
[session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) {
|
||||
if (nil != invokeError) {
|
||||
innerError = invokeError;
|
||||
}
|
||||
completion();
|
||||
}];
|
||||
}];
|
||||
if (nil != innerError && error) {
|
||||
*error = innerError;
|
||||
}
|
||||
return nil == innerError;
|
||||
}
|
||||
|
||||
@end
|
||||
80
WebDriverAgentLib/Utilities/FBXCodeCompatibility.h
Normal file
80
WebDriverAgentLib/Utilities/FBXCodeCompatibility.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 <WebDriverAgentLib/WebDriverAgentLib.h>
|
||||
#import "XCPointerEvent.h"
|
||||
|
||||
@class FBXCElementSnapshot;
|
||||
|
||||
/**
|
||||
The version of testmanagerd process which is running on the device.
|
||||
|
||||
Potentially, we can handle processes based on this version instead of iOS versions,
|
||||
iOS 10.1 -> 6
|
||||
iOS 11.0.1 -> 18
|
||||
iOS 11.4 -> 22
|
||||
iOS 12.1, 12.4 -> 26
|
||||
iOS 13.0, 13.4.1 -> 28
|
||||
|
||||
tvOS 13.3 -> 28
|
||||
|
||||
@return The version of testmanagerd
|
||||
*/
|
||||
NSInteger FBTestmanagerdVersion(void);
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface XCUIElementQuery (FBCompatibility)
|
||||
|
||||
/* Performs short-circuit UI tree traversion in iOS 11+ to get the first element matched by the query. Equals to nil if no matching elements are found */
|
||||
@property(nullable, readonly) XCUIElement *fb_firstMatch;
|
||||
|
||||
/*
|
||||
This is the local wrapper for bounded elements extraction.
|
||||
It uses either indexed or bounded binding based on the `boundElementsByIndex` configuration
|
||||
flag value.
|
||||
*/
|
||||
@property(readonly) NSArray<XCUIElement *> *fb_allMatches;
|
||||
|
||||
/**
|
||||
Returns single unique matching snapshot for the given query
|
||||
|
||||
@param error The error instance if there was a failure while retrieveing the snapshot
|
||||
@returns The cached unqiue snapshot or nil if the element is stale
|
||||
*/
|
||||
- (nullable id<FBXCElementSnapshot>)fb_uniqueSnapshotWithError:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface XCPointerEvent (FBCompatibility)
|
||||
|
||||
- (BOOL)fb_areKeyEventsSupported;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface XCUIElement (FBCompatibility)
|
||||
|
||||
/**
|
||||
Determines whether current iOS SDK supports non modal elements inlusion into snapshots
|
||||
|
||||
@return Either YES or NO
|
||||
*/
|
||||
+ (BOOL)fb_supportsNonModalElementsInclusion;
|
||||
|
||||
/**
|
||||
Retrieves element query
|
||||
|
||||
@return Element query property extended with non modal elements depending on the actual configuration
|
||||
*/
|
||||
- (XCUIElementQuery *)fb_query;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
99
WebDriverAgentLib/Utilities/FBXCodeCompatibility.m
Normal file
99
WebDriverAgentLib/Utilities/FBXCodeCompatibility.m
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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 "FBXCodeCompatibility.h"
|
||||
|
||||
#import "FBXCAXClientProxy.h"
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBErrorBuilder.h"
|
||||
#import "FBLogger.h"
|
||||
#import "XCUIApplication+FBHelpers.h"
|
||||
#import "XCUIElementQuery.h"
|
||||
#import "FBXCTestDaemonsProxy.h"
|
||||
#import "XCTestManager_ManagerInterface-Protocol.h"
|
||||
|
||||
@implementation XCUIElementQuery (FBCompatibility)
|
||||
|
||||
- (id<FBXCElementSnapshot>)fb_uniqueSnapshotWithError:(NSError **)error
|
||||
{
|
||||
return (id<FBXCElementSnapshot>)[self uniqueMatchingSnapshotWithError:error];
|
||||
}
|
||||
|
||||
- (XCUIElement *)fb_firstMatch
|
||||
{
|
||||
if (FBConfiguration.useFirstMatch) {
|
||||
XCUIElement* match = self.firstMatch;
|
||||
return [match exists] ? match : nil;
|
||||
}
|
||||
return self.fb_allMatches.firstObject;
|
||||
}
|
||||
|
||||
- (NSArray<XCUIElement *> *)fb_allMatches
|
||||
{
|
||||
return FBConfiguration.boundElementsByIndex
|
||||
? self.allElementsBoundByIndex
|
||||
: self.allElementsBoundByAccessibilityElement;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation XCUIElement (FBCompatibility)
|
||||
|
||||
+ (BOOL)fb_supportsNonModalElementsInclusion
|
||||
{
|
||||
static dispatch_once_t hasIncludingNonModalElements;
|
||||
static BOOL result;
|
||||
dispatch_once(&hasIncludingNonModalElements, ^{
|
||||
result = [XCUIApplication.fb_systemApplication.query respondsToSelector:@selector(includingNonModalElements)];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
- (XCUIElementQuery *)fb_query
|
||||
{
|
||||
return FBConfiguration.includeNonModalElements && self.class.fb_supportsNonModalElementsInclusion
|
||||
? self.query.includingNonModalElements
|
||||
: self.query;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation XCPointerEvent (FBXcodeCompatibility)
|
||||
|
||||
+ (BOOL)fb_areKeyEventsSupported
|
||||
{
|
||||
static BOOL isKbInputSupported = NO;
|
||||
static dispatch_once_t onceKbInputSupported;
|
||||
dispatch_once(&onceKbInputSupported, ^{
|
||||
isKbInputSupported = [XCPointerEvent.class respondsToSelector:@selector(keyboardEventForKeyCode:keyPhase:modifierFlags:offset:)];
|
||||
});
|
||||
return isKbInputSupported;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NSInteger FBTestmanagerdVersion(void)
|
||||
{
|
||||
static dispatch_once_t getTestmanagerdVersion;
|
||||
static NSInteger testmanagerdVersion;
|
||||
dispatch_once(&getTestmanagerdVersion, ^{
|
||||
id<XCTestManager_ManagerInterface> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
|
||||
if ([(NSObject *)proxy respondsToSelector:@selector(_XCT_exchangeProtocolVersion:reply:)]) {
|
||||
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
|
||||
[proxy _XCT_exchangeProtocolVersion:testmanagerdVersion reply:^(unsigned long long code) {
|
||||
testmanagerdVersion = (NSInteger) code;
|
||||
completion();
|
||||
}];
|
||||
}];
|
||||
} else {
|
||||
testmanagerdVersion = 0xFFFF;
|
||||
}
|
||||
});
|
||||
return testmanagerdVersion;
|
||||
}
|
||||
45
WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h
Normal file
45
WebDriverAgentLib/Utilities/FBXMLGenerationOptions.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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBXMLGenerationOptions : NSObject
|
||||
|
||||
/**
|
||||
XML buidling scope. Passing nil means the XML should be built in the default scope,
|
||||
i.e no changes to the original tree structore. If the scope is provided then the resulting
|
||||
XML tree will be put under the root, which name is equal to the given scope value.
|
||||
*/
|
||||
@property (nonatomic, nullable) NSString *scope;
|
||||
/**
|
||||
The list of attribute names to exclude from the resulting document.
|
||||
Passing nil means all the available attributes should be included
|
||||
*/
|
||||
@property (nonatomic, nullable) NSArray<NSString *> *excludedAttributes;
|
||||
|
||||
/**
|
||||
Allows to provide XML scope.
|
||||
|
||||
@param scope See the property description above
|
||||
@return self instance for chaining
|
||||
*/
|
||||
- (FBXMLGenerationOptions *)withScope:(nullable NSString *)scope;
|
||||
|
||||
/**
|
||||
Allows to provide a list of excluded XML attributes.
|
||||
|
||||
@param excludedAttributes See the property description above
|
||||
@return self instance for chaining
|
||||
*/
|
||||
- (FBXMLGenerationOptions *)withExcludedAttributes:(nullable NSArray<NSString *> *)excludedAttributes;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
25
WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m
Normal file
25
WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m
Normal file
@@ -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 "FBXMLGenerationOptions.h"
|
||||
|
||||
@implementation FBXMLGenerationOptions
|
||||
|
||||
- (FBXMLGenerationOptions *)withScope:(NSString *)scope
|
||||
{
|
||||
self.scope = scope;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (FBXMLGenerationOptions *)withExcludedAttributes:(NSArray<NSString *> *)excludedAttributes
|
||||
{
|
||||
self.excludedAttributes = excludedAttributes;
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
56
WebDriverAgentLib/Utilities/FBXPath-Private.h
Normal file
56
WebDriverAgentLib/Utilities/FBXPath-Private.h
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 <WebDriverAgentLib/FBXPath.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBXPath ()
|
||||
|
||||
/**
|
||||
Gets xmllib2-compatible XML representation of n XCElementSnapshot instance
|
||||
|
||||
@param root the root element to execute XPath query for
|
||||
@param writer the correspondig libxml2 writer object
|
||||
@param elementStore an empty dictionary to store indexes mapping or nil if no mappings should be stored
|
||||
@param query Optional XPath query value. By analyzing this query we may optimize the lookup speed.
|
||||
@param excludedAttributes The list of XML attribute names to be excluded from the generated XML representation.
|
||||
Setting nil to this argument means that none of the known attributes must be excluded.
|
||||
If `query` argument is assigned then `excludedAttributes` argument is effectively ignored.
|
||||
@return zero if the method has completed successfully
|
||||
*/
|
||||
+ (int)xmlRepresentationWithRootElement:(id<FBXCElementSnapshot>)root
|
||||
writer:(xmlTextWriterPtr)writer
|
||||
elementStore:(nullable NSMutableDictionary *)elementStore
|
||||
query:(nullable NSString*)query
|
||||
excludingAttributes:(nullable NSArray<NSString *> *)excludedAttributes;
|
||||
|
||||
/**
|
||||
Gets the list of matched snapshots from xmllib2-compatible xmlNodeSetPtr structure
|
||||
|
||||
@param nodeSet set of nodes returned after successful XPath evaluation
|
||||
@param elementStore dictionary containing index->snapshot mapping
|
||||
@return array of filtered elements or nil in case of failure. Can be empty array as well
|
||||
*/
|
||||
+ (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet elementStore:(NSMutableDictionary *)elementStore;
|
||||
|
||||
/**
|
||||
Gets the list of matched XPath nodes from xmllib2-compatible XML document
|
||||
|
||||
@param xpathQuery actual query. Should be valid XPath 1.0-compatible expression
|
||||
@param document libxml2-compatible document pointer
|
||||
@param contextNode Optonal context node instance
|
||||
@return pointer to a libxml2-compatible structure with set of matched nodes or NULL in case of failure
|
||||
*/
|
||||
+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
|
||||
document:(xmlDocPtr)doc
|
||||
contextNode:(nullable xmlNodePtr)contextNode;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
59
WebDriverAgentLib/Utilities/FBXPath.h
Normal file
59
WebDriverAgentLib/Utilities/FBXPath.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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>
|
||||
|
||||
#ifdef __clang__
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wpadded"
|
||||
#endif
|
||||
|
||||
#import <libxml/tree.h>
|
||||
#import <libxml/parser.h>
|
||||
#import <libxml/xpath.h>
|
||||
#import <libxml/xpathInternals.h>
|
||||
#import <libxml/encoding.h>
|
||||
#import <libxml/xmlwriter.h>
|
||||
|
||||
#ifdef __clang__
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
|
||||
@class FBXMLGenerationOptions;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FBXPath : NSObject
|
||||
|
||||
/**
|
||||
Returns an array of descendants matching given xpath query
|
||||
|
||||
@param root the root element to execute XPath query for
|
||||
@param xpathQuery requested xpath query
|
||||
@return an array of descendants matching the given xpath query or an empty array if no matches were found
|
||||
@throws NSException if there is an unexpected internal error during xml parsing
|
||||
*/
|
||||
+ (NSArray<id<FBXCElementSnapshot>> *)matchesWithRootElement:(id<FBElement>)root
|
||||
forQuery:(NSString *)xpathQuery;
|
||||
|
||||
/**
|
||||
Gets XML representation of XCElementSnapshot with all its descendants. This method generates the same
|
||||
representation, which is used for XPath search
|
||||
|
||||
@param root the root element
|
||||
@param options Optional values that affect the resulting XML creation process
|
||||
@return valid XML document as string or nil in case of failure
|
||||
*/
|
||||
+ (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
|
||||
options:(nullable FBXMLGenerationOptions *)options;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
954
WebDriverAgentLib/Utilities/FBXPath.m
Normal file
954
WebDriverAgentLib/Utilities/FBXPath.m
Normal file
@@ -0,0 +1,954 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import "FBXPath.h"
|
||||
|
||||
#import "FBConfiguration.h"
|
||||
#import "FBExceptions.h"
|
||||
#import "FBElementUtils.h"
|
||||
#import "FBLogger.h"
|
||||
#import "FBMacros.h"
|
||||
#import "FBXMLGenerationOptions.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
#import "NSString+FBXMLSafeString.h"
|
||||
#import "XCUIApplication.h"
|
||||
#import "XCUIElement.h"
|
||||
#import "XCUIElement+FBCaching.h"
|
||||
#import "XCUIElement+FBUtilities.h"
|
||||
#import "XCUIElement+FBWebDriverAttributes.h"
|
||||
#import "XCTestPrivateSymbols.h"
|
||||
#import "FBElementHelpers.h"
|
||||
#import "FBXCAXClientProxy.h"
|
||||
#import "FBXCAccessibilityElement.h"
|
||||
|
||||
|
||||
@interface FBElementAttribute : NSObject
|
||||
|
||||
@property (nonatomic, readonly) id<FBElement> element;
|
||||
|
||||
+ (nonnull NSString *)name;
|
||||
+ (nullable NSString *)valueForElement:(id<FBElement>)element;
|
||||
|
||||
+ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element;
|
||||
+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value;
|
||||
|
||||
+ (NSArray<Class> *)supportedAttributes;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBTypeAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBValueAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBNameAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBLabelAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBEnabledAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBVisibleAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBAccessibleAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBDimensionAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBXAttribute : FBDimensionAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBYAttribute : FBDimensionAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBWidthAttribute : FBDimensionAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBHeightAttribute : FBDimensionAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBIndexAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBHittableAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBInternalIndexAttribute : FBElementAttribute
|
||||
|
||||
@property (nonatomic, nonnull, readonly) NSString* indexValue;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBApplicationBundleIdAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBApplicationPidAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBPlaceholderValueAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBNativeFrameAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBTraitsAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBMinValueAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface FBMaxValueAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
#if TARGET_OS_TV
|
||||
|
||||
@interface FBFocusedAttribute : FBElementAttribute
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
const static char *_UTF8Encoding = "UTF-8";
|
||||
|
||||
static NSString *const kXMLIndexPathKey = @"private_indexPath";
|
||||
static NSString *const topNodeIndexPath = @"top";
|
||||
|
||||
@implementation FBXPath
|
||||
|
||||
+ (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery
|
||||
{
|
||||
NSString *reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery];
|
||||
@throw [NSException exceptionWithName:name reason:reason userInfo:@{}];
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (nullable NSString *)xmlStringWithRootElement:(id<FBElement>)root
|
||||
options:(nullable FBXMLGenerationOptions *)options
|
||||
{
|
||||
xmlDocPtr doc;
|
||||
xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
|
||||
int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
|
||||
if (rc < 0) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
|
||||
} else {
|
||||
BOOL hasScope = nil != options.scope && [options.scope length] > 0;
|
||||
if (hasScope) {
|
||||
rc = xmlTextWriterStartElement(writer,
|
||||
(xmlChar *)[[self safeXmlStringWithString:options.scope] UTF8String]);
|
||||
if (rc < 0) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", options.scope, rc];
|
||||
}
|
||||
}
|
||||
|
||||
if (rc >= 0) {
|
||||
[self waitUntilStableWithElement:root];
|
||||
// If 'includeHittableInPageSource' setting is enabled, then use native snapshots
|
||||
// to calculate a more accurate value for the 'hittable' attribute.
|
||||
rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root
|
||||
useNative:FBConfiguration.includeHittableInPageSource]
|
||||
writer:writer
|
||||
elementStore:nil
|
||||
query:nil
|
||||
excludingAttributes:options.excludedAttributes];
|
||||
}
|
||||
|
||||
if (rc >= 0 && hasScope) {
|
||||
rc = xmlTextWriterEndElement(writer);
|
||||
if (rc < 0) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
|
||||
}
|
||||
}
|
||||
|
||||
if (rc >= 0) {
|
||||
rc = xmlTextWriterEndDocument(writer);
|
||||
if (rc < 0) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rc < 0) {
|
||||
xmlFreeTextWriter(writer);
|
||||
xmlFreeDoc(doc);
|
||||
return nil;
|
||||
}
|
||||
int buffersize;
|
||||
xmlChar *xmlbuff;
|
||||
xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1);
|
||||
xmlFreeTextWriter(writer);
|
||||
xmlFreeDoc(doc);
|
||||
NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding];
|
||||
xmlFree(xmlbuff);
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (NSArray<id<FBXCElementSnapshot>> *)matchesWithRootElement:(id<FBElement>)root
|
||||
forQuery:(NSString *)xpathQuery
|
||||
{
|
||||
xmlDocPtr doc;
|
||||
|
||||
xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0);
|
||||
if (NULL == writer) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlNewTextWriterDoc for XPath query \"%@\"", xpathQuery];
|
||||
return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
|
||||
}
|
||||
NSMutableDictionary *elementStore = [NSMutableDictionary dictionary];
|
||||
int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL);
|
||||
id<FBXCElementSnapshot> lookupScopeSnapshot = nil;
|
||||
id<FBXCElementSnapshot> contextRootSnapshot = nil;
|
||||
BOOL useNativeSnapshot = nil == xpathQuery
|
||||
? NO
|
||||
: [[self.class elementAttributesWithXPathQuery:xpathQuery] containsObject:FBHittableAttribute.class];
|
||||
if (rc < 0) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc];
|
||||
} else {
|
||||
[self waitUntilStableWithElement:root];
|
||||
if (FBConfiguration.limitXpathContextScope) {
|
||||
lookupScopeSnapshot = [self snapshotWithRoot:root useNative:useNativeSnapshot];
|
||||
} else {
|
||||
if ([root isKindOfClass:XCUIElement.class]) {
|
||||
lookupScopeSnapshot = [self snapshotWithRoot:[(XCUIElement *)root application]
|
||||
useNative:useNativeSnapshot];
|
||||
contextRootSnapshot = [root isKindOfClass:XCUIApplication.class]
|
||||
? nil
|
||||
: ([(XCUIElement *)root lastSnapshot] ?: [self snapshotWithRoot:(XCUIElement *)root
|
||||
useNative:useNativeSnapshot]);
|
||||
} else {
|
||||
lookupScopeSnapshot = (id<FBXCElementSnapshot>)root;
|
||||
contextRootSnapshot = nil == lookupScopeSnapshot.parent ? nil : (id<FBXCElementSnapshot>)root;
|
||||
while (nil != lookupScopeSnapshot.parent) {
|
||||
lookupScopeSnapshot = lookupScopeSnapshot.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rc = [self xmlRepresentationWithRootElement:lookupScopeSnapshot
|
||||
writer:writer
|
||||
elementStore:elementStore
|
||||
query:xpathQuery
|
||||
excludingAttributes:nil];
|
||||
if (rc >= 0) {
|
||||
rc = xmlTextWriterEndDocument(writer);
|
||||
if (rc < 0) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndDocument. Error code: %d", rc];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rc < 0) {
|
||||
xmlFreeTextWriter(writer);
|
||||
xmlFreeDoc(doc);
|
||||
return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
|
||||
}
|
||||
|
||||
xmlXPathObjectPtr contextNodeQueryResult = [self matchNodeInDocument:doc
|
||||
elementStore:elementStore.copy
|
||||
forSnapshot:contextRootSnapshot];
|
||||
xmlNodePtr contextNode = NULL;
|
||||
if (NULL != contextNodeQueryResult) {
|
||||
xmlNodeSetPtr nodeSet = contextNodeQueryResult->nodesetval;
|
||||
if (!xmlXPathNodeSetIsEmpty(nodeSet)) {
|
||||
contextNode = nodeSet->nodeTab[0];
|
||||
}
|
||||
}
|
||||
xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery
|
||||
document:doc
|
||||
contextNode:contextNode];
|
||||
if (NULL != contextNodeQueryResult) {
|
||||
xmlXPathFreeObject(contextNodeQueryResult);
|
||||
}
|
||||
if (NULL == queryResult) {
|
||||
xmlFreeTextWriter(writer);
|
||||
xmlFreeDoc(doc);
|
||||
return [self throwException:FBInvalidXPathException forQuery:xpathQuery];
|
||||
}
|
||||
|
||||
NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval
|
||||
elementStore:elementStore];
|
||||
xmlXPathFreeObject(queryResult);
|
||||
xmlFreeTextWriter(writer);
|
||||
xmlFreeDoc(doc);
|
||||
if (nil == matchingSnapshots) {
|
||||
return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery];
|
||||
}
|
||||
return matchingSnapshots;
|
||||
}
|
||||
|
||||
+ (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet
|
||||
elementStore:(NSMutableDictionary *)elementStore
|
||||
{
|
||||
if (xmlXPathNodeSetIsEmpty(nodeSet)) {
|
||||
return @[];
|
||||
}
|
||||
NSMutableArray *matchingSnapshots = [NSMutableArray array];
|
||||
const xmlChar *indexPathKeyName = (xmlChar *)[kXMLIndexPathKey UTF8String];
|
||||
for (NSInteger i = 0; i < nodeSet->nodeNr; i++) {
|
||||
xmlNodePtr currentNode = nodeSet->nodeTab[i];
|
||||
xmlChar *attrValue = xmlGetProp(currentNode, indexPathKeyName);
|
||||
if (NULL == attrValue) {
|
||||
[FBLogger log:@"Failed to invoke libxml2>xmlGetProp"];
|
||||
return nil;
|
||||
}
|
||||
id<FBXCElementSnapshot> element = [elementStore objectForKey:(id)[NSString stringWithCString:(const char *)attrValue encoding:NSUTF8StringEncoding]];
|
||||
if (element) {
|
||||
[matchingSnapshots addObject:element];
|
||||
}
|
||||
xmlFree(attrValue);
|
||||
}
|
||||
return matchingSnapshots.copy;
|
||||
}
|
||||
|
||||
+ (nullable xmlXPathObjectPtr)matchNodeInDocument:(xmlDocPtr)doc
|
||||
elementStore:(NSDictionary<NSString *, id<FBXCElementSnapshot>> *)elementStore
|
||||
forSnapshot:(nullable id<FBXCElementSnapshot>)snapshot
|
||||
{
|
||||
if (nil == snapshot) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NSString *contextRootUid = [FBElementUtils uidWithAccessibilityElement:[(id)snapshot accessibilityElement]];
|
||||
if (nil == contextRootUid) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (NSString *key in elementStore) {
|
||||
id<FBXCElementSnapshot> value = [elementStore objectForKey:key];
|
||||
NSString *snapshotUid = [FBElementUtils uidWithAccessibilityElement:[value accessibilityElement]];
|
||||
if (nil == snapshotUid || ![snapshotUid isEqualToString:contextRootUid]) {
|
||||
continue;
|
||||
}
|
||||
NSString *indexQuery = [NSString stringWithFormat:@"//*[@%@=\"%@\"]", kXMLIndexPathKey, key];
|
||||
xmlXPathObjectPtr queryResult = [self evaluate:indexQuery
|
||||
document:doc
|
||||
contextNode:NULL];
|
||||
if (NULL != queryResult) {
|
||||
return queryResult;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+ (NSSet<Class> *)elementAttributesWithXPathQuery:(NSString *)query
|
||||
{
|
||||
if ([query rangeOfString:@"[^\\w@]@\\*[^\\w]" options:NSRegularExpressionSearch].location != NSNotFound) {
|
||||
// read all element attributes if 'star' attribute name pattern is used in xpath query
|
||||
return [NSSet setWithArray:FBElementAttribute.supportedAttributes];
|
||||
}
|
||||
NSMutableSet<Class> *result = [NSMutableSet set];
|
||||
for (Class attributeCls in FBElementAttribute.supportedAttributes) {
|
||||
if ([query rangeOfString:[NSString stringWithFormat:@"[^\\w@]@%@[^\\w]", [attributeCls name]] options:NSRegularExpressionSearch].location != NSNotFound) {
|
||||
[result addObject:attributeCls];
|
||||
}
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
+ (int)xmlRepresentationWithRootElement:(id<FBXCElementSnapshot>)root
|
||||
writer:(xmlTextWriterPtr)writer
|
||||
elementStore:(nullable NSMutableDictionary *)elementStore
|
||||
query:(nullable NSString*)query
|
||||
excludingAttributes:(nullable NSArray<NSString *> *)excludedAttributes
|
||||
{
|
||||
// Trying to be smart here and only including attributes, that were asked in the query, to the resulting document.
|
||||
// This may speed up the lookup significantly in some cases
|
||||
NSMutableSet<Class> *includedAttributes;
|
||||
if (nil == query) {
|
||||
includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes];
|
||||
if (!FBConfiguration.includeHittableInPageSource) {
|
||||
// The hittable attribute is expensive to calculate for each snapshot item
|
||||
// thus we only include it when requested explicitly
|
||||
[includedAttributes removeObject:FBHittableAttribute.class];
|
||||
}
|
||||
if (!FBConfiguration.includeNativeFrameInPageSource) {
|
||||
// Include nativeFrame only when requested
|
||||
[includedAttributes removeObject:FBNativeFrameAttribute.class];
|
||||
}
|
||||
if (!FBConfiguration.includeMinMaxValueInPageSource) {
|
||||
// minValue/maxValue are retrieved from private APIs and may be slow on deep trees
|
||||
[includedAttributes removeObject:FBMinValueAttribute.class];
|
||||
[includedAttributes removeObject:FBMaxValueAttribute.class];
|
||||
}
|
||||
if (nil != excludedAttributes) {
|
||||
for (NSString *excludedAttributeName in excludedAttributes) {
|
||||
for (Class supportedAttribute in FBElementAttribute.supportedAttributes) {
|
||||
if ([[supportedAttribute name] caseInsensitiveCompare:excludedAttributeName] == NSOrderedSame) {
|
||||
[includedAttributes removeObject:supportedAttribute];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
includedAttributes = [self.class elementAttributesWithXPathQuery:query].mutableCopy;
|
||||
}
|
||||
[FBLogger logFmt:@"The following attributes were requested to be included into the XML: %@", includedAttributes];
|
||||
|
||||
int rc = [self writeXmlWithRootElement:root
|
||||
indexPath:(elementStore != nil ? topNodeIndexPath : nil)
|
||||
elementStore:elementStore
|
||||
includedAttributes:includedAttributes.copy
|
||||
writer:writer];
|
||||
if (rc < 0) {
|
||||
[FBLogger log:@"Failed to generate XML presentation of a screen element"];
|
||||
return rc;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery
|
||||
document:(xmlDocPtr)doc
|
||||
contextNode:(nullable xmlNodePtr)contextNode
|
||||
{
|
||||
xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc);
|
||||
if (NULL == xpathCtx) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery];
|
||||
return NULL;
|
||||
}
|
||||
xpathCtx->node = NULL == contextNode ? doc->children : contextNode;
|
||||
|
||||
xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx);
|
||||
if (NULL == xpathObj) {
|
||||
xmlXPathFreeContext(xpathCtx);
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery];
|
||||
return NULL;
|
||||
}
|
||||
xmlXPathFreeContext(xpathCtx);
|
||||
return xpathObj;
|
||||
}
|
||||
|
||||
+ (nullable NSString *)safeXmlStringWithString:(NSString *)str
|
||||
{
|
||||
return [str fb_xmlSafeStringWithReplacement:@""];
|
||||
}
|
||||
|
||||
+ (int)recordElementAttributes:(xmlTextWriterPtr)writer
|
||||
forElement:(id<FBXCElementSnapshot>)element
|
||||
indexPath:(nullable NSString *)indexPath
|
||||
includedAttributes:(nullable NSSet<Class> *)includedAttributes
|
||||
{
|
||||
for (Class attributeCls in FBElementAttribute.supportedAttributes) {
|
||||
// include all supported attributes by default unless enumerated explicitly
|
||||
if (includedAttributes && ![includedAttributes containsObject:attributeCls]) {
|
||||
continue;
|
||||
}
|
||||
// Text-input placeholder (only for elements that support inner text)
|
||||
if ((attributeCls == FBPlaceholderValueAttribute.class) &&
|
||||
!FBDoesElementSupportInnerText(element.elementType)) {
|
||||
continue;
|
||||
}
|
||||
// Only for elements that support min/max value
|
||||
if ((attributeCls == FBMinValueAttribute.class || attributeCls == FBMaxValueAttribute.class) &&
|
||||
!FBDoesElementSupportMinMaxValue(element.elementType)) {
|
||||
continue;
|
||||
}
|
||||
int rc = [attributeCls recordWithWriter:writer
|
||||
forElement:[FBXCElementSnapshotWrapper ensureWrapped:element]];
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
if (nil != indexPath) {
|
||||
// index path is the special case
|
||||
return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath];
|
||||
}
|
||||
if (element.elementType == XCUIElementTypeApplication) {
|
||||
// only record process identifier and bundle identifier for the application element
|
||||
int pid = [element.accessibilityElement processIdentifier];
|
||||
if (pid > 0) {
|
||||
int rc = [FBApplicationPidAttribute recordWithWriter:writer
|
||||
forValue:[NSString stringWithFormat:@"%d", pid]];
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
XCUIApplication *app = [[FBXCAXClientProxy sharedClient]
|
||||
monitoredApplicationWithProcessIdentifier:pid];
|
||||
NSString *bundleID = [app bundleID];
|
||||
if (nil != bundleID) {
|
||||
rc = [FBApplicationBundleIdAttribute recordWithWriter:writer
|
||||
forValue:bundleID];
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+ (int)writeXmlWithRootElement:(id<FBXCElementSnapshot>)root
|
||||
indexPath:(nullable NSString *)indexPath
|
||||
elementStore:(nullable NSMutableDictionary *)elementStore
|
||||
includedAttributes:(nullable NSSet<Class> *)includedAttributes
|
||||
writer:(xmlTextWriterPtr)writer
|
||||
{
|
||||
NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil);
|
||||
|
||||
NSArray<id<FBXCElementSnapshot>> *children = root.children;
|
||||
|
||||
if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) {
|
||||
[elementStore setObject:root forKey:topNodeIndexPath];
|
||||
}
|
||||
|
||||
FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:root];
|
||||
int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]);
|
||||
if (rc < 0) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc];
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = [self recordElementAttributes:writer
|
||||
forElement:root
|
||||
indexPath:indexPath
|
||||
includedAttributes:includedAttributes];
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
for (NSUInteger i = 0; i < [children count]; i++) {
|
||||
@autoreleasepool {
|
||||
id<FBXCElementSnapshot> childSnapshot = [children objectAtIndex:i];
|
||||
NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil;
|
||||
if (elementStore != nil && newIndexPath != nil) {
|
||||
[elementStore setObject:childSnapshot forKey:(id)newIndexPath];
|
||||
}
|
||||
rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot]
|
||||
indexPath:newIndexPath
|
||||
elementStore:elementStore
|
||||
includedAttributes:includedAttributes
|
||||
writer:writer];
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rc = xmlTextWriterEndElement(writer);
|
||||
if (rc < 0) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc];
|
||||
return rc;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+ (id<FBXCElementSnapshot>)snapshotWithRoot:(id<FBElement>)root
|
||||
useNative:(BOOL)useNative
|
||||
{
|
||||
if (![root isKindOfClass:XCUIElement.class]) {
|
||||
return (id<FBXCElementSnapshot>)root;
|
||||
}
|
||||
|
||||
if (useNative) {
|
||||
return [(XCUIElement *)root fb_nativeSnapshot];
|
||||
}
|
||||
return [root isKindOfClass:XCUIApplication.class]
|
||||
? [(XCUIElement *)root fb_standardSnapshot]
|
||||
: [(XCUIElement *)root fb_customSnapshot];
|
||||
}
|
||||
|
||||
+ (void)waitUntilStableWithElement:(id<FBElement>)root
|
||||
{
|
||||
if ([root isKindOfClass:XCUIElement.class]) {
|
||||
// If the app is not idle state while we retrieve the visiblity state
|
||||
// then the snapshot retrieval operation might freeze and time out
|
||||
[[(XCUIElement *)root application] fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
static NSString *const FBAbstractMethodInvocationException = @"AbstractMethodInvocationException";
|
||||
|
||||
@implementation FBElementAttribute
|
||||
|
||||
- (instancetype)initWithElement:(id<FBElement>)element
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_element = element;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
NSString *errMsg = [NSString stringWithFormat:@"The abstract method +(NSString *)name is expected to be overriden by %@", NSStringFromClass(self.class)];
|
||||
@throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
NSString *errMsg = [NSString stringWithFormat:@"The abstract method -(NSString *)value is expected to be overriden by %@", NSStringFromClass(self.class)];
|
||||
@throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil];
|
||||
}
|
||||
|
||||
+ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id<FBElement>)element
|
||||
{
|
||||
NSString *value = [self valueForElement:element];
|
||||
return [self recordWithWriter:writer forValue:value];
|
||||
}
|
||||
|
||||
+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value
|
||||
{
|
||||
if (nil == value) {
|
||||
// Skip the attribute if the value equals to nil
|
||||
return 0;
|
||||
}
|
||||
int rc = xmlTextWriterWriteAttribute(writer,
|
||||
(xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String],
|
||||
(xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]);
|
||||
if (rc < 0) {
|
||||
[FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc];
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
+ (NSArray<Class> *)supportedAttributes
|
||||
{
|
||||
// The list of attributes to be written for each XML node
|
||||
// The enumeration order does matter here
|
||||
return @[FBTypeAttribute.class,
|
||||
FBValueAttribute.class,
|
||||
FBNameAttribute.class,
|
||||
FBLabelAttribute.class,
|
||||
FBEnabledAttribute.class,
|
||||
FBVisibleAttribute.class,
|
||||
FBAccessibleAttribute.class,
|
||||
#if TARGET_OS_TV
|
||||
FBFocusedAttribute.class,
|
||||
#endif
|
||||
FBXAttribute.class,
|
||||
FBYAttribute.class,
|
||||
FBWidthAttribute.class,
|
||||
FBHeightAttribute.class,
|
||||
FBIndexAttribute.class,
|
||||
FBHittableAttribute.class,
|
||||
FBPlaceholderValueAttribute.class,
|
||||
FBTraitsAttribute.class,
|
||||
FBNativeFrameAttribute.class,
|
||||
FBMinValueAttribute.class,
|
||||
FBMaxValueAttribute.class,
|
||||
];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBTypeAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"type";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return element.wdType;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBValueAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"value";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
id idValue = element.wdValue;
|
||||
if ([idValue isKindOfClass:[NSValue class]]) {
|
||||
return [idValue stringValue];
|
||||
} else if ([idValue isKindOfClass:[NSString class]]) {
|
||||
return idValue;
|
||||
}
|
||||
return [idValue description];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBNameAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"name";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return element.wdName;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBLabelAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"label";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return element.wdLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBEnabledAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"enabled";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return FBBoolToString(element.wdEnabled);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBVisibleAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"visible";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return FBBoolToString(element.wdVisible);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBAccessibleAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"accessible";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return FBBoolToString(element.wdAccessible);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#if TARGET_OS_TV
|
||||
|
||||
@implementation FBFocusedAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"focused";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return FBBoolToString(element.wdFocused);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
@implementation FBDimensionAttribute
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@", [element.wdRect objectForKey:[self name]]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBXAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"x";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBYAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"y";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBWidthAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"width";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBHeightAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"height";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBIndexAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"index";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return [NSString stringWithFormat:@"%lu", element.wdIndex];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBHittableAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"hittable";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return FBBoolToString(element.wdHittable);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBInternalIndexAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return kXMLIndexPathKey;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBApplicationBundleIdAttribute : FBElementAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"bundleId";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBApplicationPidAttribute : FBElementAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"processId";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBPlaceholderValueAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"placeholderValue";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return element.wdPlaceholderValue;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation FBNativeFrameAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"nativeFrame";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return NSStringFromCGRect(element.wdNativeFrame);
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation FBTraitsAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"traits";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return element.wdTraits;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBMinValueAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"minValue";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return [element.wdMinValue stringValue];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FBMaxValueAttribute
|
||||
|
||||
+ (NSString *)name
|
||||
{
|
||||
return @"maxValue";
|
||||
}
|
||||
|
||||
+ (NSString *)valueForElement:(id<FBElement>)element
|
||||
{
|
||||
return [element.wdMaxValue stringValue];
|
||||
}
|
||||
|
||||
@end
|
||||
66
WebDriverAgentLib/Utilities/LRUCache/LRUCache.h
Normal file
66
WebDriverAgentLib/Utilities/LRUCache/LRUCache.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* See the NOTICE file distributed with this work for additional
|
||||
* information regarding copyright ownership.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface LRUCache : NSObject
|
||||
|
||||
/*! Maximum cache capacity. Could only be set in the constructor */
|
||||
@property (nonatomic, readonly) NSUInteger capacity;
|
||||
|
||||
/**
|
||||
Constructs a new LRU cache instance with the given capacity
|
||||
|
||||
@param capacity Maximum cache capacity
|
||||
*/
|
||||
- (instancetype)initWithCapacity:(NSUInteger)capacity;
|
||||
|
||||
/**
|
||||
Puts a new object into the cache. nil cannot be stored in the cache.
|
||||
|
||||
@param object Object to put
|
||||
@param key Object's key
|
||||
*/
|
||||
- (void)setObject:(id)object forKey:(id<NSCopying>)key;
|
||||
|
||||
/**
|
||||
Retrieves an object from the cache. Every time this method is called the matched
|
||||
object is bumped in the cache (if exists)
|
||||
|
||||
@param key Object's key
|
||||
@returns Either the stored instance or nil if the object does not exist or has expired
|
||||
*/
|
||||
- (nullable id)objectForKey:(id<NSCopying>)key;
|
||||
|
||||
/**
|
||||
Retrieves all values from the cache ORDERED by recent bump. No bump is performed
|
||||
|
||||
@return Array of all cache values ordred by recent usage (oldest items are at the tail)
|
||||
*/
|
||||
- (NSArray *)allObjects;
|
||||
|
||||
/**
|
||||
Removes the object associated with the specified key from the cache.
|
||||
|
||||
@param key The key identifying the object to remove.
|
||||
*/
|
||||
- (void)removeObjectForKey:(id<NSCopying>)key;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
146
WebDriverAgentLib/Utilities/LRUCache/LRUCache.m
Normal file
146
WebDriverAgentLib/Utilities/LRUCache/LRUCache.m
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* See the NOTICE file distributed with this work for additional
|
||||
* information regarding copyright ownership.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "LRUCache.h"
|
||||
#import "LRUCacheNode.h"
|
||||
|
||||
@interface LRUCache ()
|
||||
@property (nonatomic) NSMutableDictionary *store;
|
||||
@property (nonatomic, nullable) LRUCacheNode *headNode;
|
||||
@property (nonatomic, nullable) LRUCacheNode *tailNode;
|
||||
@end
|
||||
|
||||
@implementation LRUCache
|
||||
|
||||
- (instancetype)initWithCapacity:(NSUInteger)capacity
|
||||
{
|
||||
if ((self = [super init])) {
|
||||
_store = [NSMutableDictionary dictionary];
|
||||
_capacity = capacity;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setObject:(id)object forKey:(id<NSCopying>)key
|
||||
{
|
||||
NSAssert(nil != object && nil != key, @"LRUCache cannot store nil objects");
|
||||
|
||||
LRUCacheNode *previousNode = self.store[key];
|
||||
if (nil != previousNode) {
|
||||
[self removeNode:previousNode];
|
||||
}
|
||||
|
||||
LRUCacheNode *newNode = [LRUCacheNode nodeWithValue:object key:key];
|
||||
self.store[key] = newNode;
|
||||
[self addNodeToHead:newNode];
|
||||
if (nil == previousNode) {
|
||||
[self alignSize];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)objectForKey:(id<NSCopying>)key
|
||||
{
|
||||
LRUCacheNode *node = self.store[key];
|
||||
return [self moveNodeToHead:node].value;
|
||||
}
|
||||
|
||||
- (NSArray *)allObjects
|
||||
{
|
||||
NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:self.store.count];
|
||||
LRUCacheNode *node = self.headNode;
|
||||
while (node != nil) {
|
||||
[result addObject:node.value];
|
||||
node = node.next;
|
||||
}
|
||||
return result.copy;
|
||||
}
|
||||
|
||||
- (nullable LRUCacheNode *)moveNodeToHead:(nullable LRUCacheNode *)node
|
||||
{
|
||||
if (nil == node || node == self.headNode) {
|
||||
return node;
|
||||
}
|
||||
|
||||
LRUCacheNode *previousNode = node.prev;
|
||||
if (nil != previousNode) {
|
||||
previousNode.next = node.next;
|
||||
}
|
||||
LRUCacheNode *nextNode = node.next;
|
||||
if (nil != nextNode) {
|
||||
nextNode.prev = node.prev;
|
||||
}
|
||||
if (node == self.tailNode) {
|
||||
self.tailNode = previousNode;
|
||||
}
|
||||
return [self addNodeToHead:node];
|
||||
}
|
||||
|
||||
- (void)removeNode:(nullable LRUCacheNode *)node
|
||||
{
|
||||
if (nil == node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nil != node.next) {
|
||||
node.next.prev = node.prev;
|
||||
}
|
||||
if (node == self.headNode) {
|
||||
self.headNode = node.next;
|
||||
}
|
||||
if (nil != node.prev) {
|
||||
node.prev.next = node.next;
|
||||
}
|
||||
if (node == self.tailNode) {
|
||||
self.tailNode = node.prev;
|
||||
}
|
||||
[self.store removeObjectForKey:(id)node.key];
|
||||
}
|
||||
|
||||
- (nullable LRUCacheNode *)addNodeToHead:(nullable LRUCacheNode *)node
|
||||
{
|
||||
if (nil == node || node == self.headNode) {
|
||||
return node;
|
||||
}
|
||||
|
||||
LRUCacheNode *previousHead = self.headNode;
|
||||
if (nil != previousHead) {
|
||||
previousHead.prev = node;
|
||||
}
|
||||
node.next = previousHead;
|
||||
node.prev = nil;
|
||||
self.headNode = node;
|
||||
if (nil == self.tailNode) {
|
||||
self.tailNode = previousHead ?: node;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
- (void)alignSize
|
||||
{
|
||||
if (self.store.count > self.capacity && nil != self.tailNode) {
|
||||
[self removeNode:self.tailNode];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeObjectForKey:(id<NSCopying>)key
|
||||
{
|
||||
LRUCacheNode *node = self.store[key];
|
||||
if (node != nil) {
|
||||
[self removeNode:node];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
43
WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.h
Normal file
43
WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* See the NOTICE file distributed with this work for additional
|
||||
* information regarding copyright ownership.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface LRUCacheNode : NSObject
|
||||
|
||||
/*! Node value */
|
||||
@property (nonatomic, readonly) id value;
|
||||
/*! Node key */
|
||||
@property (nonatomic, readonly) id<NSCopying> key;
|
||||
/*! Pointer to the next node */
|
||||
@property (nonatomic, nullable) LRUCacheNode *next;
|
||||
/*! Pointer to the previous node */
|
||||
@property (nonatomic, nullable) LRUCacheNode *prev;
|
||||
|
||||
/**
|
||||
Factory method to create a new cache node with the given value and key
|
||||
|
||||
@param value Node value
|
||||
@param key Node key
|
||||
@returns Cache node instance
|
||||
*/
|
||||
+ (instancetype)nodeWithValue:(id)value key:(id<NSCopying>)key;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
51
WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.m
Normal file
51
WebDriverAgentLib/Utilities/LRUCache/LRUCacheNode.m
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* See the NOTICE file distributed with this work for additional
|
||||
* information regarding copyright ownership.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "LRUCacheNode.h"
|
||||
|
||||
@interface LRUCacheNode ()
|
||||
@property (nonatomic, readwrite) id value;
|
||||
@property (nonatomic, readwrite) id<NSCopying> key;
|
||||
|
||||
- (instancetype)initWithValue:(id)value key:(id<NSCopying>)key;
|
||||
@end
|
||||
|
||||
@implementation LRUCacheNode
|
||||
|
||||
- (instancetype)initWithValue:(id)value key:(id<NSCopying>)key
|
||||
{
|
||||
if (nil == value) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ((self = [super init])) {
|
||||
_value = value;
|
||||
_key = key;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)nodeWithValue:(id)value key:(id<NSCopying>)key
|
||||
{
|
||||
return [[LRUCacheNode alloc] initWithValue:value key:key];
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@ %@", self.value, self.next];
|
||||
}
|
||||
|
||||
@end
|
||||
42
WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h
Normal file
42
WebDriverAgentLib/Utilities/NSPredicate+FBFormat.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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSPredicate (FBFormat)
|
||||
|
||||
/**
|
||||
Method used to normalize/verify NSPredicate 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 predicate object received from user input
|
||||
@return formatted predicate
|
||||
@throw FBUnknownPredicateKeyException in case the given property name is not declared in FBElement protocol
|
||||
*/
|
||||
+ (instancetype)fb_formatSearchPredicate:(NSPredicate *)input;
|
||||
|
||||
/**
|
||||
Creates a block predicate expression, which properly evalluates the given raw predicate agains
|
||||
xctest hierarchy. Vanilla string predicates don't work on this hierachy because "raw" snapshots
|
||||
don't have any of the custom properties declared in FBElement protocol.
|
||||
`fb_formatSearchPredicate` is called automtically on the original predicate before
|
||||
making it to a block.
|
||||
|
||||
@param input predicate object received from user input
|
||||
@return formatted predicate
|
||||
@throw FBUnknownPredicateKeyException in case the given property name is not declared in FBElement protocol
|
||||
*/
|
||||
+ (instancetype)fb_snapshotBlockPredicateWithPredicate:(NSPredicate *)input;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
69
WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m
Normal file
69
WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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 "NSPredicate+FBFormat.h"
|
||||
|
||||
#import "NSExpression+FBFormat.h"
|
||||
#import "FBXCElementSnapshotWrapper+Helpers.h"
|
||||
|
||||
@implementation NSPredicate (FBFormat)
|
||||
|
||||
+ (instancetype)fb_predicateWithPredicate:(NSPredicate *)original
|
||||
comparisonModifier:(NSPredicate *(^)(NSComparisonPredicate *))comparisonModifier
|
||||
{
|
||||
if ([original isKindOfClass:NSCompoundPredicate.class]) {
|
||||
NSCompoundPredicate *compPred = (NSCompoundPredicate *)original;
|
||||
NSMutableArray *predicates = [NSMutableArray array];
|
||||
for (NSPredicate *predicate in [compPred subpredicates]) {
|
||||
NSPredicate *newPredicate = [self.class fb_predicateWithPredicate:predicate
|
||||
comparisonModifier:comparisonModifier];
|
||||
if (nil != newPredicate) {
|
||||
[predicates addObject:newPredicate];
|
||||
}
|
||||
}
|
||||
return [[NSCompoundPredicate alloc] initWithType:compPred.compoundPredicateType
|
||||
subpredicates:predicates];
|
||||
}
|
||||
if ([original isKindOfClass:NSComparisonPredicate.class]) {
|
||||
return comparisonModifier((NSComparisonPredicate *)original);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
+ (instancetype)fb_formatSearchPredicate:(NSPredicate *)input
|
||||
{
|
||||
return [self.class fb_predicateWithPredicate:input
|
||||
comparisonModifier:^NSPredicate *(NSComparisonPredicate *cp) {
|
||||
NSExpression *left = [NSExpression fb_wdExpressionWithExpression:[cp leftExpression]];
|
||||
NSExpression *right = [NSExpression fb_wdExpressionWithExpression:[cp rightExpression]];
|
||||
return [NSComparisonPredicate predicateWithLeftExpression:left
|
||||
rightExpression:right
|
||||
modifier:cp.comparisonPredicateModifier
|
||||
type:cp.predicateOperatorType
|
||||
options:cp.options];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (instancetype)fb_snapshotBlockPredicateWithPredicate:(NSPredicate *)input
|
||||
{
|
||||
if ([NSStringFromClass(input.class) isEqualToString:@"NSBlockPredicate"]) {
|
||||
return input;
|
||||
}
|
||||
|
||||
NSPredicate *wdPredicate = [self.class fb_formatSearchPredicate:input];
|
||||
return [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject,
|
||||
NSDictionary<NSString *,id> * _Nullable bindings) {
|
||||
@autoreleasepool {
|
||||
FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:evaluatedObject];
|
||||
return [wdPredicate evaluateWithObject:wrappedSnapshot];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
74
WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h
Normal file
74
WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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>
|
||||
|
||||
@protocol XCDebugLogDelegate;
|
||||
|
||||
/*! Accessibility identifier for is visible attribute */
|
||||
extern NSNumber *FB_XCAXAIsVisibleAttribute;
|
||||
extern NSString *FB_XCAXAIsVisibleAttributeName;
|
||||
|
||||
/*! Accessibility identifier for is accessible attribute */
|
||||
extern NSNumber *FB_XCAXAIsElementAttribute;
|
||||
extern NSString *FB_XCAXAIsElementAttributeName;
|
||||
|
||||
/*! Accessibility identifier for visible frame attribute */
|
||||
extern NSString *FB_XCAXAVisibleFrameAttributeName;
|
||||
|
||||
/*! Accessibility identifier для минимума */
|
||||
extern NSNumber *FB_XCAXACustomMinValueAttribute;
|
||||
extern NSString *FB_XCAXACustomMinValueAttributeName;
|
||||
|
||||
/*! Accessibility identifier для максимума */
|
||||
extern NSNumber *FB_XCAXACustomMaxValueAttribute;
|
||||
extern NSString *FB_XCAXACustomMaxValueAttributeName;
|
||||
|
||||
/*! Getter for XCTest logger */
|
||||
extern id<XCDebugLogDelegate> (*XCDebugLogger)(void);
|
||||
|
||||
/*! Setter for XCTest logger */
|
||||
extern void (*XCSetDebugLogger)(id <XCDebugLogDelegate>);
|
||||
|
||||
/*! Maps string attributes to AX Accesibility Attributes*/
|
||||
extern NSArray<NSNumber *> *(*XCAXAccessibilityAttributesForStringAttributes)(id stringAttributes);
|
||||
|
||||
/**
|
||||
Method used to retrieve pointer for given symbol 'name' from given 'binary'
|
||||
|
||||
@param name name of the symbol
|
||||
@return pointer to symbol
|
||||
*/
|
||||
void *FBRetrieveXCTestSymbol(const char *name);
|
||||
|
||||
/*! Static constructor that will retrieve XCTest private symbols */
|
||||
__attribute__((constructor)) void FBLoadXCTestSymbols(void);
|
||||
|
||||
/**
|
||||
Method is used to tranform attribute names into the format, which
|
||||
is acceptable for the internal XCTest snpshoting API
|
||||
|
||||
@param attributeNames set of attribute names. Must be on of FB_..Name constants above
|
||||
@returns The array of tranformed values. Unknown values are silently skipped
|
||||
*/
|
||||
NSArray *FBCreateAXAttributes(NSSet<NSString *> *attributeNames);
|
||||
|
||||
/**
|
||||
Retrives the set of standard attribute names
|
||||
|
||||
@returns Array of FB_..Name constants above, which represent standard element attributes
|
||||
*/
|
||||
NSArray<NSString*> *FBStandardAttributeNames(void);
|
||||
|
||||
/**
|
||||
Retrives the set of custom attribute names. These attributes are normally not accessible
|
||||
by public XCTest calls, but are still available in the accessibility framework
|
||||
|
||||
@returns Array of FB_..Name constants above, which represent custom element attributes
|
||||
*/
|
||||
NSArray<NSString*> *FBCustomAttributeNames(void);
|
||||
96
WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m
Normal file
96
WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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 "XCTestPrivateSymbols.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "FBRuntimeUtils.h"
|
||||
#import "FBXCodeCompatibility.h"
|
||||
|
||||
NSNumber *FB_XCAXAIsVisibleAttribute;
|
||||
NSString *FB_XCAXAIsVisibleAttributeName = @"XC_kAXXCAttributeIsVisible";
|
||||
NSNumber *FB_XCAXAIsElementAttribute;
|
||||
NSString *FB_XCAXAIsElementAttributeName = @"XC_kAXXCAttributeIsElement";
|
||||
NSString *FB_XCAXAVisibleFrameAttributeName = @"XC_kAXXCAttributeVisibleFrame";
|
||||
NSNumber *FB_XCAXACustomMinValueAttribute;
|
||||
NSString *FB_XCAXACustomMinValueAttributeName = @"XC_kAXXCAttributeMinValue";
|
||||
NSNumber *FB_XCAXACustomMaxValueAttribute;
|
||||
NSString *FB_XCAXACustomMaxValueAttributeName = @"XC_kAXXCAttributeMaxValue";
|
||||
|
||||
void (*XCSetDebugLogger)(id <XCDebugLogDelegate>);
|
||||
id<XCDebugLogDelegate> (*XCDebugLogger)(void);
|
||||
|
||||
NSArray<NSNumber *> *(*XCAXAccessibilityAttributesForStringAttributes)(id);
|
||||
|
||||
__attribute__((constructor)) void FBLoadXCTestSymbols(void)
|
||||
{
|
||||
NSString *XC_kAXXCAttributeIsVisible = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsVisibleAttributeName UTF8String]);
|
||||
NSString *XC_kAXXCAttributeIsElement = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsElementAttributeName UTF8String]);
|
||||
|
||||
XCAXAccessibilityAttributesForStringAttributes =
|
||||
(NSArray<NSNumber *> *(*)(id))FBRetrieveXCTestSymbol("XCAXAccessibilityAttributesForStringAttributes");
|
||||
|
||||
XCSetDebugLogger = (void (*)(id <XCDebugLogDelegate>))FBRetrieveXCTestSymbol("XCSetDebugLogger");
|
||||
XCDebugLogger = (id<XCDebugLogDelegate>(*)(void))FBRetrieveXCTestSymbol("XCDebugLogger");
|
||||
|
||||
NSArray<NSNumber *> *accessibilityAttributes = XCAXAccessibilityAttributesForStringAttributes(@[XC_kAXXCAttributeIsVisible, XC_kAXXCAttributeIsElement]);
|
||||
FB_XCAXAIsVisibleAttribute = accessibilityAttributes[0];
|
||||
FB_XCAXAIsElementAttribute = accessibilityAttributes[1];
|
||||
|
||||
NSCAssert(FB_XCAXAIsVisibleAttribute != nil , @"Failed to retrieve FB_XCAXAIsVisibleAttribute", FB_XCAXAIsVisibleAttribute);
|
||||
NSCAssert(FB_XCAXAIsElementAttribute != nil , @"Failed to retrieve FB_XCAXAIsElementAttribute", FB_XCAXAIsElementAttribute);
|
||||
|
||||
NSString *XC_kAXXCAttributeMinValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMinValueAttributeName UTF8String]);
|
||||
NSString *XC_kAXXCAttributeMaxValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMaxValueAttributeName UTF8String]);
|
||||
|
||||
NSArray<NSNumber *> *minMaxAttrs = XCAXAccessibilityAttributesForStringAttributes(@[XC_kAXXCAttributeMinValue, XC_kAXXCAttributeMaxValue]);
|
||||
FB_XCAXACustomMinValueAttribute = minMaxAttrs[0];
|
||||
FB_XCAXACustomMaxValueAttribute = minMaxAttrs[1];
|
||||
|
||||
NSCAssert(FB_XCAXACustomMinValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMinValueAttribute", FB_XCAXACustomMinValueAttribute);
|
||||
NSCAssert(FB_XCAXACustomMaxValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMaxValueAttribute", FB_XCAXACustomMaxValueAttribute);
|
||||
}
|
||||
|
||||
void *FBRetrieveXCTestSymbol(const char *name)
|
||||
{
|
||||
Class XCTestClass = objc_lookUpClass("XCTestCase");
|
||||
NSCAssert(XCTestClass != nil, @"XCTest should be already linked", XCTestClass);
|
||||
NSString *XCTestBinary = [NSBundle bundleForClass:XCTestClass].executablePath;
|
||||
const char *binaryPath = XCTestBinary.UTF8String;
|
||||
NSCAssert(binaryPath != nil, @"XCTest binary path should not be nil", binaryPath);
|
||||
return FBRetrieveSymbolFromBinary(binaryPath, name);
|
||||
}
|
||||
|
||||
NSArray<NSString*> *FBStandardAttributeNames(void)
|
||||
{
|
||||
static NSArray<NSString *> *attributeNames;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
Class xcElementSnapshotClass = NSClassFromString(@"XCElementSnapshot");
|
||||
NSCAssert(nil != xcElementSnapshotClass, @"XCElementSnapshot class must be resolvable", xcElementSnapshotClass);
|
||||
attributeNames = [xcElementSnapshotClass sanitizedElementSnapshotHierarchyAttributesForAttributes:nil
|
||||
isMacOS:NO];
|
||||
});
|
||||
return attributeNames;
|
||||
}
|
||||
|
||||
NSArray<NSString*> *FBCustomAttributeNames(void)
|
||||
{
|
||||
static NSArray<NSString *> *customNames;
|
||||
static dispatch_once_t onceCustomAttributeNamesToken;
|
||||
dispatch_once(&onceCustomAttributeNamesToken, ^{
|
||||
customNames = @[
|
||||
FB_XCAXAIsVisibleAttributeName,
|
||||
FB_XCAXAIsElementAttributeName,
|
||||
FB_XCAXACustomMinValueAttributeName,
|
||||
FB_XCAXACustomMaxValueAttributeName
|
||||
];
|
||||
});
|
||||
return customNames;
|
||||
}
|
||||
37
WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h
Normal file
37
WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
In certain cases WebDriverAgent fails to create a session because -[XCUIApplication launch] doesn't return
|
||||
since it waits for the target app to be quiescenced.
|
||||
The reason for this seems to be that 'testmanagerd' doesn't send the events WebDriverAgent is waiting for.
|
||||
The expected events would trigger calls to '-[XCUIApplicationProcess setEventLoopHasIdled:]' and
|
||||
'-[XCUIApplicationProcess setAnimationsHaveFinished:]', which are the properties that are checked to
|
||||
determine whether an app has quiescenced or not.
|
||||
Delaying the call to on of the setters can fix this issue.
|
||||
*/
|
||||
@interface XCUIApplicationProcessDelay : NSObject
|
||||
|
||||
/**
|
||||
Delays the invocation of '-[XCUIApplicationProcess setEventLoopHasIdled:]' by the timer interval passed
|
||||
@param delay The duration of the sleep before the original method is called
|
||||
*/
|
||||
+ (void)setEventLoopHasIdledDelay:(NSTimeInterval)delay;
|
||||
|
||||
/**
|
||||
Disables the delayed invocation of '-[XCUIApplicationProcess setEventLoopHasIdled:]'.
|
||||
*/
|
||||
+ (void)disableEventLoopDelay;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
83
WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m
Normal file
83
WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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 "XCUIApplicationProcessDelay.h"
|
||||
#import <objc/runtime.h>
|
||||
#import "XCUIApplicationProcess.h"
|
||||
#import "FBLogger.h"
|
||||
|
||||
static void (*orig_set_event_loop_has_idled)(id, SEL, BOOL);
|
||||
static NSTimeInterval eventloopIdleDelay = 0;
|
||||
static BOOL isSwizzled = NO;
|
||||
// '-[XCUIApplicationProcess setEventLoopHasIdled:]' can be called from different queues.
|
||||
// Lets lock the setup and access to the 'eventloopIdleDelay' variable
|
||||
static NSLock * lock = nil;
|
||||
|
||||
@implementation XCUIApplicationProcessDelay
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wobjc-load-method"
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
lock = [[NSLock alloc] init];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
+ (void)setEventLoopHasIdledDelay:(NSTimeInterval)delay
|
||||
{
|
||||
[lock lock];
|
||||
if (!isSwizzled && delay < DBL_EPSILON) {
|
||||
// don't swizzle methods until we need to
|
||||
[lock unlock];
|
||||
return;
|
||||
}
|
||||
eventloopIdleDelay = delay;
|
||||
if (isSwizzled) {
|
||||
[lock unlock];
|
||||
return;
|
||||
}
|
||||
[self swizzleSetEventLoopHasIdled];
|
||||
[lock unlock];
|
||||
}
|
||||
|
||||
+ (void)disableEventLoopDelay
|
||||
{
|
||||
// Once the methods were swizzled they stay like that since the only change in the implementation
|
||||
// is the thread sleep, which is skipped on setting it to zero.
|
||||
[self setEventLoopHasIdledDelay:0];
|
||||
}
|
||||
|
||||
+ (void)swizzleSetEventLoopHasIdled {
|
||||
Method original = class_getInstanceMethod([XCUIApplicationProcess class], @selector(setEventLoopHasIdled:));
|
||||
if (original == nil) {
|
||||
[FBLogger log:@"Could not find method -[XCUIApplicationProcess setEventLoopHasIdled:]"];
|
||||
return;
|
||||
}
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wcast-function-type-strict"
|
||||
orig_set_event_loop_has_idled = (void(*)(id, SEL, BOOL)) method_getImplementation(original);
|
||||
#pragma clang diagnostic pop
|
||||
Method replace = class_getClassMethod([XCUIApplicationProcessDelay class], @selector(setEventLoopHasIdled:));
|
||||
method_setImplementation(original, method_getImplementation(replace));
|
||||
isSwizzled = YES;
|
||||
}
|
||||
|
||||
+ (void)setEventLoopHasIdled:(BOOL)idled {
|
||||
[lock lock];
|
||||
NSTimeInterval delay = eventloopIdleDelay;
|
||||
[lock unlock];
|
||||
if (delay > 0.0) {
|
||||
[FBLogger verboseLog:[NSString stringWithFormat:@"Delaying -[XCUIApplicationProcess setEventLoopHasIdled:] by %.2f seconds", delay]];
|
||||
[NSThread sleepForTimeInterval:delay];
|
||||
}
|
||||
orig_set_event_loop_has_idled(self, _cmd, idled);
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user