Files
custom_wda/WebDriverAgentLib/Routing/FBSession.m
2026-02-03 16:52:44 +08:00

296 lines
9.9 KiB
Objective-C

/**
* 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