| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704 |
- /**
- * 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. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- */
- #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 "FBElementTypeTransformer.h"
- #import "XCUIElement.h"
- #import "XCUIElementQuery.h"
- #import "FBXCodeCompatibility.h"
- @interface FBElementCommands ()
- @end
- @implementation FBElementCommands
- #pragma mark - <FBCommandHandler>
- + (NSArray *)routes
- {
- return
- @[
- [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)],
- [[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 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>)handleGetEnabled:(FBRouteRequest *)request
- {
- FBElementCache *elementCache = request.session.elementCache;
- XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
- BOOL isEnabled = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDEnabled;
- return FBResponseWithObject(isEnabled ? @YES : @NO);
- }
- + (id<FBResponsePayload>)handleGetRect:(FBRouteRequest *)request
- {
- FBElementCache *elementCache = request.session.elementCache;
- XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
- return FBResponseWithObject([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdRect);
- }
- + (id<FBResponsePayload>)handleGetAttribute:(FBRouteRequest *)request
- {
- FBElementCache *elementCache = request.session.elementCache;
- NSString *attributeName = request.parameters[@"name"];
- NSString *wdAttributeName = [FBElementUtils wdAttributeNameForAttributeName:attributeName];
- NSArray *additionalAttributes = nil;
- NSNumber *maxDepth = nil;
- if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDVisible)]) {
- additionalAttributes = @[FB_XCAXAIsVisibleAttributeName];
- maxDepth = @1;
- } else if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDEnabled)]) {
- additionalAttributes = @[FB_XCAXAIsElementAttributeName];
- maxDepth = @1;
- } else if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)]) {
- additionalAttributes = @[FB_XCAXAIsElementAttributeName];
- }
- XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
- resolveForAdditionalAttributes:additionalAttributes
- andMaxDepth:maxDepth];
- FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot];
- id attributeValue = [wrappedSnapshot 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"]];
- FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot];
- 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"]
- resolveForAdditionalAttributes:@[FB_XCAXAIsVisibleAttributeName]
- andMaxDepth:@1];
- return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDVisible));
- }
- + (id<FBResponsePayload>)handleGetAccessible:(FBRouteRequest *)request
- {
- FBElementCache *elementCache = request.session.elementCache;
- XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
- resolveForAdditionalAttributes:@[FB_XCAXAIsElementAttributeName]
- andMaxDepth:@1];
- return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDAccessible));
- }
- + (id<FBResponsePayload>)handleGetIsAccessibilityContainer:(FBRouteRequest *)request
- {
- FBElementCache *elementCache = request.session.elementCache;
- XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]
- resolveForAdditionalAttributes:@[FB_XCAXAIsElementAttributeName]
- andMaxDepth:nil];
- return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDAccessibilityContainer));
- }
- + (id<FBResponsePayload>)handleGetName:(FBRouteRequest *)request
- {
- FBElementCache *elementCache = request.session.elementCache;
- XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
- return FBResponseWithObject([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdType);
- }
- + (id<FBResponsePayload>)handleGetSelected:(FBRouteRequest *)request
- {
- FBElementCache *elementCache = request.session.elementCache;
- XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
- return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdSelected));
- }
- + (id<FBResponsePayload>)handleSetValue:(FBRouteRequest *)request
- {
- FBElementCache *elementCache = request.session.elementCache;
- XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
- 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 = [(id<FBXCElementSnapshot>)element.lastSnapshot 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"]];
- #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_stableInstance)];
- 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];
- if (![element respondsToSelector:@selector(pressForDuration:thenDragToElement:withVelocity:thenHoldForDuration:)]) {
- return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"This method is only supported in Xcode 12 and above"
- traceback:nil]);
- }
- [element pressForDuration:[request.arguments[@"pressDuration"] doubleValue]
- thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"]]
- 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];
- if (![startCoordinate respondsToSelector:@selector(pressForDuration:thenDragToCoordinate:withVelocity:thenHoldForDuration:)]) {
- return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"This method is only supported in Xcode 12 and above"
- traceback:nil]);
- }
- 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
- {
- NSString *elementUdid = (NSString *)request.parameters[@"uuid"];
- XCUIElement *target = nil == elementUdid
- ? request.session.activeApplication
- : [request.session.elementCache elementForUUID:elementUdid];
- 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;
- #if TARGET_OS_TV
- CGSize screenSize = app.frame.size;
- #else
- CGRect frame = app.wdFrame;
- CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation);
- #endif
- return FBResponseWithObject(@{
- @"width": @(screenSize.width),
- @"height": @(screenSize.height),
- });
- }
- + (id<FBResponsePayload>)handleElementScreenshot:(FBRouteRequest *)request
- {
- FBElementCache *elementCache = request.session.elementCache;
- XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
- NSData *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];
- 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"]];
- if ([element.lastSnapshot 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];
- }
- #endif
- @end
|