/** * 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 #import #import #import #import #import @interface FBElementCommands () @end @implementation FBElementCommands #pragma mark - + (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)handleNetWorkStatus:(FBRouteRequest *)request { BOOL reachable = FBHasExternalConnectivityViaHTTPS(); return FBResponseWithObject(@(reachable)); } // 检测网络(更稳:更长超时 + 正确处理 wait 超时 + 更清晰的日志) static BOOL FBHasExternalConnectivityViaHTTPS(void) { __block BOOL ok = NO; // 仍然保留你的 TikTok 域名探测 NSArray *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)handleGetEnabled:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; return FBResponseWithObject(@(element.isWDEnabled)); } + (id)handleGetRect:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; return FBResponseWithObject(element.wdRect); } + (id)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)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 snapshot = [element fb_customSnapshot]; FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; id text = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel); return FBResponseWithObject(text ?: @""); } + (id)handleGetDisplayed:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; return FBResponseWithObject(@(element.isWDVisible)); } + (id)handleGetAccessible:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; return FBResponseWithObject(@(element.isWDAccessible)); } + (id)handleGetIsAccessibilityContainer:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; return FBResponseWithObject(@(element.isWDAccessibilityContainer)); } + (id)handleGetName:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; return FBResponseWithObject(element.wdType); } + (id)handleGetSelected:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; return FBResponseWithObject(@(element.wdSelected)); } + (id)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)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)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)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)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)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)handleTwoFingerTap:(FBRouteRequest *)request { XCUIElement *element = [self targetFromRequest:request]; [element twoFingerTap]; return FBResponseWithOK(); } + (id)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)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)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)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)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)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)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)handleSwipe:(FBRouteRequest *)request { NSString *const direction = request.arguments[@"direction"]; if (!direction) { return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'direction' parameter" traceback:nil]); } NSArray *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)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)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)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)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)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)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)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)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)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)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