Skip to content

Commit 78321dd

Browse files
feat: Add accessibility audit extension (#727)
1 parent 6c6b4cf commit 78321dd

File tree

5 files changed

+166
-0
lines changed

5 files changed

+166
-0
lines changed

WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h

+20
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,26 @@ NS_ASSUME_NONNULL_BEGIN
103103
- (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray<NSString *> *)keyNames
104104
error:(NSError **)error;
105105

106+
/**
107+
A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc
108+
109+
@param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc
110+
@param error If there is an error, upon return contains an NSError object that describes the problem.
111+
@return List of found issues or nil if there was a failure
112+
*/
113+
- (nullable NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet<NSString *> *)auditTypes
114+
error:(NSError **)error;
115+
116+
/**
117+
A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc
118+
119+
@param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc
120+
@param error If there is an error, upon return contains an NSError object that describes the problem.
121+
@return List of found issues or nil if there was a failure
122+
*/
123+
- (nullable NSArray<NSDictionary<NSString *, NSString*> *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes
124+
error:(NSError **)error;
125+
106126
@end
107127

108128
NS_ASSUME_NONNULL_END

WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m

+104
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#import "FBElementTypeTransformer.h"
1313
#import "FBKeyboard.h"
1414
#import "FBLogger.h"
15+
#import "FBExceptions.h"
1516
#import "FBMacros.h"
1617
#import "FBMathUtils.h"
1718
#import "FBActiveAppDetectionPoint.h"
@@ -33,6 +34,54 @@
3334

3435
static NSString* const FBUnknownBundleId = @"unknown";
3536

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+
3685

3786
@implementation XCUIApplication (FBHelpers)
3887

@@ -281,4 +330,59 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray<NSString *> *)keyNames
281330
error:error];
282331
}
283332

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+
284388
@end

WebDriverAgentLib/Commands/FBCustomCommands.m

+20
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ + (NSArray *)routes
5858
[[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)],
5959
#endif
6060
[[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)],
61+
[[FBRoute POST:@"/wda/performAccessibilityAudit"] respondWithTarget:self action:@selector(handlePerformAccessibilityAudit:)],
6162
[[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)],
6263
[[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)],
6364
[[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)],
@@ -544,4 +545,23 @@ + (NSString *)timeZone
544545
}
545546
#endif
546547

548+
+ (id<FBResponsePayload>)handlePerformAccessibilityAudit:(FBRouteRequest *)request
549+
{
550+
NSError *error;
551+
NSArray *requestedTypes = request.arguments[@"auditTypes"];
552+
NSMutableSet *typesSet = [NSMutableSet set];
553+
if (nil == requestedTypes || 0 == [requestedTypes count]) {
554+
[typesSet addObject:@"XCUIAccessibilityAuditTypeAll"];
555+
} else {
556+
[typesSet addObjectsFromArray:requestedTypes];
557+
}
558+
NSArray *result = [request.session.activeApplication fb_performAccessibilityAuditWithAuditTypesSet:typesSet.copy
559+
error:&error];
560+
if (nil == result) {
561+
return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description
562+
traceback:nil]);
563+
}
564+
return FBResponseWithObject(result);
565+
}
566+
547567
@end

WebDriverAgentLib/FBApplication.m

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#import "FBXCAccessibilityElement.h"
1313
#import "FBLogger.h"
14+
#import "FBExceptions.h"
1415
#import "FBRunLoopSpinner.h"
1516
#import "FBMacros.h"
1617
#import "FBActiveAppDetectionPoint.h"

WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m

+21
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#import "FBApplication.h"
1515
#import "FBIntegrationTestCase.h"
1616
#import "FBElement.h"
17+
#import "FBMacros.h"
1718
#import "FBTestMacros.h"
1819
#import "XCUIApplication+FBHelpers.h"
1920
#import "XCUIElement+FBIsVisible.h"
@@ -94,4 +95,24 @@ - (void)testTestmanagerdVersion
9495
XCTAssertGreaterThan(FBTestmanagerdVersion(), 0);
9596
}
9697

98+
- (void)testAccessbilityAudit
99+
{
100+
if (SYSTEM_VERSION_LESS_THAN(@"17.0")) {
101+
return;
102+
}
103+
104+
NSError *error;
105+
NSArray *auditIssues1 = [FBApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypes:~0UL
106+
error:&error];
107+
XCTAssertNotNil(auditIssues1);
108+
XCTAssertNil(error);
109+
110+
NSMutableSet *set = [NSMutableSet new];
111+
[set addObject:@"XCUIAccessibilityAuditTypeAll"];
112+
NSArray *auditIssues2 = [FBApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypesSet:set.copy
113+
error:&error];
114+
XCTAssertEqualObjects(auditIssues1, auditIssues2);
115+
XCTAssertNil(error);
116+
}
117+
97118
@end

0 commit comments

Comments
 (0)