Skip to content
2 changes: 1 addition & 1 deletion PrivateHeaders/XCTest/XCUIElementQuery.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@
- (id)_debugInfoWithIndent:(id *)arg1;
- (id)_derivedExpressedIdentifiers;
- (unsigned long long)_derivedExpressedType;
- (id)initWithInputQuery:(id)arg1 queryDescription:(id)arg2 filter:(CDUnknownBlockType)arg3;
- (id)initWithInputQuery:(id)arg1 queryDescription:(id)arg2 filter:(CDUnknownBlockType /*4 args ?*/)arg3;

@end
194 changes: 190 additions & 4 deletions WebDriverAgent.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions WebDriverAgentLib/Categories/XCUIElement+CBXCoordinateGestures.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

#import "XCUIElement+FBTap.h"

NS_ASSUME_NONNULL_BEGIN

@interface XCUIElement (CBXCoordinateGestures)

/**
*/
- (BOOL)cbx_tapAtCoordinate:(CGPoint)point withError:(NSError **)error;

/**
*/
- (BOOL)cbx_twoFingerTapAtCoordinate:(CGPoint)point withError:(NSError **)error;

/**
*/
- (BOOL)cbx_pinchAtCoordinate:(CGPoint)point
scale:(double)scale
velocity:(double)velocity
withError:(NSError * _Nullable __autoreleasing *)error;

/**
*/
- (BOOL)cbx_rotateAtCoordinate:(CGPoint)point
radians:(double)radians
velocity:(double)velocity
withError:(NSError * _Nullable __autoreleasing *)error;

@end

NS_ASSUME_NONNULL_END
121 changes: 121 additions & 0 deletions WebDriverAgentLib/Categories/XCUIElement+CBXCoordinateGestures.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@

#import "XCUIElement+CBXCoordinateGestures.h"
#import "FBMathUtils.h"
#import "XCUIElement+FBUtilities.h"
#import "XCElementSnapshot-Hitpoint.h"
#import "XCEventGenerator.h"
#import "XCSynthesizedEventRecord.h"
#import "FBLogger.h"

// Determined by the width of a two-finger touch.
static float const CBX_FINGER_WIDTH = 78.0f /* Adding some buffer */ + 2.0f;
static float const CBX_HALF_FINGER = CBX_FINGER_WIDTH / 2.0f;

@implementation XCUIElement (CBXCoordinateGestures)

- (BOOL)cbx_tapAtCoordinate:(CGPoint)point withError:(NSError **)error {
return [self tapAtCoordinate:point withError:error];
}

- (BOOL)cbx_twoFingerTapAtCoordinate:(CGPoint)point withError:(NSError * _Nullable __autoreleasing *)error {
return [self generateEvent:^(XCEventGenerator *eventGenerator, XCEventGeneratorHandler handlerBlock) {
CGPoint hitPoint = FBInvertPointForApplication(point, self.application.frame.size, self.application.interfaceOrientation);

/*
The theory is that we should provide a rect just large enough to fit two fingers but
centered around the desired point.
*/
CGRect twoFingerTapRect = CGRectMake(hitPoint.x - CBX_HALF_FINGER,
hitPoint.y - CBX_HALF_FINGER,
CBX_FINGER_WIDTH,
CBX_FINGER_WIDTH);

SEL tapper = @selector(twoFingerTapInRect:orientation:handler:);
if ([eventGenerator respondsToSelector:tapper]) {
[eventGenerator twoFingerTapInRect:twoFingerTapRect
orientation:self.interfaceOrientation
handler:handlerBlock];
} else {
//If we're here, we need to pick two points to touch.
//TODO: something more intelligent.
CGPoint one, two;
one = CGPointMake(point.x - CBX_HALF_FINGER, point.y);
two = CGPointMake(point.x + CBX_HALF_FINGER, point.y);
NSValue *p1 = [NSValue valueWithCGPoint:one],
*p2 = [NSValue valueWithCGPoint:two];
[FBLogger logFmt:@"'%@' unavailable, manually tapping %@ and %@",
NSStringFromSelector(tapper),
p1,
p2];
[eventGenerator tapAtTouchLocations:@[p1, p2]
numberOfTaps:1
orientation:self.interfaceOrientation
handler:handlerBlock];
}
} error:error];
}

- (BOOL)cbx_pinchAtCoordinate:(CGPoint)point
scale:(double)scale
velocity:(double)velocity
withError:(NSError * _Nullable __autoreleasing *)error {
return [self generateEvent:^(XCEventGenerator *eventGenerator, XCEventGeneratorHandler handlerBlock) {
CGPoint hitPoint = FBInvertPointForApplication(point,
self.application.frame.size,
self.application.interfaceOrientation);
/*
TODO: The theory here is that we want a localized rect around the desired point.
The question is... how big should the rect be?
Current working theory: should fit at least two fingers
*/
CGRect twoFingerTapRect = CGRectMake(hitPoint.x - CBX_HALF_FINGER,
hitPoint.y - CBX_HALF_FINGER,
CBX_FINGER_WIDTH,
CBX_FINGER_WIDTH);

SEL pincher = @selector(pinchInRect:withScale:velocity:orientation:handler:);
if ([eventGenerator respondsToSelector:pincher]) {
[eventGenerator pinchInRect:twoFingerTapRect
withScale:scale
velocity:velocity
orientation:self.interfaceOrientation
handler:handlerBlock];
} else {
[FBLogger logFmt:@"Error: Unable to synthesize event, XCEventGenerator does not respond to %@", NSStringFromSelector(pincher)];
}
} error:error];
}

