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

feat: Add the necessary primitives to be able to automate split-screen apps #209

Merged
merged 4 commits into from
Aug 31, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,23 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (BOOL)fb_waitForAppElement:(NSTimeInterval)timeout;

/**
Retrievs the infomration about the applications the given accessiblity elements
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good typos :D

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:-P

belong to

@param axElements the list of accessibility elements
@returns The list of dictionaries. Each dictionary contains `bundleId` and `pid` items
*/
+ (NSArray<NSDictionary<NSString *, id> *> *)fb_appsInfoWithAxElements:(NSArray<XCAccessibilityElement *> *)axElements;

/**
Retrieves the information about the currently active apps

@returns The list of dictionaries. Each dictionary contains `bundleId` and `pid` items.
*/
+ (NSArray<NSDictionary<NSString *, id> *> *)fb_activeAppsInfo;


@end

NS_ASSUME_NONNULL_END
35 changes: 34 additions & 1 deletion WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -39,7 +40,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);
});
Expand All @@ -50,6 +50,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);
}];
Expand All @@ -69,6 +71,37 @@ - (BOOL)fb_waitForAppElement:(NSTimeInterval)timeout
}];
}

+ (NSArray<NSDictionary<NSString *, id> *> *)fb_appsInfoWithAxElements:(NSArray<XCAccessibilityElement *> *)axElements
{
NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
id<XCTestManager_ManagerInterface> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
for (XCAccessibilityElement *axElement in axElements) {
NSMutableDictionary<NSString *, id> *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 ?: @"";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we set it to Unknown instead of empty?

Copy link
Author

@mykola-mokhnach mykola-mokhnach Aug 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any strong opinion on that. Will set to "unknown"

[result addObject:appInfo.copy];
}
return result.copy;
}

+ (NSArray<NSDictionary<NSString *, id> *> *)fb_activeAppsInfo
{
return [self fb_appsInfoWithAxElements:[FBXCAXClientProxy.sharedClient activeApplications]];
}

- (BOOL)fb_deactivateWithDuration:(NSTimeInterval)duration error:(NSError **)error
{
if(![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:error]) {
Expand Down
8 changes: 4 additions & 4 deletions WebDriverAgentLib/Commands/FBCustomCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ + (NSArray *)routes
+ (id<FBResponsePayload>)handleDismissKeyboardCommand:(FBRouteRequest *)request
{
#if TARGET_OS_TV
if ([self isKeyboardPresent]) {
if ([self isKeyboardPresentForApplication:request.session.activeApplication]) {
[[XCUIRemote sharedRemote] pressButton: XCUIRemoteButtonMenu];
}
#else
Expand All @@ -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) {
Expand All @@ -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;
}

Expand Down
12 changes: 12 additions & 0 deletions WebDriverAgentLib/Commands/FBSessionCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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

Expand All @@ -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 POST:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleActiveAppsList:)],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep POST, since we might want to add some additional filtering arguments to the call in the future. GET request does not allow this though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 make sense

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be still get and we should pass the further filtering with query params

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately selenium API does not really support passing of query parameters. Although, this endpoint is going to be put under mobile: call , so I could do that

[[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)],
[[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)],
[[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)],
Expand Down Expand Up @@ -162,6 +165,11 @@ + (NSArray *)routes
return FBResponseWithObject(@(state));
}

+ (id<FBResponsePayload>)handleActiveAppsList:(FBRouteRequest *)request
{
return FBResponseWithObject([XCUIApplication fb_activeAppsInfo]);
Copy link
Member

@KazuCocoa KazuCocoa Aug 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDA has an endpoint to return appinfo, bundleid, process id, name and environment variables. It has not been released yet. So, what do you think of modifying here like below and remove it?

https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBCustomCommands.m#L60

+ (id<FBResponsePayload>)handleActiveAppsList:(FBRouteRequest *)request
{
  NSMutableArray<NSDictionary<NSString *, id> *> *result = [[NSMutableArray alloc] init];
  NSArray *appsInfo = [XCUIApplication fb_activeAppsInfo];
  for (NSDictionary *appInfo in appsInfo) {
    FBApplication *app = [FBApplication fb_activeApplicationWithDefaultBundleId:appInfo[@"bundleId"]];
    NSDictionary *processArguments = @{};
    if (app != nil) {
      processArguments = @{
        @"args": app.launchArguments,
        @"env": app.launchEnvironment
      };
    }

    [result addObject:@{
      @"pid": appInfo[@"pid"] ?: @"",
      @"bundleId": appInfo[@"bundleId"] ?: @"",
      @"name": app.identifier ?: @"",
      @"processArguments": processArguments,
    }];
  }
  return FBResponseWithObject(result);
}

If ^ sounds good, I'll drop https://github.com/appium/appium/pull/13028/files for 1.15.0 release

Copy link
Author

@mykola-mokhnach mykola-mokhnach Aug 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to reduce the /list endpoint to a minimum and avoid extra calls to the accessibility if possible. Creation of FBApplication instance is already quite expensive as it touches the internal process monitoring machinery. Lets keep it at minimum for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 make sense

}

+ (id<FBResponsePayload>)handleGetActiveSession:(FBRouteRequest *)request
{
return FBResponseWithObject(FBSessionCommands.sessionInformation);
Expand Down Expand Up @@ -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,
}
);
}
Expand Down Expand Up @@ -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 = [settings objectForKey:DEFAULT_ACTIVE_APPLICATION];
}

return [self handleGetSettings:request];
}
Expand Down
8 changes: 8 additions & 0 deletions WebDriverAgentLib/FBApplication.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
37 changes: 30 additions & 7 deletions WebDriverAgentLib/FBApplication.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,44 @@ @interface FBApplication ()
@implementation FBApplication

+ (instancetype)fb_activeApplication
{
return [self fb_activeApplicationWithDefaultBundleId:nil];
}

+ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId
{
NSArray<XCAccessibilityElement *> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications];
XCAccessibilityElement *activeApplicationElement = [activeApplicationElements firstObject];
XCAccessibilityElement *activeApplicationElement = nil;
if (activeApplicationElements.count > 1) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be now bigger than 0 instead of 1?

Copy link
Author

@mykola-mokhnach mykola-mokhnach Aug 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is fine. If it equals to 1 then the first array element is set in else block. If the length of the array is zero then firstObject will return nil, which will lead to an exception later

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<NSDictionary *> *appsInfo = [self fb_appsInfoWithAxElements:activeApplicationElements];
NSUInteger index = 0;
for (NSDictionary *appInfo in appsInfo) {
index++;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused here. When we are iterating over the appsInfo and we find an appInfo where bundleIds are equal, we will then get index + 1 object in the activeApplicationElements. Is this intended?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, we need to increment the index after the condition

if ([appInfo[@"bundleId"] isEqualToString:(id)bundleId]) {
activeApplicationElement = [activeApplicationElements objectAtIndex:index];
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];
}
Expand Down
11 changes: 0 additions & 11 deletions WebDriverAgentLib/FBSpringboardApplication.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,7 @@

#import "FBSpringboardApplication.h"

#import "FBErrorBuilder.h"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 deletions

#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
Expand Down
2 changes: 2 additions & 0 deletions WebDriverAgentLib/Routing/FBSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
12 changes: 11 additions & 1 deletion WebDriverAgentLib/Routing/FBSession.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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];
Expand Down
14 changes: 14 additions & 0 deletions WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,18 @@ - (void)testActiveElement
((id<FBElement>)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