Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for accessibility custom actions #1295

Merged
merged 12 commits into from
Aug 1, 2024
39 changes: 39 additions & 0 deletions KIF Tests/CustomActionTests_ViewTestActor.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// CustomActionTests_ViewTestActor.m
// KIF Tests
//
// Created by Alex Odawa on 09/07/2024.
//

#import <KIF/KIF.h>

@interface CustomActionTests_ViewTestActor : KIFTestCase
@end


@implementation CustomActionTests_ViewTestActor

- (void)beforeEach
{
[[viewTester usingLabel:@"Tapping"] tap];
}

- (void)afterEach
{
[[[viewTester usingLabel:@"Test Suite"] usingTraits:UIAccessibilityTraitButton] tap];
}

- (void)testCustomActions
{
if (@available(iOS 13.0, *)) {
[[viewTester usingLabel:@"theStepper"] activateCustomActionWithName:@"Action With block handler"];
}

for (NSString *name in @[@"Action without argument", @"Action with argument"]) {
[[viewTester usingLabel:@"theStepper"] activateCustomActionWithName:name];
}

[[viewTester usingLabel:@"theStepper"] activateCustomActionWithName:@"Action that fails" expectedResult:NO];
}

@end
16 changes: 16 additions & 0 deletions KIF.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@
9CC881AC1AD4CE4B002CD34C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AAB0726B139719AC008AF393 /* Foundation.framework */; };
9CC881AD1AD4CE50002CD34C /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9CC881A21AD4CAAC002CD34C /* CoreFoundation.framework */; };
9CC967401AD4B1B600576D13 /* KIF.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CC9673F1AD4B1B600576D13 /* KIF.h */; settings = {ATTRIBUTES = (Public, ); }; };
ACA242E42C3DA55400E6F1B6 /* CustomActionTests_ViewTestActor.m in Sources */ = {isa = PBXBuildFile; fileRef = ACA242E32C3DA55400E6F1B6 /* CustomActionTests_ViewTestActor.m */; };
ACA242E72C3DB47400E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = ACA242E52C3DB46A00E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.m */; };
ACA242E92C3DB4EA00E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = ACA242E82C3DB4B000E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.h */; settings = {ATTRIBUTES = (Private, ); }; };
ACA242EA2C3DB4EB00E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = ACA242E82C3DB4B000E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.h */; settings = {ATTRIBUTES = (Private, ); }; };
ACA242EB2C3DC32500E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = ACA242E52C3DB46A00E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.m */; };
AE62FCD01A1D20E5002B10DA /* WebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AE62FCCF1A1D20E5002B10DA /* WebViewTests.m */; };
AE62FCD61A1D2447002B10DA /* WebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE62FCD51A1D2447002B10DA /* WebViewController.m */; };
AE62FCD81A1D2667002B10DA /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = AE62FCD71A1D2667002B10DA /* index.html */; };
Expand Down Expand Up @@ -427,6 +432,9 @@
9CC9673F1AD4B1B600576D13 /* KIF.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KIF.h; sourceTree = "<group>"; };
AAB0726B139719AC008AF393 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
AAB072B413971AEA008AF393 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
ACA242E32C3DA55400E6F1B6 /* CustomActionTests_ViewTestActor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomActionTests_ViewTestActor.m; sourceTree = "<group>"; };
ACA242E52C3DB46A00E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIAccessibilityCustomAction+KIFAdditions.m"; sourceTree = "<group>"; };
ACA242E82C3DB4B000E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIAccessibilityCustomAction+KIFAdditions.h"; sourceTree = "<group>"; };
AE62FCCF1A1D20E5002B10DA /* WebViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebViewTests.m; sourceTree = "<group>"; };
AE62FCD51A1D2447002B10DA /* WebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebViewController.m; sourceTree = "<group>"; };
AE62FCD71A1D2667002B10DA /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = index.html; sourceTree = "<group>"; };
Expand Down Expand Up @@ -639,6 +647,8 @@
2229D56F25BEF47D0093296C /* NSPredicate+KIFAdditions.m */,
2229D56125BEF47D0093296C /* NSString+KIFAdditions.h */,
2229D54B25BEF47D0093296C /* NSString+KIFAdditions.m */,
ACA242E82C3DB4B000E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.h */,
ACA242E52C3DB46A00E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.m */,
2229D56425BEF47D0093296C /* UIAccessibilityElement-KIFAdditions.h */,
2229D54825BEF47D0093296C /* UIAccessibilityElement-KIFAdditions.m */,
2229D54C25BEF47D0093296C /* UIApplication-KIFAdditions.h */,
Expand Down Expand Up @@ -922,6 +932,7 @@
FA8A3C5A1A77281900206350 /* WebViewTests_ViewTestActor.m */,
FA8A3C601A77320000206350 /* SystemAlertTests_ViewTestActor.m */,
8E654FC329D79BD8007F7811 /* OffscreenTests_ViewTestActor.m */,
ACA242E32C3DA55400E6F1B6 /* CustomActionTests_ViewTestActor.m */,
);
name = "KIFUIViewTestActor Tests";
sourceTree = "<group>";
Expand Down Expand Up @@ -1000,6 +1011,7 @@
8EC2CB2029DB3D3B001BE493 /* NSObject+KIFSwizzle.h in Headers */,
22B1376F25D6E32700D88061 /* KIFTestActor_Private.h in Headers */,
22B1375025D6E32700D88061 /* KIFTouchVisualizerView.h in Headers */,
ACA242EA2C3DB4EB00E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.h in Headers */,
22B1377925D6E32700D88061 /* KIFSystemTestActor.h in Headers */,
22B1376D25D6E32700D88061 /* KIFUITestActor-ConditionalTests.h in Headers */,
22B1374F25D6E32700D88061 /* NSString+KIFAdditions.h in Headers */,
Expand Down Expand Up @@ -1037,6 +1049,7 @@
2229D5AB25BEF47D0093296C /* NSError-KIFAdditions.h in Headers */,
2229D5B825BEF47D0093296C /* UIScreen+KIFAdditions.h in Headers */,
2229D5AA25BEF47D0093296C /* UIView-KIFAdditions.h in Headers */,
ACA242E92C3DB4EA00E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.h in Headers */,
2229D5CD25BEF47D0093296C /* KIFTestStepValidation.h in Headers */,
2229D5C225BEF47D0093296C /* LoadableCategory.h in Headers */,
2229D5D625BEF47D0093296C /* KIFTestCase.h in Headers */,
Expand Down Expand Up @@ -1325,6 +1338,7 @@
22B1373A25D6E32700D88061 /* KIFUIObject.m in Sources */,
22B1377425D6E32700D88061 /* KIFTestActor.m in Sources */,
22B1374C25D6E32700D88061 /* UIApplication-KIFAdditions.m in Sources */,
ACA242EB2C3DC32500E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.m in Sources */,
8EC2CB2229DB3D3B001BE493 /* NSObject+KIFSwizzle.m in Sources */,
22B1375C25D6E32700D88061 /* KIFUITestActor-ConditionalTests.m in Sources */,
22B1376525D6E32700D88061 /* KIFTypist.m in Sources */,
Expand Down Expand Up @@ -1372,6 +1386,7 @@
2229D5D925BEF47D0093296C /* KIFTestStepValidation.m in Sources */,
2229D5A525BEF47D0093296C /* UIView-Debugging.m in Sources */,
2229D5C125BEF47D0093296C /* NSError-KIFAdditions.m in Sources */,
ACA242E72C3DB47400E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.m in Sources */,
8EC2CB2129DB3D3B001BE493 /* NSObject+KIFSwizzle.m in Sources */,
2229D5B525BEF47D0093296C /* UIApplication-KIFAdditions.m in Sources */,
2229D5C025BEF47D0093296C /* UIEvent+KIFAdditions.m in Sources */,
Expand Down Expand Up @@ -1423,6 +1438,7 @@
5A62B0AE1BB205CA00A3F480 /* PullToRefreshTests.m in Sources */,
97E8A5D11B0A63D100124E3B /* BackgroundTests.m in Sources */,
EABD46BD1857A0F300A5F081 /* LandscapeTests.m in Sources */,
ACA242E42C3DA55400E6F1B6 /* CustomActionTests_ViewTestActor.m in Sources */,
EABD46BE1857A0F300A5F081 /* TableViewTests.m in Sources */,
FA4915601A7823E500A78E57 /* ScrollViewTests_ViewTestActor.m in Sources */,
EABD46BF1857A0F300A5F081 /* GestureTests.m in Sources */,
Expand Down
21 changes: 21 additions & 0 deletions Sources/KIF/Additions/UIAccessibilityCustomAction+KIFAdditions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// UIAccessibilityCustomAction+KIFAdditions.h
// KIF
//
// Created by Alex Odawa on 09/07/2024.
//

