Skip to content

Commit

Permalink
feat: Add coordinate-based APIs for gesture calls (#843)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The  /wda/tap/:uuid endpoint has been replaced by /wda/element/:uuid/tap and /wda/tap ones
  • Loading branch information
mykola-mokhnach authored Jan 31, 2024
1 parent 5d345d5 commit feda373
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 96 deletions.
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

0 comments on commit feda373

Please sign in to comment.