XCUIApplication+FBHelpers.m 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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 "XCUIApplication+FBHelpers.h"
  10. #import "FBActiveAppDetectionPoint.h"
  11. #import "FBElementTypeTransformer.h"
  12. #import "FBKeyboard.h"
  13. #import "FBLogger.h"
  14. #import "FBExceptions.h"
  15. #import "FBMacros.h"
  16. #import "FBMathUtils.h"
  17. #import "FBRunLoopSpinner.h"
  18. #import "FBXCodeCompatibility.h"
  19. #import "FBXPath.h"
  20. #import "FBXCAccessibilityElement.h"
  21. #import "FBXCTestDaemonsProxy.h"
  22. #import "FBXCElementSnapshotWrapper+Helpers.h"
  23. #import "FBXCAXClientProxy.h"
  24. #import "FBXMLGenerationOptions.h"
  25. #import "XCTestManager_ManagerInterface-Protocol.h"
  26. #import "XCTestPrivateSymbols.h"
  27. #import "XCTRunnerDaemonSession.h"
  28. #import "XCUIApplication.h"
  29. #import "XCUIApplicationImpl.h"
  30. #import "XCUIApplicationProcess.h"
  31. #import "XCUIDevice+FBHelpers.h"
  32. #import "XCUIElement.h"
  33. #import "XCUIElement+FBCaching.h"
  34. #import "XCUIElement+FBIsVisible.h"
  35. #import "XCUIElement+FBUtilities.h"
  36. #import "XCUIElement+FBWebDriverAttributes.h"
  37. #import "XCUIElementQuery.h"
  38. static NSString* const FBUnknownBundleId = @"unknown";
  39. _Nullable id extractIssueProperty(id issue, NSString *propertyName) {
  40. SEL selector = NSSelectorFromString(propertyName);
  41. NSMethodSignature *methodSignature = [issue methodSignatureForSelector:selector];
  42. if (nil == methodSignature) {
  43. return nil;
  44. }
  45. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
  46. [invocation setSelector:selector];
  47. [invocation invokeWithTarget:issue];
  48. id __unsafe_unretained result;
  49. [invocation getReturnValue:&result];
  50. return result;
  51. }
  52. NSDictionary<NSString *, NSNumber *> *auditTypeNamesToValues(void) {
  53. static dispatch_once_t onceToken;
  54. static NSDictionary *result;
  55. dispatch_once(&onceToken, ^{
  56. // https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc
  57. result = @{
  58. @"XCUIAccessibilityAuditTypeAction": @(1UL << 32),
  59. @"XCUIAccessibilityAuditTypeAll": @(~0UL),
  60. @"XCUIAccessibilityAuditTypeContrast": @(1UL << 0),
  61. @"XCUIAccessibilityAuditTypeDynamicType": @(1UL << 16),
  62. @"XCUIAccessibilityAuditTypeElementDetection": @(1UL << 1),
  63. @"XCUIAccessibilityAuditTypeHitRegion": @(1UL << 2),
  64. @"XCUIAccessibilityAuditTypeParentChild": @(1UL << 33),
  65. @"XCUIAccessibilityAuditTypeSufficientElementDescription": @(1UL << 3),
  66. @"XCUIAccessibilityAuditTypeTextClipped": @(1UL << 17),
  67. @"XCUIAccessibilityAuditTypeTrait": @(1UL << 18),
  68. };
  69. });
  70. return result;
  71. }
  72. NSDictionary<NSNumber *, NSString *> *auditTypeValuesToNames(void) {
  73. static dispatch_once_t onceToken;
  74. static NSDictionary *result;
  75. dispatch_once(&onceToken, ^{
  76. NSMutableDictionary *inverted = [NSMutableDictionary new];
  77. [auditTypeNamesToValues() enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSNumber *value, BOOL *stop) {
  78. inverted[value] = key;
  79. }];
  80. result = inverted.copy;
  81. });
  82. return result;
  83. }
  84. @implementation XCUIApplication (FBHelpers)
  85. - (BOOL)fb_waitForAppElement:(NSTimeInterval)timeout
  86. {
  87. __block BOOL canDetectAxElement = YES;
  88. int currentProcessIdentifier = [self.accessibilityElement processIdentifier];
  89. BOOL result = [[[FBRunLoopSpinner new]
  90. timeout:timeout]
  91. spinUntilTrue:^BOOL{
  92. id<FBXCAccessibilityElement> currentAppElement = FBActiveAppDetectionPoint.sharedInstance.axElement;
  93. canDetectAxElement = nil != currentAppElement;
  94. if (!canDetectAxElement) {
  95. return YES;
  96. }
  97. return currentAppElement.processIdentifier == currentProcessIdentifier;
  98. }];
  99. return canDetectAxElement
  100. ? result
  101. : [self waitForExistenceWithTimeout:timeout];
  102. }
  103. + (NSArray<NSDictionary<NSString *, id> *> *)fb_appsInfoWithAxElements:(NSArray<id<FBXCAccessibilityElement>> *)axElements
  104. {
  105. NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
  106. id<XCTestManager_ManagerInterface> proxy = [FBXCTestDaemonsProxy testRunnerProxy];
  107. for (id<FBXCAccessibilityElement> axElement in axElements) {
  108. NSMutableDictionary<NSString *, id> *appInfo = [NSMutableDictionary dictionary];
  109. pid_t pid = axElement.processIdentifier;
  110. appInfo[@"pid"] = @(pid);
  111. __block NSString *bundleId = nil;
  112. dispatch_semaphore_t sem = dispatch_semaphore_create(0);
  113. [proxy _XCT_requestBundleIDForPID:pid
  114. reply:^(NSString *bundleID, NSError *error) {
  115. if (nil == error) {
  116. bundleId = bundleID;
  117. } else {
  118. [FBLogger logFmt:@"Cannot request the bundle ID for process ID %@: %@", @(pid), error.description];
  119. }
  120. dispatch_semaphore_signal(sem);
  121. }];
  122. dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)));
  123. appInfo[@"bundleId"] = bundleId ?: FBUnknownBundleId;
  124. [result addObject:appInfo.copy];
  125. }
  126. return result.copy;
  127. }
  128. + (NSArray<NSDictionary<NSString *, id> *> *)fb_activeAppsInfo
  129. {
  130. return [self fb_appsInfoWithAxElements:[FBXCAXClientProxy.sharedClient activeApplications]];
  131. }
  132. - (BOOL)fb_deactivateWithDuration:(NSTimeInterval)duration error:(NSError **)error
  133. {
  134. if(![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:error]) {
  135. return NO;
  136. }
  137. [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:MAX(duration, .0)]];
  138. [self activate];
  139. return YES;
  140. }
  141. - (NSDictionary *)fb_tree
  142. {
  143. id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
  144. ? self.lastSnapshot
  145. : [self fb_snapshotWithAllAttributesAndMaxDepth:nil];
  146. return [self.class dictionaryForElement:snapshot recursive:YES];
  147. }
  148. - (NSDictionary *)fb_accessibilityTree
  149. {
  150. id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
  151. ? self.lastSnapshot
  152. : [self fb_snapshotWithAllAttributesAndMaxDepth:nil];
  153. return [self.class accessibilityInfoForElement:snapshot];
  154. }
  155. + (NSDictionary *)dictionaryForElement:(id<FBXCElementSnapshot>)snapshot recursive:(BOOL)recursive
  156. {
  157. NSMutableDictionary *info = [[NSMutableDictionary alloc] init];
  158. info[@"type"] = [FBElementTypeTransformer shortStringWithElementType:snapshot.elementType];
  159. info[@"rawIdentifier"] = FBValueOrNull([snapshot.identifier isEqual:@""] ? nil : snapshot.identifier);
  160. FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
  161. info[@"name"] = FBValueOrNull(wrappedSnapshot.wdName);
  162. info[@"value"] = FBValueOrNull(wrappedSnapshot.wdValue);
  163. info[@"label"] = FBValueOrNull(wrappedSnapshot.wdLabel);
  164. info[@"rect"] = wrappedSnapshot.wdRect;
  165. info[@"frame"] = NSStringFromCGRect(wrappedSnapshot.wdFrame);
  166. info[@"isEnabled"] = [@([wrappedSnapshot isWDEnabled]) stringValue];
  167. info[@"isVisible"] = [@([wrappedSnapshot isWDVisible]) stringValue];
  168. info[@"isAccessible"] = [@([wrappedSnapshot isWDAccessible]) stringValue];
  169. info[@"isFocused"] = [@([wrappedSnapshot isWDFocused]) stringValue];
  170. if (!recursive) {
  171. return info.copy;
  172. }
  173. NSArray *childElements = snapshot.children;
  174. if ([childElements count]) {
  175. info[@"children"] = [[NSMutableArray alloc] init];
  176. for (id<FBXCElementSnapshot> childSnapshot in childElements) {
  177. [info[@"children"] addObject:[self dictionaryForElement:childSnapshot recursive:YES]];
  178. }
  179. }
  180. return info;
  181. }
  182. + (NSDictionary *)accessibilityInfoForElement:(id<FBXCElementSnapshot>)snapshot
  183. {
  184. FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
  185. BOOL isAccessible = [wrappedSnapshot isWDAccessible];
  186. BOOL isVisible = [wrappedSnapshot isWDVisible];
  187. NSMutableDictionary *info = [[NSMutableDictionary alloc] init];
  188. if (isAccessible) {
  189. if (isVisible) {
  190. info[@"value"] = FBValueOrNull(wrappedSnapshot.wdValue);
  191. info[@"label"] = FBValueOrNull(wrappedSnapshot.wdLabel);
  192. }
  193. } else {
  194. NSMutableArray *children = [[NSMutableArray alloc] init];
  195. for (id<FBXCElementSnapshot> childSnapshot in snapshot.children) {
  196. NSDictionary *childInfo = [self accessibilityInfoForElement:childSnapshot];
  197. if ([childInfo count]) {
  198. [children addObject: childInfo];
  199. }
  200. }
  201. if ([children count]) {
  202. info[@"children"] = [children copy];
  203. }
  204. }
  205. if ([info count]) {
  206. info[@"type"] = [FBElementTypeTransformer shortStringWithElementType:snapshot.elementType];
  207. info[@"rawIdentifier"] = FBValueOrNull([snapshot.identifier isEqual:@""] ? nil : snapshot.identifier);
  208. info[@"name"] = FBValueOrNull(wrappedSnapshot.wdName);
  209. } else {
  210. return nil;
  211. }
  212. return info;
  213. }
  214. - (NSString *)fb_xmlRepresentation
  215. {
  216. return [self fb_xmlRepresentationWithOptions:nil];
  217. }
  218. - (NSString *)fb_xmlRepresentationWithOptions:(FBXMLGenerationOptions *)options
  219. {
  220. return [FBXPath xmlStringWithRootElement:self options:options];
  221. }
  222. - (NSString *)fb_descriptionRepresentation
  223. {
  224. NSMutableArray<NSString *> *childrenDescriptions = [NSMutableArray array];
  225. for (XCUIElement *child in [self.fb_query childrenMatchingType:XCUIElementTypeAny].allElementsBoundByIndex) {
  226. [childrenDescriptions addObject:child.debugDescription];
  227. }
  228. // debugDescription property of XCUIApplication instance shows descendants addresses in memory
  229. // instead of the actual information about them, however the representation works properly
  230. // for all descendant elements
  231. return (0 == childrenDescriptions.count) ? self.debugDescription : [childrenDescriptions componentsJoinedByString:@"\n\n"];
  232. }
  233. - (XCUIElement *)fb_activeElement
  234. {
  235. return [[[self.fb_query descendantsMatchingType:XCUIElementTypeAny]
  236. matchingPredicate:[NSPredicate predicateWithFormat:@"hasKeyboardFocus == YES"]]
  237. fb_firstMatch];
  238. }
  239. #if TARGET_OS_TV
  240. - (XCUIElement *)fb_focusedElement
  241. {
  242. return [[[self.fb_query descendantsMatchingType:XCUIElementTypeAny]
  243. matchingPredicate:[NSPredicate predicateWithFormat:@"hasFocus == true"]]
  244. fb_firstMatch];
  245. }
  246. #endif
  247. - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray<NSString *> *)keyNames
  248. error:(NSError **)error
  249. {
  250. BOOL (^isKeyboardInvisible)(void) = ^BOOL(void) {
  251. return ![FBKeyboard waitUntilVisibleForApplication:self
  252. timeout:0
  253. error:nil];
  254. };
  255. if (isKeyboardInvisible()) {
  256. // Short circuit if the keyboard is not visible
  257. return YES;
  258. }
  259. #if TARGET_OS_TV
  260. [[XCUIRemote sharedRemote] pressButton:XCUIRemoteButtonMenu];
  261. #else
  262. NSArray<XCUIElement *> *(^findMatchingKeys)(NSPredicate *) = ^NSArray<XCUIElement *> *(NSPredicate * predicate) {
  263. NSPredicate *keysPredicate = [NSPredicate predicateWithFormat:@"elementType == %@", @(XCUIElementTypeKey)];
  264. XCUIElementQuery *parentView = [[self.keyboard descendantsMatchingType:XCUIElementTypeOther]
  265. containingPredicate:keysPredicate];
  266. return [[parentView childrenMatchingType:XCUIElementTypeAny]
  267. matchingPredicate:predicate].allElementsBoundByIndex;
  268. };
  269. if (nil != keyNames && keyNames.count > 0) {
  270. NSPredicate *searchPredicate = [NSPredicate predicateWithBlock:^BOOL(id<FBXCElementSnapshot> snapshot, NSDictionary *bindings) {
  271. if (snapshot.elementType != XCUIElementTypeKey && snapshot.elementType != XCUIElementTypeButton) {
  272. return NO;
  273. }
  274. return (nil != snapshot.identifier && [keyNames containsObject:snapshot.identifier])
  275. || (nil != snapshot.label && [keyNames containsObject:snapshot.label]);
  276. }];
  277. NSArray *matchedKeys = findMatchingKeys(searchPredicate);
  278. if (matchedKeys.count > 0) {
  279. for (XCUIElement *matchedKey in matchedKeys) {
  280. if (!matchedKey.exists) {
  281. continue;
  282. }
  283. [matchedKey tap];
  284. if (isKeyboardInvisible()) {
  285. return YES;
  286. }
  287. }
  288. }
  289. }
  290. if ([UIDevice.currentDevice userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
  291. NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"elementType IN %@",
  292. @[@(XCUIElementTypeKey), @(XCUIElementTypeButton)]];
  293. NSArray *matchedKeys = findMatchingKeys(searchPredicate);
  294. if (matchedKeys.count > 0) {
  295. [matchedKeys[matchedKeys.count - 1] tap];
  296. }
  297. }
  298. #endif
  299. NSString *errorDescription = @"Did not know how to dismiss the keyboard. Try to dismiss it in the way supported by your application under test.";
  300. return [[[[FBRunLoopSpinner new]
  301. timeout:3]
  302. timeoutErrorMessage:errorDescription]
  303. spinUntilTrue:isKeyboardInvisible
  304. error:error];
  305. }
  306. - (NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet<NSString *> *)auditTypes
  307. error:(NSError **)error;
  308. {
  309. uint64_t numTypes = 0;
  310. NSDictionary *namesMap = auditTypeNamesToValues();
  311. for (NSString *value in auditTypes) {
  312. NSNumber *typeValue = namesMap[value];
  313. if (nil == typeValue) {
  314. NSString *reason = [NSString stringWithFormat:@"Audit type value '%@' is not known. Only the following audit types are supported: %@", value, namesMap.allKeys];
  315. @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:@{}];
  316. }
  317. numTypes |= [typeValue unsignedLongLongValue];
  318. }
  319. return [self fb_performAccessibilityAuditWithAuditTypes:numTypes error:error];
  320. }
  321. - (NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes
  322. error:(NSError **)error;
  323. {
  324. SEL selector = NSSelectorFromString(@"performAccessibilityAuditWithAuditTypes:issueHandler:error:");
  325. if (![self respondsToSelector:selector]) {
  326. [[[FBErrorBuilder alloc]
  327. withDescription:@"Accessibility audit is only supported since iOS 17/Xcode 15"]
  328. buildError:error];
  329. return nil;
  330. }
  331. NSMutableArray<NSDictionary *> *resultArray = [NSMutableArray array];
  332. NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];
  333. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
  334. [invocation setSelector:selector];
  335. [invocation setArgument:&auditTypes atIndex:2];
  336. BOOL (^issueHandler)(id) = ^BOOL(id issue) {
  337. NSString *auditType = @"";
  338. NSDictionary *valuesToNamesMap = auditTypeValuesToNames();
  339. NSNumber *auditTypeValue = [issue valueForKey:@"auditType"];
  340. if (nil != auditTypeValue) {
  341. auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue];
  342. }
  343. id extractedElement = extractIssueProperty(issue, @"element");
  344. id<FBXCElementSnapshot> elementSnapshot = [extractedElement fb_takeSnapshot];
  345. NSDictionary *elementAttributes = elementSnapshot ? [self.class dictionaryForElement:elementSnapshot recursive:NO] : @{};
  346. [resultArray addObject:@{
  347. @"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"",
  348. @"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"",
  349. @"auditType": auditType,
  350. @"element": [extractedElement description] ?: @"",
  351. @"elementDescription": [extractedElement debugDescription] ?: @"",
  352. @"elementAttributes": elementAttributes ?: @{},
  353. }];
  354. return YES;
  355. };
  356. [invocation setArgument:&issueHandler atIndex:3];
  357. [invocation setArgument:&error atIndex:4];
  358. [invocation invokeWithTarget:self];
  359. BOOL isSuccessful;
  360. [invocation getReturnValue:&isSuccessful];
  361. return isSuccessful ? resultArray.copy : nil;
  362. }
  363. + (instancetype)fb_activeApplication
  364. {
  365. return [self fb_activeApplicationWithDefaultBundleId:nil];
  366. }
  367. + (NSArray<XCUIApplication *> *)fb_activeApplications
  368. {
  369. NSArray<id<FBXCAccessibilityElement>> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications];
  370. NSMutableArray<XCUIApplication *> *result = [NSMutableArray array];
  371. if (activeApplicationElements.count > 0) {
  372. for (id<FBXCAccessibilityElement> applicationElement in activeApplicationElements) {
  373. XCUIApplication *app = [XCUIApplication fb_applicationWithPID:applicationElement.processIdentifier];
  374. if (nil != app) {
  375. [result addObject:app];
  376. }
  377. }
  378. }
  379. return result.count > 0 ? result.copy : @[self.class.fb_systemApplication];
  380. }
  381. + (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId
  382. {
  383. NSArray<id<FBXCAccessibilityElement>> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications];
  384. id<FBXCAccessibilityElement> activeApplicationElement = nil;
  385. id<FBXCAccessibilityElement> currentElement = nil;
  386. if (nil != bundleId) {
  387. currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement;
  388. if (nil != currentElement) {
  389. NSArray<NSDictionary *> *appInfos = [self fb_appsInfoWithAxElements:@[currentElement]];
  390. [FBLogger logFmt:@"Detected on-screen application: %@", appInfos.firstObject[@"bundleId"]];
  391. if ([[appInfos.firstObject objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) {
  392. activeApplicationElement = currentElement;
  393. }
  394. }
  395. }
  396. if (nil == activeApplicationElement && activeApplicationElements.count > 1) {
  397. if (nil != bundleId) {
  398. NSArray<NSDictionary *> *appInfos = [self fb_appsInfoWithAxElements:activeApplicationElements];
  399. NSMutableArray<NSString *> *bundleIds = [NSMutableArray array];
  400. for (NSDictionary *appInfo in appInfos) {
  401. [bundleIds addObject:(NSString *)appInfo[@"bundleId"]];
  402. }
  403. [FBLogger logFmt:@"Detected system active application(s): %@", bundleIds];
  404. // Try to select the desired application first
  405. for (NSUInteger appIdx = 0; appIdx < appInfos.count; appIdx++) {
  406. if ([[[appInfos objectAtIndex:appIdx] objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) {
  407. activeApplicationElement = [activeApplicationElements objectAtIndex:appIdx];
  408. break;
  409. }
  410. }
  411. }
  412. // Fall back to the "normal" algorithm if the desired application is either
  413. // not set or is not active
  414. if (nil == activeApplicationElement) {
  415. if (nil == currentElement) {
  416. currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement;
  417. }
  418. if (nil == currentElement) {
  419. [FBLogger log:@"Cannot precisely detect the current application. Will use the system's recently active one"];
  420. if (nil == bundleId) {
  421. [FBLogger log:@"Consider changing the 'defaultActiveApplication' setting to the bundle identifier of the desired application under test"];
  422. }
  423. } else {
  424. for (id<FBXCAccessibilityElement> appElement in activeApplicationElements) {
  425. if (appElement.processIdentifier == currentElement.processIdentifier) {
  426. activeApplicationElement = appElement;
  427. break;
  428. }
  429. }
  430. }
  431. }
  432. }
  433. if (nil != activeApplicationElement) {
  434. XCUIApplication *application = [XCUIApplication fb_applicationWithPID:activeApplicationElement.processIdentifier];
  435. if (nil != application) {
  436. return application;
  437. }
  438. [FBLogger log:@"Cannot translate the active process identifier into an application object"];
  439. }
  440. if (activeApplicationElements.count > 0) {
  441. [FBLogger logFmt:@"Getting the most recent active application (out of %@ total items)", @(activeApplicationElements.count)];
  442. for (id<FBXCAccessibilityElement> appElement in activeApplicationElements) {
  443. XCUIApplication *application = [XCUIApplication fb_applicationWithPID:appElement.processIdentifier];
  444. if (nil != application) {
  445. return application;
  446. }
  447. }
  448. }
  449. [FBLogger log:@"Cannot retrieve any active applications. Assuming the system application is the active one"];
  450. return [self fb_systemApplication];
  451. }
  452. + (instancetype)fb_systemApplication
  453. {
  454. return [self fb_applicationWithPID:
  455. [[FBXCAXClientProxy.sharedClient systemApplication] processIdentifier]];
  456. }
  457. + (instancetype)fb_applicationWithPID:(pid_t)processID
  458. {
  459. return [FBXCAXClientProxy.sharedClient monitoredApplicationWithProcessIdentifier:processID];
  460. }
  461. + (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error
  462. {
  463. XCUIApplication *systemApp = self.fb_systemApplication;
  464. @try {
  465. if (!systemApp.running) {
  466. [systemApp launch];
  467. } else {
  468. [systemApp activate];
  469. }
  470. } @catch (NSException *e) {
  471. return [[[FBErrorBuilder alloc]
  472. withDescription:nil == e ? @"Cannot open the home screen" : e.reason]
  473. buildError:error];
  474. }
  475. return [[[[FBRunLoopSpinner new]
  476. timeout:5]
  477. timeoutErrorMessage:@"Timeout waiting until the home screen is visible"]
  478. spinUntilTrue:^BOOL{
  479. return [systemApp fb_isSameAppAs:self.fb_activeApplication];
  480. }
  481. error:error];
  482. }
  483. - (BOOL)fb_isSameAppAs:(nullable XCUIApplication *)otherApp
  484. {
  485. if (nil == otherApp) {
  486. return NO;
  487. }
  488. return self == otherApp || [self.bundleID isEqualToString:(NSString *)otherApp.bundleID];
  489. }
  490. @end