Skip to content

Commit

Permalink
feat: Add support of the simulated geolocation setting (#662)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Feb 20, 2023
1 parent 6f0fc77 commit ebb9e60
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 10 deletions.
9 changes: 9 additions & 0 deletions PrivateHeaders/XCTest/XCTRunnerDaemonSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
#import <UIKit/UIKit.h>

@class NSMutableDictionary, NSXPCConnection, XCSynthesizedEventRecord;
#if !TARGET_OS_TV // tvOS does not provide relevant APIs
@class CLLocation;
#endif
@protocol XCTUIApplicationMonitor, XCTAXClient, XCTestManager_ManagerInterface;

// iOS since 10.3
Expand Down Expand Up @@ -70,5 +73,11 @@
// Since Xcode 14.3
- (void)openURL:(NSURL *)arg1 usingApplication:(NSString *)arg2 completion:(void (^)(_Bool, NSError *))arg3;
- (void)openDefaultApplicationForURL:(NSURL *)arg1 completion:(void (^)(_Bool, NSError *))arg2;
#if !TARGET_OS_TV // tvOS does not provide relevant APIs
- (void)setSimulatedLocation:(CLLocation *)arg1 completion:(void (^)(_Bool, NSError *))arg2;
- (void)getSimulatedLocationWithReply:(void (^)(CLLocation *, NSError *))arg1;
- (void)clearSimulatedLocationWithReply:(void (^)(_Bool, NSError *))arg1;
@property(readonly) _Bool supportsLocationSimulation;
#endif

@end
34 changes: 34 additions & 0 deletions WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

#import <XCTest/XCTest.h>

#if !TARGET_OS_TV
#import <CoreLocation/CoreLocation.h>
#endif

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) {
Expand Down Expand Up @@ -154,6 +158,36 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) {
*/
- (nullable NSNumber *)fb_getAppearance;

#if !TARGET_OS_TV
/**
Allows to set a simulated geolocation coordinates.
Only works since Xcode 14.3/iOS 16.4
@param location The simlated location coordinates to set
@param error If there is an error, upon return contains an NSError object that describes the problem.
@return YES if the simulated location has been successfully set
*/
- (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error;

/**
Allows to get a simulated geolocation coordinates.
Only works since Xcode 14.3/iOS 16.4
@param error If there is an error, upon return contains an NSError object that describes the problem.
@return The current simulated location or nil in case of failure
*/
- (nullable CLLocation *)fb_getSimulatedLocation:(NSError **)error;

/**
Allows to clear a previosuly set simulated geolocation coordinates.
Only works since Xcode 14.3/iOS 16.4
@param error If there is an error, upon return contains an NSError object that describes the problem.
@return YES if the simulated location has been successfully cleared
*/
- (BOOL)fb_clearSimulatedLocation:(NSError **)error;
#endif

@end

NS_ASSUME_NONNULL_END
17 changes: 17 additions & 0 deletions WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m
Original file line number Diff line number Diff line change
Expand Up @@ -355,4 +355,21 @@ - (NSNumber *)fb_getAppearance
: nil;
}

#if !TARGET_OS_TV
- (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error
{
return [FBXCTestDaemonsProxy setSimulatedLocation:location error:error];
}

- (nullable CLLocation *)fb_getSimulatedLocation:(NSError **)error
{
return [FBXCTestDaemonsProxy getSimulatedLocation:error];
}

- (BOOL)fb_clearSimulatedLocation:(NSError **)error
{
return [FBXCTestDaemonsProxy clearSimulatedLocation:error];
}
#endif

@end
74 changes: 64 additions & 10 deletions WebDriverAgentLib/Commands/FBCustomCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ + (NSArray *)routes
[[FBRoute POST:@"/wda/device/appearance"].withoutSession respondWithTarget:self action:@selector(handleSetDeviceAppearance:)],
[[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)],
[[FBRoute GET:@"/wda/device/location"].withoutSession respondWithTarget:self action:@selector(handleGetLocation:)],
#if !TARGET_OS_TV // tvOS does not provide relevant APIs
[[FBRoute GET:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
[[FBRoute GET:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedLocation:)],
[[FBRoute POST:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleSetSimulatedLocation:)],
[[FBRoute POST:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleSetSimulatedLocation:)],
[[FBRoute DELETE:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleClearSimulatedLocation:)],
[[FBRoute DELETE:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleClearSimulatedLocation:)],
#endif
[[FBRoute OPTIONS:@"/*"].withoutSession respondWithTarget:self action:@selector(handlePingCommand:)],
];
}
Expand Down Expand Up @@ -106,9 +114,9 @@ + (NSArray *)routes
BOOL isDismissed = [request.session.activeApplication fb_dismissKeyboardWithKeyNames:request.arguments[@"keyNames"]
error:&error];
return isDismissed
? FBResponseWithOK()
: FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:nil]);
? FBResponseWithOK()
: FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:nil]);
}

