Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add coordinate-based APIs for gesture calls #843

Merged
merged 5 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,18 @@ NS_ASSUME_NONNULL_BEGIN

@end

#if !TARGET_OS_TV
@interface XCUICoordinate (FBSwiping)

/**
* Performs swipe gesture on the coordinate
*
* @param direction Swipe direction. The following values are supported: up, down, left and right
* @param velocity Swipe speed in pixels per second
*/
- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity;

@end
#endif

NS_ASSUME_NONNULL_END
37 changes: 27 additions & 10 deletions WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,46 @@
#import "FBLogger.h"
#import "XCUIElement.h"

@implementation XCUIElement (FBSwiping)

- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity
{
void swipeWithDirection(NSObject *target, NSString *direction, NSNumber* _Nullable velocity) {
double velocityValue = .0;
if (nil != velocity) {
velocityValue = [velocity doubleValue];
}

if (velocityValue > 0) {
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@WithVelocity:", direction.lowercaseString.capitalizedString]);
NSMethodSignature *signature = [self methodSignatureForSelector:selector];
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@WithVelocity:",
direction.lowercaseString.capitalizedString]);
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation setArgument:&velocityValue atIndex:2];
[invocation invokeWithTarget:self];
[invocation invokeWithTarget:target];
} else {
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@", direction.lowercaseString.capitalizedString]);
NSMethodSignature *signature = [self methodSignatureForSelector:selector];
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@",
direction.lowercaseString.capitalizedString]);
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation invokeWithTarget:self];
[invocation invokeWithTarget:target];
}
}

@implementation XCUIElement (FBSwiping)

- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity
{
swipeWithDirection(self, direction, velocity);
}

@end

#if !TARGET_OS_TV
@implementation XCUICoordinate (FBSwiping)

- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity
{
swipeWithDirection(self, direction, velocity);
}

@end
#endif
198 changes: 112 additions & 86 deletions WebDriverAgentLib/Commands/FBElementCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,46 @@ + (NSArray *)routes
[[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/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self action:@selector(handleTapWithNumberOfTaps:)],
[[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/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)],
[[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)],
[[FBRoute POST:@"/wda/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragCoordinateWithVelocity:)],
[[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)],
[[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHoldCoordinate:)],
[[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)],
[[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)],

[[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:)],
];
Expand Down Expand Up @@ -285,65 +307,51 @@ + (NSArray *)routes
#else
+ (id<FBResponsePayload>)handleDoubleTap:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
[element doubleTap];
return FBResponseWithOK();
}

+ (id<FBResponsePayload>)handleDoubleTapCoordinate:(FBRouteRequest *)request
{
CGVector offset = CGVectorMake([request.arguments[@"x"] doubleValue],
[request.arguments[@"y"] doubleValue]);
XCUICoordinate *doubleTapCoordinate = [self.class gestureCoordinateWithOffset:offset
element:request.session.activeApplication];
[doubleTapCoordinate doubleTap];
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
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
XCUIElement *element = [self targetFromRequest:request];
[element twoFingerTap];
return FBResponseWithOK();
}

+ (id<FBResponsePayload>)handleTapWithNumberOfTaps:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
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 = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
XCUIElement *element = [self targetFromRequest:request];
[element tapWithNumberOfTaps:[request.arguments[@"numberOfTaps"] integerValue]
numberOfTouches:[request.arguments[@"numberOfTouches"] integerValue]];
return FBResponseWithOK();
}

+ (id<FBResponsePayload>)handleTouchAndHold:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
[element pressForDuration:[request.arguments[@"duration"] doubleValue]];
return FBResponseWithOK();
}

+ (id<FBResponsePayload>)handleTouchAndHoldCoordinate:(FBRouteRequest *)request
{
CGVector offset = CGVectorMake([request.arguments[@"x"] doubleValue],
[request.arguments[@"y"] doubleValue]);
XCUICoordinate *pressCoordinate = [self.class gestureCoordinateWithOffset:offset
element:request.session.activeApplication];
[pressCoordinate pressForDuration:[request.arguments[@"duration"] doubleValue]];
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 = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
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]);
Expand Down Expand Up @@ -379,8 +387,7 @@ + (NSArray *)routes

+ (id<FBResponsePayload>)handleScroll:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
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"];
Expand Down Expand Up @@ -440,73 +447,60 @@ + (NSArray *)routes
traceback:nil]);
}

+ (id<FBResponsePayload>)handleDragCoordinate:(FBRouteRequest *)request
{
FBSession *session = request.session;
CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue],
[request.arguments[@"fromY"] doubleValue]);
XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset
element:session.activeApplication];
CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue],
[request.arguments[@"toY"] doubleValue]);
XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset
element:session.activeApplication];
NSTimeInterval duration = [request.arguments[@"duration"] doubleValue];
[startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate];
return FBResponseWithOK();
}

+ (id<FBResponsePayload>)handleDrag:(FBRouteRequest *)request
{
FBSession *session = request.session;
FBElementCache *elementCache = session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
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:element];
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:element];
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
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
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]) {
return FBResponseWithStatus([FBCommandStatus
invalidArgumentErrorWithMessage:[NSString stringWithFormat: @"Unsupported swipe direction '%@'. Only the following directions are supported: %@", direction, supportedDirections]
traceback:nil]);
NSString *message = [NSString stringWithFormat:@"Unsupported swipe direction '%@'. Only the following directions are supported: %@", direction, supportedDirections];
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message
traceback:nil]);
}
[element fb_swipeWithDirection:direction.lowercaseString velocity:request.arguments[@"velocity"]];
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
{
FBElementCache *elementCache = request.session.elementCache;
CGVector offset = CGVectorMake([request.arguments[@"x"] doubleValue],
[request.arguments[@"y"] doubleValue]);
XCUIElement *element = [elementCache hasElementWithUUID:request.parameters[@"uuid"]]
? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]
: request.session.activeApplication;
XCUICoordinate *tapCoordinate = [self.class gestureCoordinateWithOffset:offset element:element];
[tapCoordinate tap];
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
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
XCUIElement *element = [self targetFromRequest:request];
CGFloat scale = (CGFloat)[request.arguments[@"scale"] doubleValue];
CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue];
[element pinchWithScale:scale velocity:velocity];
Expand All @@ -515,8 +509,7 @@ + (NSArray *)routes

+ (id<FBResponsePayload>)handleRotate:(FBRouteRequest *)request
{
FBElementCache *elementCache = request.session.elementCache;
XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
XCUIElement *element = [self targetFromRequest:request];
CGFloat rotation = (CGFloat)[request.arguments[@"rotation"] doubleValue];
CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue];
[element rotate:rotation withVelocity:velocity];
Expand All @@ -525,13 +518,7 @@ + (NSArray *)routes

+ (id<FBResponsePayload>)handleForceTouch:(FBRouteRequest *)request
{
XCUIElement *element = nil;
if (nil == request.parameters[@"uuid"]) {
element = XCUIApplication.fb_activeApplication;
} else {
FBElementCache *elementCache = request.session.elementCache;
element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]];
}
XCUIElement *element = [self targetFromRequest:request];
NSNumber *pressure = request.arguments[@"pressure"];
NSNumber *duration = request.arguments[@"duration"];
NSNumber *x = request.arguments[@"x"];
Expand Down Expand Up @@ -673,6 +660,45 @@ + (XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset
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
Loading