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

Introducing tryFindingViewInFrame() to avoid scroll #1296

Merged
21 changes: 21 additions & 0 deletions KIF Tests/AccessibilityIdentifierTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down
43 changes: 43 additions & 0 deletions KIF Tests/AccessibilityIdentifierTests_ViewTestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
23 changes: 23 additions & 0 deletions Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
140 changes: 81 additions & 59 deletions Sources/KIF/Additions/UIAccessibilityElement-KIFAdditions.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -105,19 +115,24 @@ + (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 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];
}
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;
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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]) {
Expand Down Expand Up @@ -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, \
Expand All @@ -270,15 +292,15 @@ + (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;
}

UIAccessibilityElement *match = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^BOOL (UIAccessibilityElement *element) {
return [aPredicate evaluateWithObject:element];
}];
} disableScroll:scrollDisabled];
if (match) {
return aPredicate;
}
Expand All @@ -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;
}
Expand Down
7 changes: 7 additions & 0 deletions Sources/KIF/Additions/UIApplication-KIFAdditions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading