|
12 | 12 | #import "FBElementTypeTransformer.h"
|
13 | 13 | #import "FBKeyboard.h"
|
14 | 14 | #import "FBLogger.h"
|
| 15 | +#import "FBExceptions.h" |
15 | 16 | #import "FBMacros.h"
|
16 | 17 | #import "FBMathUtils.h"
|
17 | 18 | #import "FBActiveAppDetectionPoint.h"
|
|
33 | 34 |
|
34 | 35 | static NSString* const FBUnknownBundleId = @"unknown";
|
35 | 36 |
|
| 37 | +_Nullable id extractIssueProperty(id issue, NSString *propertyName) { |
| 38 | + SEL selector = NSSelectorFromString(propertyName); |
| 39 | + NSMethodSignature *methodSignature = [issue methodSignatureForSelector:selector]; |
| 40 | + if (nil == methodSignature) { |
| 41 | + return nil; |
| 42 | + } |
| 43 | + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; |
| 44 | + [invocation setSelector:selector]; |
| 45 | + [invocation invokeWithTarget:issue]; |
| 46 | + id __unsafe_unretained result; |
| 47 | + [invocation getReturnValue:&result]; |
| 48 | + return result; |
| 49 | +} |
| 50 | + |
| 51 | +NSDictionary<NSString *, NSNumber *> *auditTypeNamesToValues(void) { |
| 52 | + static dispatch_once_t onceToken; |
| 53 | + static NSDictionary *result; |
| 54 | + dispatch_once(&onceToken, ^{ |
| 55 | + // https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc |
| 56 | + result = @{ |
| 57 | + @"XCUIAccessibilityAuditTypeAction": @(1UL << 32), |
| 58 | + @"XCUIAccessibilityAuditTypeAll": @(~0UL), |
| 59 | + @"XCUIAccessibilityAuditTypeContrast": @(1UL << 0), |
| 60 | + @"XCUIAccessibilityAuditTypeDynamicType": @(1UL << 16), |
| 61 | + @"XCUIAccessibilityAuditTypeElementDetection": @(1UL << 1), |
| 62 | + @"XCUIAccessibilityAuditTypeHitRegion": @(1UL << 2), |
| 63 | + @"XCUIAccessibilityAuditTypeParentChild": @(1UL << 33), |
| 64 | + @"XCUIAccessibilityAuditTypeSufficientElementDescription": @(1UL << 3), |
| 65 | + @"XCUIAccessibilityAuditTypeTextClipped": @(1UL << 17), |
| 66 | + @"XCUIAccessibilityAuditTypeTrait": @(1UL << 18), |
| 67 | + }; |
| 68 | + }); |
| 69 | + return result; |
| 70 | +} |
| 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 | + |
36 | 85 |
|
37 | 86 | @implementation XCUIApplication (FBHelpers)
|
38 | 87 |
|
@@ -281,4 +330,59 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray<NSString *> *)keyNames
|
281 | 330 | error:error];
|
282 | 331 | }
|
283 | 332 |
|
| 333 | +- (NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet<NSString *> *)auditTypes |
| 334 | + error:(NSError **)error; |
| 335 | +{ |
| 336 | + uint64_t numTypes = 0; |
| 337 | + NSDictionary *namesMap = auditTypeNamesToValues(); |
| 338 | + for (NSString *value in auditTypes) { |
| 339 | + NSNumber *typeValue = namesMap[value]; |
| 340 | + if (nil == typeValue) { |
| 341 | + NSString *reason = [NSString stringWithFormat:@"Audit type value '%@' is not known. Only the following audit types are supported: %@", value, namesMap.allKeys]; |
| 342 | + @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:@{}]; |
| 343 | + } |
| 344 | + numTypes |= [typeValue unsignedLongLongValue]; |
| 345 | + } |
| 346 | + return [self fb_performAccessibilityAuditWithAuditTypes:numTypes error:error]; |
| 347 | +} |
| 348 | + |
| 349 | +- (NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes |
| 350 | + error:(NSError **)error; |
| 351 | +{ |
| 352 | + SEL selector = NSSelectorFromString(@"performAccessibilityAuditWithAuditTypes:issueHandler:error:"); |
| 353 | + if (![self respondsToSelector:selector]) { |
| 354 | + [[[FBErrorBuilder alloc] |
| 355 | + withDescription:@"Accessibility audit is only supported since iOS 17/Xcode 15"] |
| 356 | + buildError:error]; |
| 357 | + return nil; |
| 358 | + } |
| 359 | + |
| 360 | + NSMutableArray<NSDictionary *> *resultArray = [NSMutableArray array]; |
| 361 | + NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; |
| 362 | + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; |
| 363 | + [invocation setSelector:selector]; |
| 364 | + [invocation setArgument:&auditTypes atIndex:2]; |
| 365 | + BOOL (^issueHandler)(id) = ^BOOL(id issue) { |
| 366 | + NSString *auditType = @""; |
| 367 | + NSDictionary *valuesToNamesMap = auditTypeValuesToNames(); |
| 368 | + NSNumber *auditTypeValue = [issue valueForKey:@"auditType"]; |
| 369 | + if (nil != auditTypeValue) { |
| 370 | + auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue]; |
| 371 | + } |
| 372 | + [resultArray addObject:@{ |
| 373 | + @"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"", |
| 374 | + @"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"", |
| 375 | + @"auditType": auditType, |
| 376 | + @"element": [extractIssueProperty(issue, @"element") description] ?: @"", |
| 377 | + }]; |
| 378 | + return YES; |
| 379 | + }; |
| 380 | + [invocation setArgument:&issueHandler atIndex:3]; |
| 381 | + [invocation setArgument:&error atIndex:4]; |
| 382 | + [invocation invokeWithTarget:self]; |
| 383 | + BOOL isSuccessful; |
| 384 | + [invocation getReturnValue:&isSuccessful]; |
| 385 | + return isSuccessful ? resultArray.copy : nil; |
| 386 | +} |
| 387 | + |
284 | 388 | @end
|
0 commit comments