296 lines
9.9 KiB
Mathematica
296 lines
9.9 KiB
Mathematica
|
|
/**
|
||
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||
|
|
* All rights reserved.
|
||
|
|
*
|
||
|
|
* This source code is licensed under the BSD-style license found in the
|
||
|
|
* LICENSE file in the root directory of this source tree.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#import "FBSession.h"
|
||
|
|
#import "FBSession-Private.h"
|
||
|
|
|
||
|
|
#import <objc/runtime.h>
|
||
|
|
|
||
|
|
#import "FBXCAccessibilityElement.h"
|
||
|
|
#import "FBAlertsMonitor.h"
|
||
|
|
#import "FBConfiguration.h"
|
||
|
|
#import "FBElementCache.h"
|
||
|
|
#import "FBExceptions.h"
|
||
|
|
#import "FBMacros.h"
|
||
|
|
#import "FBScreenRecordingContainer.h"
|
||
|
|
#import "FBScreenRecordingPromise.h"
|
||
|
|
#import "FBScreenRecordingRequest.h"
|
||
|
|
#import "FBXCodeCompatibility.h"
|
||
|
|
#import "FBXCTestDaemonsProxy.h"
|
||
|
|
#import "XCUIApplication+FBQuiescence.h"
|
||
|
|
#import "XCUIElement.h"
|
||
|
|
#import "XCUIElement+FBClassChain.h"
|
||
|
|
|
||
|
|
/*!
|
||
|
|
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";
|
||
|
|
|
||
|
|
NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
|
||
|
|
|
||
|
|
@interface FBSession ()
|
||
|
|
@property (nullable, nonatomic) XCUIApplication *testedApplication;
|
||
|
|
@property (nonatomic) BOOL isTestedApplicationExpectedToRun;
|
||
|
|
@property (nonatomic) BOOL shouldAppsWaitForQuiescence;
|
||
|
|
@property (nonatomic, nullable) FBAlertsMonitor *alertsMonitor;
|
||
|
|
@property (nonatomic, readwrite) NSMutableDictionary<NSNumber *, NSMutableDictionary<NSString *, NSNumber *> *> *elementsVisibilityCache;
|
||
|
|
@end
|
||
|
|
|
||
|
|
@interface FBSession (FBAlertsMonitorDelegate)
|
||
|
|
|
||
|
|
- (void)didDetectAlert:(FBAlert *)alert;
|
||
|
|
|
||
|
|
@end
|
||
|
|
|
||
|
|
@implementation FBSession (FBAlertsMonitorDelegate)
|
||
|
|
|
||
|
|
- (void)didDetectAlert:(FBAlert *)alert
|
||
|
|
{
|
||
|
|
NSString *autoClickAlertSelector = FBConfiguration.autoClickAlertSelector;
|
||
|
|
if ([autoClickAlertSelector length] > 0) {
|
||
|
|
@try {
|
||
|
|
NSArray<XCUIElement*> *matches = [alert.alertElement fb_descendantsMatchingClassChain:autoClickAlertSelector
|
||
|
|
shouldReturnAfterFirstMatch:YES];
|
||
|
|
if (matches.count > 0) {
|
||
|
|
[[matches objectAtIndex:0] tap];
|
||
|
|
}
|
||
|
|
} @catch (NSException *e) {
|
||
|
|
[FBLogger logFmt:@"Could not click at the alert element '%@'. Original error: %@",
|
||
|
|
autoClickAlertSelector, e.description];
|
||
|
|
}
|
||
|
|
// This setting has priority over other settings if enabled
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (nil == self.defaultAlertAction || 0 == self.defaultAlertAction.length) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
NSError *error;
|
||
|
|
if ([self.defaultAlertAction isEqualToString:@"accept"]) {
|
||
|
|
if (![alert acceptWithError:&error]) {
|
||
|
|
[FBLogger logFmt:@"Cannot accept the alert. Original error: %@", error.description];
|
||
|
|
}
|
||
|
|
} else if ([self.defaultAlertAction isEqualToString:@"dismiss"]) {
|
||
|
|
if (![alert dismissWithError:&error]) {
|
||
|
|
[FBLogger logFmt:@"Cannot dismiss the alert. Original error: %@", error.description];
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
[FBLogger logFmt:@"'%@' default alert action is unsupported", self.defaultAlertAction];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@end
|
||
|
|
|
||
|
|
@implementation FBSession
|
||
|
|
|
||
|
|
static FBSession *_activeSession = nil;
|
||
|
|
|
||
|
|
+ (instancetype)activeSession
|
||
|
|
{
|
||
|
|
return _activeSession;
|
||
|
|
}
|
||
|
|
|
||
|
|
+ (void)markSessionActive:(FBSession *)session
|
||
|
|
{
|
||
|
|
if (_activeSession) {
|
||
|
|
[_activeSession kill];
|
||
|
|
}
|
||
|
|
_activeSession = session;
|
||
|
|
}
|
||
|
|
|
||
|
|
+ (instancetype)sessionWithIdentifier:(NSString *)identifier
|
||
|
|
{
|
||
|
|
if (!identifier) {
|
||
|
|
return nil;
|
||
|
|
}
|
||
|
|
if (![identifier isEqualToString:_activeSession.identifier]) {
|
||
|
|
return nil;
|
||
|
|
}
|
||
|
|
return _activeSession;
|
||
|
|
}
|
||
|
|
|
||
|
|
+ (instancetype)initWithApplication:(XCUIApplication *)application
|
||
|
|
{
|
||
|
|
FBSession *session = [FBSession new];
|
||
|
|
session.useNativeCachingStrategy = YES;
|
||
|
|
session.alertsMonitor = nil;
|
||
|
|
session.defaultAlertAction = nil;
|
||
|
|
session.elementsVisibilityCache = [NSMutableDictionary dictionary];
|
||
|
|
session.identifier = [[NSUUID UUID] UUIDString];
|
||
|
|
session.defaultActiveApplication = FBDefaultApplicationAuto;
|
||
|
|
session.testedApplication = nil;
|
||
|
|
session.isTestedApplicationExpectedToRun = nil != application && application.running;
|
||
|
|
if (application) {
|
||
|
|
session.testedApplication = application;
|
||
|
|
session.shouldAppsWaitForQuiescence = application.fb_shouldWaitForQuiescence;
|
||
|
|
}
|
||
|
|
session.elementCache = [FBElementCache new];
|
||
|
|
[FBSession markSessionActive:session];
|
||
|
|
return session;
|
||
|
|
}
|
||
|
|
|
||
|
|
+ (instancetype)initWithApplication:(nullable XCUIApplication *)application
|
||
|
|
defaultAlertAction:(NSString *)defaultAlertAction
|
||
|
|
{
|
||
|
|
FBSession *session = [self.class initWithApplication:application];
|
||
|
|
session.defaultAlertAction = [defaultAlertAction lowercaseString];
|
||
|
|
[session enableAlertsMonitor];
|
||
|
|
return session;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (BOOL)enableAlertsMonitor
|
||
|
|
{
|
||
|
|
if (nil != self.alertsMonitor) {
|
||
|
|
return NO;
|
||
|
|
}
|
||
|
|
|
||
|
|
self.alertsMonitor = [[FBAlertsMonitor alloc] init];
|
||
|
|
self.alertsMonitor.delegate = (id<FBAlertsMonitorDelegate>)self;
|
||
|
|
[self.alertsMonitor enable];
|
||
|
|
return YES;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (BOOL)disableAlertsMonitor
|
||
|
|
{
|
||
|
|
if (nil == self.alertsMonitor) {
|
||
|
|
return NO;
|
||
|
|
}
|
||
|
|
|
||
|
|
[self.alertsMonitor disable];
|
||
|
|
self.alertsMonitor = nil;
|
||
|
|
return YES;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (void)kill
|
||
|
|
{
|
||
|
|
if (nil == _activeSession) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
[self disableAlertsMonitor];
|
||
|
|
|
||
|
|
FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise;
|
||
|
|
if (nil != activeScreenRecording) {
|
||
|
|
NSError *error;
|
||
|
|
if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:activeScreenRecording.identifier error:&error]) {
|
||
|
|
[FBLogger logFmt:@"%@", error];
|
||
|
|
}
|
||
|
|
[FBScreenRecordingContainer.sharedInstance reset];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (nil != self.testedApplication
|
||
|
|
&& FBConfiguration.shouldTerminateApp
|
||
|
|
&& self.testedApplication.running
|
||
|
|
&& ![self.testedApplication fb_isSameAppAs:XCUIApplication.fb_systemApplication]) {
|
||
|
|
@try {
|
||
|
|
[self.testedApplication terminate];
|
||
|
|
} @catch (NSException *e) {
|
||
|
|
[FBLogger logFmt:@"%@", e.description];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_activeSession = nil;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (XCUIApplication *)activeApplication
|
||
|
|
{
|
||
|
|
BOOL isAuto = [self.defaultActiveApplication isEqualToString:FBDefaultApplicationAuto];
|
||
|
|
NSString *defaultBundleId = isAuto ? nil : self.defaultActiveApplication;
|
||
|
|
|
||
|
|
if (nil != defaultBundleId && [self applicationStateWithBundleId:defaultBundleId] >= XCUIApplicationStateRunningForeground) {
|
||
|
|
return [self makeApplicationWithBundleId:defaultBundleId];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (nil != self.testedApplication) {
|
||
|
|
XCUIApplicationState testedAppState = self.testedApplication.state;
|
||
|
|
if (testedAppState >= XCUIApplicationStateRunningForeground) {
|
||
|
|
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"%K == %@ OR %K IN {%@, %@}",
|
||
|
|
@"elementType", @(XCUIElementTypeAlert),
|
||
|
|
// To look for `SBTransientOverlayWindow` elements. See https://github.com/appium/WebDriverAgent/pull/946
|
||
|
|
@"identifier", @"SBTransientOverlayWindow",
|
||
|
|
// To look for 'criticalAlertSetting' elements https://developer.apple.com/documentation/usernotifications/unnotificationsettings/criticalalertsetting
|
||
|
|
// See https://github.com/appium/appium/issues/20835
|
||
|
|
@"NotificationShortLookView"];
|
||
|
|
if ([FBConfiguration shouldRespectSystemAlerts]
|
||
|
|
&& [[XCUIApplication.fb_systemApplication descendantsMatchingType:XCUIElementTypeAny]
|
||
|
|
matchingPredicate:searchPredicate].count > 0) {
|
||
|
|
return XCUIApplication.fb_systemApplication;
|
||
|
|
}
|
||
|
|
return (XCUIApplication *)self.testedApplication;
|
||
|
|
}
|
||
|
|
if (self.isTestedApplicationExpectedToRun && testedAppState <= XCUIApplicationStateNotRunning) {
|
||
|
|
NSString *description = [NSString stringWithFormat:@"The application under test with bundle id '%@' is not running, possibly crashed", self.testedApplication.bundleID];
|
||
|
|
@throw [NSException exceptionWithName:FBApplicationCrashedException reason:description userInfo:nil];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return [XCUIApplication fb_activeApplicationWithDefaultBundleId:defaultBundleId];
|
||
|
|
}
|
||
|
|
|
||
|
|
- (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier
|
||
|
|
shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence
|
||
|
|
arguments:(nullable NSArray<NSString *> *)arguments
|
||
|
|
environment:(nullable NSDictionary <NSString *, NSString *> *)environment
|
||
|
|
{
|
||
|
|
XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier];
|
||
|
|
if (nil == shouldWaitForQuiescence) {
|
||
|
|
// Iherit the quiescence check setting from the main app under test by default
|
||
|
|
app.fb_shouldWaitForQuiescence = nil != self.testedApplication && self.shouldAppsWaitForQuiescence;
|
||
|
|
} else {
|
||
|
|
app.fb_shouldWaitForQuiescence = [shouldWaitForQuiescence boolValue];
|
||
|
|
}
|
||
|
|
if (!app.running) {
|
||
|
|
app.launchArguments = arguments ?: @[];
|
||
|
|
app.launchEnvironment = environment ?: @{};
|
||
|
|
[app launch];
|
||
|
|
} else {
|
||
|
|
[app activate];
|
||
|
|
}
|
||
|
|
if ([app fb_isSameAppAs:self.testedApplication]) {
|
||
|
|
self.isTestedApplicationExpectedToRun = YES;
|
||
|
|
}
|
||
|
|
return app;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (XCUIApplication *)activateApplicationWithBundleId:(NSString *)bundleIdentifier
|
||
|
|
{
|
||
|
|
XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier];
|
||
|
|
[app activate];
|
||
|
|
return app;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (BOOL)terminateApplicationWithBundleId:(NSString *)bundleIdentifier
|
||
|
|
{
|
||
|
|
XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier];
|
||
|
|
if ([app fb_isSameAppAs:self.testedApplication]) {
|
||
|
|
self.isTestedApplicationExpectedToRun = NO;
|
||
|
|
}
|
||
|
|
if (app.running) {
|
||
|
|
[app terminate];
|
||
|
|
return YES;
|
||
|
|
}
|
||
|
|
return NO;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (NSUInteger)applicationStateWithBundleId:(NSString *)bundleIdentifier
|
||
|
|
{
|
||
|
|
return [self makeApplicationWithBundleId:bundleIdentifier].state;
|
||
|
|
}
|
||
|
|
|
||
|
|
- (XCUIApplication *)makeApplicationWithBundleId:(NSString *)bundleIdentifier
|
||
|
|
{
|
||
|
|
return nil != self.testedApplication && [bundleIdentifier isEqualToString:(NSString *)self.testedApplication.bundleID]
|
||
|
|
? self.testedApplication
|
||
|
|
: [[XCUIApplication alloc] initWithBundleIdentifier:bundleIdentifier];
|
||
|
|
}
|
||
|
|
|
||
|
|
@end
|