- (BOOL)cbx_rotateAtCoordinate:(CGPoint)point
radians:(double)radians
velocity:(double)velocity
withError:(NSError * _Nullable __autoreleasing *)error {
return [self generateEvent:^(XCEventGenerator *eventGenerator, XCEventGeneratorHandler handlerBlock) {
CGPoint hitPoint = FBInvertPointForApplication(point,
self.application.frame.size,
self.application.interfaceOrientation);
/*
TODO: The theory here is that we want a localized rect around the desired point.
The question is... how big should the rect be?
Current working theory: should fit at least two fingers
*/
CGRect twoFingerRect = CGRectMake(hitPoint.x - CBX_HALF_FINGER,
hitPoint.y - CBX_HALF_FINGER,
CBX_FINGER_WIDTH,
CBX_FINGER_WIDTH);

//TODO: I am assuming the second param is radians, based on -[XCUIElement rotate:withVelocity:]
SEL rotater = @selector(rotateInRect:withRotation:velocity:orientation:handler:);
if ([eventGenerator respondsToSelector:rotater]) {
[eventGenerator rotateInRect:twoFingerRect
withRotation:radians
velocity:velocity
orientation:self.interfaceOrientation
handler:handlerBlock];
} else {
[FBLogger logFmt:@"Error: Unable to synthesize event, XCEventGenerator does not respond to %@", NSStringFromSelector(rotater)];
}
} error:error];
}

@end
11 changes: 11 additions & 0 deletions WebDriverAgentLib/Categories/XCUIElement+FBTap.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
*/

#import <WebDriverAgentLib/XCUIElement.h>
#import "XCEventGenerator.h"

NS_ASSUME_NONNULL_BEGIN

typedef void(^eventGenerationBlock)(XCEventGenerator *eventGenerator, XCEventGeneratorHandler handlerBlock);

@interface XCUIElement (FBTap)

/**
Expand All @@ -21,6 +24,14 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (BOOL)fb_tapWithError:(NSError **)error;

/**
CBX Additions
*/
- (BOOL)tapAtCoordinate:(CGPoint)point withError:(NSError * _Nullable __autoreleasing *)error;

//TODO: Move this somewhere more general
- (BOOL)generateEvent:(eventGenerationBlock)eventBlock error:(NSError * _Nullable __autoreleasing *)error;

@end

NS_ASSUME_NONNULL_END
61 changes: 38 additions & 23 deletions WebDriverAgentLib/Categories/XCUIElement+FBTap.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,48 @@

@implementation XCUIElement (FBTap)

typedef void(^eventGenerationBlock)(XCEventGenerator *eventGenerator, XCEventGeneratorHandler handlerBlock);

- (BOOL)generateEvent:(eventGenerationBlock)eventBlock error:(NSError * _Nullable __autoreleasing *)error {
[self fb_waitUntilFrameIsStable];
__block BOOL didSucceed;
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)()){
XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *record, NSError *commandError) {
if (commandError) {
[FBLogger logFmt:@"Failed to perform tap: %@", commandError];
}
if (error) {
*error = commandError;
}
didSucceed = (commandError == nil);
completion();
};
eventBlock([XCEventGenerator sharedGenerator], handlerBlock);
}];
return didSucceed;
}

- (BOOL)tapAtCoordinate:(CGPoint)point withError:(NSError * _Nullable __autoreleasing *)error {
return [self generateEvent:^(XCEventGenerator *eventGenerator, XCEventGeneratorHandler handlerBlock) {
CGPoint hitPoint = FBInvertPointForApplication(point, self.application.frame.size, self.application.interfaceOrientation);
if ([eventGenerator respondsToSelector:@selector(tapAtTouchLocations:numberOfTaps:orientation:handler:)]) {
[eventGenerator tapAtTouchLocations:@[[NSValue valueWithCGPoint:hitPoint]]
numberOfTaps:1
orientation:self.interfaceOrientation
handler:handlerBlock];
} else {
[eventGenerator tapAtPoint:hitPoint
orientation:self.interfaceOrientation
handler:handlerBlock];
}
} error:error];
}

