XCUIApplication+FBHelpers.m 25 KB

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