+ (id<FBResponsePayload>)handlePingCommand:(FBRouteRequest *)request
Expand All @@ -123,12 +131,12 @@ + (NSArray *)routes
FBSession *session = request.session;
CGSize statusBarSize = [FBScreen statusBarSizeForApplication:session.activeApplication];
return FBResponseWithObject(
@{
@{
@"statusBarSize": @{@"width": @(statusBarSize.width),
@"height": @(statusBarSize.height),
},
},
@"scale": @([FBScreen scale]),
});
});
}

+ (id<FBResponsePayload>)handleLock:(FBRouteRequest *)request
Expand Down Expand Up @@ -325,7 +333,7 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app
CLAuthorizationStatus authStatus;
if ([locationManager respondsToSelector:@selector(authorizationStatus)]) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[locationManager class]
instanceMethodSignatureForSelector:@selector(authorizationStatus)]];
instanceMethodSignatureForSelector:@selector(authorizationStatus)]];
[invocation setSelector:@selector(authorizationStatus)];
[invocation setTarget:locationManager];
[invocation invoke];
Expand Down Expand Up @@ -379,8 +387,8 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app
}

FBUIInterfaceAppearance appearance = [name isEqualToString:@"light"]
? FBUIInterfaceAppearanceLight
: FBUIInterfaceAppearanceDark;
? FBUIInterfaceAppearanceLight
: FBUIInterfaceAppearanceDark;
NSError *error;
if (![XCUIDevice.sharedDevice fb_setAppearance:appearance error:&error]) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
Expand All @@ -397,7 +405,7 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app
NSString *currentLocale = [[NSLocale autoupdatingCurrentLocale] localeIdentifier];

NSMutableDictionary *deviceInfo = [NSMutableDictionary dictionaryWithDictionary:
@{
@{
@"currentLocale": currentLocale,
@"timeZone": self.timeZone,
@"name": UIDevice.currentDevice.name,
Expand Down Expand Up @@ -490,4 +498,50 @@ + (NSString *)timeZone
return [localTimeZone name];
}

#if !TARGET_OS_TV // tvOS does not provide relevant APIs
+ (id<FBResponsePayload>)handleGetSimulatedLocation:(FBRouteRequest *)request
{
NSError *error;
CLLocation *location = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error];
if (nil == location) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithObject(@{
@"latitude": @(location.coordinate.latitude),
@"longiture": @(location.coordinate.longitude),
@"altitude": @(location.altitude),
});
}

+ (id<FBResponsePayload>)handleSetSimulatedLocation:(FBRouteRequest *)request
{
NSNumber *longitude = request.arguments[@"longitude"];
NSNumber *latitude = request.arguments[@"latitude"];

if (nil == longitude || nil == latitude) {
return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both latitude and longitude must be provided"
traceback:nil]);
}
NSError *error;
CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude.doubleValue
longitude:longitude.doubleValue];
if (![XCUIDevice.sharedDevice fb_setSimulatedLocation:location error:&error]) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithOK();
}

+ (id<FBResponsePayload>)handleClearSimulatedLocation:(FBRouteRequest *)request
{
NSError *error;
if (![XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]) {
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
traceback:nil]);
}
return FBResponseWithOK();
}
#endif

@end
11 changes: 11 additions & 0 deletions WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
*/

#import <XCTest/XCTest.h>

#if !TARGET_OS_TV
#import <CoreLocation/CoreLocation.h>
#endif

#import "XCSynthesizedEventRecord.h"

NS_ASSUME_NONNULL_BEGIN
Expand All @@ -28,6 +33,12 @@ NS_ASSUME_NONNULL_BEGIN
+ (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError **)error;
+ (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError **)error;

