初始化提交

This commit is contained in:
2026-02-03 16:52:44 +08:00
commit d2f9806384
512 changed files with 65167 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBAlertViewCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,130 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBAlertViewCommands.h"
#import "FBAlert.h"
#import "FBRouteRequest.h"
#import "FBSession.h"
#import "XCUIApplication+FBHelpers.h"
@implementation FBAlertViewCommands
#pragma mark - <FBCommandHandler>
+ (NSArray *)routes
{
return
@[
[[FBRoute GET:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertGetTextCommand:)],
[[FBRoute GET:@"/alert/text"].withoutSession respondWithTarget:self action:@selector(handleAlertGetTextCommand:)],
[[FBRoute POST:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertSetTextCommand:)],
[[FBRoute POST:@"/alert/accept"] respondWithTarget:self action:@selector(handleAlertAcceptCommand:)],
[[FBRoute POST:@"/alert/accept"].withoutSession respondWithTarget:self action:@selector(handleAlertAcceptCommand:)],
[[FBRoute POST:@"/alert/dismiss"] respondWithTarget:self action:@selector(handleAlertDismissCommand:)],
[[FBRoute POST:@"/alert/dismiss"].withoutSession respondWithTarget:self action:@selector(handleAlertDismissCommand:)],
[[FBRoute GET:@"/wda/alert/buttons"] respondWithTarget:self action:@selector(handleGetAlertButtonsCommand:)],
];
}
#pragma mark - Commands
+ (id<FBResponsePayload>)handleAlertGetTextCommand:(FBRouteRequest *)request
{
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
NSString *alertText = [FBAlert alertWithApplication:application].text;
if (!alertText) {
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
traceback:nil]);
}
return FBResponseWithObject(alertText);
}
+ (id<FBResponsePayload>)handleAlertSetTextCommand:(FBRouteRequest *)request
{
FBSession *session = request.session;
id value = request.arguments[@"value"];
if (!value) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'value' parameter" traceback:nil]);
}
FBAlert *alert = [FBAlert alertWithApplication:session.activeApplication];
if (!alert.isPresent) {
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
traceback:nil]);
}
NSString *textToType = value;
if ([value isKindOfClass:[NSArray class]]) {
textToType = [value componentsJoinedByString:@""];
}
NSError *error;
if (![alert typeText:textToType error:&error]) {
return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:error.description
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleAlertAcceptCommand:(FBRouteRequest *)request
{
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
NSString *name = request.arguments[@"name"];
FBAlert *alert = [FBAlert alertWithApplication:application];
NSError *error;
if (!alert.isPresent) {
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
traceback:nil]);
}
if (name) {
if (![alert clickAlertButton:name error:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
} else if (![alert acceptWithError:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleAlertDismissCommand:(FBRouteRequest *)request
{
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
NSString *name = request.arguments[@"name"];
FBAlert *alert = [FBAlert alertWithApplication:application];
NSError *error;
if (!alert.isPresent) {
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
traceback:nil]);
}
if (name) {
if (![alert clickAlertButton:name error:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
} else if (![alert dismissWithError:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleGetAlertButtonsCommand:(FBRouteRequest *)request {
FBSession *session = request.session;
FBAlert *alert = [FBAlert alertWithApplication:session.activeApplication];
if (!alert.isPresent) {
return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil
traceback:nil]);
}
NSArray *labels = alert.buttonLabels;
return FBResponseWithObject(labels);
}
@end

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBCustomCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,633 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBCustomCommands.h"
#import <XCTest/XCUIDevice.h>
#import <CoreLocation/CoreLocation.h>
#import "FBConfiguration.h"
#import "FBKeyboard.h"
#import "FBNotificationsHelper.h"
#import "FBMathUtils.h"
#import "FBPasteboard.h"
#import "FBResponsePayload.h"
#import "FBRoute.h"
#import "FBRouteRequest.h"
#import "FBRunLoopSpinner.h"
#import "FBScreen.h"
#import "FBSession.h"
#import "FBXCodeCompatibility.h"
#import "XCUIApplication.h"
#import "XCUIApplication+FBHelpers.h"
#import "XCUIDevice+FBHelpers.h"
#import "XCUIElement.h"
#import "XCUIElement+FBIsVisible.h"
#import "XCUIElementQuery.h"
#import "FBUnattachedAppLauncher.h"
@implementation FBCustomCommands
+ (NSArray *)routes
{
return
@[
[[FBRoute POST:@"/timeouts"] respondWithTarget:self action:@selector(handleTimeouts:)],
[[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)],
[[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)],
[[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)],
[[FBRoute POST:@"/wda/lock"].withoutSession respondWithTarget:self action:@selector(handleLock:)],
[[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)],
[[FBRoute POST:@"/wda/unlock"].withoutSession respondWithTarget:self action:@selector(handleUnlock:)],
[[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)],
[[FBRoute GET:@"/wda/locked"].withoutSession respondWithTarget:self action:@selector(handleIsLocked:)],
[[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)],
[[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)],
[[FBRoute GET:@"/wda/screen"].withoutSession respondWithTarget:self action:@selector(handleGetScreen:)],
[[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)],
[[FBRoute GET:@"/wda/activeAppInfo"].withoutSession respondWithTarget:self action:@selector(handleActiveAppInfo:)],
#if !TARGET_OS_TV // tvOS does not provide relevant APIs
[[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)],
[[FBRoute POST:@"/wda/setPasteboard"].withoutSession respondWithTarget:self action:@selector(handleSetPasteboard:)],
[[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)],
[[FBRoute POST:@"/wda/getPasteboard"].withoutSession respondWithTarget:self action:@selector(handleGetPasteboard:)],
[[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)],
#endif
[[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)],
[[FBRoute POST:@"/wda/performAccessibilityAudit"] respondWithTarget:self action:@selector(handlePerformAccessibilityAudit:)],
[[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)],
[[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)],
[[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)],
[[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)],
[[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
[[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)],
[[FBRoute GET:@"/wda/device/info"].withoutSession respondWithTarget:self action:@selector(handleGetDeviceInfo:)],
[[FBRoute POST:@"/wda/device/appearance"].withoutSession respondWithTarget:self action:@selector(handleSetDeviceAppearance:)],
[[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)],
[[FBRoute GET:@"/wda/device/location"].withoutSession respondWithTarget:self action:@selector(handleGetLocation:)],
#if !TARGET_OS_TV // tvOS does not provide relevant APIs
#if __clang_major__ >= 15
[[FBRoute POST:@"/wda/element/:uuid/keyboardInput"] respondWithTarget:self action:@selector(handleKeyboardInput:)],
#endif
[[FBRoute GET:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
[[FBRoute GET:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
[[FBRoute POST:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleSetSimulatedLocation:)],
[[FBRoute POST:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleSetSimulatedLocation:)],
[[FBRoute DELETE:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleClearSimulatedLocation:)],
[[FBRoute DELETE:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleClearSimulatedLocation:)],
#endif
[[FBRoute OPTIONS:@"/*"].withoutSession respondWithTarget:self action:@selector(handlePingCommand:)],
];
}
#pragma mark - Commands
+ (id<FBResponsePayload>)handleHomescreenCommand:(FBRouteRequest *)request
{
NSError *error;
if (![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:&error]) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleDeactivateAppCommand:(FBRouteRequest *)request
{
NSNumber *requestedDuration = request.arguments[@"duration"];
NSTimeInterval duration = (requestedDuration ? requestedDuration.doubleValue : 3.);
NSError *error;
if (![request.session.activeApplication fb_deactivateWithDuration:duration error:&error]) {
return FBResponseWithUnknownError(error);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleTimeouts:(FBRouteRequest *)request
{
// This method is intentionally not supported.
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleDismissKeyboardCommand:(FBRouteRequest *)request
{
NSError *error;
BOOL isDismissed = [request.session.activeApplication fb_dismissKeyboardWithKeyNames:request.arguments[@"keyNames"]
error:&error];
return isDismissed
? FBResponseWithOK()
: FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:nil]);
}
+ (id<FBResponsePayload>)handlePingCommand:(FBRouteRequest *)request
{
return FBResponseWithOK();
}
#pragma mark - Helpers
+ (id<FBResponsePayload>)handleGetScreen:(FBRouteRequest *)request
{
XCUIApplication *app = XCUIApplication.fb_systemApplication;
XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject;
CGSize statusBarSize = (nil == mainStatusBar) ? CGSizeZero : mainStatusBar.frame.size;
#if TARGET_OS_TV
CGSize screenSize = app.frame.size;
#else
CGSize screenSize = FBAdjustDimensionsForApplication(app.wdFrame.size, app.interfaceOrientation);
#endif
return FBResponseWithObject(
@{
@"screenSize":@{@"width": @(screenSize.width),
@"height": @(screenSize.height)
},
@"statusBarSize": @{@"width": @(statusBarSize.width),
@"height": @(statusBarSize.height),
},
@"scale": @([FBScreen scale]),
});
}
+ (id<FBResponsePayload>)handleLock:(FBRouteRequest *)request
{
NSError *error;
if (![[XCUIDevice sharedDevice] fb_lockScreen:&error]) {
return FBResponseWithUnknownError(error);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleIsLocked:(FBRouteRequest *)request
{
BOOL isLocked = [XCUIDevice sharedDevice].fb_isScreenLocked;
return FBResponseWithObject(isLocked ? @YES : @NO);
}
+ (id<FBResponsePayload>)handleUnlock:(FBRouteRequest *)request
{
NSError *error;
if (![[XCUIDevice sharedDevice] fb_unlockScreen:&error]) {
return FBResponseWithUnknownError(error);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleActiveAppInfo:(FBRouteRequest *)request
{
XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
return FBResponseWithObject(@{
@"pid": @(app.processID),
@"bundleId": app.bundleID,
@"name": app.identifier,
@"processArguments": [self processArguments:app],
});
}
/**
* Returns current active app and its arguments of active session
*
* @return The dictionary of current active bundleId and its process/environment argumens
*
* @example
*
* [self currentActiveApplication]
* //=> {
* // "processArguments" : {
* // "env" : {
* // "HAPPY" : "testing"
* // },
* // "args" : [
* // "happy",
* // "tseting"
* // ]
* // }
*
* [self currentActiveApplication]
* //=> {}
*/
+ (NSDictionary *)processArguments:(XCUIApplication *)app
{
// Can be nil if no active activation is defined by XCTest
if (app == nil) {
return @{};
}
return
@{
@"args": app.launchArguments,
@"env": app.launchEnvironment
};
}
#if !TARGET_OS_TV
+ (id<FBResponsePayload>)handleSetPasteboard:(FBRouteRequest *)request
{
NSString *contentType = request.arguments[@"contentType"] ?: @"plaintext";
NSData *content = [[NSData alloc] initWithBase64EncodedString:(NSString *)request.arguments[@"content"]
options:NSDataBase64DecodingIgnoreUnknownCharacters];
if (nil == content) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Cannot decode the pasteboard content from base64" traceback:nil]);
}
NSError *error;
if (![FBPasteboard setData:content forType:contentType error:&error]) {
return FBResponseWithUnknownError(error);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleGetPasteboard:(FBRouteRequest *)request
{
NSString *contentType = request.arguments[@"contentType"] ?: @"plaintext";
NSError *error;
id result = [FBPasteboard dataForType:contentType error:&error];
if (nil == result) {
return FBResponseWithUnknownError(error);
}
return FBResponseWithObject([result base64EncodedStringWithOptions:0]);
}
+ (id<FBResponsePayload>)handleGetBatteryInfo:(FBRouteRequest *)request
{
if (![[UIDevice currentDevice] isBatteryMonitoringEnabled]) {
[[UIDevice currentDevice] setBatteryMonitoringEnabled:YES];
}
return FBResponseWithObject(@{
@"level": @([UIDevice currentDevice].batteryLevel),
@"state": @([UIDevice currentDevice].batteryState)
});
}
#endif
+ (id<FBResponsePayload>)handlePressButtonCommand:(FBRouteRequest *)request
{
NSError *error;
if (![XCUIDevice.sharedDevice fb_pressButton:(id)request.arguments[@"name"]
forDuration:(NSNumber *)request.arguments[@"duration"]
error:&error]) {
return FBResponseWithUnknownError(error);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleActivateSiri:(FBRouteRequest *)request
{
NSError *error;
if (![XCUIDevice.sharedDevice fb_activateSiriVoiceRecognitionWithText:(id)request.arguments[@"text"] error:&error]) {
return FBResponseWithUnknownError(error);
}
return FBResponseWithOK();
}
+ (id <FBResponsePayload>)handlePeformIOHIDEvent:(FBRouteRequest *)request
{
NSNumber *page = request.arguments[@"page"];
NSNumber *usage = request.arguments[@"usage"];
NSNumber *duration = request.arguments[@"duration"];
NSError *error;
if (![XCUIDevice.sharedDevice fb_performIOHIDEventWithPage:page.unsignedIntValue
usage:usage.unsignedIntValue
duration:duration.doubleValue
error:&error]) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithOK();
}
+ (id <FBResponsePayload>)handleLaunchUnattachedApp:(FBRouteRequest *)request
{
NSString *bundle = (NSString *)request.arguments[@"bundleId"];
if ([FBUnattachedAppLauncher launchAppWithBundleId:bundle]) {
return FBResponseWithOK();
}
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:@"LSApplicationWorkspace failed to launch app" traceback:nil]);
}
+ (id <FBResponsePayload>)handleResetAppAuth:(FBRouteRequest *)request
{
NSNumber *resource = request.arguments[@"resource"];
if (nil == resource) {
NSString *errMsg = @"The 'resource' argument must be set to a valid resource identifier (numeric value). See https://developer.apple.com/documentation/xctest/xcuiprotectedresource?language=objc";
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg traceback:nil]);
}
[request.session.activeApplication resetAuthorizationStatusForResource:(XCUIProtectedResource)resource.longLongValue];
return FBResponseWithOK();
}
/**
Returns device location data.
It requires to configure location access permission by manual.
The response of 'latitude', 'longitude' and 'altitude' are always zero (0) without authorization.
'authorizationStatus' indicates current authorization status. '3' is 'Always'.
https://developer.apple.com/documentation/corelocation/clauthorizationstatus
Settings -> Privacy -> Location Service -> WebDriverAgent-Runner -> Always
The return value could be zero even if the permission is set to 'Always'
since the location service needs some time to update the location data.
*/
+ (id<FBResponsePayload>)handleGetLocation:(FBRouteRequest *)request
{
#if TARGET_OS_TV
return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"unsupported"
traceback:nil]);
#else
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
[locationManager setDistanceFilter:kCLHeadingFilterNone];
// Always return the best acurate location data
[locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
[locationManager setPausesLocationUpdatesAutomatically:NO];
[locationManager startUpdatingLocation];
CLAuthorizationStatus authStatus;
if ([locationManager respondsToSelector:@selector(authorizationStatus)]) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[locationManager class]
instanceMethodSignatureForSelector:@selector(authorizationStatus)]];
[invocation setSelector:@selector(authorizationStatus)];
[invocation setTarget:locationManager];
[invocation invoke];
[invocation getReturnValue:&authStatus];
} else {
authStatus = [CLLocationManager authorizationStatus];
}
return FBResponseWithObject(@{
@"authorizationStatus": @(authStatus),
@"latitude": @(locationManager.location.coordinate.latitude),
@"longitude": @(locationManager.location.coordinate.longitude),
@"altitude": @(locationManager.location.altitude),
});
#endif
}
+ (id<FBResponsePayload>)handleExpectNotification:(FBRouteRequest *)request
{
NSString *name = request.arguments[@"name"];
if (nil == name) {
NSString *message = @"Notification name argument must be provided";
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
}
NSNumber *timeout = request.arguments[@"timeout"] ?: @60;
NSString *type = request.arguments[@"type"] ?: @"plain";
XCTWaiterResult result;
if ([type isEqualToString:@"plain"]) {
result = [FBNotificationsHelper waitForNotificationWithName:name timeout:timeout.doubleValue];
} else if ([type isEqualToString:@"darwin"]) {
result = [FBNotificationsHelper waitForDarwinNotificationWithName:name timeout:timeout.doubleValue];
} else {
NSString *message = [NSString stringWithFormat:@"Notification type could only be 'plain' or 'darwin'. Got '%@' instead", type];
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
}
if (result != XCTWaiterResultCompleted) {
NSString *message = [NSString stringWithFormat:@"Did not receive any expected %@ notifications within %@s",
name, timeout];
return FBResponseWithStatus([FBCommandStatus timeoutErrorWithMessage:message traceback:nil]);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleSetDeviceAppearance:(FBRouteRequest *)request
{
NSString *name = [request.arguments[@"name"] lowercaseString];
if (nil == name || !([name isEqualToString:@"light"] || [name isEqualToString:@"dark"])) {
NSString *message = @"The appearance name must be either 'light' or 'dark'";
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message traceback:nil]);
}
FBUIInterfaceAppearance appearance = [name isEqualToString:@"light"]
? FBUIInterfaceAppearanceLight
: FBUIInterfaceAppearanceDark;
NSError *error;
if (![XCUIDevice.sharedDevice fb_setAppearance:appearance error:&error]) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleGetDeviceInfo:(FBRouteRequest *)request
{
// Returns locale like ja_EN and zh-Hant_US. The format depends on OS
// Developers should use this locale by default
// https://developer.apple.com/documentation/foundation/nslocale/1414388-autoupdatingcurrentlocale
NSString *currentLocale = [[NSLocale autoupdatingCurrentLocale] localeIdentifier];
NSMutableDictionary *deviceInfo = [NSMutableDictionary dictionaryWithDictionary:
@{
@"currentLocale": currentLocale,
@"timeZone": self.timeZone,
@"name": UIDevice.currentDevice.name,
@"model": UIDevice.currentDevice.model,
@"uuid": [UIDevice.currentDevice.identifierForVendor UUIDString] ?: @"unknown",
// https://developer.apple.com/documentation/uikit/uiuserinterfaceidiom?language=objc
@"userInterfaceIdiom": @(UIDevice.currentDevice.userInterfaceIdiom),
@"userInterfaceStyle": self.userInterfaceStyle,
#if TARGET_OS_SIMULATOR
@"isSimulator": @(YES),
#else
@"isSimulator": @(NO),
#endif
}];
// https://developer.apple.com/documentation/foundation/nsprocessinfothermalstate
deviceInfo[@"thermalState"] = @(NSProcessInfo.processInfo.thermalState);
return FBResponseWithObject(deviceInfo);
}
/**
* @return Current user interface style as a string
*/
+ (NSString *)userInterfaceStyle
{
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) {
// Only iOS 15+ simulators/devices return correct data while
// the api itself works in iOS 13 and 14 that has style preference.
NSNumber *appearance = [XCUIDevice.sharedDevice fb_getAppearance];
if (appearance != nil) {
return [self getAppearanceName:appearance];
}
}
static id userInterfaceStyle = nil;
static dispatch_once_t styleOnceToken;
dispatch_once(&styleOnceToken, ^{
if ([UITraitCollection respondsToSelector:NSSelectorFromString(@"currentTraitCollection")]) {
id currentTraitCollection = [UITraitCollection performSelector:NSSelectorFromString(@"currentTraitCollection")];
if (nil != currentTraitCollection) {
userInterfaceStyle = [currentTraitCollection valueForKey:@"userInterfaceStyle"];
}
}
});
if (nil == userInterfaceStyle) {
return @"unsupported";
}
return [self getAppearanceName:userInterfaceStyle];
}
+ (NSString *)getAppearanceName:(NSNumber *)appearance
{
switch ([appearance longLongValue]) {
case FBUIInterfaceAppearanceUnspecified:
return @"automatic";
case FBUIInterfaceAppearanceLight:
return @"light";
case FBUIInterfaceAppearanceDark:
return @"dark";
default:
return @"unknown";
}
}
/**
* @return The string of TimeZone. Returns TZ timezone id by default. Returns TimeZone name by Apple if TZ timezone id is not available.
*/
+ (NSString *)timeZone
{
NSTimeZone *localTimeZone = [NSTimeZone localTimeZone];
// Apple timezone name like "US/New_York"
NSString *timeZoneAbb = [localTimeZone abbreviation];
if (timeZoneAbb == nil) {
return [localTimeZone name];
}
// Convert timezone name to ids like "America/New_York" as TZ database Time Zones format
// https://developer.apple.com/documentation/foundation/nstimezone
NSString *timeZoneId = [[NSTimeZone timeZoneWithAbbreviation:timeZoneAbb] name];
if (timeZoneId != nil) {
return timeZoneId;
}
return [localTimeZone name];
}
#if !TARGET_OS_TV // tvOS does not provide relevant APIs
+ (id<FBResponsePayload>)handleGetSimulatedLocation:(FBRouteRequest *)request
{
NSError *error;
CLLocation *location = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error];
if (nil != error) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithObject(@{
@"latitude": location ? @(location.coordinate.latitude) : NSNull.null,
@"longitude": location ? @(location.coordinate.longitude) : NSNull.null,
@"altitude": location ? @(location.altitude) : NSNull.null,
});
}
+ (id<FBResponsePayload>)handleSetSimulatedLocation:(FBRouteRequest *)request
{
NSNumber *longitude = request.arguments[@"longitude"];
NSNumber *latitude = request.arguments[@"latitude"];
if (nil == longitude || nil == latitude) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both latitude and longitude must be provided"
traceback:nil]);
}
NSError *error;
CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude.doubleValue
longitude:longitude.doubleValue];
if (![XCUIDevice.sharedDevice fb_setSimulatedLocation:location error:&error]) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleClearSimulatedLocation:(FBRouteRequest *)request
{
NSError *error;
if (![XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithOK();
}
#if __clang_major__ >= 15
+ (id<FBResponsePayload>)handleKeyboardInput:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
BOOL hasElement = ![request.parameters[@"uuid"] isEqual:@"0"];
XCUIElement *destination = hasElement
? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
checkStaleness:YES]
: request.session.activeApplication;
id keys = request.arguments[@"keys"];
if (![destination respondsToSelector:@selector(typeKey:modifierFlags:)]) {
NSString *message = @"typeKey API is only supported since Xcode15 and iPadOS 17";
return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:message
traceback:nil]);
}
if (![keys isKindOfClass:NSArray.class]) {
NSString *message = @"The 'keys' argument must be an array";
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
traceback:nil]);
}
for (id item in (NSArray *)keys) {
if ([item isKindOfClass:NSString.class]) {
NSString *keyValue = [FBKeyboard keyValueForName:item] ?: item;
[destination typeKey:keyValue modifierFlags:XCUIKeyModifierNone];
} else if ([item isKindOfClass:NSDictionary.class]) {
id key = [(NSDictionary *)item objectForKey:@"key"];
if (![key isKindOfClass:NSString.class]) {
NSString *message = [NSString stringWithFormat:@"All dictionaries of 'keys' array must have the 'key' item of type string. Got '%@' instead in the item %@", key, item];
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
traceback:nil]);
}
id modifiers = [(NSDictionary *)item objectForKey:@"modifierFlags"];
NSUInteger modifierFlags = XCUIKeyModifierNone;
if ([modifiers isKindOfClass:NSNumber.class]) {
modifierFlags = [(NSNumber *)modifiers unsignedIntValue];
}
NSString *keyValue = [FBKeyboard keyValueForName:item] ?: key;
[destination typeKey:keyValue modifierFlags:modifierFlags];
} else {
NSString *message = @"All items of the 'keys' array must be either dictionaries or strings";
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
traceback:nil]);
}
}
return FBResponseWithOK();
}
#endif
#endif
+ (id<FBResponsePayload>)handlePerformAccessibilityAudit:(FBRouteRequest *)request
{
NSError *error;
NSArray *requestedTypes = request.arguments[@"auditTypes"];
NSMutableSet *typesSet = [NSMutableSet set];
if (nil == requestedTypes || 0 == [requestedTypes count]) {
[typesSet addObject:@"XCUIAccessibilityAuditTypeAll"];
} else {
[typesSet addObjectsFromArray:requestedTypes];
}
NSArray *result = [request.session.activeApplication fb_performAccessibilityAuditWithAuditTypesSet:typesSet.copy
error:&error];
if (nil == result) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithObject(result);
}
@end

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBDebugCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBDebugCommands.h"
#import "FBRouteRequest.h"
#import "FBSession.h"
#import "FBXMLGenerationOptions.h"
#import "XCUIApplication+FBHelpers.h"
#import "XCUIElement+FBUtilities.h"
#import "FBXPath.h"
@implementation FBDebugCommands
#pragma mark - <FBCommandHandler>
+ (NSArray *)routes
{
return
@[
[[FBRoute GET:@"/source"] respondWithTarget:self action:@selector(handleGetSourceCommand:)],
[[FBRoute GET:@"/source"].withoutSession respondWithTarget:self action:@selector(handleGetSourceCommand:)],
[[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)],
[[FBRoute GET:@"/wda/accessibleSource"].withoutSession respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)],
];
}
#pragma mark - Commands
static NSString *const SOURCE_FORMAT_XML = @"xml";
static NSString *const SOURCE_FORMAT_JSON = @"json";
static NSString *const SOURCE_FORMAT_DESCRIPTION = @"description";
+ (id<FBResponsePayload>)handleGetSourceCommand:(FBRouteRequest *)request
{
// This method might be called without session
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
NSString *sourceType = request.parameters[@"format"] ?: SOURCE_FORMAT_XML;
NSString *sourceScope = request.parameters[@"scope"];
id result;
if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_XML] == NSOrderedSame) {
NSArray<NSString *> *excludedAttributes = nil == request.parameters[@"excluded_attributes"]
? nil
: [request.parameters[@"excluded_attributes"] componentsSeparatedByString:@","];
result = [application fb_xmlRepresentationWithOptions:
[[[FBXMLGenerationOptions new]
withExcludedAttributes:excludedAttributes]
withScope:sourceScope]];
} else if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_JSON] == NSOrderedSame) {
NSString *excludedAttributesString = request.parameters[@"excluded_attributes"];
NSSet<NSString *> *excludedAttributes = (excludedAttributesString == nil)
? nil
: [NSSet setWithArray:[excludedAttributesString componentsSeparatedByString:@","]];
result = [application fb_tree:excludedAttributes];
} else if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_DESCRIPTION] == NSOrderedSame) {
result = application.fb_descriptionRepresentation;
} else {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:[NSString stringWithFormat:@"Unknown source format '%@'. Only %@ source formats are supported.",
sourceType, @[SOURCE_FORMAT_XML, SOURCE_FORMAT_JSON, SOURCE_FORMAT_DESCRIPTION]] traceback:nil]);
}
if (nil == result) {
return FBResponseWithUnknownErrorFormat(@"Cannot get '%@' source of the current application", sourceType);
}
return FBResponseWithObject(result);
}
+ (id<FBResponsePayload>)handleGetAccessibleSourceCommand:(FBRouteRequest *)request
{
// This method might be called without session
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
return FBResponseWithObject(application.fb_accessibilityTree ?: @{});
}
@end

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBElementCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,819 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBElementCommands.h"
#import "FBConfiguration.h"
#import "FBKeyboard.h"
#import "FBRoute.h"
#import "FBRouteRequest.h"
#import "FBRunLoopSpinner.h"
#import "FBElementCache.h"
#import "FBErrorBuilder.h"
#import "FBSession.h"
#import "FBElementUtils.h"
#import "FBMacros.h"
#import "FBMathUtils.h"
#import "FBRuntimeUtils.h"
#import "NSPredicate+FBFormat.h"
#import "XCTestPrivateSymbols.h"
#import "XCUICoordinate.h"
#import "XCUIDevice.h"
#import "XCUIElement+FBIsVisible.h"
#import "XCUIElement+FBPickerWheel.h"
#import "XCUIElement+FBScrolling.h"
#import "XCUIElement+FBForceTouch.h"
#import "XCUIElement+FBSwiping.h"
#import "XCUIElement+FBTyping.h"
#import "XCUIElement+FBUtilities.h"
#import "XCUIElement+FBWebDriverAttributes.h"
#import "XCUIElement+FBTVFocuse.h"
#import "XCUIElement+FBResolve.h"
#import "XCUIElement+FBUID.h"
#import "FBElementTypeTransformer.h"
#import "XCUIElement.h"
#import "XCUIElementQuery.h"
#import "FBXCodeCompatibility.h"
//
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>
#import <fcntl.h>
#import <unistd.h>
#import <errno.h>
@interface FBElementCommands ()
@end
@implementation FBElementCommands
#pragma mark - <FBCommandHandler>
+ (NSArray *)routes
{
return
@[
[[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)],
[[FBRoute GET:@"/window/rect"] respondWithTarget:self action:@selector(handleGetWindowRect:)],
[[FBRoute GET:@"/window/size"].withoutSession respondWithTarget:self action:@selector(handleGetWindowSize:)],
[[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)],
[[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)],
[[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)],
[[FBRoute GET:@"/element/:uuid/text"] respondWithTarget:self action:@selector(handleGetText:)],
[[FBRoute GET:@"/element/:uuid/displayed"] respondWithTarget:self action:@selector(handleGetDisplayed:)],
[[FBRoute GET:@"/element/:uuid/selected"] respondWithTarget:self action:@selector(handleGetSelected:)],
[[FBRoute GET:@"/element/:uuid/name"] respondWithTarget:self action:@selector(handleGetName:)],
[[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)],
[[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)],
[[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)],
// W3C element screenshot
[[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)],
// JSONWP element screenshot
[[FBRoute GET:@"/screenshot/:uuid"] respondWithTarget:self action:@selector(handleElementScreenshot:)],
[[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)],
[[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)],
#if TARGET_OS_TV
[[FBRoute GET:@"/element/:uuid/attribute/focused"] respondWithTarget:self action:@selector(handleGetFocused:)],
[[FBRoute POST:@"/wda/element/:uuid/focuse"] respondWithTarget:self action:@selector(handleFocuse:)],
#else
[[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)],
[[FBRoute POST:@"/wda/swipe"] respondWithTarget:self action:@selector(handleSwipe:)],
[[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)],
[[FBRoute POST:@"/wda/pinch"] respondWithTarget:self action:@selector(handlePinch:)],
[[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)],
[[FBRoute POST:@"/wda/rotate"] respondWithTarget:self action:@selector(handleRotate:)],
[[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)],
[[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)],
[[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)],
[[FBRoute POST:@"/wda/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)],
[[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self
action:@selector(handleTapWithNumberOfTaps:)],
[[FBRoute POST:@"/wda/tapWithNumberOfTaps"] respondWithTarget:self
action:@selector(handleTapWithNumberOfTaps:)],
[[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)],
[[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)],
[[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)],
[[FBRoute POST:@"/wda/scroll"] respondWithTarget:self action:@selector(handleScroll:)],
[[FBRoute POST:@"/wda/element/:uuid/scrollTo"] respondWithTarget:self action:@selector(handleScrollTo:)],
[[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)],
[[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)],
[[FBRoute POST:@"/wda/element/:uuid/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragWithVelocity:)],
[[FBRoute POST:@"/wda/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragCoordinateWithVelocity:)],
[[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)],
[[FBRoute POST:@"/wda/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)],
[[FBRoute POST:@"/wda/element/:uuid/tap"] respondWithTarget:self action:@selector(handleTap:)],
[[FBRoute POST:@"/wda/tap"] respondWithTarget:self action:@selector(handleTap:)],
//
[[FBRoute GET:@"/wda/netWorkStatus"].withoutSession respondWithTarget:self action:@selector(handleNetWorkStatus:)],
[[FBRoute POST:@"/wda/netWorkStatus"].withoutSession respondWithTarget:self action:@selector(handleNetWorkStatus:)],
[[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)],
#endif
[[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
];
}
#pragma mark - Commands
//
+ (id<FBResponsePayload>)handleNetWorkStatus:(FBRouteRequest *)request
{
BOOL reachable = FBHasExternalConnectivityViaHTTPS();
return FBResponseWithObject(@(reachable));
}
// + wait +
static BOOL FBHasExternalConnectivityViaHTTPS(void) {
__block BOOL ok = NO;
// TikTok
NSArray<NSString *> *urlStrings = @[
@"https://www.tiktok.com/robots.txt",
@"https://www.tiktok.com/",
@"https://m.tiktok.com/",
@"https://www.tiktokv.com/",
@"https://api.tiktokv.com/"
];
// default
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
cfg.timeoutIntervalForRequest = 12.0;
cfg.timeoutIntervalForResource = 12.0;
if (@available(iOS 11.0, *)) {
// /
cfg.waitsForConnectivity = YES;
}
NSURLSession *s = [NSURLSession sessionWithConfiguration:cfg];
// URL 12 timeoutIntervalForRequest
const NSTimeInterval perURLWaitSeconds = 12.0;
for (NSString *urlStr in urlStrings) {
if (ok) { break; }
NSURL *url = [NSURL URLWithString:urlStr];
if (!url) {
NSLog(@"[NetCheck] invalid url: %@", urlStr);
continue;
}å
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
// request便 UA//
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
req.HTTPMethod = @"GET";
req.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
req.timeoutInterval = perURLWaitSeconds;
// UA WAF UA
[req setValue:@"Mozilla/5.0 (iPhone; CPU iPhone OS like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile"
forHTTPHeaderField:@"User-Agent"];
__block NSString *localErrDomain = nil;
__block NSInteger localErrCode = 0;
__block NSInteger localHttpCode = -1;
[[s dataTaskWithRequest:req completionHandler:^(NSData *d, NSURLResponse *r, NSError *e) {
if (e) {
localErrDomain = e.domain ?: @"";
localErrCode = e.code;
NSLog(@"[NetCheck] error (%@): domain=%@ code=%ld desc=%@ userInfo=%@",
urlStr, localErrDomain, (long)localErrCode, e.localizedDescription, e.userInfo);
} else {
if ([r isKindOfClass:NSHTTPURLResponse.class]) {
localHttpCode = ((NSHTTPURLResponse *)r).statusCode;
NSLog(@"[NetCheck] HTTP (%@) = %ld", urlStr, (long)localHttpCode);
// HTTP 301/403/404
// ok
ok = YES;
} else {
NSLog(@"[NetCheck] response (%@): %@", urlStr, r);
ok = YES;
}
}
dispatch_semaphore_signal(sem);
}] resume];
long waitResult = dispatch_semaphore_wait(
sem,
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(perURLWaitSeconds * NSEC_PER_SEC))
);
//
if (waitResult != 0) {
NSLog(@"[NetCheck] wait timeout (%@) after %.0fs (http=%ld err=%@/%ld)",
urlStr, perURLWaitSeconds, (long)localHttpCode,
localErrDomain ?: @"", (long)localErrCode);
// ok NO NO
}
// ok
if (ok) { break; }
}
[s finishTasksAndInvalidate];
NSLog(@"[NetCheck] TikTok reachability via HTTPS: %@", ok ? @"YES" : @"NO");
return ok;
}
+ (id<FBResponsePayload>)handleGetEnabled:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
return FBResponseWithObject(@(element.isWDEnabled));
}
+ (id<FBResponsePayload>)handleGetRect:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
return FBResponseWithObject(element.wdRect);
}
+ (id<FBResponsePayload>)handleGetAttribute:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
NSString *attributeName = request.parameters[@"name"];
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
id attributeValue = [element fb_valueForWDAttributeName:attributeName];
return FBResponseWithObject(attributeValue ?: [NSNull null]);
}
+ (id<FBResponsePayload>)handleGetText:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
// https://github.com/appium/appium-xcuitest-driver/issues/2552
id<FBXCElementSnapshot> snapshot = [element fb_customSnapshot];
FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
id text = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel);
return FBResponseWithObject(text ?: @"");
}
+ (id<FBResponsePayload>)handleGetDisplayed:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
return FBResponseWithObject(@(element.isWDVisible));
}
+ (id<FBResponsePayload>)handleGetAccessible:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
return FBResponseWithObject(@(element.isWDAccessible));
}
+ (id<FBResponsePayload>)handleGetIsAccessibilityContainer:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
return FBResponseWithObject(@(element.isWDAccessibilityContainer));
}
+ (id<FBResponsePayload>)handleGetName:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
return FBResponseWithObject(element.wdType);
}
+ (id<FBResponsePayload>)handleGetSelected:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
return FBResponseWithObject(@(element.wdSelected));
}
+ (id<FBResponsePayload>)handleSetValue:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
checkStaleness:YES];
id value = request.arguments[@"value"] ?: request.arguments[@"text"];
if (!value) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Neither 'value' nor 'text' parameter is provided" traceback:nil]);
}
NSString *textToType = [value isKindOfClass:NSArray.class]
? [value componentsJoinedByString:@""]
: value;
XCUIElementType elementType = [element elementType];
#if !TARGET_OS_TV
if (elementType == XCUIElementTypePickerWheel) {
[element adjustToPickerWheelValue:textToType];
return FBResponseWithOK();
}
#endif
if (elementType == XCUIElementTypeSlider) {
CGFloat sliderValue = textToType.floatValue;
if (sliderValue < 0.0 || sliderValue > 1.0 ) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Value of slider should be in 0..1 range" traceback:nil]);
}
[element adjustToNormalizedSliderPosition:sliderValue];
return FBResponseWithOK();
}
NSUInteger frequency = (NSUInteger)[request.arguments[@"frequency"] longLongValue] ?: [FBConfiguration maxTypingFrequency];
NSError *error = nil;
if (![element fb_typeText:textToType
shouldClear:NO
frequency:frequency
error:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleClick:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] checkStaleness:YES];
#if TARGET_OS_IOS
[element tap];
#elif TARGET_OS_TV
NSError *error = nil;
if (![element fb_selectWithError:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
}
#endif
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleClear:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
NSError *error;
if (![element fb_clearTextWithError:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
}
return FBResponseWithOK();
}
#if TARGET_OS_TV
+ (id<FBResponsePayload>)handleGetFocused:(FBRouteRequest *)request
{
// `BOOL isFocused = [elementCache elementForUUID:request.parameters[@"uuid"]];`
// returns wrong true/false after moving focus by key up/down, for example.
// Thus, ensure the focus compares the status with `fb_focusedElement`.
BOOL isFocused = NO;
XCUIElement *focusedElement = request.session.activeApplication.fb_focusedElement;
if (focusedElement != nil) {
FBElementCache *elementCache = request.session.elementCache;
BOOL useNativeCachingStrategy = request.session.useNativeCachingStrategy;
NSString *focusedUUID = [elementCache storeElement:(useNativeCachingStrategy
? focusedElement
: [focusedElement fb_stableInstanceWithUid:focusedElement.fb_uid])];
focusedElement.lastSnapshot = nil;
if (focusedUUID && [focusedUUID isEqualToString:(id)request.parameters[@"uuid"]]) {
isFocused = YES;
}
}
return FBResponseWithObject(@(isFocused));
}
+ (id<FBResponsePayload>)handleFocuse:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
NSError *error;
if (![element fb_setFocusWithError:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
}
return FBResponseWithStatus([FBCommandStatus okWithValue: FBDictionaryResponseWithElement(element, FBConfiguration.shouldUseCompactResponses)]);
}
#else
+ (id<FBResponsePayload>)handleDoubleTap:(FBRouteRequest *)request
{
NSError *error;
id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
if (nil == target) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
traceback:nil]);
}
[target doubleTap];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleTwoFingerTap:(FBRouteRequest *)request
{
XCUIElement *element = [self targetFromRequest:request];
[element twoFingerTap];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleTapWithNumberOfTaps:(FBRouteRequest *)request
{
if (nil == request.arguments[@"numberOfTaps"] || nil == request.arguments[@"numberOfTouches"]) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both 'numberOfTaps' and 'numberOfTouches' arguments must be provided"
traceback:nil]);
}
XCUIElement *element = [self targetFromRequest:request];
[element tapWithNumberOfTaps:[request.arguments[@"numberOfTaps"] integerValue]
numberOfTouches:[request.arguments[@"numberOfTouches"] integerValue]];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleTouchAndHold:(FBRouteRequest *)request
{
NSError *error;
id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
if (nil == target) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
traceback:nil]);
}
[target pressForDuration:[request.arguments[@"duration"] doubleValue]];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handlePressAndDragWithVelocity:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [self targetFromRequest:request];
[element pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"] checkStaleness:YES]
withVelocity:[request.arguments[@"velocity"] doubleValue]
thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handlePressAndDragCoordinateWithVelocity:(FBRouteRequest *)request
{
FBSession *session = request.session;
CGVector startOffset = CGVectorMake((CGFloat)[request.arguments[@"fromX"] doubleValue],
(CGFloat)[request.arguments[@"fromY"] doubleValue]);
XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset
element:session.activeApplication];
CGVector endOffset = CGVectorMake((CGFloat)[request.arguments[@"toX"] doubleValue],
(CGFloat)[request.arguments[@"toY"] doubleValue]);
XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset
element:session.activeApplication];
[startCoordinate pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
thenDragToCoordinate:endCoordinate
withVelocity:[request.arguments[@"velocity"] doubleValue]
thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleScroll:(FBRouteRequest *)request
{
XCUIElement *element = [self targetFromRequest:request];
// Using presence of arguments as a way to convey control flow seems like a pretty bad idea but it's
// what ios-driver did and sadly, we must copy them.
NSString *const name = request.arguments[@"name"];
if (name) {
XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny]
matchingIdentifier:name] allElementsBoundByIndex] lastObject];
if (!childElement) {
return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' identifier didn't match any elements", name]
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
return [self.class handleScrollElementToVisible:childElement withRequest:request];
}
NSString *const direction = request.arguments[@"direction"];
if (direction) {
NSString *const distanceString = request.arguments[@"distance"] ?: @"1.0";
CGFloat distance = (CGFloat)distanceString.doubleValue;
if ([direction isEqualToString:@"up"]) {
[element fb_scrollUpByNormalizedDistance:distance];
} else if ([direction isEqualToString:@"down"]) {
[element fb_scrollDownByNormalizedDistance:distance];
} else if ([direction isEqualToString:@"left"]) {
[element fb_scrollLeftByNormalizedDistance:distance];
} else if ([direction isEqualToString:@"right"]) {
[element fb_scrollRightByNormalizedDistance:distance];
}
return FBResponseWithOK();
}
NSString *const predicateString = request.arguments[@"predicateString"];
if (predicateString) {
NSPredicate *formattedPredicate = [NSPredicate fb_snapshotBlockPredicateWithPredicate:[NSPredicate
predicateWithFormat:predicateString]];
XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny]
matchingPredicate:formattedPredicate] allElementsBoundByIndex] lastObject];
if (!childElement) {
return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' predicate didn't match any elements", predicateString]
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
return [self.class handleScrollElementToVisible:childElement withRequest:request];
}
if (request.arguments[@"toVisible"]) {
return [self.class handleScrollElementToVisible:element withRequest:request];
}
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Unsupported scroll type" traceback:nil]);
}
+ (id<FBResponsePayload>)handleScrollTo:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
NSError *error;
return [element fb_nativeScrollToVisibleWithError:&error]
? FBResponseWithOK()
: FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:nil]);
}
+ (id<FBResponsePayload>)handleDrag:(FBRouteRequest *)request
{
XCUIElement *target = [self targetFromRequest:request];
CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue],
[request.arguments[@"fromY"] doubleValue]);
XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:target];
CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue],
[request.arguments[@"toY"] doubleValue]);
XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset element:target];
NSTimeInterval duration = [request.arguments[@"duration"] doubleValue];
[startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleSwipe:(FBRouteRequest *)request
{
NSString *const direction = request.arguments[@"direction"];
if (!direction) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'direction' parameter" traceback:nil]);
}
NSArray<NSString *> *supportedDirections = @[@"up", @"down", @"left", @"right"];
if (![supportedDirections containsObject:direction.lowercaseString]) {
NSString *message = [NSString stringWithFormat:@"Unsupported swipe direction '%@'. Only the following directions are supported: %@", direction, supportedDirections];
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
traceback:nil]);
}
NSError *error;
id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
if (nil == target) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
traceback:nil]);
}
[target fb_swipeWithDirection:direction velocity:request.arguments[@"velocity"]];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleTap:(FBRouteRequest *)request
{
NSError *error;
id target = [self targetWithXyCoordinatesFromRequest:request error:&error];
if (nil == target) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
traceback:nil]);
}
[target tap];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handlePinch:(FBRouteRequest *)request
{
XCUIElement *element = [self targetFromRequest:request];
CGFloat scale = (CGFloat)[request.arguments[@"scale"] doubleValue];
CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue];
[element pinchWithScale:scale velocity:velocity];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleRotate:(FBRouteRequest *)request
{
XCUIElement *element = [self targetFromRequest:request];
CGFloat rotation = (CGFloat)[request.arguments[@"rotation"] doubleValue];
CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue];
[element rotate:rotation withVelocity:velocity];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleForceTouch:(FBRouteRequest *)request
{
XCUIElement *element = [self targetFromRequest:request];
NSNumber *pressure = request.arguments[@"pressure"];
NSNumber *duration = request.arguments[@"duration"];
NSNumber *x = request.arguments[@"x"];
NSNumber *y = request.arguments[@"y"];
NSValue *hitPoint = (nil == x || nil == y)
? nil
: [NSValue valueWithCGPoint:CGPointMake((CGFloat)[x doubleValue], (CGFloat)[y doubleValue])];
NSError *error;
BOOL didSucceed = [element fb_forceTouchCoordinate:hitPoint
pressure:pressure
duration:duration
error:&error];
return didSucceed
? FBResponseWithOK()
: FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:nil]);
}
#endif
+ (id<FBResponsePayload>)handleKeys:(FBRouteRequest *)request
{
NSString *textToType = [request.arguments[@"value"] componentsJoinedByString:@""];
NSUInteger frequency = [request.arguments[@"frequency"] unsignedIntegerValue] ?: [FBConfiguration maxTypingFrequency];
NSError *error;
if (!FBTypeText(textToType, frequency, &error)) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleGetWindowSize:(FBRouteRequest *)request
{
XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
CGRect frame = app.wdFrame;
#if TARGET_OS_TV
CGSize screenSize = frame.size;
#else
CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation);
#endif
return FBResponseWithObject(@{
@"width": @(screenSize.width),
@"height": @(screenSize.height),
});
}
+ (id<FBResponsePayload>)handleGetWindowRect:(FBRouteRequest *)request
{
XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
CGRect frame = app.wdFrame;
#if TARGET_OS_TV
CGSize screenSize = frame.size;
#else
CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation);
#endif
return FBResponseWithObject(@{
@"x": @(frame.origin.x),
@"y": @(frame.origin.y),
@"width": @(screenSize.width),
@"height": @(screenSize.height),
});
}
+ (id<FBResponsePayload>)handleElementScreenshot:(FBRouteRequest *)request
{
@autoreleasepool {
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
checkStaleness:YES];
NSData *screenshotData = nil;
@autoreleasepool {
screenshotData = [element.screenshot PNGRepresentation];
if (nil == screenshotData) {
NSString *errMsg = [NSString stringWithFormat:@"Cannot take a screenshot of %@", element.description];
return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:errMsg
traceback:nil]);
}
}
NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0];
screenshotData = nil;
return FBResponseWithObject(screenshot);
}
}
#if !TARGET_OS_TV
static const CGFloat DEFAULT_PICKER_OFFSET = (CGFloat)0.2;
static const NSInteger DEFAULT_MAX_PICKER_ATTEMPTS = 25;
+ (id<FBResponsePayload>)handleWheelSelect:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
checkStaleness:YES];
if ([element elementType] != XCUIElementTypePickerWheel) {
NSString *errMsg = [NSString stringWithFormat:@"The element is expected to be a valid Picker Wheel control. '%@' was given instead", element.wdType];
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
NSString* order = [request.arguments[@"order"] lowercaseString];
CGFloat offset = DEFAULT_PICKER_OFFSET;
if (request.arguments[@"offset"]) {
offset = (CGFloat)[request.arguments[@"offset"] doubleValue];
if (offset <= 0.0 || offset > 0.5) {
NSString *errMsg = [NSString stringWithFormat:@"'offset' value is expected to be in range (0.0, 0.5]. '%@' was given instead", request.arguments[@"offset"]];
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
}
NSNumber *maxAttempts = request.arguments[@"maxAttempts"] ?: @(DEFAULT_MAX_PICKER_ATTEMPTS);
NSString *expectedValue = request.arguments[@"value"];
NSInteger attempt = 0;
while (attempt < [maxAttempts integerValue]) {
BOOL isSuccessful = false;
NSError *error;
if ([order isEqualToString:@"next"]) {
isSuccessful = [element fb_selectNextOptionWithOffset:offset error:&error];
} else if ([order isEqualToString:@"previous"]) {
isSuccessful = [element fb_selectPreviousOptionWithOffset:offset error:&error];
} else {
NSString *errMsg = [NSString stringWithFormat:@"Only 'previous' and 'next' order values are supported. '%@' was given instead", request.arguments[@"order"]];
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
if (!isSuccessful) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]);
}
if (nil == expectedValue || [element.wdValue isEqualToString:expectedValue]) {
return FBResponseWithOK();
}
attempt++;
}
NSString *errMsg = [NSString stringWithFormat:@"Cannot select the expected picker wheel value '%@' after %ld attempts", expectedValue, attempt];
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:errMsg traceback:nil]);
}
#pragma mark - Helpers
+ (id<FBResponsePayload>)handleScrollElementToVisible:(XCUIElement *)element withRequest:(FBRouteRequest *)request
{
NSError *error;
if (!element.exists) {
return FBResponseWithStatus([FBCommandStatus elementNotVisibleErrorWithMessage:@"Can't scroll to element that does not exist" traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
if (![element fb_scrollToVisibleWithError:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
return FBResponseWithOK();
}
/**
Returns gesture coordinate for the element based on absolute coordinate
@param offset absolute screen offset for the given application
@param element the element instance to perform the gesture on
@return translated gesture coordinates ready to be passed to XCUICoordinate methods
*/
+ (XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset
element:(XCUIElement *)element
{
return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset];
}
/**
Returns either coordinates or the target element for the given request that expects 'x' and 'y' coordannates
@param request HTTP request object
@param error Error instance if any
@return Either XCUICoordinate or XCUIElement instance. nil if the input data is invalid
*/
+ (nullable id)targetWithXyCoordinatesFromRequest:(FBRouteRequest *)request error:(NSError **)error
{
NSNumber *x = request.arguments[@"x"];
NSNumber *y = request.arguments[@"y"];
if (nil == x && nil == y) {
return [self targetFromRequest:request];
}
if ((nil == x && nil != y) || (nil != x && nil == y)) {
[[[FBErrorBuilder alloc]
withDescription:@"Both x and y coordinates must be provided"]
buildError:error];
return nil;
}
return [self gestureCoordinateWithOffset:CGVectorMake(x.doubleValue, y.doubleValue)
element:[self targetFromRequest:request]];
}
/**
Returns the target element for the given request
@param request HTTP request object
@return Matching XCUIElement instance
*/
+ (XCUIElement *)targetFromRequest:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
NSString *elementUuid = (NSString *)request.parameters[@"uuid"];
return nil == elementUuid
? request.session.activeApplication
: [elementCache elementForUUID:elementUuid checkStaleness:YES];
}
#endif
@end

View File

@@ -0,0 +1,18 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBFindElementCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,186 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBFindElementCommands.h"
#import "FBAlert.h"
#import "FBConfiguration.h"
#import "FBElementCache.h"
#import "FBExceptions.h"
#import "FBMacros.h"
#import "FBRouteRequest.h"
#import "FBSession.h"
#import "XCTestPrivateSymbols.h"
#import "XCUIApplication+FBHelpers.h"
#import "XCUIElement+FBClassChain.h"
#import "XCUIElement+FBFind.h"
#import "XCUIElement+FBIsVisible.h"
#import "XCUIElement+FBUID.h"
#import "XCUIElement+FBUtilities.h"
#import "XCUIElement+FBWebDriverAttributes.h"
static id<FBResponsePayload> FBNoSuchElementErrorResponseForRequest(FBRouteRequest *request)
{
return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"unable to find an element using '%@', value '%@'", request.arguments[@"using"], request.arguments[@"value"]]
traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]);
}
@implementation FBFindElementCommands
#pragma mark - <FBCommandHandler>
+ (NSArray *)routes
{
return
@[
[[FBRoute POST:@"/element"] respondWithTarget:self action:@selector(handleFindElement:)],
[[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)],
[[FBRoute POST:@"/element/:uuid/element"] respondWithTarget:self action:@selector(handleFindSubElement:)],
[[FBRoute POST:@"/element/:uuid/elements"] respondWithTarget:self action:@selector(handleFindSubElements:)],
[[FBRoute GET:@"/wda/element/:uuid/getVisibleCells"] respondWithTarget:self action:@selector(handleFindVisibleCells:)],
#if TARGET_OS_TV
[[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetFocusedElement:)],
#else
[[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetActiveElement:)],
#endif
];
}
#pragma mark - Commands
+ (id<FBResponsePayload>)handleFindElement:(FBRouteRequest *)request
{
FBSession *session = request.session;
XCUIElement *element = [self.class elementUsing:request.arguments[@"using"]
withValue:request.arguments[@"value"]
under:session.activeApplication];
if (!element) {
return FBNoSuchElementErrorResponseForRequest(request);
}
return FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
}
+ (id<FBResponsePayload>)handleFindElements:(FBRouteRequest *)request
{
FBSession *session = request.session;
NSArray *elements = [self.class elementsUsing:request.arguments[@"using"]
withValue:request.arguments[@"value"]
under:session.activeApplication
shouldReturnAfterFirstMatch:NO];
return FBResponseWithCachedElements(elements, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
}
+ (id<FBResponsePayload>)handleFindVisibleCells:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
id<FBXCElementSnapshot> snapshot = [element fb_customSnapshot];
NSArray<id<FBXCElementSnapshot>> *visibleCellSnapshots = [snapshot descendantsByFilteringWithBlock:^BOOL(id<FBXCElementSnapshot> shot) {
return shot.elementType == XCUIElementTypeCell
&& [FBXCElementSnapshotWrapper ensureWrapped:shot].wdVisible;
}];
NSArray *cells = [element fb_filterDescendantsWithSnapshots:visibleCellSnapshots
onlyChildren:NO];
return FBResponseWithCachedElements(cells, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
}
+ (id<FBResponsePayload>)handleFindSubElement:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
checkStaleness:NO];
XCUIElement *foundElement = [self.class elementUsing:request.arguments[@"using"]
withValue:request.arguments[@"value"]
under:element];
if (!foundElement) {
return FBNoSuchElementErrorResponseForRequest(request);
}
return FBResponseWithCachedElement(foundElement, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
}
+ (id<FBResponsePayload>)handleFindSubElements:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
checkStaleness:NO];
NSArray *foundElements = [self.class elementsUsing:request.arguments[@"using"]
withValue:request.arguments[@"value"]
under:element
shouldReturnAfterFirstMatch:NO];
return FBResponseWithCachedElements(foundElements, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
}
+ (id<FBResponsePayload>)handleGetActiveElement:(FBRouteRequest *)request
{
XCUIElement *element = request.session.activeApplication.fb_activeElement;
if (nil == element) {
return FBNoSuchElementErrorResponseForRequest(request);
}
return FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
}
#if TARGET_OS_TV
+ (id<FBResponsePayload>)handleGetFocusedElement:(FBRouteRequest *)request
{
XCUIElement *element = request.session.activeApplication.fb_focusedElement;
return element == nil
? FBNoSuchElementErrorResponseForRequest(request)
: FBResponseWithCachedElement(element, request.session.elementCache, FBConfiguration.shouldUseCompactResponses);
}
#endif
#pragma mark - Helpers
+ (XCUIElement *)elementUsing:(NSString *)usingText withValue:(NSString *)value under:(XCUIElement *)element
{
return [[self elementsUsing:usingText
withValue:value
under:element
shouldReturnAfterFirstMatch:YES] firstObject];
}
+ (NSArray *)elementsUsing:(NSString *)usingText
withValue:(NSString *)value
under:(XCUIElement *)element
shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch
{
if ([usingText isEqualToString:@"partial link text"]
|| [usingText isEqualToString:@"link text"]) {
NSArray *components = [value componentsSeparatedByString:@"="];
NSString *propertyValue = components.lastObject;
NSString *propertyName = (components.count < 2 ? @"name" : components.firstObject);
return [element fb_descendantsMatchingProperty:propertyName
value:propertyValue
partialSearch:[usingText containsString:@"partial"]];
} else if ([usingText isEqualToString:@"class name"]) {
return [element fb_descendantsMatchingClassName:value
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
} else if ([usingText isEqualToString:@"class chain"]) {
return [element fb_descendantsMatchingClassChain:value
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
} else if ([usingText isEqualToString:@"xpath"]) {
return [element fb_descendantsMatchingXPathQuery:value
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
} else if ([usingText isEqualToString:@"predicate string"]) {
return [element fb_descendantsMatchingPredicate:[NSPredicate predicateWithFormat:value]
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
} else if ([usingText isEqualToString:@"name"]
|| [usingText isEqualToString:@"id"]
|| [usingText isEqualToString:@"accessibility id"]) {
return [element fb_descendantsMatchingIdentifier:value
shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch];
} else {
@throw [NSException exceptionWithName:FBElementAttributeUnknownException
reason:[NSString stringWithFormat:@"Invalid locator requested: %@", usingText]
userInfo:nil];
}
}
@end

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBOrientationCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,185 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBOrientationCommands.h"
#import "XCUIDevice+FBRotation.h"
#import "FBRouteRequest.h"
#import "FBMacros.h"
#import "FBSession.h"
#import "XCUIApplication.h"
#import "XCUIApplication+FBHelpers.h"
#import "XCUIDevice.h"
extern const struct FBWDOrientationValues {
FBLiteralString portrait;
FBLiteralString landscapeLeft;
FBLiteralString landscapeRight;
FBLiteralString portraitUpsideDown;
} FBWDOrientationValues;
const struct FBWDOrientationValues FBWDOrientationValues = {
.portrait = @"PORTRAIT",
.landscapeLeft = @"LANDSCAPE",
.landscapeRight = @"UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT",
.portraitUpsideDown = @"UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN",
};
#if !TARGET_OS_TV
@implementation FBOrientationCommands
#pragma mark - <FBCommandHandler>
+ (NSArray *)routes
{
return
@[
[[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)],
[[FBRoute GET:@"/orientation"].withoutSession respondWithTarget:self action:@selector(handleGetOrientation:)],
[[FBRoute POST:@"/orientation"] respondWithTarget:self action:@selector(handleSetOrientation:)],
[[FBRoute POST:@"/orientation"].withoutSession respondWithTarget:self action:@selector(handleSetOrientation:)],
[[FBRoute GET:@"/rotation"] respondWithTarget:self action:@selector(handleGetRotation:)],
[[FBRoute GET:@"/rotation"].withoutSession respondWithTarget:self action:@selector(handleGetRotation:)],
[[FBRoute POST:@"/rotation"] respondWithTarget:self action:@selector(handleSetRotation:)],
[[FBRoute POST:@"/rotation"].withoutSession respondWithTarget:self action:@selector(handleSetRotation:)],
];
}
#pragma mark - Commands
+ (id<FBResponsePayload>)handleGetOrientation:(FBRouteRequest *)request
{
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
NSString *orientation = [self.class interfaceOrientationForApplication:application];
return FBResponseWithObject([[self _wdOrientationsMapping] objectForKey:orientation]);
}
+ (id<FBResponsePayload>)handleSetOrientation:(FBRouteRequest *)request
{
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
if ([self.class setDeviceOrientation:request.arguments[@"orientation"] forApplication:application]) {
return FBResponseWithOK();
}
return FBResponseWithUnknownErrorFormat(@"Unable To Rotate Device");
}
+ (id<FBResponsePayload>)handleGetRotation:(FBRouteRequest *)request
{
XCUIDevice *device = [XCUIDevice sharedDevice];
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
UIInterfaceOrientation orientation = application.interfaceOrientation;
return FBResponseWithObject(device.fb_rotationMapping[@(orientation)]);
}
+ (id<FBResponsePayload>)handleSetRotation:(FBRouteRequest *)request
{
if (nil == request.arguments[@"x"] || nil == request.arguments[@"y"] || nil == request.arguments[@"z"]) {
NSString *errMessage = [NSString stringWithFormat:@"x, y and z arguments must exist in the request body: %@", request.arguments];
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMessage
traceback:nil]);
}
NSDictionary* rotation = @{
@"x": request.arguments[@"x"] ?: @0,
@"y": request.arguments[@"y"] ?: @0,
@"z": request.arguments[@"z"] ?: @0,
};
NSArray<NSDictionary *> *supportedRotations = XCUIDevice.sharedDevice.fb_rotationMapping.allValues;
if (![supportedRotations containsObject:rotation]) {
NSString *errMessage = [
NSString stringWithFormat:@"%@ rotation is not supported. Only the following values are supported: %@",
rotation, supportedRotations
];
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMessage
traceback:nil]);
}
XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication;
if (![self.class setDeviceRotation:request.arguments forApplication:application]) {
NSString *errMessage = [
NSString stringWithFormat:@"The current rotation cannot be set to %@. Make sure the %@ application supports it",
rotation, application.bundleID
];
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:errMessage
traceback:nil]);
}
return FBResponseWithOK();
}
#pragma mark - Helpers
+ (NSString *)interfaceOrientationForApplication:(XCUIApplication *)application
{
NSNumber *orientation = @(application.interfaceOrientation);
NSSet *keys = [[self _orientationsMapping] keysOfEntriesPassingTest:^BOOL(id key, NSNumber *obj, BOOL *stop) {
return [obj isEqualToNumber:orientation];
}];
if (keys.count == 0) {
return @"Unknown orientation";
}
return keys.anyObject;
}
+ (BOOL)setDeviceRotation:(NSDictionary *)rotationObj forApplication:(XCUIApplication *)application
{
return [[XCUIDevice sharedDevice] fb_setDeviceRotation:rotationObj];
}
+ (BOOL)setDeviceOrientation:(NSString *)orientation forApplication:(XCUIApplication *)application
{
NSNumber *orientationValue = [[self _orientationsMapping] objectForKey:[orientation uppercaseString]];
if (orientationValue == nil) {
return NO;
}
return [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientationValue.integerValue];
}
+ (NSDictionary *)_orientationsMapping
{
static NSDictionary *orientationMap;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
orientationMap =
@{
FBWDOrientationValues.portrait : @(UIDeviceOrientationPortrait),
FBWDOrientationValues.portraitUpsideDown : @(UIDeviceOrientationPortraitUpsideDown),
FBWDOrientationValues.landscapeLeft : @(UIDeviceOrientationLandscapeLeft),
FBWDOrientationValues.landscapeRight : @(UIDeviceOrientationLandscapeRight),
};
});
return orientationMap;
}
/*
We already have FBWDOrientationValues as orientation descriptions, however the strings are not valid
WebDriver responses. WebDriver can only receive 'portrait' or 'landscape'. So we can pass the keys
through this additional filter to ensure we get one of those. It's essentially a mapping from
FBWDOrientationValues to the valid subset of itself we can return to the client
*/
+ (NSDictionary *)_wdOrientationsMapping
{
static NSDictionary *orientationMap;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
orientationMap =
@{
FBWDOrientationValues.portrait : FBWDOrientationValues.portrait,
FBWDOrientationValues.portraitUpsideDown : FBWDOrientationValues.portrait,
FBWDOrientationValues.landscapeLeft : FBWDOrientationValues.landscapeLeft,
FBWDOrientationValues.landscapeRight : FBWDOrientationValues.landscapeLeft,
};
});
return orientationMap;
}
@end
#endif

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBScreenshotCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBScreenshotCommands.h"
#import "XCUIDevice+FBHelpers.h"
@implementation FBScreenshotCommands
#pragma mark - <FBCommandHandler>
+ (NSArray *)routes
{
return
@[
[[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)],
[[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)],
];
}
#pragma mark - Commands
+ (id<FBResponsePayload>)handleGetScreenshot:(FBRouteRequest *)request
{
NSError *error;
NSData *screenshotData = [[XCUIDevice sharedDevice] fb_screenshotWithError:&error];
if (nil == screenshotData) {
return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:error.description traceback:nil]);
}
NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0];
return FBResponseWithObject(screenshot);
}
@end

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBSessionCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,586 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBSessionCommands.h"
#import "FBCapabilities.h"
#import "FBClassChainQueryParser.h"
#import "FBConfiguration.h"
#import "FBExceptions.h"
#import "FBLogger.h"
#import "FBProtocolHelpers.h"
#import "FBRouteRequest.h"
#import "FBSession.h"
#import "FBSettings.h"
#import "FBRuntimeUtils.h"
#import "FBActiveAppDetectionPoint.h"
#import "FBXCodeCompatibility.h"
#import "XCUIApplication+FBHelpers.h"
#import "XCUIApplication+FBQuiescence.h"
#import "XCUIDevice.h"
#import "XCUIDevice+FBHealthCheck.h"
#import "XCUIDevice+FBHelpers.h"
#import "XCUIApplicationProcessDelay.h"
@implementation FBSessionCommands
#pragma mark - <FBCommandHandler>
+ (NSArray *)routes
{
return
@[
[[FBRoute POST:@"/url"] respondWithTarget:self action:@selector(handleOpenURL:)],
[[FBRoute POST:@"/session"].withoutSession respondWithTarget:self action:@selector(handleCreateSession:)],
[[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)],
[[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)],
[[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)],
[[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)],
[[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)],
[[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)],
[[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)],
[[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)],
// Health check might modify simulator state so it should only be called in-between testing sessions
[[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)],
// Settings endpoints
[[FBRoute GET:@"/appium/settings"] respondWithTarget:self action:@selector(handleGetSettings:)],
[[FBRoute POST:@"/appium/settings"] respondWithTarget:self action:@selector(handleSetSettings:)],
];
}
#pragma mark - Commands
+ (id<FBResponsePayload>)handleOpenURL:(FBRouteRequest *)request
{
NSString *urlString = request.arguments[@"url"];
if (!urlString) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"URL is required" traceback:nil]);
}
NSString* bundleId = request.arguments[@"bundleId"];
NSNumber* idleTimeoutMs = request.arguments[@"idleTimeoutMs"];
NSError *error;
if (nil == bundleId) {
if (![XCUIDevice.sharedDevice fb_openUrl:urlString error:&error]) {
return FBResponseWithUnknownError(error);
}
} else {
if (![XCUIDevice.sharedDevice fb_openUrl:urlString withApplication:bundleId error:&error]) {
return FBResponseWithUnknownError(error);
}
if (idleTimeoutMs.doubleValue > 0) {
XCUIApplication *app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleId];
[app fb_waitUntilStableWithTimeout:FBMillisToSeconds(idleTimeoutMs.doubleValue)];
}
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleCreateSession:(FBRouteRequest *)request
{
if (nil != FBSession.activeSession) {
[FBSession.activeSession kill];
}
NSDictionary<NSString *, id> *capabilities;
NSError *error;
if (![request.arguments[@"capabilities"] isKindOfClass:NSDictionary.class]) {
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:@"'capabilities' is mandatory to create a new session"
traceback:nil]);
}
if (nil == (capabilities = FBParseCapabilities((NSDictionary *)request.arguments[@"capabilities"], &error))) {
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:error.localizedDescription traceback:nil]);
}
[FBConfiguration resetSessionSettings];
[FBConfiguration setShouldUseTestManagerForVisibilityDetection:[capabilities[FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION] boolValue]];
if (capabilities[FB_SETTING_USE_COMPACT_RESPONSES]) {
[FBConfiguration setShouldUseCompactResponses:[capabilities[FB_SETTING_USE_COMPACT_RESPONSES] boolValue]];
}
NSString *elementResponseAttributes = capabilities[FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES];
if (elementResponseAttributes) {
[FBConfiguration setElementResponseAttributes:elementResponseAttributes];
}
if (capabilities[FB_CAP_MAX_TYPING_FREQUENCY]) {
[FBConfiguration setMaxTypingFrequency:[capabilities[FB_CAP_MAX_TYPING_FREQUENCY] unsignedIntegerValue]];
}
if (capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER]) {
[FBConfiguration setShouldUseSingletonTestManager:[capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER] boolValue]];
}
if (capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS]) {
if ([capabilities[FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS] boolValue]) {
[FBConfiguration disableScreenshots];
} else {
[FBConfiguration enableScreenshots];
}
}
if (capabilities[FB_CAP_SHOULD_TERMINATE_APP]) {
[FBConfiguration setShouldTerminateApp:[capabilities[FB_CAP_SHOULD_TERMINATE_APP] boolValue]];
}
NSNumber *delay = capabilities[FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC];
if ([delay doubleValue] > 0.0) {
[XCUIApplicationProcessDelay setEventLoopHasIdledDelay:[delay doubleValue]];
} else {
[XCUIApplicationProcessDelay disableEventLoopDelay];
}
if (nil != capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) {
FBConfiguration.waitForIdleTimeout = [capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue];
}
if (nil == capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] ||
[capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] boolValue]) {
[FBConfiguration forceSimulatorSoftwareKeyboardPresence];
}
NSString *bundleID = capabilities[FB_CAP_BUNDLE_ID];
NSString *initialUrl = capabilities[FB_CAP_INITIAL_URL];
XCUIApplication *app = nil;
if (bundleID != nil) {
app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleID];
BOOL forceAppLaunch = YES;
if (nil != capabilities[FB_CAP_FORCE_APP_LAUNCH]) {
forceAppLaunch = [capabilities[FB_CAP_FORCE_APP_LAUNCH] boolValue];
}
XCUIApplicationState appState = app.state;
BOOL isAppRunning = appState >= XCUIApplicationStateRunningBackground;
if (!isAppRunning || (isAppRunning && forceAppLaunch)) {
app.fb_shouldWaitForQuiescence = nil == capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE]
|| [capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] boolValue];
app.launchArguments = (NSArray<NSString *> *)capabilities[FB_CAP_ARGUMENTS] ?: @[];
app.launchEnvironment = (NSDictionary <NSString *, NSString *> *)capabilities[FB_CAP_ENVIRNOMENT] ?: @{};
if (nil != initialUrl) {
if (app.running) {
[app terminate];
}
id<FBResponsePayload> errorResponse = [self openDeepLink:initialUrl
withApplication:bundleID
timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]];
if (nil != errorResponse) {
return errorResponse;
}
} else {
NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout();
if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) {
_XCTSetApplicationStateTimeout([capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC] doubleValue]);
}
@try {
[app launch];
} @catch (NSException *e) {
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:e.reason traceback:nil]);
} @finally {
if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) {
_XCTSetApplicationStateTimeout(defaultTimeout);
}
}
}
if (!app.running) {
NSString *errorMsg = [NSString stringWithFormat:@"Cannot launch %@ application. Make sure the correct bundle identifier has been provided in capabilities and check the device log for possible crash report occurrences", bundleID];
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg
traceback:nil]);
}
} else if (appState == XCUIApplicationStateRunningBackground && !forceAppLaunch) {
if (nil != initialUrl) {
id<FBResponsePayload> errorResponse = [self openDeepLink:initialUrl
withApplication:bundleID
timeout:nil];
if (nil != errorResponse) {
return errorResponse;
}
} else {
[app activate];
}
}
}
if (nil != initialUrl && nil == bundleID) {
id<FBResponsePayload> errorResponse = [self openDeepLink:initialUrl
withApplication:nil
timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]];
if (nil != errorResponse) {
return errorResponse;
}
}
if (capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]) {
[FBSession initWithApplication:app
defaultAlertAction:(id)capabilities[FB_SETTING_DEFAULT_ALERT_ACTION]];
} else {
[FBSession initWithApplication:app];
}
if (nil != capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY]) {
FBSession.activeSession.useNativeCachingStrategy = [capabilities[FB_CAP_USE_NATIVE_CACHING_STRATEGY] boolValue];
}
return FBResponseWithObject(FBSessionCommands.sessionInformation);
}
+ (id<FBResponsePayload>)handleSessionAppLaunch:(FBRouteRequest *)request
{
[request.session launchApplicationWithBundleId:(id)request.arguments[@"bundleId"]
shouldWaitForQuiescence:request.arguments[@"shouldWaitForQuiescence"]
arguments:request.arguments[@"arguments"]
environment:request.arguments[@"environment"]];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleSessionAppActivate:(FBRouteRequest *)request
{
[request.session activateApplicationWithBundleId:(id)request.arguments[@"bundleId"]];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleSessionAppTerminate:(FBRouteRequest *)request
{
BOOL result = [request.session terminateApplicationWithBundleId:(id)request.arguments[@"bundleId"]];
return FBResponseWithObject(@(result));
}
+ (id<FBResponsePayload>)handleSessionAppState:(FBRouteRequest *)request
{
NSUInteger state = [request.session applicationStateWithBundleId:(id)request.arguments[@"bundleId"]];
return FBResponseWithObject(@(state));
}
+ (id<FBResponsePayload>)handleGetActiveAppsList:(FBRouteRequest *)request
{
return FBResponseWithObject([XCUIApplication fb_activeAppsInfo]);
}
+ (id<FBResponsePayload>)handleGetActiveSession:(FBRouteRequest *)request
{
return FBResponseWithObject(FBSessionCommands.sessionInformation);
}
+ (id<FBResponsePayload>)handleDeleteSession:(FBRouteRequest *)request
{
[request.session kill];
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleGetStatus:(FBRouteRequest *)request
{
// For updatedWDABundleId capability by Appium
NSString *productBundleIdentifier = @"com.facebook.WebDriverAgentRunner";
NSString *envproductBundleIdentifier = NSProcessInfo.processInfo.environment[@"WDA_PRODUCT_BUNDLE_IDENTIFIER"];
if (envproductBundleIdentifier && [envproductBundleIdentifier length] != 0) {
productBundleIdentifier = NSProcessInfo.processInfo.environment[@"WDA_PRODUCT_BUNDLE_IDENTIFIER"];
}
NSMutableDictionary *buildInfo = [NSMutableDictionary dictionaryWithDictionary:@{
@"time" : [self.class buildTimestamp],
@"productBundleIdentifier" : productBundleIdentifier,
}];
NSString *upgradeTimestamp = NSProcessInfo.processInfo.environment[@"UPGRADE_TIMESTAMP"];
if (nil != upgradeTimestamp && upgradeTimestamp.length > 0) {
[buildInfo setObject:upgradeTimestamp forKey:@"upgradedAt"];
}
NSDictionary *infoDict = [[NSBundle bundleForClass:self.class] infoDictionary];
NSString *version = [infoDict objectForKey:@"CFBundleShortVersionString"];
if (nil != version) {
[buildInfo setObject:version forKey:@"version"];
}
return FBResponseWithObject(
@{
@"ready" : @YES,
@"message" : @"WebDriverAgent is ready to accept commands",
@"state" : @"success",
@"os" :
@{
@"name" : [[UIDevice currentDevice] systemName],
@"version" : [[UIDevice currentDevice] systemVersion],
@"sdkVersion": FBSDKVersion() ?: @"unknown",
@"testmanagerdVersion": @(FBTestmanagerdVersion()),
},
@"ios" :
@{
#if TARGET_OS_SIMULATOR
@"simulatorVersion" : [[UIDevice currentDevice] systemVersion],
#endif
@"ip" : [XCUIDevice sharedDevice].fb_wifiIPAddress ?: [NSNull null]
},
@"build" : buildInfo.copy,
@"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom]
}
);
}
+ (id<FBResponsePayload>)handleGetHealthCheck:(FBRouteRequest *)request
{
if (![[XCUIDevice sharedDevice] fb_healthCheckWithApplication:[XCUIApplication fb_activeApplication]]) {
return FBResponseWithUnknownErrorFormat(@"Health check failed");
}
return FBResponseWithOK();
}
+ (id<FBResponsePayload>)handleGetSettings:(FBRouteRequest *)request
{
return FBResponseWithObject(
@{
FB_SETTING_USE_COMPACT_RESPONSES: @([FBConfiguration shouldUseCompactResponses]),
FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES: [FBConfiguration elementResponseAttributes],
FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY: @([FBConfiguration mjpegServerScreenshotQuality]),
FB_SETTING_MJPEG_SERVER_FRAMERATE: @([FBConfiguration mjpegServerFramerate]),
FB_SETTING_MJPEG_SCALING_FACTOR: @([FBConfiguration mjpegScalingFactor]),
FB_SETTING_MJPEG_FIX_ORIENTATION: @([FBConfiguration mjpegShouldFixOrientation]),
FB_SETTING_SCREENSHOT_QUALITY: @([FBConfiguration screenshotQuality]),
FB_SETTING_KEYBOARD_AUTOCORRECTION: @([FBConfiguration keyboardAutocorrection]),
FB_SETTING_KEYBOARD_PREDICTION: @([FBConfiguration keyboardPrediction]),
FB_SETTING_SNAPSHOT_MAX_DEPTH: @([FBConfiguration snapshotMaxDepth]),
FB_SETTING_USE_FIRST_MATCH: @([FBConfiguration useFirstMatch]),
FB_SETTING_WAIT_FOR_IDLE_TIMEOUT: @([FBConfiguration waitForIdleTimeout]),
FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT: @([FBConfiguration animationCoolOffTimeout]),
FB_SETTING_BOUND_ELEMENTS_BY_INDEX: @([FBConfiguration boundElementsByIndex]),
FB_SETTING_REDUCE_MOTION: @([FBConfiguration reduceMotionEnabled]),
FB_SETTING_DEFAULT_ACTIVE_APPLICATION: request.session.defaultActiveApplication,
FB_SETTING_ACTIVE_APP_DETECTION_POINT: FBActiveAppDetectionPoint.sharedInstance.stringCoordinates,
FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS: @([FBConfiguration includeNonModalElements]),
FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR: FBConfiguration.acceptAlertButtonSelector,
FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR: FBConfiguration.dismissAlertButtonSelector,
FB_SETTING_AUTO_CLICK_ALERT_SELECTOR: FBConfiguration.autoClickAlertSelector,
FB_SETTING_DEFAULT_ALERT_ACTION: request.session.defaultAlertAction ?: @"",
FB_SETTING_MAX_TYPING_FREQUENCY: @([FBConfiguration maxTypingFrequency]),
FB_SETTING_RESPECT_SYSTEM_ALERTS: @([FBConfiguration shouldRespectSystemAlerts]),
FB_SETTING_USE_CLEAR_TEXT_SHORTCUT: @([FBConfiguration useClearTextShortcut]),
FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE: @([FBConfiguration includeHittableInPageSource]),
FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE: @([FBConfiguration includeNativeFrameInPageSource]),
FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE: @([FBConfiguration includeMinMaxValueInPageSource]),
FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE: @([FBConfiguration limitXpathContextScope]),
#if !TARGET_OS_TV
FB_SETTING_SCREENSHOT_ORIENTATION: [FBConfiguration humanReadableScreenshotOrientation],
#endif
}
);
}
// TODO if we get lots more settings, handling them with a series of if-statements will be unwieldy
// and this should be refactored
+ (id<FBResponsePayload>)handleSetSettings:(FBRouteRequest *)request
{
NSDictionary* settings = request.arguments[@"settings"];
if (nil != [settings objectForKey:FB_SETTING_USE_COMPACT_RESPONSES]) {
[FBConfiguration setShouldUseCompactResponses:[[settings objectForKey:FB_SETTING_USE_COMPACT_RESPONSES] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]) {
[FBConfiguration setElementResponseAttributes:(NSString *)[settings objectForKey:FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES]];
}
if (nil != [settings objectForKey:FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY]) {
[FBConfiguration setMjpegServerScreenshotQuality:[[settings objectForKey:FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY] unsignedIntegerValue]];
}
if (nil != [settings objectForKey:FB_SETTING_MJPEG_SERVER_FRAMERATE]) {
[FBConfiguration setMjpegServerFramerate:[[settings objectForKey:FB_SETTING_MJPEG_SERVER_FRAMERATE] unsignedIntegerValue]];
}
if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY]) {
[FBConfiguration setScreenshotQuality:[[settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY] unsignedIntegerValue]];
}
if (nil != [settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR]) {
[FBConfiguration setMjpegScalingFactor:[[settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR] floatValue]];
}
if (nil != [settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION]) {
[FBConfiguration setMjpegShouldFixOrientation:[[settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION]) {
[FBConfiguration setKeyboardAutocorrection:[[settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION]) {
[FBConfiguration setKeyboardPrediction:[[settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS]) {
[FBConfiguration setShouldRespectSystemAlerts:[[settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH]) {
[FBConfiguration setSnapshotMaxDepth:[[settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH] intValue]];
}
if (nil != [settings objectForKey:FB_SETTING_USE_FIRST_MATCH]) {
[FBConfiguration setUseFirstMatch:[[settings objectForKey:FB_SETTING_USE_FIRST_MATCH] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_BOUND_ELEMENTS_BY_INDEX]) {
[FBConfiguration setBoundElementsByIndex:[[settings objectForKey:FB_SETTING_BOUND_ELEMENTS_BY_INDEX] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_REDUCE_MOTION]) {
[FBConfiguration setReduceMotionEnabled:[[settings objectForKey:FB_SETTING_REDUCE_MOTION] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_DEFAULT_ACTIVE_APPLICATION]) {
request.session.defaultActiveApplication = (NSString *)[settings objectForKey:FB_SETTING_DEFAULT_ACTIVE_APPLICATION];
}
if (nil != [settings objectForKey:FB_SETTING_ACTIVE_APP_DETECTION_POINT]) {
NSError *error;
if (![FBActiveAppDetectionPoint.sharedInstance setCoordinatesWithString:(NSString *)[settings objectForKey:FB_SETTING_ACTIVE_APP_DETECTION_POINT]
error:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
traceback:nil]);
}
}
if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS]) {
if ([XCUIElement fb_supportsNonModalElementsInclusion]) {
[FBConfiguration setIncludeNonModalElements:[[settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS] boolValue]];
} else {
[FBLogger logFmt:@"'%@' settings value cannot be assigned, because non modal elements inclusion is not supported by the current iOS SDK", FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS];
}
}
if (nil != [settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]) {
[FBConfiguration setAcceptAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]];
}
if (nil != [settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]) {
[FBConfiguration setDismissAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]];
}
if (nil != [settings objectForKey:FB_SETTING_AUTO_CLICK_ALERT_SELECTOR]) {
FBCommandStatus *status = [self.class configureAutoClickAlertWithSelector:settings[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR]
forSession:request.session];
if (status.hasError) {
return FBResponseWithStatus(status);
}
}
if (nil != [settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) {
[FBConfiguration setWaitForIdleTimeout:[[settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]];
}
if (nil != [settings objectForKey:FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT]) {
[FBConfiguration setAnimationCoolOffTimeout:[[settings objectForKey:FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] doubleValue]];
}
if ([[settings objectForKey:FB_SETTING_DEFAULT_ALERT_ACTION] isKindOfClass:NSString.class]) {
request.session.defaultAlertAction = [settings[FB_SETTING_DEFAULT_ALERT_ACTION] lowercaseString];
}
if (nil != [settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY]) {
[FBConfiguration setMaxTypingFrequency:[[settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY] unsignedIntegerValue]];
}
if (nil != [settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT]) {
[FBConfiguration setUseClearTextShortcut:[[settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE]) {
[FBConfiguration setIncludeHittableInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE]) {
[FBConfiguration setIncludeNativeFrameInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE]) {
[FBConfiguration setIncludeMinMaxValueInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] boolValue]];
}
if (nil != [settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE]) {
[FBConfiguration setLimitXpathContextScope:[[settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] boolValue]];
}
#if !TARGET_OS_TV
if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION]) {
NSError *error;
if (![FBConfiguration setScreenshotOrientation:(NSString *)[settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION]
error:&error]) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription
traceback:nil]);
}
}
#endif
return [self handleGetSettings:request];
}
#pragma mark - Helpers
+ (FBCommandStatus *)configureAutoClickAlertWithSelector:(NSString *)selector
forSession:(FBSession *)session
{
if (0 == [selector length]) {
[FBConfiguration setAutoClickAlertSelector:selector];
[session disableAlertsMonitor];
return [FBCommandStatus ok];
}
NSError *error;
FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:selector error:&error];
if (nil == parsedChain) {
return [FBCommandStatus invalidSelectorErrorWithMessage:error.localizedDescription
traceback:nil];
}
[FBConfiguration setAutoClickAlertSelector:selector];
[session enableAlertsMonitor];
return [FBCommandStatus ok];
}
+ (NSString *)buildTimestamp
{
return [NSString stringWithFormat:@"%@ %@",
[NSString stringWithUTF8String:__DATE__],
[NSString stringWithUTF8String:__TIME__]
];
}
/**
Return current session information.
This response does not have any active application information.
*/
+ (NSDictionary *)sessionInformation
{
return
@{
@"sessionId" : [FBSession activeSession].identifier ?: NSNull.null,
@"capabilities" : FBSessionCommands.currentCapabilities
};
}
/*
Return the device kind as lower case
*/
+ (NSString *)deviceNameByUserInterfaceIdiom:(UIUserInterfaceIdiom) userInterfaceIdiom
{
if (userInterfaceIdiom == UIUserInterfaceIdiomPad) {
return @"ipad";
} else if (userInterfaceIdiom == UIUserInterfaceIdiomTV) {
return @"apple tv";
} else if (userInterfaceIdiom == UIUserInterfaceIdiomPhone) {
return @"iphone";
}
// CarPlay, Mac, Vision UI or unknown are possible
return @"Unknown";
}
+ (NSDictionary *)currentCapabilities
{
return
@{
@"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom],
@"sdkVersion": [[UIDevice currentDevice] systemVersion]
};
}
+(nullable id<FBResponsePayload>)openDeepLink:(NSString *)initialUrl
withApplication:(nullable NSString *)bundleID
timeout:(nullable NSNumber *)timeout
{
NSError *openError;
NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout();
if (nil != timeout) {
_XCTSetApplicationStateTimeout([timeout doubleValue]);
}
@try {
BOOL result = nil == bundleID
? [XCUIDevice.sharedDevice fb_openUrl:initialUrl
error:&openError]
: [XCUIDevice.sharedDevice fb_openUrl:initialUrl
withApplication:(id)bundleID
error:&openError];
if (result) {
return nil;
}
NSString *errorMsg = [NSString stringWithFormat:@"Cannot open the URL %@ with the %@ application. Original error: %@",
initialUrl, bundleID ?: @"default", openError.localizedDescription];
return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg traceback:nil]);
} @finally {
if (nil != timeout) {
_XCTSetApplicationStateTimeout(defaultTimeout);
}
}
}
@end

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBTouchActionCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBTouchActionCommands.h"
#import "FBRoute.h"
#import "FBRouteRequest.h"
#import "FBSession.h"
#import "XCUIApplication+FBTouchAction.h"
@implementation FBTouchActionCommands
#pragma mark - <FBCommandHandler>
+ (NSArray *)routes
{
return
@[
[[FBRoute POST:@"/actions"] respondWithTarget:self action:@selector(handlePerformW3CTouchActions:)],
];
}
#pragma mark - Commands
+ (id<FBResponsePayload>)handlePerformW3CTouchActions:(FBRouteRequest *)request
{
XCUIApplication *application = request.session.activeApplication;
NSArray *actions = (NSArray *)request.arguments[@"actions"];
NSError *error;
if (![application fb_performW3CActions:actions elementCache:request.session.elementCache error:&error]) {
return FBResponseWithUnknownError(error);
}
return FBResponseWithOK();
}
@end

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBTouchIDCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBTouchIDCommands.h"
#import "FBRouteRequest.h"
#import "XCUIDevice+FBHelpers.h"
@implementation FBTouchIDCommands
+ (NSArray *)routes
{
return @[
[[FBRoute POST:@"/wda/touch_id"] respondWithBlock: ^ id<FBResponsePayload> (FBRouteRequest *request) {
BOOL isMatch = [request.arguments[@"match"] boolValue];
if (![[XCUIDevice sharedDevice] fb_fingerTouchShouldMatch:isMatch]) {
return FBResponseWithUnknownErrorFormat(@"Cannot perform Touch Id %@match", isMatch ? @"" : @"non-");
}
return FBResponseWithOK();
}],
];
}
@end

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBUnknownCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBUnknownCommands.h"
#import "FBRouteRequest.h"
@implementation FBUnknownCommands
#pragma mark - <FBCommandHandler>
+ (BOOL)shouldRegisterAutomatically
{
return NO;
}
+ (NSArray *)routes
{
return
@[
[[FBRoute GET:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)],
[[FBRoute POST:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)],
[[FBRoute PUT:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)],
[[FBRoute DELETE:@"/*"].withoutSession respondWithTarget:self action:@selector(unhandledHandler:)]
];
}
+ (id<FBResponsePayload>)unhandledHandler:(FBRouteRequest *)request
{
return FBResponseWithStatus([FBCommandStatus unknownCommandErrorWithMessage:[NSString stringWithFormat:@"Unhandled endpoint: %@ with parameters %@", request.URL, request.parameters]
traceback:nil]);
}
@end

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <WebDriverAgentLib/FBCommandHandler.h>
NS_ASSUME_NONNULL_BEGIN
@interface FBVideoCommands : NSObject <FBCommandHandler>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "FBVideoCommands.h"
#import "FBRouteRequest.h"
#import "FBScreenRecordingContainer.h"
#import "FBScreenRecordingPromise.h"
#import "FBScreenRecordingRequest.h"
#import "FBSession.h"
#import "FBXCTestDaemonsProxy.h"
const NSUInteger DEFAULT_FPS = 24;
const NSUInteger DEFAULT_CODEC = 0;
@implementation FBVideoCommands
+ (NSArray *)routes
{
return
@[
[[FBRoute POST:@"/wda/video/start"] respondWithTarget:self action:@selector(handleStartVideoRecording:)],
[[FBRoute POST:@"/wda/video/stop"] respondWithTarget:self action:@selector(handleStopVideoRecording:)],
[[FBRoute GET:@"/wda/video"] respondWithTarget:self action:@selector(handleGetVideoRecording:)],
[[FBRoute POST:@"/wda/video/start"].withoutSession respondWithTarget:self action:@selector(handleStartVideoRecording:)],
[[FBRoute POST:@"/wda/video/stop"].withoutSession respondWithTarget:self action:@selector(handleStopVideoRecording:)],
[[FBRoute GET:@"/wda/video"].withoutSession respondWithTarget:self action:@selector(handleGetVideoRecording:)],
];
}
+ (id<FBResponsePayload>)handleStartVideoRecording:(FBRouteRequest *)request
{
FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise;
if (nil != activeScreenRecording) {
return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary] ?: [NSNull null]);
}
NSNumber *fps = (NSNumber *)request.arguments[@"fps"] ?: @(DEFAULT_FPS);
NSNumber *codec = (NSNumber *)request.arguments[@"codec"] ?: @(DEFAULT_CODEC);
FBScreenRecordingRequest *recordingRequest = [[FBScreenRecordingRequest alloc] initWithFps:fps.integerValue
codec:codec.longLongValue];
NSError *error;
FBScreenRecordingPromise* promise = [FBXCTestDaemonsProxy startScreenRecordingWithRequest:recordingRequest
error:&error];
if (nil == promise) {
[FBScreenRecordingContainer.sharedInstance reset];
return FBResponseWithUnknownError(error);
}
[FBScreenRecordingContainer.sharedInstance storeScreenRecordingPromise:promise
fps:fps.integerValue
codec:codec.longLongValue];
return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary]);
}
+ (id<FBResponsePayload>)handleStopVideoRecording:(FBRouteRequest *)request
{
FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise;
if (nil == activeScreenRecording) {
return FBResponseWithOK();
}
NSUUID *recordingId = activeScreenRecording.identifier;
NSDictionary *response = [FBScreenRecordingContainer.sharedInstance toDictionary];
NSError *error;
if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:recordingId error:&error]) {
[FBScreenRecordingContainer.sharedInstance reset];
return FBResponseWithUnknownError(error);
}
[FBScreenRecordingContainer.sharedInstance reset];
return FBResponseWithObject(response);
}
+ (id<FBResponsePayload>)handleGetVideoRecording:(FBRouteRequest *)request
{
return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary] ?: [NSNull null]);
}
@end