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