#if !TARGET_OS_TV
+ (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError **)error;
+ (nullable CLLocation *)getSimulatedLocation:(NSError **)error;
+ (BOOL)clearSimulatedLocation:(NSError **)error;
#endif

@end

NS_ASSUME_NONNULL_END
103 changes: 103 additions & 0 deletions WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,107 @@ + (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasin
return didSucceed;
}

#if !TARGET_OS_TV
+ (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleasing*)error
{
XCTRunnerDaemonSession *session = [FBXCTRunnerDaemonSessionClass sharedSession];
if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) {
if (error) {
[[[FBErrorBuilder builder]
withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
buildError:error];
}
return NO;
}
if (![session supportsLocationSimulation]) {
if (error) {
[[[FBErrorBuilder builder]
withDescriptionFormat:@"Your device does not support location simulation"]
buildError:error];
}
return NO;
}

__block BOOL didSucceed = NO;
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
[session setSimulatedLocation:location completion:^(bool result, NSError *invokeError) {
if (error) {
*error = invokeError;
}
didSucceed = invokeError == nil && result;
completion();
}];
}];
return didSucceed;
}

+ (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error;
{
XCTRunnerDaemonSession *session = [FBXCTRunnerDaemonSessionClass sharedSession];
if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) {
if (error) {
[[[FBErrorBuilder builder]
withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
buildError:error];
}
return nil;
}
if (![session supportsLocationSimulation]) {
if (error) {
[[[FBErrorBuilder builder]
withDescriptionFormat:@"Your device does not support location simulation"]
buildError:error];
}
return nil;
}

__block CLLocation *location = nil;
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
[session getSimulatedLocationWithReply:^(CLLocation *reply, NSError *invokeError) {
if (error) {
*error = invokeError;
}
if (nil == invokeError) {
location = reply;
}
completion();
}];
}];
return location;
}

+ (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error
{
XCTRunnerDaemonSession *session = [FBXCTRunnerDaemonSessionClass sharedSession];
if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) {
if (error) {
[[[FBErrorBuilder builder]
withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"]
buildError:error];
}
return NO;
}
if (![session supportsLocationSimulation]) {
if (error) {
[[[FBErrorBuilder builder]
withDescriptionFormat:@"Your device does not support location simulation"]
buildError:error];
}
return NO;
}

__block BOOL didSucceed = NO;
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
[session clearSimulatedLocationWithReply:^(bool result, NSError *invokeError) {
if (error) {
*error = invokeError;
}
didSucceed = invokeError == nil && result;
completion();
}];
}];
return didSucceed;
}
#endif

@end
25 changes: 25 additions & 0 deletions WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,31 @@ - (void)testUrlSchemeActivationWithApp
XCTAssertNil(error);
}

#if !TARGET_OS_TV
- (void)testSimulatedLocationSetup
{
if (SYSTEM_VERSION_LESS_THAN(@"16.4")) {
return;
}

CLLocation *simulatedLocation = [[CLLocation alloc] initWithLatitude:50 longitude:50];
NSError *error;
XCTAssertTrue([XCUIDevice.sharedDevice fb_setSimulatedLocation:simulatedLocation error:&error]);
XCTAssertNil(error);
CLLocation *currentLocation = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error];
XCTAssertNil(error);
XCTAssertNotNil(currentLocation);
XCTAssertEqualWithAccuracy(simulatedLocation.coordinate.latitude, currentLocation.coordinate.latitude, 0.1);
XCTAssertEqualWithAccuracy(simulatedLocation.coordinate.longitude, currentLocation.coordinate.longitude, 0.1);
XCTAssertTrue([XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]);
XCTAssertNil(error);
currentLocation = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error];
XCTAssertNil(error);
XCTAssertNotEqualWithAccuracy(simulatedLocation.coordinate.latitude, currentLocation.coordinate.latitude, 0.1);
XCTAssertNotEqualWithAccuracy(simulatedLocation.coordinate.longitude, currentLocation.coordinate.longitude, 0.1);
}
#endif

- (void)testPressingUnsupportedButton
{
NSError *error;
Expand Down

0 comments on commit ebb9e60

Please sign in to comment.