diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h index 9d190a381..13fabcf88 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h @@ -103,6 +103,26 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames error:(NSError **)error; +/** + A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc + + @param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return List of found issues or nil if there was a failure + */ +- (nullable NSArray *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet *)auditTypes + error:(NSError **)error; + +/** + A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc + + @param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return List of found issues or nil if there was a failure + */ +- (nullable NSArray *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes + error:(NSError **)error; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m index ec752c5a0..7c9ff9967 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m @@ -12,6 +12,7 @@ #import "FBElementTypeTransformer.h" #import "FBKeyboard.h" #import "FBLogger.h" +#import "FBExceptions.h" #import "FBMacros.h" #import "FBMathUtils.h" #import "FBActiveAppDetectionPoint.h" @@ -33,6 +34,54 @@ static NSString* const FBUnknownBundleId = @"unknown"; +_Nullable id extractIssueProperty(id issue, NSString *propertyName) { + SEL selector = NSSelectorFromString(propertyName); + NSMethodSignature *methodSignature = [issue methodSignatureForSelector:selector]; + if (nil == methodSignature) { + return nil; + } + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation invokeWithTarget:issue]; + id __unsafe_unretained result; + [invocation getReturnValue:&result]; + return result; +} + +NSDictionary *auditTypeNamesToValues(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + // https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc + result = @{ + @"XCUIAccessibilityAuditTypeAction": @(1UL << 32), + @"XCUIAccessibilityAuditTypeAll": @(~0UL), + @"XCUIAccessibilityAuditTypeContrast": @(1UL << 0), + @"XCUIAccessibilityAuditTypeDynamicType": @(1UL << 16), + @"XCUIAccessibilityAuditTypeElementDetection": @(1UL << 1), + @"XCUIAccessibilityAuditTypeHitRegion": @(1UL << 2), + @"XCUIAccessibilityAuditTypeParentChild": @(1UL << 33), + @"XCUIAccessibilityAuditTypeSufficientElementDescription": @(1UL << 3), + @"XCUIAccessibilityAuditTypeTextClipped": @(1UL << 17), + @"XCUIAccessibilityAuditTypeTrait": @(1UL << 18), + }; + }); + return result; +} + +NSDictionary *auditTypeValuesToNames(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *inverted = [NSMutableDictionary new]; + [auditTypeNamesToValues() enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSNumber *value, BOOL *stop) { + inverted[value] = key; + }]; + result = inverted.copy; + }); + return result; +} + @implementation XCUIApplication (FBHelpers) @@ -281,4 +330,59 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames error:error]; } +- (NSArray *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet *)auditTypes + error:(NSError **)error; +{ + uint64_t numTypes = 0; + NSDictionary *namesMap = auditTypeNamesToValues(); + for (NSString *value in auditTypes) { + NSNumber *typeValue = namesMap[value]; + if (nil == typeValue) { + NSString *reason = [NSString stringWithFormat:@"Audit type value '%@' is not known. Only the following audit types are supported: %@", value, namesMap.allKeys]; + @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:@{}]; + } + numTypes |= [typeValue unsignedLongLongValue]; + } + return [self fb_performAccessibilityAuditWithAuditTypes:numTypes error:error]; +} + +- (NSArray *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes + error:(NSError **)error; +{ + SEL selector = NSSelectorFromString(@"performAccessibilityAuditWithAuditTypes:issueHandler:error:"); + if (![self respondsToSelector:selector]) { + [[[FBErrorBuilder alloc] + withDescription:@"Accessibility audit is only supported since iOS 17/Xcode 15"] + buildError:error]; + return nil; + } + + NSMutableArray *resultArray = [NSMutableArray array]; + NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation setArgument:&auditTypes atIndex:2]; + BOOL (^issueHandler)(id) = ^BOOL(id issue) { + NSString *auditType = @""; + NSDictionary *valuesToNamesMap = auditTypeValuesToNames(); + NSNumber *auditTypeValue = [issue valueForKey:@"auditType"]; + if (nil != auditTypeValue) { + auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue]; + } + [resultArray addObject:@{ + @"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"", + @"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"", + @"auditType": auditType, + @"element": [extractIssueProperty(issue, @"element") description] ?: @"", + }]; + return YES; + }; + [invocation setArgument:&issueHandler atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invokeWithTarget:self]; + BOOL isSuccessful; + [invocation getReturnValue:&isSuccessful]; + return isSuccessful ? resultArray.copy : nil; +} + @end diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.m b/WebDriverAgentLib/Commands/FBCustomCommands.m index c2cde607e..9d8e324d7 100644 --- a/WebDriverAgentLib/Commands/FBCustomCommands.m +++ b/WebDriverAgentLib/Commands/FBCustomCommands.m @@ -58,6 +58,7 @@ + (NSArray *)routes [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)], #endif [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)], + [[FBRoute POST:@"/wda/performAccessibilityAudit"] respondWithTarget:self action:@selector(handlePerformAccessibilityAudit:)], [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)], [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)], [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)], @@ -544,4 +545,23 @@ + (NSString *)timeZone } #endif ++ (id)handlePerformAccessibilityAudit:(FBRouteRequest *)request +{ + NSError *error; + NSArray *requestedTypes = request.arguments[@"auditTypes"]; + NSMutableSet *typesSet = [NSMutableSet set]; + if (nil == requestedTypes || 0 == [requestedTypes count]) { + [typesSet addObject:@"XCUIAccessibilityAuditTypeAll"]; + } else { + [typesSet addObjectsFromArray:requestedTypes]; + } + NSArray *result = [request.session.activeApplication fb_performAccessibilityAuditWithAuditTypesSet:typesSet.copy + error:&error]; + if (nil == result) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithObject(result); +} + @end diff --git a/WebDriverAgentLib/FBApplication.m b/WebDriverAgentLib/FBApplication.m index 8a9a48ba0..fe46393fb 100644 --- a/WebDriverAgentLib/FBApplication.m +++ b/WebDriverAgentLib/FBApplication.m @@ -11,6 +11,7 @@ #import "FBXCAccessibilityElement.h" #import "FBLogger.h" +#import "FBExceptions.h" #import "FBRunLoopSpinner.h" #import "FBMacros.h" #import "FBActiveAppDetectionPoint.h" diff --git a/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m b/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m index 8a7d47ae7..41ee134a1 100644 --- a/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m +++ b/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m @@ -14,6 +14,7 @@ #import "FBApplication.h" #import "FBIntegrationTestCase.h" #import "FBElement.h" +#import "FBMacros.h" #import "FBTestMacros.h" #import "XCUIApplication+FBHelpers.h" #import "XCUIElement+FBIsVisible.h" @@ -94,4 +95,24 @@ - (void)testTestmanagerdVersion XCTAssertGreaterThan(FBTestmanagerdVersion(), 0); } +- (void)testAccessbilityAudit +{ + if (SYSTEM_VERSION_LESS_THAN(@"17.0")) { + return; + } + + NSError *error; + NSArray *auditIssues1 = [FBApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypes:~0UL + error:&error]; + XCTAssertNotNil(auditIssues1); + XCTAssertNil(error); + + NSMutableSet *set = [NSMutableSet new]; + [set addObject:@"XCUIAccessibilityAuditTypeAll"]; + NSArray *auditIssues2 = [FBApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypesSet:set.copy + error:&error]; + XCTAssertEqualObjects(auditIssues1, auditIssues2); + XCTAssertNil(error); +} + @end