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

Widen firstResponder searches to all windows #1143

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Additions/UIApplication-KIFAdditions.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ CF_EXPORT SInt32 KIFRunLoopRunInModeRelativeToAnimationSpeed(CFStringRef mode, C
*/
- (NSArray *)windowsWithKeyWindow;

/// @discussion The first responders are ordered in the reverse order of @c -windowsWithKeyWindow
/// to return in order of nearest visually.
/// @returns All first responders in the application.
- (NSArray<UIResponder *> *)firstResponders;

/*!
The current Core Animation speed of the keyWindow's CALayer.
*/
Expand Down
15 changes: 15 additions & 0 deletions Additions/UIApplication-KIFAdditions.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// which Square, Inc. licenses this file to you.

#import "UIApplication-KIFAdditions.h"
#import "UIWindow-KIFAdditions.h"
#import "LoadableCategory.h"
#import "UIView-KIFAdditions.h"
#import "NSError-KIFAdditions.h"
Expand Down Expand Up @@ -99,6 +100,20 @@ - (UIWindow *)dimmingViewWindow;
return [self getWindowForSubviewClass:@"UIDimmingView"];
}

- (NSArray<UIResponder *> *)firstResponders;
{
NSMutableArray *responders = [NSMutableArray array];

for (UIWindow *window in [[self windowsWithKeyWindow] reverseObjectEnumerator]) {
UIResponder *responder = window.firstResponder;
if (responder) {
[responders addObject:responder];
}
}

return [responders copy];
}

