初始化提交
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user