#import <UIKit/UIKit.h>

@interface NSObject (KIFCustomActionAdditions)

- (UIAccessibilityCustomAction *)KIF_customActionWithName:(NSString *)name;

@end

@interface UIAccessibilityCustomAction (KIFAdditions)

- (BOOL)KIF_activate;

@end

85 changes: 85 additions & 0 deletions Sources/KIF/Additions/UIAccessibilityCustomAction+KIFAdditions.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// UIAccessibilityCustomAction+KIFAdditions.m
// KIF Tests
//
// Created by Alex Odawa on 09/07/2024.
//

#import <UIKit/UIKit.h>
#import "UIAccessibilityCustomAction+KIFAdditions.h"



@interface UIAccessibilityCustomAction (KIFPrivate)

- (NSString *)KIF_normalizedName;

@end

@implementation UIAccessibilityCustomAction (KIFAdditions)

- (BOOL)KIF_activate;
{
if (@available(iOS 13.0, *)) {
if (self.actionHandler) {
return self.actionHandler(self);
}
}

if ([self.target respondsToSelector:self.selector]) {
NSMethodSignature *signature = [self.target methodSignatureForSelector:self.selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.selector = self.selector;
invocation.target = self.target;

/*
https://developer.apple.com/documentation/uikit/uiaccessibilitycustomaction/1620499-init
The method signature must take one of the following forms:
- (BOOL)myPerformActionMethod
- (BOOL)myPerformActionMethod:(UIAccessibilityCustomAction *)action
*/
if (signature.numberOfArguments == 3) {
id arg = self;
[invocation setArgument: &arg atIndex:2];
}

[invocation invoke];
BOOL returnValue = NO;
[invocation getReturnValue:&returnValue];
return returnValue;
}
NSString *targetStr = [self.target description];
NSString *selectorStr = NSStringFromSelector(self.selector);
[[NSException exceptionWithName:@"KIFUIAccessibilityCustomActionActivationException"
reason:@"UIAccessibilityCustomAction Target does not respond to provided Selector."
userInfo:@{@"Target" : targetStr, @"Selector" : selectorStr}]
raise];

return NO;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to raise if we attempted to perform an action on an element that says it supports it, but hasn't implemented the selector? I'd kind of expect this to crash or raise an error in some way that is distinct from the selector returning false from the action.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, nice idea

}

- (NSString *)KIF_normalizedName;
{
NSString *name = [self name];
if ([name isKindOfClass:[NSAttributedString class]]) {
name = [(NSAttributedString *)name string];
}
return name;
}

@end


@implementation NSObject (KIFCustomActionAdditions)

- (UIAccessibilityCustomAction *)KIF_customActionWithName:(NSString *)name;
{
for (UIAccessibilityCustomAction *action in [self.accessibilityCustomActions copy]) {
if ([name isEqualToString: [action KIF_normalizedName]]) {
return action;
}
}
return nil;
}

@end
24 changes: 23 additions & 1 deletion Sources/KIF/Classes/KIFUIViewTestActor.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,20 @@ extern NSString *const inputFieldTestString;

/*!
@abstract Adds a given predicate to the tester's search predicate.
@description The given predicate will be evaluated when searching for a matching view. You likely wont need this method very often, and should rely on the accessibility properties when possibile.
@discussion The given predicate will be evaluated when searching for a matching view. You likely wont need this method very often, and should rely on the accessibility properties when possibile.
@param predicate The predicate to add to the tester's search predicate.
@return The message reciever, these methods are intended to be chained together.
*/
- (instancetype)usingPredicate:(NSPredicate *)predicate;

/*!
@abstract Adds a check for an accessibility custom action with a provided name to the tester's search predicate.
@discussion The tester will evaluate accessibility elements looking for a matching accessibility custom action.
@param name The name of the custom action providd by element to match.
@return The message reciever, these methods are intended to be chained together.
*/
- (instancetype)usingCustomActionWithName:(NSString *)name;

#pragma mark - Acting on Accessibility Elements

/*!
Expand Down Expand Up @@ -171,6 +179,20 @@ extern NSString *const inputFieldTestString;
*/
- (void)swipeFromEdge:(UIRectEdge)edge;

/*!
@abstract Activates a custom accessibility action available on the element..
@param name The name of the custom action to activate.
*/
- (void)activateCustomActionWithName:(NSString *)name;

/*!
@abstract Activates a custom accessibility action available on the element..
@param name The name of the custom action to activate.
@param expectedResult The expected boolean return from activation the custom action.
*/
- (void)activateCustomActionWithName:(NSString *)name expectedResult:(BOOL)expectedResult;


#pragma mark Waiting & Finding

/*!
Expand Down
29 changes: 29 additions & 0 deletions Sources/KIF/Classes/KIFUIViewTestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#import "NSPredicate+KIFAdditions.h"
#import "NSString+KIFAdditions.h"
#import "UIAccessibilityElement-KIFAdditions.h"
#import "UIAccessibilityCustomAction+KIFAdditions.h"
#import "UIApplication-KIFAdditions.h"
#import "UIWindow-KIFAdditions.h"
#import "UIDatePicker+KIFAdditions.h"
Expand Down Expand Up @@ -155,6 +156,15 @@ - (instancetype)usingFirstResponder;
return [self usingPredicate:predicate];
}

- (instancetype)usingCustomActionWithName:(NSString *)name
{
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
return ([evaluatedObject KIF_customActionWithName:name] != nil);
}];
predicate.kifPredicateDescription = [NSString stringWithFormat:@"Custom Action with name equal to \"%@\"", name];
return [self usingPredicate:predicate];
}

#pragma mark - System Actions

#if TARGET_IPHONE_SIMULATOR
Expand Down Expand Up @@ -385,6 +395,25 @@ - (void)swipeFromEdge:(UIRectEdge)edge
[self.actor swipeFromEdge:edge];
}

- (void)activateCustomActionWithName:(NSString *)name;{
[self activateCustomActionWithName:name expectedResult:YES];
}

- (void)activateCustomActionWithName:(NSString *)name expectedResult:(BOOL)expectedResult;
{
@autoreleasepool {
KIFUIObject *found = [self _predicateSearchWithRequiresMatch:YES mustBeTappable:NO];

[self runBlock:^KIFTestStepResult(NSError **error) {
if([[found.element KIF_customActionWithName:name] KIF_activate] == expectedResult) {
return KIFTestStepResultSuccess;
}
return KIFTestStepResultFailure;
}];
}

}

#pragma mark - Scroll/Table/CollectionView Actions

- (void)scrollByFractionOfSizeHorizontal:(CGFloat)horizontalFraction vertical:(CGFloat)verticalFraction;
Expand Down
54 changes: 54 additions & 0 deletions Test Host/TapViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

#import <UIKit/UIKit.h>
#import <UIKit/UIAccessibilityCustomAction.h>

@interface TapViewController : UIViewController<UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate>
@property (weak, nonatomic) IBOutlet UISlider *slider;
Expand All @@ -28,6 +29,7 @@ - (void)viewDidLoad
self.lineBreakLabel.accessibilityLabel = @"A\nB\nC\n\n";
self.stepper.isAccessibilityElement = YES;
self.stepper.accessibilityLabel = @"theStepper";
self.stepper.accessibilityCustomActions = self.customActions;
}

- (void)memoryWarningNotification:(NSNotification *)notification
Expand Down Expand Up @@ -93,6 +95,58 @@ - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRang
return YES;
}


- (NSArray *)customActions
{
NSArray *actions = @[self.customActionWithoutArgument, self.customActionWithArgument, self.customActionThatFails];
if (@available(iOS 13.0, *)) {
return [actions arrayByAddingObject: self.customActionWithBlock];
}
return actions;
}

- (UIAccessibilityCustomAction *)customActionWithBlock
{
if (@available(iOS 13.0, *)) {
return [[UIAccessibilityCustomAction alloc] initWithName: @"Action With block handler"
actionHandler:^BOOL(UIAccessibilityCustomAction * _Nonnull customAction) {
return YES;
}];
} else {
return nil;
}
}

- (UIAccessibilityCustomAction *)customActionWithoutArgument
{
return [[UIAccessibilityCustomAction alloc] initWithName:@"Action without argument" target:self selector:@selector(customActionHandlerWithoutArgument)];
}

- (UIAccessibilityCustomAction *)customActionWithArgument
{
return [[UIAccessibilityCustomAction alloc] initWithName:@"Action with argument" target:self selector:@selector(customActionHandlerWithArgument:)];
}

- (UIAccessibilityCustomAction *)customActionThatFails
{
return [[UIAccessibilityCustomAction alloc] initWithName:@"Action that fails" target:self selector:@selector(customActionThatFails)];
}

- (BOOL)customActionHandlerWithoutArgument
{
return YES;
}

- (BOOL)customActionHandlerWithArgument:(UIAccessibilityCustomAction *)action
{
return YES;
}

- (BOOL)customActionHandlerThatFails
{
return NO;
}

#pragma mark - <UIImagePickerControllerDelegate>

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
Expand Down
Loading