Skip to content

Commit

Permalink
Widen firstResponder searches to all windows
Browse files Browse the repository at this point in the history
Previously searches for `UIWindow.firstResponder` were limited to the
`UIApplication.keyWindow`. In practice, it is very possible for users to
add additional valid windows that are not the key window and have first
responders. To accommodate those cases, this change broadens the search
for a first responder of an application to an array of first responders
found in `UIApplication.windowsWithKeyWindow`.
  • Loading branch information
zradke committed Mar 20, 2020
1 parent ecfc02d commit c0cebe1
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 48 deletions.
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
80 changes: 56 additions & 24 deletions Classes/KIFUITestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -471,21 +471,27 @@ - (void)enterTextIntoCurrentFirstResponder:(NSString *)text fallbackView:(UIView
{
if (![KIFTypist enterCharacter:characterString]) {
// Attempt to cheat if we couldn't find the character
UIView * fallback = fallbackView;
if (!fallback) {
UIResponder *firstResponder = [[[UIApplication sharedApplication] keyWindow] firstResponder];
NSMutableArray<UIView *> *fallbackViews = [NSMutableArray array];

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

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];
for (UIView *fallback in fallbackViews) {
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]];
return;
}
}

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

Expand Down Expand Up @@ -551,9 +557,10 @@ - (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];
for (UIResponder *firstResponder in [[UIApplication sharedApplication] firstResponders]) {
if ([firstResponder isKindOfClass:[UIView class]]) {
[self clearTextFromElement:(UIAccessibilityElement *)firstResponder inView:(UIView *)firstResponder];
}
}
}
}
Expand Down Expand Up @@ -1182,13 +1189,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;
UIResponder *foundResponder = nil;
for (UIResponder *firstResponder in [[UIApplication sharedApplication] firstResponders]) {
foundResponder = firstResponder;

if ([foundResponder isKindOfClass:NSClassFromString(@"UISearchBarTextField")]) {
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 accessibility label for first responder to be '%@', got '%@'", label, [foundResponder accessibilityLabel]);

return KIFTestStepResultSuccess;
}];
Expand All @@ -1197,13 +1215,27 @@ - (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;
UIResponder *foundResponder = nil;
for (UIResponder *firstResponder in [[UIApplication sharedApplication] firstResponders]) {
foundResponder = firstResponder;

if (foundResponder.accessibilityLabel == label || [foundResponder.accessibilityLabel isEqualToString:label]) {
didMatchLabel = YES;
}
if (foundResponder.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 accessibility label for first responder to be '%@', got '%@'", label, foundResponder.accessibilityLabel);
KIFTestWaitCondition(didMatchTraits, error, @"Found first responder with accessibility label, but not traits. First responder: %@", foundResponder);

return KIFTestStepResultSuccess;
}];
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
13 changes: 11 additions & 2 deletions Classes/KIFUIViewTestActor.m
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,18 @@ - (void)waitToBecomeTappable;
- (void)waitToBecomeFirstResponder;
{
[self runBlock:^KIFTestStepResult(NSError **error) {
UIResponder *firstResponder = [[[UIApplication sharedApplication] keyWindow] firstResponder];
BOOL didMatch = NO;
UIResponder *foundResponder = nil;
for (UIResponder *firstResponder in [[UIApplication sharedApplication] firstResponders]) {
foundResponder = firstResponder;

if ([self.predicate evaluateWithObject:foundResponder]) {
didMatch = YES;
break;
}
}

KIFTestWaitCondition([self.predicate evaluateWithObject:firstResponder], error, @"Expected first responder to match '%@', got '%@'", self.predicate, firstResponder);
KIFTestWaitCondition(didMatch, error, @"Expected first responder to match '%@', got '%@'", self.predicate.kifPredicateDescription, foundResponder);
return KIFTestStepResultSuccess;
}];
}
Expand Down
40 changes: 22 additions & 18 deletions IdentifierTests/KIFUITestActor-IdentifierTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import "KIFUITestActor-IdentifierTests.h"
#import "UIAccessibilityElement-KIFAdditions.h"
#import "NSError-KIFAdditions.h"
#import "UIApplication-KIFAdditions.h"
#import "UIWindow-KIFAdditions.h"

@implementation KIFUITestActor (IdentifierTests)
Expand Down Expand Up @@ -158,25 +159,28 @@ - (void)setValue:(float)value forSliderWithAccessibilityIdentifier:(NSString *)a
- (void)waitForFirstResponderWithAccessibilityIdentifier:(NSString *)accessibilityIdentifier
{
[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]]);
}
UIResponder<UIAccessibilityIdentification>* firstResponderIdentification = nil;
if ([firstResponder conformsToProtocol:@protocol(UIAccessibilityIdentification)])
{
firstResponderIdentification = (UIResponder<UIAccessibilityIdentification>*)firstResponder;
}
else
{
[self failWithError:[NSError KIFErrorWithFormat:@"First responder does not conform to UIAccessibilityIdentification %@", NSStringFromClass([firstResponder class])] stopTest:YES];

}
KIFTestWaitCondition([[firstResponderIdentification accessibilityIdentifier] isEqualToString:accessibilityIdentifier],
BOOL didMatch = NO;
NSString *foundIdentifier = nil;
for (UIResponder __strong *firstResponder in [[UIApplication sharedApplication] firstResponders]) {
if ([firstResponder isKindOfClass:NSClassFromString(@"UISearchBarTextField")]) {
do {
firstResponder = [(UIView *)firstResponder superview];
} while (firstResponder && ![firstResponder isKindOfClass:[UISearchBar class]]);
}

if ([firstResponder conformsToProtocol:@protocol(UIAccessibilityIdentification)]) {
foundIdentifier = [(UIResponder<UIAccessibilityIdentification> *)firstResponder accessibilityIdentifier];
}

if ([foundIdentifier isEqualToString:accessibilityIdentifier]) {
didMatch = YES;
break;
}
}

KIFTestWaitCondition(didMatch,
error, @"Expected accessibility identifier for first responder to be '%@', got '%@'",
accessibilityIdentifier, [firstResponderIdentification accessibilityIdentifier]);
accessibilityIdentifier, foundIdentifier);

return KIFTestStepResultSuccess;
}];
Expand Down

0 comments on commit c0cebe1

Please sign in to comment.