diff --git a/KIF Tests/CustomActionTests_ViewTestActor.m b/KIF Tests/CustomActionTests_ViewTestActor.m new file mode 100644 index 00000000..acb9cc53 --- /dev/null +++ b/KIF Tests/CustomActionTests_ViewTestActor.m @@ -0,0 +1,39 @@ +// +// CustomActionTests_ViewTestActor.m +// KIF Tests +// +// Created by Alex Odawa on 09/07/2024. +// + +#import + +@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 diff --git a/KIF.xcodeproj/project.pbxproj b/KIF.xcodeproj/project.pbxproj index bd469678..504824c6 100644 --- a/KIF.xcodeproj/project.pbxproj +++ b/KIF.xcodeproj/project.pbxproj @@ -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 */; }; @@ -427,6 +432,9 @@ 9CC9673F1AD4B1B600576D13 /* KIF.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KIF.h; sourceTree = ""; }; 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 = ""; }; + ACA242E52C3DB46A00E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIAccessibilityCustomAction+KIFAdditions.m"; sourceTree = ""; }; + ACA242E82C3DB4B000E6F1B6 /* UIAccessibilityCustomAction+KIFAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIAccessibilityCustomAction+KIFAdditions.h"; sourceTree = ""; }; AE62FCCF1A1D20E5002B10DA /* WebViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebViewTests.m; sourceTree = ""; }; AE62FCD51A1D2447002B10DA /* WebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebViewController.m; sourceTree = ""; }; AE62FCD71A1D2667002B10DA /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = index.html; sourceTree = ""; }; @@ -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 */, @@ -922,6 +932,7 @@ FA8A3C5A1A77281900206350 /* WebViewTests_ViewTestActor.m */, FA8A3C601A77320000206350 /* SystemAlertTests_ViewTestActor.m */, 8E654FC329D79BD8007F7811 /* OffscreenTests_ViewTestActor.m */, + ACA242E32C3DA55400E6F1B6 /* CustomActionTests_ViewTestActor.m */, ); name = "KIFUIViewTestActor Tests"; sourceTree = ""; @@ -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 */, @@ -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 */, @@ -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 */, @@ -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 */, @@ -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 */, diff --git a/Sources/KIF/Additions/UIAccessibilityCustomAction+KIFAdditions.h b/Sources/KIF/Additions/UIAccessibilityCustomAction+KIFAdditions.h new file mode 100644 index 00000000..1a62c7f2 --- /dev/null +++ b/Sources/KIF/Additions/UIAccessibilityCustomAction+KIFAdditions.h @@ -0,0 +1,21 @@ +// +// UIAccessibilityCustomAction+KIFAdditions.h +// KIF +// +// Created by Alex Odawa on 09/07/2024. +// + +#import + +@interface NSObject (KIFCustomActionAdditions) + +- (UIAccessibilityCustomAction *)KIF_customActionWithName:(NSString *)name; + +@end + +@interface UIAccessibilityCustomAction (KIFAdditions) + +- (BOOL)KIF_activate; + +@end + diff --git a/Sources/KIF/Additions/UIAccessibilityCustomAction+KIFAdditions.m b/Sources/KIF/Additions/UIAccessibilityCustomAction+KIFAdditions.m new file mode 100644 index 00000000..2b6cea0a --- /dev/null +++ b/Sources/KIF/Additions/UIAccessibilityCustomAction+KIFAdditions.m @@ -0,0 +1,85 @@ +// +// UIAccessibilityCustomAction+KIFAdditions.m +// KIF Tests +// +// Created by Alex Odawa on 09/07/2024. +// + +#import +#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; +} + +- (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 diff --git a/Sources/KIF/Classes/KIFUIViewTestActor.h b/Sources/KIF/Classes/KIFUIViewTestActor.h index 4858ce4e..52673a84 100644 --- a/Sources/KIF/Classes/KIFUIViewTestActor.h +++ b/Sources/KIF/Classes/KIFUIViewTestActor.h @@ -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 /*! @@ -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 of the custom action. + */ +- (void)activateCustomActionWithName:(NSString *)name expectedResult:(BOOL)expectedResult; + + #pragma mark Waiting & Finding /*! diff --git a/Sources/KIF/Classes/KIFUIViewTestActor.m b/Sources/KIF/Classes/KIFUIViewTestActor.m index e27ed576..bdc80910 100644 --- a/Sources/KIF/Classes/KIFUIViewTestActor.m +++ b/Sources/KIF/Classes/KIFUIViewTestActor.m @@ -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" @@ -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 @@ -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; diff --git a/Test Host/TapViewController.m b/Test Host/TapViewController.m index 7ffbab72..ad087637 100644 --- a/Test Host/TapViewController.m +++ b/Test Host/TapViewController.m @@ -7,6 +7,7 @@ // #import +#import @interface TapViewController : UIViewController @property (weak, nonatomic) IBOutlet UISlider *slider; @@ -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 @@ -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 - - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {