From 29ac3e1516b6e115d446dd4be97fa9678a2aea9f Mon Sep 17 00:00:00 2001 From: Simone Orazio Scionti <44984797+SimoncelloCT@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:08:27 +0200 Subject: [PATCH] Introducing tryFindingViewInFrame() to avoid scroll (#1296) Introduce `-[KIFUITestActor tryFindingViewInFrame]` and `-[KIFUIViewTestActor usingCurrentFrame]`. This can be used to avoid scrolling when looking for a view. This can be helpful where implicit table/collection view scrolling that happens when attempting to find UI elements triggers unwanted behavior (pagination fetch, abort pull to refresh, etc). This includes a behavior change in `isTappable` where it no longer automatically treats an element as tappable just for having a tap gesture recognizer on it, even though it might be occluded. See this discussion for more context: https://github.com/kif-framework/KIF/pull/1296#discussion_r1696013400 --------- Authored-by: Simone Scionti --- KIF Tests/AccessibilityIdentifierTests.m | 21 +++ ...cessibilityIdentifierTests_ViewTestActor.m | 43 ++++++ .../UIAccessibilityElement-KIFAdditions.h | 23 +++ .../UIAccessibilityElement-KIFAdditions.m | 140 ++++++++++-------- .../Additions/UIApplication-KIFAdditions.h | 7 + .../Additions/UIApplication-KIFAdditions.m | 7 +- Sources/KIF/Additions/UIView-KIFAdditions.h | 8 + Sources/KIF/Additions/UIView-KIFAdditions.m | 37 +++-- .../Classes/KIFUITestActor-ConditionalTests.h | 2 + .../Classes/KIFUITestActor-ConditionalTests.m | 7 +- Sources/KIF/Classes/KIFUITestActor.h | 13 ++ Sources/KIF/Classes/KIFUITestActor.m | 7 +- Sources/KIF/Classes/KIFUIViewTestActor.h | 8 + Sources/KIF/Classes/KIFUIViewTestActor.m | 12 +- .../KIFUITestActor-IdentifierTests.h | 7 + .../KIFUITestActor-IdentifierTests.m | 8 + .../Base.lproj/MainStoryboard.storyboard | 40 ++++- 17 files changed, 306 insertions(+), 84 deletions(-) diff --git a/KIF Tests/AccessibilityIdentifierTests.m b/KIF Tests/AccessibilityIdentifierTests.m index 92f23248f..7201e20b6 100644 --- a/KIF Tests/AccessibilityIdentifierTests.m +++ b/KIF Tests/AccessibilityIdentifierTests.m @@ -96,6 +96,27 @@ - (void)testTryFindingViewWithAccessibilityIdentifier } } +- (void)testTryFindingOutOfFrameViewWithAccessibilityIdentifier +{ + if (![tester tryFindingViewWithAccessibilityIdentifier:@"outOfFrameView"]) + { + [tester fail]; + } +} + +- (void)testTryFindingViewInFrameWithAccessibilityIdentifier +{ + if (![tester tryFindingViewInFrameWithAccessibilityIdentifier:@"idGreeting"]) + { + [tester fail]; + } + + if ([tester tryFindingViewInFrameWithAccessibilityIdentifier:@"outOfFrameView"]) + { + [tester fail]; + } +} + - (void) testTappingStepperIncrement { UILabel *uiLabel = (UILabel *)[tester waitForViewWithAccessibilityIdentifier:@"tapViewController.stepperValue"]; diff --git a/KIF Tests/AccessibilityIdentifierTests_ViewTestActor.m b/KIF Tests/AccessibilityIdentifierTests_ViewTestActor.m index dc83f2305..ce96ff3a8 100644 --- a/KIF Tests/AccessibilityIdentifierTests_ViewTestActor.m +++ b/KIF Tests/AccessibilityIdentifierTests_ViewTestActor.m @@ -73,6 +73,49 @@ - (void)testClearingAndEnteringTextIntoViewWithAccessibilityLabel [[viewTester usingIdentifier:@"idGreeting"] clearAndEnterText:@"Yo"]; } +- (void)testTryFindingOutOfFrameViewWithAccessibilityIdentifier +{ + if (![[viewTester usingIdentifier:@"outOfFrameView"] tryFindingView]) + { + [tester fail]; + } +} + +- (void)testTryFindingViewInFrameWithAccessibilityIdentifier +{ + if (![[[viewTester usingCurrentFrame] usingIdentifier:@"idGreeting"] tryFindingView]) + { + [tester fail]; + } + + if ([[[viewTester usingCurrentFrame] usingIdentifier:@"outOfFrameView"] tryFindingView]) + { + [tester fail]; + } +} + +- (void)testTryFindingTappableViewInFrameWithAccessibilityIdentifier +{ + if (![[[viewTester usingCurrentFrame] usingIdentifier:@"idGreeting"] tryFindingTappableView]) + { + [tester fail]; + } + + if ([[[viewTester usingCurrentFrame] usingIdentifier:@"outOfFrameView"] tryFindingTappableView]) + { + [tester fail]; + } +} + +- (void)testTryFindingOccludedTappableViewInFrameWithAccessibilityIdentifier +{ + + if ([[[viewTester usingCurrentFrame] usingIdentifier:@"occludedView"] tryFindingTappableView]) + { + [tester fail]; + } +} + - (void)afterEach { [[[viewTester usingLabel:@"Test Suite"] usingTraits:UIAccessibilityTraitButton] tap]; diff --git a/Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.h b/Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.h index 501859bc0..608ce9156 100644 --- a/Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.h +++ b/Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.h @@ -92,6 +92,17 @@ */ + (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(out UIView **)foundView withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable error:(out NSError **)error; +/*! + @abstract Finds an accessibility element and view where the element passes the predicate, optionally passing a tappability test. + @param foundElement The found accessibility element or @c nil if the method returns @c NO. Can be @c NULL. + @param foundView The first matching view for @c foundElement as determined by the accessibility API or @c nil if the view is hidden or fails the tappability test. Can be @c NULL. + @param predicate The predicate to test the accessibility element on. + @param error A reference to an error object to be populated when no matching element or view is found. Can be @c NULL. + @param scrollDisabled Disable scroll performing the search only in the current visible frame. + @result @c YES if the element and view were found. Otherwise @c NO. + */ ++ (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(out UIView **)foundView withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable error:(out NSError **)error disableScroll:(BOOL)scrollDisabled; + /*! @abstract Finds and attempts to make visible a view for a given accessibility element. @discussion If the element is found, off screen, and is inside a scroll view, this method will attempt to programmatically scroll the view onto the screen before performing any logic as to if the view is tappable. @@ -103,6 +114,18 @@ */ + (UIView *)viewContainingAccessibilityElement:(UIAccessibilityElement *)element tappable:(BOOL)mustBeTappable error:(NSError **)error; +/*! + @abstract Finds and attempts to make visible a view for a given accessibility element. + @discussion If the element is found, off screen, and is inside a scroll view, this method will attempt to programmatically scroll the view onto the screen before performing any logic as to if the view is tappable. + + @param element The accessibility element. + @param mustBeTappable If @c YES, a tappability test will be performed. + @param error A reference to an error object to be populated when no element is found. Can be @c NULL. + @param scrollDisabled Disable scroll performing the search only in the current visible frame. + @return The first matching view as determined by the accessibility API or nil if the view is hidden or fails the tappability test. + */ ++ (UIView *)viewContainingAccessibilityElement:(UIAccessibilityElement *)element tappable:(BOOL)mustBeTappable error:(NSError **)error disableScroll:(BOOL)scrollDisabled; + /*! @abstract Returns a human readable string of UIAccessiblityTrait names, derived from UIAccessibilityConstants.h. @param traits The accessibility traits to list. diff --git a/Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.m b/Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.m index 1dede63cf..b26d83569 100644 --- a/Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.m +++ b/Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.m @@ -58,13 +58,18 @@ + (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(o } + (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(out UIView **)foundView withLabel:(NSString *)label value:(NSString *)value traits:(UIAccessibilityTraits)traits fromRootView:(UIView *)fromView tappable:(BOOL)mustBeTappable error:(out NSError **)error +{ + return [self accessibilityElement:foundElement view:foundView withLabel:label value:value traits:traits fromRootView:fromView tappable:mustBeTappable error:error disableScroll:NO]; +} + ++ (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(out UIView **)foundView withLabel:(NSString *)label value:(NSString *)value traits:(UIAccessibilityTraits)traits fromRootView:(UIView *)fromView tappable:(BOOL)mustBeTappable error:(out NSError **)error disableScroll:(BOOL)scrollDisabled { UIAccessibilityElement *element = [self accessibilityElementWithLabel:label value:value traits:traits fromRootView:fromView error:error]; if (!element) { return NO; } - UIView *view = [self viewContainingAccessibilityElement:element tappable:mustBeTappable error:error]; + UIView *view = [self viewContainingAccessibilityElement:element tappable:mustBeTappable error:error disableScroll:scrollDisabled]; if (!view) { return NO; } @@ -82,19 +87,24 @@ + (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(o } + (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(out UIView **)foundView withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable error:(out NSError **)error; +{ + return [self accessibilityElement:foundElement view:foundView withElementMatchingPredicate:predicate tappable:mustBeTappable error:error disableScroll:NO]; +} + ++ (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(out UIView **)foundView withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable error:(out NSError **)error disableScroll:(BOOL)scrollDisabled; { UIAccessibilityElement *element = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^BOOL(UIAccessibilityElement *element) { return [predicate evaluateWithObject:element]; - }]; - + } disableScroll: scrollDisabled]; + if (!element) { if (error) { - *error = [self errorForFailingPredicate:predicate]; + *error = [self errorForFailingPredicate:predicate disableScroll:scrollDisabled]; } return NO; } - UIView *view = [UIAccessibilityElement viewContainingAccessibilityElement:element tappable:mustBeTappable error:error]; + UIView *view = [UIAccessibilityElement viewContainingAccessibilityElement:element tappable:mustBeTappable error:error disableScroll:scrollDisabled]; if (!view) { return NO; } @@ -105,11 +115,16 @@ + (BOOL)accessibilityElement:(out UIAccessibilityElement **)foundElement view:(o } + (BOOL)accessibilityElement:(out UIAccessibilityElement *__autoreleasing *)foundElement view:(out UIView *__autoreleasing *)foundView withElementMatchingPredicate:(NSPredicate *)predicate fromRootView:(UIView *)fromView tappable:(BOOL)mustBeTappable error:(out NSError *__autoreleasing *)error +{ + return [self accessibilityElement:foundElement view:foundView withElementMatchingPredicate:predicate fromRootView:fromView tappable:mustBeTappable error:error disableScroll:NO]; +} + ++ (BOOL)accessibilityElement:(out UIAccessibilityElement *__autoreleasing *)foundElement view:(out UIView *__autoreleasing *)foundView withElementMatchingPredicate:(NSPredicate *)predicate fromRootView:(UIView *)fromView tappable:(BOOL)mustBeTappable error:(out NSError *__autoreleasing *)error disableScroll:(BOOL)scrollDisabled { UIAccessibilityElement *element = [fromView accessibilityElementMatchingBlock:^BOOL(UIAccessibilityElement *element) { return [predicate evaluateWithObject:element]; - }]; - + } disableScroll:scrollDisabled]; + if (!element) { if (error) { *error = [NSError KIFErrorWithFormat:@"Could not find view matching: %@", predicate]; @@ -117,7 +132,7 @@ + (BOOL)accessibilityElement:(out UIAccessibilityElement *__autoreleasing *)foun return NO; } - UIView *view = [UIAccessibilityElement viewContainingAccessibilityElement:element tappable:mustBeTappable error:error]; + UIView *view = [UIAccessibilityElement viewContainingAccessibilityElement:element tappable:mustBeTappable error:error disableScroll:scrollDisabled]; if (!view) { return NO; } @@ -163,6 +178,11 @@ + (UIAccessibilityElement *)accessibilityElementWithLabel:(NSString *)label valu } + (UIView *)viewContainingAccessibilityElement:(UIAccessibilityElement *)element tappable:(BOOL)mustBeTappable error:(NSError **)error; +{ + return [self viewContainingAccessibilityElement:element tappable:mustBeTappable error:error disableScroll:NO]; +} + ++ (UIView *)viewContainingAccessibilityElement:(UIAccessibilityElement *)element tappable:(BOOL)mustBeTappable error:(NSError **)error disableScroll:(BOOL)scrollDisabled; { // Small safety mechanism. If someone calls this method after a failing call to accessibilityElementWithLabel:..., we don't want to wipe out the error message. if (!element && error && *error) { @@ -178,60 +198,62 @@ + (UIView *)viewContainingAccessibilityElement:(UIAccessibilityElement *)element return nil; } - // Scroll the view (and superviews) to be visible if necessary - UIView *superview = view; - while (superview) { - if ([superview isKindOfClass:[UIScrollView class]]) { - UIScrollView *scrollView = (UIScrollView *)superview; - BOOL animationEnabled = [KIFUITestActor testActorAnimationsEnabled]; - - if (((UIAccessibilityElement *)view == element) && ![view isKindOfClass:[UITableViewCell class]]) { - [scrollView scrollViewToVisible:view animated:animationEnabled]; - } else { - if ([view isKindOfClass:[UITableViewCell class]] && [scrollView.superview isKindOfClass:[UITableView class]]) { - UITableViewCell *cell = (UITableViewCell *)view; - UITableView *tableView = (UITableView *)scrollView.superview; - NSIndexPath *indexPath = [tableView indexPathForCell:cell]; - [tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:animationEnabled]; + if(!scrollDisabled) { + // Scroll the view (and superviews) to be visible if necessary + UIView *superview = view; + while (superview) { + if ([superview isKindOfClass:[UIScrollView class]]) { + UIScrollView *scrollView = (UIScrollView *)superview; + BOOL animationEnabled = [KIFUITestActor testActorAnimationsEnabled]; + + if (((UIAccessibilityElement *)view == element) && ![view isKindOfClass:[UITableViewCell class]]) { + [scrollView scrollViewToVisible:view animated:animationEnabled]; } else { - CGRect elementFrame = [view.window convertRect:element.accessibilityFrame toView:scrollView]; - CGRect visibleRect = CGRectMake(scrollView.contentOffset.x, scrollView.contentOffset.y, CGRectGetWidth(scrollView.bounds), CGRectGetHeight(scrollView.bounds)); - - UIEdgeInsets contentInset; -#ifdef __IPHONE_11_0 - if (@available(iOS 11.0, *)) { - contentInset = scrollView.adjustedContentInset; - } else { + if ([view isKindOfClass:[UITableViewCell class]] && [scrollView.superview isKindOfClass:[UITableView class]]) { + UITableViewCell *cell = (UITableViewCell *)view; + UITableView *tableView = (UITableView *)scrollView.superview; + NSIndexPath *indexPath = [tableView indexPathForCell:cell]; + [tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:animationEnabled]; + } else { + CGRect elementFrame = [view.window convertRect:element.accessibilityFrame toView:scrollView]; + CGRect visibleRect = CGRectMake(scrollView.contentOffset.x, scrollView.contentOffset.y, CGRectGetWidth(scrollView.bounds), CGRectGetHeight(scrollView.bounds)); + + UIEdgeInsets contentInset; + #ifdef __IPHONE_11_0 + if (@available(iOS 11.0, *)) { + contentInset = scrollView.adjustedContentInset; + } else { + contentInset = scrollView.contentInset; + } + #else contentInset = scrollView.contentInset; + #endif + visibleRect = UIEdgeInsetsInsetRect(visibleRect, contentInset); + + // Only call scrollRectToVisible if the element isn't already visible + // iOS 8 will sometimes incorrectly scroll table views so the element scrolls out of view + if (!CGRectContainsRect(visibleRect, elementFrame)) { + [scrollView scrollRectToVisible:elementFrame animated:animationEnabled]; } -#else - contentInset = scrollView.contentInset; -#endif - visibleRect = UIEdgeInsetsInsetRect(visibleRect, contentInset); - - // Only call scrollRectToVisible if the element isn't already visible - // iOS 8 will sometimes incorrectly scroll table views so the element scrolls out of view - if (!CGRectContainsRect(visibleRect, elementFrame)) { - [scrollView scrollRectToVisible:elementFrame animated:animationEnabled]; } - } - // Give the scroll view a small amount of time to perform the scroll. - CFTimeInterval delay = animationEnabled ? 0.3 : 0.05; - KIFRunLoopRunInModeRelativeToAnimationSpeed(kCFRunLoopDefaultMode, delay, false); - - // Because of cell reuse the first found view could be different after we scroll. - // Find the same element's view to ensure that after we have scrolled we get the same view back. - UIView *checkedView = [UIAccessibilityElement viewContainingAccessibilityElement:element]; - // intentionally doing a memory address check vs a isEqual check because - // we want to ensure that the memory address hasn't changed after scroll. - if(view != checkedView) { - view = checkedView; + // Give the scroll view a small amount of time to perform the scroll. + CFTimeInterval delay = animationEnabled ? 0.3 : 0.05; + KIFRunLoopRunInModeRelativeToAnimationSpeed(kCFRunLoopDefaultMode, delay, false); + + // Because of cell reuse the first found view could be different after we scroll. + // Find the same element's view to ensure that after we have scrolled we get the same view back. + UIView *checkedView = [UIAccessibilityElement viewContainingAccessibilityElement:element]; + // intentionally doing a memory address check vs a isEqual check because + // we want to ensure that the memory address hasn't changed after scroll. + if(view != checkedView) { + view = checkedView; + } } } + + superview = superview.superview; } - - superview = superview.superview; } if ([[UIApplication sharedApplication] isIgnoringInteractionEvents]) { @@ -259,9 +281,9 @@ + (UIView *)viewContainingAccessibilityElement:(UIAccessibilityElement *)element return view; } -+ (NSError *)errorForFailingPredicate:(NSPredicate*)failingPredicate; ++ (NSError *)errorForFailingPredicate:(NSPredicate*)failingPredicate disableScroll:(BOOL) scrollDisabled; { - NSPredicate *closestMatchingPredicate = [self findClosestMatchingPredicate:failingPredicate]; + NSPredicate *closestMatchingPredicate = [self findClosestMatchingPredicate:failingPredicate disableScroll:scrollDisabled]; if (closestMatchingPredicate) { return [NSError KIFErrorWithFormat:@"Found element with %@ but not %@", \ closestMatchingPredicate.kifPredicateDescription, \ @@ -270,7 +292,7 @@ + (NSError *)errorForFailingPredicate:(NSPredicate*)failingPredicate; return [NSError KIFErrorWithFormat:@"Could not find element with %@", failingPredicate.kifPredicateDescription]; } -+ (NSPredicate *)findClosestMatchingPredicate:(NSPredicate *)aPredicate; ++ (NSPredicate *)findClosestMatchingPredicate:(NSPredicate *)aPredicate disableScroll:(BOOL) scrollDisabled; { if (!aPredicate) { return nil; @@ -278,7 +300,7 @@ + (NSPredicate *)findClosestMatchingPredicate:(NSPredicate *)aPredicate; UIAccessibilityElement *match = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^BOOL (UIAccessibilityElement *element) { return [aPredicate evaluateWithObject:element]; - }]; + } disableScroll:scrollDisabled]; if (match) { return aPredicate; } @@ -296,7 +318,7 @@ + (NSPredicate *)findClosestMatchingPredicate:(NSPredicate *)aPredicate; if (predicateMinusOneCondition) { UIAccessibilityElement *match = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^BOOL (UIAccessibilityElement *element) { return [predicateMinusOneCondition evaluateWithObject:element]; - }]; + } disableScroll:scrollDisabled]; if (match) { return predicateMinusOneCondition; } diff --git a/Sources/KIF/Additions/UIApplication-KIFAdditions.h b/Sources/KIF/Additions/UIApplication-KIFAdditions.h index fef032ef9..fbfa5f9af 100644 --- a/Sources/KIF/Additions/UIApplication-KIFAdditions.h +++ b/Sources/KIF/Additions/UIApplication-KIFAdditions.h @@ -49,6 +49,13 @@ CF_EXPORT SInt32 KIFRunLoopRunInModeRelativeToAnimationSpeed(CFStringRef mode, C @param matchBlock A block to be performed on each element to see if it passes. */ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock; +/*! + @abstract Finds an accessibility element where @c matchBlock returns @c YES, across all windows in the application starting at the fronmost window. + @discussion This method should be used if @c accessibilityElementWithLabel:accessibilityValue:traits: does not meet your requirements. For example, if you are searching for an element that begins with a pattern or if of a certain view type. + @param matchBlock A block to be performed on each element to see if it passes. + @param scrollDisabled Disable scroll performing the search only in the current visible frame. + */ +- (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock disableScroll:(BOOL)scrollDisabled; /*! @returns The window containing the keyboard or @c nil if the keyboard is not visible. diff --git a/Sources/KIF/Additions/UIApplication-KIFAdditions.m b/Sources/KIF/Additions/UIApplication-KIFAdditions.m index a1bdfc566..84a456cbc 100644 --- a/Sources/KIF/Additions/UIApplication-KIFAdditions.m +++ b/Sources/KIF/Additions/UIApplication-KIFAdditions.m @@ -62,9 +62,14 @@ - (UIAccessibilityElement *)accessibilityElementWithLabel:(NSString *)label acce } - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock; +{ + return [self accessibilityElementMatchingBlock:matchBlock disableScroll:NO]; +} + +- (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock disableScroll:(BOOL)scrollDisabled; { for (UIWindow *window in [self.windowsWithKeyWindow reverseObjectEnumerator]) { - UIAccessibilityElement *element = [window accessibilityElementMatchingBlock:matchBlock]; + UIAccessibilityElement *element = [window accessibilityElementMatchingBlock:matchBlock disableScroll:scrollDisabled]; if (element) { return element; } diff --git a/Sources/KIF/Additions/UIView-KIFAdditions.h b/Sources/KIF/Additions/UIView-KIFAdditions.h index 31995d818..a78ae5b48 100644 --- a/Sources/KIF/Additions/UIView-KIFAdditions.h +++ b/Sources/KIF/Additions/UIView-KIFAdditions.h @@ -31,6 +31,14 @@ typedef CGPoint KIFDisplacement; @result The matching accessibility element. */ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock; +/*! + @method accessibilityElementMatchingBlock: + @abstract Finds the descendent accessibility element that matches the conditions defined by the match block. + @param matchBlock A block which returns YES for matching elements. + @param scrollDisabled Disable scroll performing the search only in the current visible frame. + @result The matching accessibility element. + */ +- (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock disableScroll:(BOOL)scrollDisabled; - (UIView *)subviewWithClassNamePrefix:(NSString *)prefix __deprecated; - (NSArray *)subviewsWithClassNamePrefix:(NSString *)prefix; diff --git a/Sources/KIF/Additions/UIView-KIFAdditions.m b/Sources/KIF/Additions/UIView-KIFAdditions.m index 93bec4075..97e9c7076 100644 --- a/Sources/KIF/Additions/UIView-KIFAdditions.m +++ b/Sources/KIF/Additions/UIView-KIFAdditions.m @@ -132,7 +132,12 @@ + (BOOL)accessibilityElement:(UIAccessibilityElement *)element hasLabel:(NSStrin - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock; { - return [self accessibilityElementMatchingBlock:matchBlock notHidden:YES]; + return [self accessibilityElementMatchingBlock:matchBlock disableScroll:NO]; +} + +- (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock disableScroll:(BOOL)scrollDisabled; +{ + return [self accessibilityElementMatchingBlock:matchBlock notHidden:YES disableScroll:scrollDisabled]; } - (BOOL)isPossiblyVisibleInWindow @@ -176,7 +181,7 @@ - (BOOL)isPossiblyVisibleInWindow } } -- (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock notHidden:(BOOL)notHidden; +- (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessibilityElement *))matchBlock notHidden:(BOOL)notHidden disableScroll:(BOOL)scrollDisabled; { if (notHidden && self.hidden) { return nil; @@ -204,11 +209,12 @@ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessi // rather than the real subviews it contains. We want the real views if possible. // UITableViewCell is such an offender. for (UIView *view in [self.subviews reverseObjectEnumerator]) { - UIAccessibilityElement *element = [view accessibilityElementMatchingBlock:matchBlock]; - + + UIAccessibilityElement *element = [view accessibilityElementMatchingBlock:matchBlock disableScroll:scrollDisabled]; + if (!element) { UIView* fallbackView = [self tryGetiOS16KeyboardFallbackViewFromParentView:view]; - element = [fallbackView accessibilityElementMatchingBlock:matchBlock]; + element = [fallbackView accessibilityElementMatchingBlock:matchBlock disableScroll:scrollDisabled]; } if (!element) { @@ -220,7 +226,7 @@ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessi if ([viewForElement isTappableInRect:accessibilityFrame]) { return element; - } else { + } else if (!scrollDisabled || [viewForElement isVisibleInWindowFrame]){ matchingButOccludedElement = element; } } @@ -239,7 +245,7 @@ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessi if ([viewForElement isTappableInRect:accessibilityFrame]) { return element; - } else { + } else if (!scrollDisabled || [viewForElement isVisibleInWindowFrame]){ matchingButOccludedElement = element; continue; } @@ -272,9 +278,11 @@ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessi } } - if (!matchingButOccludedElement && self.window) { + if (!scrollDisabled && !matchingButOccludedElement && self.window) { CGPoint scrollContentOffset = {-1.0, -1.0}; UIScrollView *scrollView = nil; + + // Table view - scroll to non visible cells if ([self isKindOfClass:[UITableView class]]) { NSString * subViewName = nil; //special case for UIPickerView (which has a private class UIPickerTableView) @@ -325,7 +333,7 @@ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessi [tableView scrollRectToVisible:sectionRect animated:NO]; UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; - UIAccessibilityElement *element = [cell accessibilityElementMatchingBlock:matchBlock notHidden:NO]; + UIAccessibilityElement *element = [cell accessibilityElementMatchingBlock:matchBlock notHidden:NO disableScroll:NO]; // Skip this cell if it isn't the one we're looking for if (!element) { @@ -335,7 +343,7 @@ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessi // Note: using KIFRunLoopRunInModeRelativeToAnimationSpeed here may cause tests to stall CFRunLoopRunInMode(UIApplicationCurrentRunMode, delay, false); - return [self accessibilityElementMatchingBlock:matchBlock]; + return [self accessibilityElementMatchingBlock:matchBlock disableScroll:NO]; } } @@ -373,8 +381,8 @@ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessi [collectionView.delegate collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; } - UIAccessibilityElement *element = [cell accessibilityElementMatchingBlock:matchBlock notHidden:NO]; - + UIAccessibilityElement *element = [cell accessibilityElementMatchingBlock:matchBlock notHidden:NO disableScroll:NO]; + // Remove the cell from the collection view so that it doesn't stick around [cell removeFromSuperview]; @@ -392,7 +400,7 @@ - (UIAccessibilityElement *)accessibilityElementMatchingBlock:(BOOL(^)(UIAccessi CFRunLoopRunInMode(UIApplicationCurrentRunMode, 0.5, false); // Now try finding the element again - return [self accessibilityElementMatchingBlock:matchBlock]; + return [self accessibilityElementMatchingBlock:matchBlock disableScroll:NO]; } } } @@ -865,8 +873,7 @@ - (BOOL)isProbablyTappable // Is this view currently on screen? - (BOOL)isTappable; { - return ([self hasTapGestureRecognizerAndIsControlEnabled] || - [self isTappableInRect:self.bounds]); + return [self isTappableInRect:self.bounds]; } - (BOOL)hasTapGestureRecognizerAndIsControlEnabled diff --git a/Sources/KIF/Classes/KIFUITestActor-ConditionalTests.h b/Sources/KIF/Classes/KIFUITestActor-ConditionalTests.h index 10cdc3349..344355a43 100644 --- a/Sources/KIF/Classes/KIFUITestActor-ConditionalTests.h +++ b/Sources/KIF/Classes/KIFUITestActor-ConditionalTests.h @@ -65,4 +65,6 @@ - (BOOL)tryFindingAccessibilityElement:(out UIAccessibilityElement **)element view:(out UIView **)view withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable error:(out NSError **)error; +- (BOOL)tryFindingAccessibilityElement:(out UIAccessibilityElement **)element view:(out UIView **)view withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable error:(out NSError **)error disableScroll:(BOOL) scrollDisabled; + @end diff --git a/Sources/KIF/Classes/KIFUITestActor-ConditionalTests.m b/Sources/KIF/Classes/KIFUITestActor-ConditionalTests.m index 9ad980ec7..a42438f8f 100644 --- a/Sources/KIF/Classes/KIFUITestActor-ConditionalTests.m +++ b/Sources/KIF/Classes/KIFUITestActor-ConditionalTests.m @@ -66,9 +66,14 @@ - (BOOL)tryFindingAccessibilityElement:(out UIAccessibilityElement **)element vi } - (BOOL)tryFindingAccessibilityElement:(out UIAccessibilityElement * __autoreleasing *)element view:(out UIView * __autoreleasing *)view withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable error:(out NSError **)error +{ + return [self tryFindingAccessibilityElement:element view:view withElementMatchingPredicate:predicate tappable:mustBeTappable error:error disableScroll:NO]; +} + +- (BOOL)tryFindingAccessibilityElement:(out UIAccessibilityElement * __autoreleasing *)element view:(out UIView * __autoreleasing *)view withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable error:(out NSError **)error disableScroll:(BOOL)scrollDisabled { return [self tryRunningBlock:^KIFTestStepResult(NSError *__autoreleasing *error) { - return [UIAccessibilityElement accessibilityElement:element view:view withElementMatchingPredicate:predicate tappable:mustBeTappable error:error] ? KIFTestStepResultSuccess : KIFTestStepResultWait; + return [UIAccessibilityElement accessibilityElement:element view:view withElementMatchingPredicate:predicate tappable:mustBeTappable error:error disableScroll:scrollDisabled] ? KIFTestStepResultSuccess : KIFTestStepResultWait; } complete:nil timeout:1.0 error:error]; } diff --git a/Sources/KIF/Classes/KIFUITestActor.h b/Sources/KIF/Classes/KIFUITestActor.h index 17dd4a397..5ab0aed20 100644 --- a/Sources/KIF/Classes/KIFUITestActor.h +++ b/Sources/KIF/Classes/KIFUITestActor.h @@ -239,6 +239,19 @@ typedef NS_ENUM(NSUInteger, KIFPullToRefreshTiming) { */ - (void)waitForAccessibilityElement:(UIAccessibilityElement **)element view:(out UIView **)view withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable; +/*! + @abstract Waits for an accessibility element and its containing view based on a predicate. + @discussion This method provides a more verbose API for achieving what is available in the waitForView/waitForTappableView family of methods, exposing both the found element and its containing view. The results can be used in other methods such as @c tapAccessibilityElement:inView: + + This method provides more flexability than @c waitForAccessibilityElement:view:withLabel:value:traits:tappable: but less precise error messages. This message will tell you why the method failed but not whether or not the element met some of the criteria. + @param element To be populated with the matching accessibility element when found. Can be NULL. + @param view To be populated with the matching view when found. Can be NULL. + @param predicate The predicate to match. + @param mustBeTappable If YES, only an element that can be tapped on will be returned. + @param scrollDisabled If YES, disable scroll performing the search only in the current visible frame. + */ +- (void)waitForAccessibilityElement:(UIAccessibilityElement **)element view:(out UIView **)view withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable disableScroll:(BOOL)scrollDisabled; + /*! @abstract Waits until an accessibility element is no longer present. @discussion The accessibility element matching the given predicate is found in the view hierarchy. If the element is found, then the step will attempt to wait until it isn't. Note that the associated view does not necessarily have to be visible on the screen, and may be behind another view or offscreen. Views with their hidden property set to YES are considered absent. diff --git a/Sources/KIF/Classes/KIFUITestActor.m b/Sources/KIF/Classes/KIFUITestActor.m index 8b753ae20..08c4ad4af 100644 --- a/Sources/KIF/Classes/KIFUITestActor.m +++ b/Sources/KIF/Classes/KIFUITestActor.m @@ -143,9 +143,14 @@ - (void)waitForAccessibilityElement:(UIAccessibilityElement *__autoreleasing *)e } - (void)waitForAccessibilityElement:(UIAccessibilityElement * __autoreleasing *)element view:(out UIView * __autoreleasing *)view withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable +{ + [self waitForAccessibilityElement:element view:view withElementMatchingPredicate:predicate tappable:mustBeTappable disableScroll:NO]; +} + +- (void)waitForAccessibilityElement:(UIAccessibilityElement * __autoreleasing *)element view:(out UIView * __autoreleasing *)view withElementMatchingPredicate:(NSPredicate *)predicate tappable:(BOOL)mustBeTappable disableScroll:(BOOL) scrollDisabled { [self runBlock:^KIFTestStepResult(NSError **error) { - return [UIAccessibilityElement accessibilityElement:element view:view withElementMatchingPredicate:predicate tappable:mustBeTappable error:error] ? KIFTestStepResultSuccess : KIFTestStepResultWait; + return [UIAccessibilityElement accessibilityElement:element view:view withElementMatchingPredicate:predicate tappable:mustBeTappable error:error disableScroll:scrollDisabled] ? KIFTestStepResultSuccess : KIFTestStepResultWait; }]; } diff --git a/Sources/KIF/Classes/KIFUIViewTestActor.h b/Sources/KIF/Classes/KIFUIViewTestActor.h index 52673a846..06d3b6538 100644 --- a/Sources/KIF/Classes/KIFUIViewTestActor.h +++ b/Sources/KIF/Classes/KIFUIViewTestActor.h @@ -39,6 +39,14 @@ extern NSString *const inputFieldTestString; */ - (instancetype)validateEnteredText:(BOOL)validateEnteredText; +/*! + @abstract Controls if we want to disable the automatic scroll while looking for an element. + @discussion This method limits the search in the current frame for each operation that searches for an element. + + @return The message reciever, these methods are intended to be chained together. + */ +- (instancetype)usingCurrentFrame; + #pragma mark - Searching for Accessibility Elements /*! diff --git a/Sources/KIF/Classes/KIFUIViewTestActor.m b/Sources/KIF/Classes/KIFUIViewTestActor.m index bdc80910f..5bf505479 100644 --- a/Sources/KIF/Classes/KIFUIViewTestActor.m +++ b/Sources/KIF/Classes/KIFUIViewTestActor.m @@ -25,6 +25,7 @@ @interface KIFUIViewTestActor () @property (nonatomic, strong, readonly) KIFUITestActor *actor; @property (nonatomic, strong, readwrite) NSPredicate *predicate; @property (nonatomic, assign) BOOL validateEnteredText; +@property (nonatomic, assign) BOOL disablingAutomaticScroll; @end @@ -40,6 +41,7 @@ - (instancetype)initWithFile:(NSString *)file line:(NSInteger)line delegate:(id< self = [super initWithFile:file line:line delegate:delegate]; NSParameterAssert(self); _validateEnteredText = YES; + _disablingAutomaticScroll = NO; return self; } @@ -51,6 +53,12 @@ - (instancetype)validateEnteredText:(BOOL)validateEnteredText; return self; } +- (instancetype)usingCurrentFrame; +{ + self.disablingAutomaticScroll = YES; + return self; +} + #pragma mark - Searching for Accessibility Elements - (instancetype)usingPredicate:(NSPredicate *)predicate; @@ -636,11 +644,11 @@ - (KIFUIObject *)_predicateSearchWithRequiresMatch:(BOOL)requiresMatch mustBeTap __block UIAccessibilityElement *foundElement = nil; if (requiresMatch) { - [self.actor waitForAccessibilityElement:&foundElement view:&foundView withElementMatchingPredicate:self.predicate tappable:tappable]; + [self.actor waitForAccessibilityElement:&foundElement view:&foundView withElementMatchingPredicate:self.predicate tappable:tappable disableScroll:self.disablingAutomaticScroll]; } else { NSError *error; [self tryRunningBlock:^KIFTestStepResult(NSError **error) { - KIFTestWaitCondition([self.actor tryFindingAccessibilityElement:&foundElement view:&foundView withElementMatchingPredicate:self.predicate tappable:tappable error:error], error, @"Waiting on view matching %@", self.predicate.kifPredicateDescription); + KIFTestWaitCondition([self.actor tryFindingAccessibilityElement:&foundElement view:&foundView withElementMatchingPredicate:self.predicate tappable:tappable error:error disableScroll:self.disablingAutomaticScroll], error, @"Waiting on view matching %@", self.predicate.kifPredicateDescription); return KIFTestStepResultSuccess; } complete:nil timeout:1.0 error:&error]; } diff --git a/Sources/KIF/IdentifierTests/KIFUITestActor-IdentifierTests.h b/Sources/KIF/IdentifierTests/KIFUITestActor-IdentifierTests.h index 7da13b44c..f841ca2d4 100644 --- a/Sources/KIF/IdentifierTests/KIFUITestActor-IdentifierTests.h +++ b/Sources/KIF/IdentifierTests/KIFUITestActor-IdentifierTests.h @@ -85,6 +85,13 @@ */ - (BOOL) tryFindingViewWithAccessibilityIdentifier:(NSString *) accessibilityIdentifier; +/*! + @abstract returns YES or NO if the element is visible in the current frame (without scrolling). + @discussion if the element described by the accessibility identifier is visible, the method returns true. + @param accessibilityIdentifier The accessibility identifier of the element to query for + */ +- (BOOL) tryFindingViewInFrameWithAccessibilityIdentifier:(NSString *) accessibilityIdentifier; + /*! @abstract Swipes a particular view in the view hierarchy in the given direction. @discussion This step will get the view with the specified accessibility identifier and swipe the screen in the given direction from the view's center. diff --git a/Sources/KIF/IdentifierTests/KIFUITestActor-IdentifierTests.m b/Sources/KIF/IdentifierTests/KIFUITestActor-IdentifierTests.m index dbf6d7d2c..aa06f5d92 100644 --- a/Sources/KIF/IdentifierTests/KIFUITestActor-IdentifierTests.m +++ b/Sources/KIF/IdentifierTests/KIFUITestActor-IdentifierTests.m @@ -191,6 +191,14 @@ - (void)waitForFirstResponderWithAccessibilityIdentifier:(NSString *)accessibili }]; } +- (BOOL) tryFindingViewInFrameWithAccessibilityIdentifier:(NSString *)accessibilityIdentifier +{ + NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { + return [[evaluatedObject accessibilityIdentifier] isEqualToString:accessibilityIdentifier]; + }]; + return [UIAccessibilityElement accessibilityElement:nil view:nil withElementMatchingPredicate:predicate tappable:NO error:nil disableScroll:YES]; +} + - (BOOL) tryFindingViewWithAccessibilityIdentifier:(NSString *)accessibilityIdentifier { NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { diff --git a/Test Host/Base.lproj/MainStoryboard.storyboard b/Test Host/Base.lproj/MainStoryboard.storyboard index bfb6a6862..9f91fdb4c 100644 --- a/Test Host/Base.lproj/MainStoryboard.storyboard +++ b/Test Host/Base.lproj/MainStoryboard.storyboard @@ -1,9 +1,9 @@ - + - + @@ -1537,6 +1537,21 @@ + + + + + + + + + + + + + + + @@ -1573,6 +1588,21 @@ + + + + + + + + + + + + + + +