diff --git a/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h b/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h index 5f5d5937f..8b87a7591 100644 --- a/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h +++ b/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h @@ -9,6 +9,8 @@ @class NSArray, NSDictionary, NSNumber, NSString, NSUUID, XCAccessibilityElement, XCDeviceEvent, XCSynthesizedEventRecord, XCTouchGesture, NSXPCListenerEndpoint, XCElementSnapshot; @protocol XCTestManager_ManagerInterface +// since Xcode9 +- (void)_XCT_requestBundleIDForPID:(int)arg1 reply:(void (^)(NSString *, NSError *))arg2; - (void)_XCT_loadAccessibilityWithTimeout:(double)arg1 reply:(void (^)(BOOL, NSError *))arg2; - (void)_XCT_injectVoiceRecognitionAudioInputPaths:(NSArray *)arg1 completion:(void (^)(BOOL, NSError *))arg2; - (void)_XCT_injectAssistantRecognitionStrings:(NSArray *)arg1 completion:(void (^)(BOOL, NSError *))arg2; diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h index f578632bb..981075782 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h @@ -69,6 +69,23 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)fb_waitForAppElement:(NSTimeInterval)timeout; +/** + Retrieves the information about the applications the given accessiblity elements + belong to + + @param axElements the list of accessibility elements + @returns The list of dictionaries. Each dictionary contains `bundleId` and `pid` items + */ ++ (NSArray *> *)fb_appsInfoWithAxElements:(NSArray *)axElements; + +/** + Retrieves the information about the currently active apps + + @returns The list of dictionaries. Each dictionary contains `bundleId` and `pid` items. + */ ++ (NSArray *> *)fb_activeAppsInfo; + + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m index e0ec1fdc9..ea720f2e7 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m @@ -12,6 +12,7 @@ #import "FBSpringboardApplication.h" #import "XCElementSnapshot.h" #import "FBElementTypeTransformer.h" +#import "FBLogger.h" #import "FBMacros.h" #import "FBMathUtils.h" #import "FBXCodeCompatibility.h" @@ -29,6 +30,8 @@ #import "XCTRunnerDaemonSession.h" const static NSTimeInterval FBMinimumAppSwitchWait = 3.0; +static NSString* const FBUnknownBundleId = @"unknown"; + @implementation XCUIApplication (FBHelpers) @@ -39,7 +42,6 @@ + (XCAccessibilityElement *)fb_onScreenElement dispatch_once(&oncePoint, ^{ CGSize screenSize = [UIScreen mainScreen].bounds.size; // Consider the element, which is located close to the top left corner of the screen the on-screen one. - // FIXME: Improve this algorithm for split-screen automation CGFloat pointDistance = MIN(screenSize.width, screenSize.height) * 0.2; screenPoint = CGPointMake(pointDistance, pointDistance); }); @@ -50,6 +52,8 @@ + (XCAccessibilityElement *)fb_onScreenElement reply:^(XCAccessibilityElement *element, NSError *error) { if (nil == error) { onScreenElement = element; + } else { + [FBLogger logFmt:@"Cannot request the screen point at %@: %@", [NSValue valueWithCGPoint:screenPoint], error.description]; } dispatch_semaphore_signal(sem); }]; @@ -69,6 +73,37 @@ - (BOOL)fb_waitForAppElement:(NSTimeInterval)timeout }]; } ++ (NSArray *> *)fb_appsInfoWithAxElements:(NSArray *)axElements +{ + NSMutableArray *> *result = [NSMutableArray array]; + id proxy = [FBXCTestDaemonsProxy testRunnerProxy]; + for (XCAccessibilityElement *axElement in axElements) { + NSMutableDictionary *appInfo = [NSMutableDictionary dictionary]; + pid_t pid = axElement.processIdentifier; + appInfo[@"pid"] = @(pid); + __block NSString *bundleId = nil; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + [proxy _XCT_requestBundleIDForPID:pid + reply:^(NSString *bundleID, NSError *error) { + if (nil == error) { + bundleId = bundleID; + } else { + [FBLogger logFmt:@"Cannot request the bundle ID for process ID %@: %@", @(pid), error.description]; + } + dispatch_semaphore_signal(sem); + }]; + dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC))); + appInfo[@"bundleId"] = bundleId ?: FBUnknownBundleId; + [result addObject:appInfo.copy]; + } + return result.copy; +} + ++ (NSArray *> *)fb_activeAppsInfo +{ + return [self fb_appsInfoWithAxElements:[FBXCAXClientProxy.sharedClient activeApplications]]; +} + - (BOOL)fb_deactivateWithDuration:(NSTimeInterval)duration error:(NSError **)error { if(![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:error]) { diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.m b/WebDriverAgentLib/Commands/FBCustomCommands.m index 3c875e441..667f67fa1 100644 --- a/WebDriverAgentLib/Commands/FBCustomCommands.m +++ b/WebDriverAgentLib/Commands/FBCustomCommands.m @@ -94,7 +94,7 @@ + (NSArray *)routes + (id)handleDismissKeyboardCommand:(FBRouteRequest *)request { #if TARGET_OS_TV - if ([self isKeyboardPresent]) { + if ([self isKeyboardPresentForApplication:request.session.activeApplication]) { [[XCUIRemote sharedRemote] pressButton: XCUIRemoteButtonMenu]; } #else @@ -110,7 +110,7 @@ + (NSArray *)routes timeout:5] timeoutErrorMessage:errorDescription] spinUntilTrue:^BOOL{ - return ![self isKeyboardPresent]; + return ![self isKeyboardPresentForApplication:request.session.activeApplication]; } error:&error]; if (!isKeyboardNotPresent) { @@ -121,8 +121,8 @@ + (NSArray *)routes #pragma mark - Helpers -+ (BOOL)isKeyboardPresent { - XCUIElement *foundKeyboard = [[FBApplication fb_activeApplication].query descendantsMatchingType:XCUIElementTypeKeyboard].fb_firstMatch; ++ (BOOL)isKeyboardPresentForApplication:(XCUIApplication *)application { + XCUIElement *foundKeyboard = [application.query descendantsMatchingType:XCUIElementTypeKeyboard].fb_firstMatch; return foundKeyboard && foundKeyboard.fb_isVisible; } diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgentLib/Commands/FBSessionCommands.m index 586bc2def..f600072cf 100644 --- a/WebDriverAgentLib/Commands/FBSessionCommands.m +++ b/WebDriverAgentLib/Commands/FBSessionCommands.m @@ -16,6 +16,7 @@ #import "FBSession.h" #import "FBApplication.h" #import "FBRuntimeUtils.h" +#import "XCUIApplication+FBHelpers.h" #import "XCUIDevice.h" #import "XCUIDevice+FBHealthCheck.h" #import "XCUIDevice+FBHelpers.h" @@ -33,6 +34,7 @@ static NSString* const SNAPSHOT_TIMEOUT = @"snapshotTimeout"; static NSString* const USE_FIRST_MATCH = @"useFirstMatch"; static NSString* const REDUCE_MOTION = @"reduceMotion"; +static NSString* const DEFAULT_ACTIVE_APPLICATION = @"defaultActiveApplication"; @implementation FBSessionCommands @@ -48,6 +50,7 @@ + (NSArray *)routes [[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)], [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)], [[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)], + [[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)], [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)], [[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)], [[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)], @@ -85,7 +88,7 @@ + (NSArray *)routes return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:@"'capabilities' is mandatory to create a new session" traceback:nil]); } - if (nil == (requirements = FBParseCapabilities(request.arguments[@"capabilities"], &error))) { + if (nil == (requirements = FBParseCapabilities((NSDictionary *)request.arguments[@"capabilities"], &error))) { return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:error.description traceback:nil]); } [FBConfiguration setShouldUseTestManagerForVisibilityDetection:[requirements[@"shouldUseTestManagerForVisibilityDetection"] boolValue]]; @@ -162,6 +165,11 @@ + (NSArray *)routes return FBResponseWithObject(@(state)); } ++ (id)handleGetActiveAppsList:(FBRouteRequest *)request +{ + return FBResponseWithObject([XCUIApplication fb_activeAppsInfo]); +} + + (id)handleGetActiveSession:(FBRouteRequest *)request { return FBResponseWithObject(FBSessionCommands.sessionInformation); @@ -235,6 +243,7 @@ + (NSArray *)routes SNAPSHOT_TIMEOUT: @([FBConfiguration snapshotTimeout]), USE_FIRST_MATCH: @([FBConfiguration useFirstMatch]), REDUCE_MOTION: @([FBConfiguration reduceMotionEnabled]), + DEFAULT_ACTIVE_APPLICATION: request.session.defaultActiveApplication, } ); } @@ -278,6 +287,9 @@ + (NSArray *)routes if ([settings objectForKey:REDUCE_MOTION]) { [FBConfiguration setReduceMotionEnabled:[[settings objectForKey:REDUCE_MOTION] boolValue]]; } + if ([settings objectForKey:DEFAULT_ACTIVE_APPLICATION]) { + request.session.defaultActiveApplication = (NSString *)[settings objectForKey:DEFAULT_ACTIVE_APPLICATION]; + } return [self handleGetSettings:request]; } diff --git a/WebDriverAgentLib/FBApplication.h b/WebDriverAgentLib/FBApplication.h index 65dd7eca0..e94498a23 100644 --- a/WebDriverAgentLib/FBApplication.h +++ b/WebDriverAgentLib/FBApplication.h @@ -18,6 +18,14 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)fb_activeApplication; +/** + Constructor used to get current active application + + @param bundleId The bundle identifier of an app, which should be selected as active by default + if it is present in the list of active applications + */ ++ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId; + /** Constructor used to get the system application */ diff --git a/WebDriverAgentLib/FBApplication.m b/WebDriverAgentLib/FBApplication.m index 269032eca..469f5da13 100644 --- a/WebDriverAgentLib/FBApplication.m +++ b/WebDriverAgentLib/FBApplication.m @@ -35,21 +35,42 @@ @interface FBApplication () @implementation FBApplication + (instancetype)fb_activeApplication +{ + return [self fb_activeApplicationWithDefaultBundleId:nil]; +} + ++ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId { NSArray *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications]; - XCAccessibilityElement *activeApplicationElement = [activeApplicationElements firstObject]; + XCAccessibilityElement *activeApplicationElement = nil; if (activeApplicationElements.count > 1) { - XCAccessibilityElement *currentElement = self.class.fb_onScreenElement; - if (nil != currentElement) { - for (XCAccessibilityElement *appElement in activeApplicationElements) { - if (appElement.processIdentifier == currentElement.processIdentifier) { - activeApplicationElement = appElement; + if (nil != bundleId) { + // Try to select the desired application first + NSArray *appsInfo = [self fb_appsInfoWithAxElements:activeApplicationElements]; + for (NSUInteger appIdx = 0; appIdx < appsInfo.count; appIdx++) { + if ([[[appsInfo objectAtIndex:appIdx] objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) { + activeApplicationElement = [activeApplicationElements objectAtIndex:appIdx]; break; } } } + // Fall back to the "normal" algorithm if the desired application is either + // not set or is not active + if (nil == activeApplicationElement) { + XCAccessibilityElement *currentElement = self.class.fb_onScreenElement; + if (nil != currentElement) { + for (XCAccessibilityElement *appElement in activeApplicationElements) { + if (appElement.processIdentifier == currentElement.processIdentifier) { + activeApplicationElement = appElement; + break; + } + } + } + } } - if (nil == activeApplicationElement) { + if (nil == activeApplicationElement && activeApplicationElements.count > 0) { + activeApplicationElement = [activeApplicationElements firstObject]; + } else { NSString *errMsg = @"No applications are currently active"; @throw [NSException exceptionWithName:FBElementNotVisibleException reason:errMsg userInfo:nil]; } diff --git a/WebDriverAgentLib/FBSpringboardApplication.m b/WebDriverAgentLib/FBSpringboardApplication.m index 127c4e7f6..804c28742 100644 --- a/WebDriverAgentLib/FBSpringboardApplication.m +++ b/WebDriverAgentLib/FBSpringboardApplication.m @@ -9,18 +9,7 @@ #import "FBSpringboardApplication.h" -#import "FBErrorBuilder.h" -#import "FBMathUtils.h" #import "FBRunLoopSpinner.h" -#import "XCElementSnapshot+FBHelpers.h" -#import "XCElementSnapshot.h" -#import "XCUIApplication+FBHelpers.h" -#import "XCUIElement+FBIsVisible.h" -#import "XCUIElement+FBUtilities.h" -#import "XCUIElement+FBTap.h" -#import "XCUIElement+FBScrolling.h" -#import "XCUIElement.h" -#import "XCUIElementQuery.h" #import "FBXCodeCompatibility.h" #if TARGET_OS_TV diff --git a/WebDriverAgentLib/Routing/FBSession.h b/WebDriverAgentLib/Routing/FBSession.h index 5f3083ba1..32bd4e51a 100644 --- a/WebDriverAgentLib/Routing/FBSession.h +++ b/WebDriverAgentLib/Routing/FBSession.h @@ -31,6 +31,8 @@ extern NSString *const FBApplicationCrashedException; /*! Element cache related to that session */ @property (nonatomic, strong, readonly) FBElementCache *elementCache; +@property (nonatomic, copy) NSString *defaultActiveApplication; + + (nullable instancetype)activeSession; /** diff --git a/WebDriverAgentLib/Routing/FBSession.m b/WebDriverAgentLib/Routing/FBSession.m index 8c7295208..7a3c59b57 100644 --- a/WebDriverAgentLib/Routing/FBSession.m +++ b/WebDriverAgentLib/Routing/FBSession.m @@ -23,6 +23,12 @@ #import "XCUIElement.h" NSString *const FBApplicationCrashedException = @"FBApplicationCrashedException"; +/*! + The intial value for the default application property. + Setting this value to `defaultActiveApplication` property forces WDA to use the internal + automated algorithm to determine the active on-screen application + */ +NSString *const FBDefaultApplicationAuto = @"auto"; @interface FBSession () @property (nonatomic) NSString *testedApplicationBundleId; @@ -95,6 +101,7 @@ + (instancetype)initWithApplication:(FBApplication *)application session.alertsMonitor = nil; session.defaultAlertAction = nil; session.identifier = [[NSUUID UUID] UUIDString]; + session.defaultActiveApplication = FBDefaultApplicationAuto; session.testedApplicationBundleId = nil; NSMutableDictionary *apps = [NSMutableDictionary dictionary]; if (application) { @@ -133,7 +140,10 @@ - (void)kill - (FBApplication *)activeApplication { - FBApplication *application = [FBApplication fb_activeApplication]; + NSString *defaultBundleId = [self.defaultActiveApplication isEqualToString:FBDefaultApplicationAuto] + ? nil + : self.defaultActiveApplication; + FBApplication *application = [FBApplication fb_activeApplicationWithDefaultBundleId:defaultBundleId]; FBApplication *testedApplication = nil; if (self.testedApplicationBundleId) { testedApplication = [self.applications objectForKey:self.testedApplicationBundleId]; diff --git a/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m b/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m index c9da65e7a..96eec3f37 100644 --- a/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m +++ b/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m @@ -80,4 +80,18 @@ - (void)testActiveElement ((id)textField).wdUID); } +- (void)testActiveApplicationsInfo +{ + NSArray *appsInfo = [XCUIApplication fb_activeAppsInfo]; + XCTAssertTrue(appsInfo.count > 0); + BOOL isAppActive = NO; + for (NSDictionary *appInfo in appsInfo) { + if ([appInfo[@"bundleId"] isEqualToString:self.testedApplication.bundleID]) { + isAppActive = YES; + break; + } + } + XCTAssertTrue(isAppActive); +} + @end