- (BOOL)fb_tapWithError:(NSError **)error
{
[self fb_waitUntilFrameIsStable];
__block BOOL didSucceed;
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)()){
NSValue *hitpointValue = self.lastSnapshot.suggestedHitpoints.firstObject;
CGPoint hitPoint = hitpointValue ? hitpointValue.CGPointValue : [self coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)].screenPoint;
hitPoint = FBInvertPointForApplication(hitPoint, self.application.frame.size, self.application.interfaceOrientation);
XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *record, NSError *commandError) {
if (commandError) {
[FBLogger logFmt:@"Failed to perform tap: %@", commandError];
}
if (error) {
*error = commandError;
}
didSucceed = (commandError == nil);
completion();
};
XCEventGenerator *eventGenerator = [XCEventGenerator sharedGenerator];
if ([eventGenerator respondsToSelector:@selector(tapAtTouchLocations:numberOfTaps:orientation:handler:)]) {
[eventGenerator tapAtTouchLocations:@[[NSValue valueWithCGPoint:hitPoint]] numberOfTaps:1 orientation:self.interfaceOrientation handler:handlerBlock];
}
else {
[eventGenerator tapAtPoint:hitPoint orientation:self.interfaceOrientation handler:handlerBlock];
}
}];
return didSucceed;
return [self tapAtCoordinate:hitPoint withError:error];
}

@end
16 changes: 16 additions & 0 deletions WebDriverAgentLib/Commands/CBXCommands/CBXCommands.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

#import <Foundation/Foundation.h>

#import <WebDriverAgentLib/FBCommandHandler.h>
#import "FBApplication.h"
#import "FBRouteRequest.h"
#import "FBSession.h"
#import "FBApplication.h"
#import "XCUIDevice.h"
#import "XCUIDevice+FBHealthCheck.h"

#import "CBXMacros.h"

@interface CBXCommands : NSObject/* Do not implement FBCommandHandler */
+ (XCUICoordinate *)tapCoordinateForX:(CGFloat)x y:(CGFloat)y;
@end
67 changes: 67 additions & 0 deletions WebDriverAgentLib/Commands/CBXCommands/CBXCommands.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@

#import "XCUICoordinate.h"
#import "XCTestDriver.h"
#import "XCUIElement.h"
#import "XCUIApplication.h"
#import "XCUIElementQuery.h"
#import "XCElementSnapshot.h"

#import "CBXCommands.h"
#import "CBXCoordinate.h"

#import "FBRunLoopSpinner.h"
#import "FBElementCache.h"

@implementation CBXCommands

+ (XCUIElement *)elementFromSpecifiers:(NSDictionary *)specifiers {
XCUIElement *element;
if ([specifiers hasKey:@"uuid"]) {
FBElementCache *elementCache = [FBSession activeSessionCache];
element = [elementCache elementForUUID:specifiers[@"uuid"]];
} else if ([specifiers hasKey:@"coordinate"]) {
CBXCoordinate *coord = [CBXCoordinate withJSON:specifiers[@"coordinate"]];
element = [CBXCommands elementAtPoint:coord.cgpoint error:nil];
}
return element;
}

+ (XCUICoordinate *)tapCoordinateForX:(CGFloat)x y:(CGFloat)y {
if ([FBSession activeSession]) {
XCUICoordinate *appCoordinate = [[XCUICoordinate alloc] initWithElement:[FBSession activeSession].application
normalizedOffset:CGVectorMake(0, 0)];
XCUICoordinate *tapCoordinate = [[XCUICoordinate alloc] initWithCoordinate:appCoordinate
pointsOffset:CGVectorMake(x, y)];

return tapCoordinate;
}
NSLog(@"WARN: Requested tap coordinate when active session is nil!");
return nil;
}

+ (XCUIElement *)elementAtPoint:(CGPoint)point error:(NSError *__autoreleasing *)error {
__block XCUIElement *element = nil;
__block XCAccessibilityElement *accEl = nil;
__block NSError *outer;
[FBRunLoopSpinner spinUntilCompletion:^(void (^ _Nonnull completion)()) {
[[XCTestDriver sharedTestDriver].managerProxy _XCT_requestElementAtPoint:point
reply:^(XCAccessibilityElement *axEl,
NSError *inner) {
outer = inner;
accEl = axEl;
completion();
}];
}];
XCElementSnapshot *snap = [[FBSession activeSession].application lastSnapshot];
XCElementSnapshot *elSnap = [snap elementSnapshotMatchingAccessibilityElement:accEl];
XCUIElementQuery *appQuery = [[FBSession activeSession].application.query descendantsMatchingType:elSnap.elementType];
element = [appQuery _elementMatchingAccessibilityElementOfSnapshot:elSnap];

//TODO: this can cause failures if no element found. Is it safe?
[element resolve];
if (error) {
*error = outer;
}
return element;
}
@end
6 changes: 6 additions & 0 deletions WebDriverAgentLib/Commands/CBXCommands/CBXGestureCommands.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

#import "CBXCommands.h"

@interface CBXGestureCommands : CBXCommands<FBCommandHandler>
+ (BOOL)handleTouch:(NSDictionary *)specifiers options:(NSDictionary *)options;
@end
Loading