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

iOSAutomationAgent #1

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
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