- (UIWindow *)getWindowForSubviewClass:(NSString*)className;
{
for (UIWindow *window in self.windowsWithKeyWindow) {
Expand Down
4 changes: 2 additions & 2 deletions Classes/KIFUITestActor.h
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ typedef NS_ENUM(NSUInteger, KIFPullToRefreshTiming) {
/*!
@abstract Waits until a view or accessibility element is the first responder.
@discussion The first responder is found by searching the view hierarchy of the application's
main window and its accessibility label is compared to the given value. If they match, the
windows and its accessibility label is compared to the given value. If they match, the
step returns success else it will attempt to wait until they do.
@param label The accessibility label of the element to wait for.
*/
Expand All @@ -687,7 +687,7 @@ typedef NS_ENUM(NSUInteger, KIFPullToRefreshTiming) {
/*!
@abstract Waits until a view or accessibility element is the first responder.
@discussion The first responder is found by searching the view hierarchy of the application's
main window and its accessibility label is compared to the given value. If they match, the
windows and its accessibility label is compared to the given value. If they match, the
step returns success else it will attempt to wait until they do.
@param label The accessibility label of the element to wait for.
@param traits The accessibility traits of the element to wait for. Elements that do not include at least these traits are ignored.
Expand Down
176 changes: 111 additions & 65 deletions Classes/KIFUITestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ - (void)waitForAnimationsToFinishWithTimeout:(NSTimeInterval)timeout stabilizati
[self waitForTimeInterval:maximumWaitingTimeInterval relativeToAnimationSpeed:YES];
}
} else {

// Wait for the view to stabilize and give them a chance to start animations before we wait for them.
[self waitForTimeInterval:stabilizationTime relativeToAnimationSpeed:YES];
maximumWaitingTimeInterval -= stabilizationTime;
Expand Down Expand Up @@ -468,24 +468,38 @@ - (void)enterTextIntoCurrentFirstResponder:(NSString *)text fallbackView:(UIView
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock: ^(NSString *characterString,NSRange substringRange,NSRange enclosingRange,BOOL * stop)
{
{
if (![KIFTypist enterCharacter:characterString]) {
NSLog(@"KIF: Unable to find keyboard key for %@. Will attempt to insert manually.", characterString);

// Attempt to cheat if we couldn't find the character
UIView * fallback = fallbackView;
if (!fallback) {
UIResponder *firstResponder = [[[UIApplication sharedApplication] keyWindow] firstResponder];
NSMutableArray *fallbackViews = [NSMutableArray array];

if ([firstResponder isKindOfClass:[UIView class]]) {
fallback = (UIView *)firstResponder;
if (fallbackView) {
[fallbackViews addObject:fallbackView];
} else {
[fallbackViews addObjectsFromArray:[[UIApplication sharedApplication] firstResponders]];
}

for (id fallback in [fallbackViews copy]) {
if (![fallback isKindOfClass:[UITextField class]] &&
![fallback isKindOfClass:[UITextView class]] &&
![fallback isKindOfClass:[UISearchBar class]]) {
[fallbackViews removeObject:fallback];
}
}

if ([fallback isKindOfClass:[UITextField class]] || [fallback isKindOfClass:[UITextView class]] || [fallback isKindOfClass:[UISearchBar class]]) {
NSLog(@"KIF: Unable to find keyboard key for %@. Inserting manually.", characterString);
[(UITextField *)fallback setText:[[(UITextField *)fallback text] stringByAppendingString:characterString]];
} else {
[self failWithError:[NSError KIFErrorWithFormat:@"Failed to find key for character \"%@\"", characterString] stopTest:YES];
UITextField *fallbackTextView = fallbackViews.firstObject;
if (fallbackTextView) {
if (fallbackViews.count > 1) {
NSLog(@"KIF: Found multiple possible fallback views for entering text: %@. Will use: %@", fallbackViews, fallbackTextView);
}

[fallbackTextView setText:[[fallbackTextView text] stringByAppendingString:characterString]];
return;
}

[self failWithError:[NSError KIFErrorWithFormat:@"Failed to find key for character \"%@\"", characterString] stopTest:YES];
}
}];

Expand Down Expand Up @@ -551,9 +565,17 @@ - (void)expectView:(UIView *)view toContainText:(NSString *)expectedResult
- (void)clearTextFromFirstResponder
{
@autoreleasepool {
UIView *firstResponder = (id)[[[UIApplication sharedApplication] keyWindow] firstResponder];
if ([firstResponder isKindOfClass:[UIView class]]) {
[self clearTextFromElement:(UIAccessibilityElement *)firstResponder inView:firstResponder];
NSArray *firstResponders = [[UIApplication sharedApplication] firstResponders];

for (UIResponder *firstResponder in firstResponders) {
if ([firstResponder isKindOfClass:[UIView class]]) {
if (firstResponders.count > 1) {
NSLog(@"KIF: Found multiple first responders while attempting to clear text: %@. Will use: %@.", firstResponders, firstResponder);
}

[self clearTextFromElement:(UIAccessibilityElement *)firstResponder inView:(UIView *)firstResponder];
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably should only do this from one element and not multiple.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean that I should revert this portion so it continues to only check UIApplication.keyWindow?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess what I mean is to bail from the loop after clearing the text from some first responder. Probably would want to do the same as above and output a warning if there are multiple matching fields and we're only clearing the contents of one of them.

break;
}
}
}
}
Expand Down Expand Up @@ -976,7 +998,7 @@ - (void)choosePhotoInAlbum:(NSString *)albumName atRow:(NSInteger)row column:(NS

// Tap the desired photo in the grid
// TODO: This currently only works for the first page of photos. It should scroll appropriately at some point.
UIAccessibilityElement *headerElt = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^(UIAccessibilityElement *element) {
UIAccessibilityElement *headerElt = [[UIApplication sharedApplication] accessibilityElementMatchingBlock:^(UIAccessibilityElement *element) {
return [NSStringFromClass(element.class) isEqual:@"UINavigationItemButtonView"];
}];
UIView* headerView = [UIAccessibilityElement viewContainingAccessibilityElement:headerElt];
Expand Down Expand Up @@ -1095,56 +1117,56 @@ - (void)swipeViewWithAccessibilityLabel:(NSString *)label value:(NSString *)valu
- (void)swipeAccessibilityElement:(UIAccessibilityElement *)element inView:(UIView *)viewToSwipe inDirection:(KIFSwipeDirection)direction
{
// The original version of this came from http://groups.google.com/group/kif-framework/browse_thread/thread/df3f47eff9f5ac8c

const NSUInteger kNumberOfPointsInSwipePath = 20;

// Within this method, all geometry is done in the coordinate system of the view to swipe.
CGRect elementFrame = [self elementFrameForElement:element andView:viewToSwipe];

CGPoint swipeStart = CGPointCenteredInRect(elementFrame);

KIFDisplacement swipeDisplacement = [self _displacementForSwipingInDirection:direction];

[viewToSwipe dragFromPoint:swipeStart displacement:swipeDisplacement steps:kNumberOfPointsInSwipePath];
}

- (void)pullToRefreshViewWithAccessibilityLabel:(NSString *)label
{
[self pullToRefreshViewWithAccessibilityLabel:label value:nil pullDownDuration:0 traits:UIAccessibilityTraitNone];
[self pullToRefreshViewWithAccessibilityLabel:label value:nil pullDownDuration:0 traits:UIAccessibilityTraitNone];
}

- (void)pullToRefreshViewWithAccessibilityLabel:(NSString *)label pullDownDuration:(KIFPullToRefreshTiming) pullDownDuration
{
[self pullToRefreshViewWithAccessibilityLabel:label value:nil pullDownDuration:pullDownDuration traits:UIAccessibilityTraitNone];
[self pullToRefreshViewWithAccessibilityLabel:label value:nil pullDownDuration:pullDownDuration traits:UIAccessibilityTraitNone];
}

- (void)pullToRefreshViewWithAccessibilityLabel:(NSString *)label value:(NSString *)value
{
[self pullToRefreshViewWithAccessibilityLabel:label value:value pullDownDuration:0 traits:UIAccessibilityTraitNone];
[self pullToRefreshViewWithAccessibilityLabel:label value:value pullDownDuration:0 traits:UIAccessibilityTraitNone];
}

- (void)pullToRefreshViewWithAccessibilityLabel:(NSString *)label value:(NSString *)value pullDownDuration:(KIFPullToRefreshTiming) pullDownDuration traits:(UIAccessibilityTraits)traits
{
UIView *viewToSwipe = nil;
UIAccessibilityElement *element = nil;
UIView *viewToSwipe = nil;
UIAccessibilityElement *element = nil;

[self waitForAccessibilityElement:&element view:&viewToSwipe withLabel:label value:value traits:traits tappable:YES];
[self waitForAccessibilityElement:&element view:&viewToSwipe withLabel:label value:value traits:traits tappable:YES];

[self pullToRefreshAccessibilityElement:element inView:viewToSwipe pullDownDuration:pullDownDuration];
[self pullToRefreshAccessibilityElement:element inView:viewToSwipe pullDownDuration:pullDownDuration];
}

- (void)pullToRefreshAccessibilityElement:(UIAccessibilityElement *)element inView:(UIView *)viewToSwipe pullDownDuration:(KIFPullToRefreshTiming) pullDownDuration
{
//Based on swipeAccessibilityElement
//Based on swipeAccessibilityElement

const NSUInteger kNumberOfPointsInSwipePath = pullDownDuration ? pullDownDuration : KIFPullToRefreshInAboutAHalfSecond;
const NSUInteger kNumberOfPointsInSwipePath = pullDownDuration ? pullDownDuration : KIFPullToRefreshInAboutAHalfSecond;

// Can handle only the touchable space.
CGRect elementFrame = [viewToSwipe convertRect:viewToSwipe.bounds toView:[UIApplication sharedApplication].keyWindow.rootViewController.view];
CGPoint swipeStart = CGPointCenteredInRect(elementFrame);
CGPoint swipeDisplacement = CGPointMake(CGRectGetMidX(elementFrame), CGRectGetMaxY(elementFrame));
CGPoint swipeDisplacement = CGPointMake(CGRectGetMidX(elementFrame), CGRectGetMaxY(elementFrame));

[viewToSwipe dragFromPoint:swipeStart displacement:swipeDisplacement steps:kNumberOfPointsInSwipePath];
[viewToSwipe dragFromPoint:swipeStart displacement:swipeDisplacement steps:kNumberOfPointsInSwipePath];
}

- (void)scrollViewWithAccessibilityLabel:(NSString *)label byFractionOfSizeHorizontal:(CGFloat)horizontalFraction vertical:(CGFloat)verticalFraction
Expand Down Expand Up @@ -1182,13 +1204,24 @@ - (void)scrollAccessibilityElement:(UIAccessibilityElement *)element inView:(UIV
- (void)waitForFirstResponderWithAccessibilityLabel:(NSString *)label
{
[self runBlock:^KIFTestStepResult(NSError **error) {
UIResponder *firstResponder = [[[UIApplication sharedApplication] keyWindow] firstResponder];
if ([firstResponder isKindOfClass:NSClassFromString(@"UISearchBarTextField")]) {
do {
firstResponder = [(UIView *)firstResponder superview];
} while (firstResponder && ![firstResponder isKindOfClass:[UISearchBar class]]);
BOOL didMatch = NO;
NSArray *firstResponders = [[UIApplication sharedApplication] firstResponders];

for (UIResponder *firstResponder in firstResponders) {
UIResponder *foundResponder = firstResponder;
if ([foundResponder isKindOfClass:NSClassFromString(@"UISearchBarTextField")]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

:eyeroll: at this special case being here, but not for the other method w/ traits matching below... (no action for you, just noticing this while reviewing)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I noticed a lot of inconsistencies around accessibilityLabel checking. Might warrant a deeper follow up to unify that logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

In general, KIFUITestActor is the old way of doing things and should eventually be deprecated. It probably isn't worth the time, as everything is already unified in KIFUIViewTestActor (new way).

do {
foundResponder = [(UIView *)foundResponder superview];
} while (foundResponder && ![foundResponder isKindOfClass:[UISearchBar class]]);
}

if (foundResponder.accessibilityLabel == label || [foundResponder.accessibilityLabel isEqualToString:label]) {
didMatch = YES;
break;
}
}
KIFTestWaitCondition([[firstResponder accessibilityLabel] isEqualToString:label], error, @"Expected accessibility label for first responder to be '%@', got '%@'", label, [firstResponder accessibilityLabel]);

KIFTestWaitCondition(didMatch, error, @"Expected to find a first responder with the accessibility label '%@', got: %@", label, firstResponders);

return KIFTestStepResultSuccess;
}];
Expand All @@ -1197,13 +1230,26 @@ - (void)waitForFirstResponderWithAccessibilityLabel:(NSString *)label
- (void)waitForFirstResponderWithAccessibilityLabel:(NSString *)label traits:(UIAccessibilityTraits)traits
{
[self runBlock:^KIFTestStepResult(NSError **error) {
UIResponder *firstResponder = [[[UIApplication sharedApplication] keyWindow] firstResponder];

NSString *foundLabel = firstResponder.accessibilityLabel;
BOOL didMatchLabel = NO;
BOOL didMatchTraits = NO;
NSArray *firstResponders = [[UIApplication sharedApplication] firstResponders];

for (UIResponder *firstResponder in firstResponders) {
if (firstResponder.accessibilityLabel == label || [firstResponder.accessibilityLabel isEqualToString:label]) {
didMatchLabel = YES;
}
if (firstResponder.accessibilityTraits & traits) {
didMatchTraits = YES;
}

if (didMatchLabel && didMatchTraits) {
break;
}
}

// foundLabel == label checks for the case where both are nil.
KIFTestWaitCondition(foundLabel == label || [foundLabel isEqualToString:label], error, @"Expected accessibility label for first responder to be '%@', got '%@'", label, foundLabel);
KIFTestWaitCondition(firstResponder.accessibilityTraits & traits, error, @"Found first responder with accessibility label, but not traits. First responder: %@", firstResponder);
KIFTestWaitCondition(didMatchLabel, error, @"Expected to find a first responder with the accessibility label '%@', got: %@", label, firstResponders);
KIFTestWaitCondition(didMatchTraits, error, @"Expected to find a first responder with accessibility traits, got: %@", firstResponders);

return KIFTestStepResultSuccess;
}];
Expand Down Expand Up @@ -1392,43 +1438,43 @@ - (void)deactivateAppForDuration:(NSTimeInterval)duration {

-(void) tapStepperWithAccessibilityLabel: (NSString *)accessibilityLabel increment: (KIFStepperDirection) stepperDirection
{
@autoreleasepool {
UIView *view = nil;
UIAccessibilityElement *element = nil;
[self waitForAccessibilityElement:&element view:&view withLabel:accessibilityLabel value:nil traits:UIAccessibilityTraitNone tappable:YES];
[self tapStepperWithAccessibilityElement:element increment:stepperDirection inView:view];
}
@autoreleasepool {
UIView *view = nil;
UIAccessibilityElement *element = nil;
[self waitForAccessibilityElement:&element view:&view withLabel:accessibilityLabel value:nil traits:UIAccessibilityTraitNone tappable:YES];
[self tapStepperWithAccessibilityElement:element increment:stepperDirection inView:view];
}
}

//inspired by http://www.raywenderlich.com/61419/ios-ui-testing-with-kif
- (void)tapStepperWithAccessibilityElement:(UIAccessibilityElement *)element increment: (KIFStepperDirection) stepperDirection inView:(UIView *)view
{
[self runBlock:^KIFTestStepResult(NSError **error) {
[self runBlock:^KIFTestStepResult(NSError **error) {

KIFTestWaitCondition(view.isUserInteractionActuallyEnabled, error, @"View is not enabled for interaction: %@", view);
KIFTestWaitCondition(view.isUserInteractionActuallyEnabled, error, @"View is not enabled for interaction: %@", view);

CGPoint stepperPointToTap = [self tappablePointInElement:element andView:view];

switch (stepperDirection)
{
case KIFStepperDirectionIncrement:
stepperPointToTap.x += CGRectGetWidth(view.frame) / 4;
break;
case KIFStepperDirectionDecrement:
stepperPointToTap.x -= CGRectGetWidth(view.frame) / 4;
break;
}
switch (stepperDirection)
{
case KIFStepperDirectionIncrement:
stepperPointToTap.x += CGRectGetWidth(view.frame) / 4;
break;
case KIFStepperDirectionDecrement:
stepperPointToTap.x -= CGRectGetWidth(view.frame) / 4;
break;
}

// This is mostly redundant of the test in _accessibilityElementWithLabel:
KIFTestWaitCondition(!isnan(stepperPointToTap.x), error, @"View is not tappable: %@", view);
[view tapAtPoint:stepperPointToTap];
// This is mostly redundant of the test in _accessibilityElementWithLabel:
KIFTestWaitCondition(!isnan(stepperPointToTap.x), error, @"View is not tappable: %@", view);
[view tapAtPoint:stepperPointToTap];

KIFTestCondition(![view canBecomeFirstResponder] || [view isDescendantOfFirstResponder], error, @"Failed to make the view into the first responder: %@", view);
KIFTestCondition(![view canBecomeFirstResponder] || [view isDescendantOfFirstResponder], error, @"Failed to make the view into the first responder: %@", view);

return KIFTestStepResultSuccess;
}];
return KIFTestStepResultSuccess;
}];

[self waitForAnimationsToFinish];
[self waitForAnimationsToFinish];
}

- (CGRect) elementFrameForElement:(UIAccessibilityElement *)element andView:(UIView *)view
Expand Down
4 changes: 2 additions & 2 deletions Classes/KIFUIViewTestActor.h
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ extern NSString *const inputFieldTestString;

/*!
@abstract Waits until a view or accessibility element matching the tester's search predicate is the first responder.
@discussion The first responder is found by searching the view hierarchy of the application's
main window and its accessibility label is compared to the given value. If they match, the
@discussion The first responder is found by searching the view hierarchy of all the application's
windows and its accessibility label is compared to the given value. If they match, the
step returns success else it will attempt to wait until they do.
*/
- (void)waitToBecomeFirstResponder;
Expand Down
12 changes: 10 additions & 2 deletions Classes/KIFUIViewTestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,17 @@ - (void)waitToBecomeTappable;
- (void)waitToBecomeFirstResponder;
{
[self runBlock:^KIFTestStepResult(NSError **error) {
UIResponder *firstResponder = [[[UIApplication sharedApplication] keyWindow] firstResponder];
BOOL didMatch = NO;
NSArray *firstResponders = [[UIApplication sharedApplication] firstResponders];

KIFTestWaitCondition([self.predicate evaluateWithObject:firstResponder], error, @"Expected first responder to match '%@', got '%@'", self.predicate, firstResponder);
for (UIResponder *firstResponder in firstResponders) {
if ([self.predicate evaluateWithObject:firstResponder]) {
didMatch = YES;
break;
}
}

KIFTestWaitCondition(didMatch, error, @"Expected to find a first responder matching '%@', got: %@", self.predicate.kifPredicateDescription, firstResponders);
return KIFTestStepResultSuccess;
}];
}
Expand Down
Loading