FBSession.m 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. /**
  2. * Copyright (c) 2015-present, Facebook, Inc.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. */
  9. #import "FBSession.h"
  10. #import "FBSession-Private.h"
  11. #import <objc/runtime.h>
  12. #import "FBXCAccessibilityElement.h"
  13. #import "FBAlertsMonitor.h"
  14. #import "FBConfiguration.h"
  15. #import "FBElementCache.h"
  16. #import "FBExceptions.h"
  17. #import "FBMacros.h"
  18. #import "FBScreenRecordingContainer.h"
  19. #import "FBScreenRecordingPromise.h"
  20. #import "FBScreenRecordingRequest.h"
  21. #import "FBXCodeCompatibility.h"
  22. #import "FBXCTestDaemonsProxy.h"
  23. #import "XCUIApplication+FBQuiescence.h"
  24. #import "XCUIElement.h"
  25. #import "XCUIElement+FBClassChain.h"
  26. /*!
  27. The intial value for the default application property.
  28. Setting this value to `defaultActiveApplication` property forces WDA to use the internal
  29. automated algorithm to determine the active on-screen application
  30. */
  31. NSString *const FBDefaultApplicationAuto = @"auto";
  32. NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari";
  33. @interface FBSession ()
  34. @property (nullable, nonatomic) XCUIApplication *testedApplication;
  35. @property (nonatomic) BOOL isTestedApplicationExpectedToRun;
  36. @property (nonatomic) BOOL shouldAppsWaitForQuiescence;
  37. @property (nonatomic, nullable) FBAlertsMonitor *alertsMonitor;
  38. @property (nonatomic, readwrite) NSMutableDictionary<NSNumber *, NSMutableDictionary<NSString *, NSNumber *> *> *elementsVisibilityCache;
  39. @end
  40. @interface FBSession (FBAlertsMonitorDelegate)
  41. - (void)didDetectAlert:(FBAlert *)alert;
  42. @end
  43. @implementation FBSession (FBAlertsMonitorDelegate)
  44. - (void)didDetectAlert:(FBAlert *)alert
  45. {
  46. NSString *autoClickAlertSelector = FBConfiguration.autoClickAlertSelector;
  47. if ([autoClickAlertSelector length] > 0) {
  48. @try {
  49. NSArray<XCUIElement*> *matches = [alert.alertElement fb_descendantsMatchingClassChain:autoClickAlertSelector
  50. shouldReturnAfterFirstMatch:YES];
  51. if (matches.count > 0) {
  52. [[matches objectAtIndex:0] tap];
  53. }
  54. } @catch (NSException *e) {
  55. [FBLogger logFmt:@"Could not click at the alert element '%@'. Original error: %@",
  56. autoClickAlertSelector, e.description];
  57. }
  58. // This setting has priority over other settings if enabled
  59. return;
  60. }
  61. if (nil == self.defaultAlertAction || 0 == self.defaultAlertAction.length) {
  62. return;
  63. }
  64. NSError *error;
  65. if ([self.defaultAlertAction isEqualToString:@"accept"]) {
  66. if (![alert acceptWithError:&error]) {
  67. [FBLogger logFmt:@"Cannot accept the alert. Original error: %@", error.description];
  68. }
  69. } else if ([self.defaultAlertAction isEqualToString:@"dismiss"]) {
  70. if (![alert dismissWithError:&error]) {
  71. [FBLogger logFmt:@"Cannot dismiss the alert. Original error: %@", error.description];
  72. }
  73. } else {
  74. [FBLogger logFmt:@"'%@' default alert action is unsupported", self.defaultAlertAction];
  75. }
  76. }
  77. @end
  78. @implementation FBSession
  79. static FBSession *_activeSession = nil;
  80. + (instancetype)activeSession
  81. {
  82. return _activeSession;
  83. }
  84. + (void)markSessionActive:(FBSession *)session
  85. {
  86. if (_activeSession) {
  87. [_activeSession kill];
  88. }
  89. _activeSession = session;
  90. }
  91. + (instancetype)sessionWithIdentifier:(NSString *)identifier
  92. {
  93. if (!identifier) {
  94. return nil;
  95. }
  96. if (![identifier isEqualToString:_activeSession.identifier]) {
  97. return nil;
  98. }
  99. return _activeSession;
  100. }
  101. + (instancetype)initWithApplication:(XCUIApplication *)application
  102. {
  103. FBSession *session = [FBSession new];
  104. session.useNativeCachingStrategy = YES;
  105. session.alertsMonitor = nil;
  106. session.defaultAlertAction = nil;
  107. session.elementsVisibilityCache = [NSMutableDictionary dictionary];
  108. session.identifier = [[NSUUID UUID] UUIDString];
  109. session.defaultActiveApplication = FBDefaultApplicationAuto;
  110. session.testedApplication = nil;
  111. session.isTestedApplicationExpectedToRun = nil != application && application.running;
  112. if (application) {
  113. session.testedApplication = application;
  114. session.shouldAppsWaitForQuiescence = application.fb_shouldWaitForQuiescence;
  115. }
  116. session.elementCache = [FBElementCache new];
  117. [FBSession markSessionActive:session];
  118. return session;
  119. }
  120. + (instancetype)initWithApplication:(nullable XCUIApplication *)application
  121. defaultAlertAction:(NSString *)defaultAlertAction
  122. {
  123. FBSession *session = [self.class initWithApplication:application];
  124. session.defaultAlertAction = [defaultAlertAction lowercaseString];
  125. [session enableAlertsMonitor];
  126. return session;
  127. }
  128. - (BOOL)enableAlertsMonitor
  129. {
  130. if (nil != self.alertsMonitor) {
  131. return NO;
  132. }
  133. self.alertsMonitor = [[FBAlertsMonitor alloc] init];
  134. self.alertsMonitor.delegate = (id<FBAlertsMonitorDelegate>)self;
  135. [self.alertsMonitor enable];
  136. return YES;
  137. }
  138. - (BOOL)disableAlertsMonitor
  139. {
  140. if (nil == self.alertsMonitor) {
  141. return NO;
  142. }
  143. [self.alertsMonitor disable];
  144. self.alertsMonitor = nil;
  145. return YES;
  146. }
  147. - (void)kill
  148. {
  149. if (nil == _activeSession) {
  150. return;
  151. }
  152. [self disableAlertsMonitor];
  153. FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise;
  154. if (nil != activeScreenRecording) {
  155. NSError *error;
  156. if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:activeScreenRecording.identifier error:&error]) {
  157. [FBLogger logFmt:@"%@", error];
  158. }
  159. [FBScreenRecordingContainer.sharedInstance reset];
  160. }
  161. if (nil != self.testedApplication
  162. && FBConfiguration.shouldTerminateApp
  163. && self.testedApplication.running
  164. && ![self.testedApplication fb_isSameAppAs:XCUIApplication.fb_systemApplication]) {
  165. @try {
  166. [self.testedApplication terminate];
  167. } @catch (NSException *e) {
  168. [FBLogger logFmt:@"%@", e.description];
  169. }
  170. }
  171. _activeSession = nil;
  172. }
  173. - (XCUIApplication *)activeApplication
  174. {
  175. BOOL isAuto = [self.defaultActiveApplication isEqualToString:FBDefaultApplicationAuto];
  176. NSString *defaultBundleId = isAuto ? nil : self.defaultActiveApplication;
  177. if (nil != defaultBundleId && [self applicationStateWithBundleId:defaultBundleId] >= XCUIApplicationStateRunningForeground) {
  178. return [self makeApplicationWithBundleId:defaultBundleId];
  179. }
  180. if (nil != self.testedApplication) {
  181. XCUIApplicationState testedAppState = self.testedApplication.state;
  182. if (testedAppState >= XCUIApplicationStateRunningForeground) {
  183. NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"%K == %@ OR %K IN {%@, %@}",
  184. @"elementType", @(XCUIElementTypeAlert),
  185. // To look for `SBTransientOverlayWindow` elements. See https://github.com/appium/WebDriverAgent/pull/946
  186. @"identifier", @"SBTransientOverlayWindow",
  187. // To look for 'criticalAlertSetting' elements https://developer.apple.com/documentation/usernotifications/unnotificationsettings/criticalalertsetting
  188. // See https://github.com/appium/appium/issues/20835
  189. @"NotificationShortLookView"];
  190. if ([FBConfiguration shouldRespectSystemAlerts]
  191. && [[XCUIApplication.fb_systemApplication descendantsMatchingType:XCUIElementTypeAny]
  192. matchingPredicate:searchPredicate].count > 0) {
  193. return XCUIApplication.fb_systemApplication;
  194. }
  195. return (XCUIApplication *)self.testedApplication;
  196. }
  197. if (self.isTestedApplicationExpectedToRun && testedAppState <= XCUIApplicationStateNotRunning) {
  198. NSString *description = [NSString stringWithFormat:@"The application under test with bundle id '%@' is not running, possibly crashed", self.testedApplication.bundleID];
  199. @throw [NSException exceptionWithName:FBApplicationCrashedException reason:description userInfo:nil];
  200. }
  201. }
  202. return [XCUIApplication fb_activeApplicationWithDefaultBundleId:defaultBundleId];
  203. }
  204. - (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier
  205. shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence
  206. arguments:(nullable NSArray<NSString *> *)arguments
  207. environment:(nullable NSDictionary <NSString *, NSString *> *)environment
  208. {
  209. XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier];
  210. if (nil == shouldWaitForQuiescence) {
  211. // Iherit the quiescence check setting from the main app under test by default
  212. app.fb_shouldWaitForQuiescence = nil != self.testedApplication && self.shouldAppsWaitForQuiescence;
  213. } else {
  214. app.fb_shouldWaitForQuiescence = [shouldWaitForQuiescence boolValue];
  215. }
  216. if (!app.running) {
  217. app.launchArguments = arguments ?: @[];
  218. app.launchEnvironment = environment ?: @{};
  219. [app launch];
  220. } else {
  221. [app activate];
  222. }
  223. if ([app fb_isSameAppAs:self.testedApplication]) {
  224. self.isTestedApplicationExpectedToRun = YES;
  225. }
  226. return app;
  227. }
  228. - (XCUIApplication *)activateApplicationWithBundleId:(NSString *)bundleIdentifier
  229. {
  230. XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier];
  231. [app activate];
  232. return app;
  233. }
  234. - (BOOL)terminateApplicationWithBundleId:(NSString *)bundleIdentifier
  235. {
  236. XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier];
  237. if ([app fb_isSameAppAs:self.testedApplication]) {
  238. self.isTestedApplicationExpectedToRun = NO;
  239. }
  240. if (app.running) {
  241. [app terminate];
  242. return YES;
  243. }
  244. return NO;
  245. }
  246. - (NSUInteger)applicationStateWithBundleId:(NSString *)bundleIdentifier
  247. {
  248. return [self makeApplicationWithBundleId:bundleIdentifier].state;
  249. }
  250. - (XCUIApplication *)makeApplicationWithBundleId:(NSString *)bundleIdentifier
  251. {
  252. return nil != self.testedApplication && [bundleIdentifier isEqualToString:(NSString *)self.testedApplication.bundleID]
  253. ? self.testedApplication
  254. : [[XCUIApplication alloc] initWithBundleIdentifier:bundleIdentifier];
  255. }
  256. @end