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

Extract keyboard automation code into its own KIFTypist class #141

Merged
merged 1 commit into from
Dec 7, 2012
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
138 changes: 2 additions & 136 deletions Classes/KIFTestStep.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#import "UITouch-KIFAdditions.h"
#import "UIView-KIFAdditions.h"
#import "UIWindow-KIFAdditions.h"
#import "KIFTypist.h"


static NSTimeInterval KIFTestStepDefaultTimeout = 10.0;
Expand All @@ -30,10 +31,6 @@ @interface KIFTestStep ()

+ (BOOL)_isUserInteractionEnabledForView:(UIView *)view;

+ (BOOL)_enterCharacter:(NSString *)characterString;
+ (BOOL)_enterCharacter:(NSString *)characterString history:(NSMutableDictionary *)history;
+ (BOOL)_enterCustomKeyboardCharacter:(NSString *)characterString;

+ (UIAccessibilityElement *)_accessibilityElementWithLabel:(NSString *)label accessibilityValue:(NSString *)value tappable:(BOOL)mustBeTappable traits:(UIAccessibilityTraits)traits error:(out NSError **)error;

typedef CGPoint KIFDisplacement;
Expand Down Expand Up @@ -383,7 +380,7 @@ + (id)stepToEnterText:(NSString *)text intoViewWithAccessibilityLabel:(NSString
for (NSUInteger characterIndex = 0; characterIndex < [text length]; characterIndex++) {
NSString *characterString = [text substringWithRange:NSMakeRange(characterIndex, 1)];

if (![self _enterCharacter:characterString]) {
if (![KIFTypist enterCharacter:characterString]) {
// Attempt to cheat if we couldn't find the character
if ([view isKindOfClass:[UITextField class]] || [view isKindOfClass:[UITextView class]]) {
NSLog(@"KIF: Unable to find keyboard key for %@. Inserting manually.", characterString);
Expand Down Expand Up @@ -773,137 +770,6 @@ + (NSString *)_representedKeyboardStringForCharacter:(NSString *)characterString
return characterString;
}

+ (BOOL)_enterCharacter:(NSString *)characterString;
{
return [self _enterCharacter:characterString history:[NSMutableDictionary dictionary]];
}

+ (BOOL)_enterCharacter:(NSString *)characterString history:(NSMutableDictionary *)history;
{
const NSTimeInterval keystrokeDelay = 0.05f;

// Each key on the keyboard does not have its own view, so we have to ask for the list of keys,
// find the appropriate one, and tap inside the frame of that key on the main keyboard view.
if (!characterString.length) {
return YES;
}

UIWindow *keyboardWindow = [[UIApplication sharedApplication] keyboardWindow];
UIView *keyboardView = [[keyboardWindow subviewsWithClassNamePrefix:@"UIKBKeyplaneView"] lastObject];

// If we didn't find the standard keyboard view, then we may have a custom keyboard
if (!keyboardView) {
return [self _enterCustomKeyboardCharacter:characterString];
}
id /*UIKBKeyplane*/ keyplane = [keyboardView valueForKey:@"keyplane"];
BOOL isShiftKeyplane = [[keyplane valueForKey:@"isShiftKeyplane"] boolValue];

NSMutableArray *unvisitedForKeyplane = [history objectForKey:[NSValue valueWithNonretainedObject:keyplane]];
if (!unvisitedForKeyplane) {
unvisitedForKeyplane = [NSMutableArray arrayWithObjects:@"More", @"International", nil];
if (!isShiftKeyplane) {
[unvisitedForKeyplane insertObject:@"Shift" atIndex:0];
}
[history setObject:unvisitedForKeyplane forKey:[NSValue valueWithNonretainedObject:keyplane]];
}

NSArray *keys = [keyplane valueForKey:@"keys"];

// Interpret control characters appropriately
characterString = [self _representedKeyboardStringForCharacter:characterString];

id keyToTap = nil;
id modifierKey = nil;
NSString *selectedModifierRepresentedString = nil;

while (YES) {
for (id/*UIKBKey*/ key in keys) {
NSString *representedString = [key valueForKey:@"representedString"];

// Find the key based on the key's represented string
if ([representedString isEqual:characterString]) {
keyToTap = key;
}

if (!modifierKey && unvisitedForKeyplane.count && [[unvisitedForKeyplane objectAtIndex:0] isEqual:representedString]) {
modifierKey = key;
selectedModifierRepresentedString = representedString;
[unvisitedForKeyplane removeObjectAtIndex:0];
}
}

if (keyToTap) {
break;
}

if (modifierKey) {
break;
}

if (!unvisitedForKeyplane.count) {
return NO;
}

// If we didn't find the key or the modifier, then this modifier must not exist on this keyboard. Remove it.
[unvisitedForKeyplane removeObjectAtIndex:0];
}

if (keyToTap) {
[keyboardView tapAtPoint:CGPointCenteredInRect([keyToTap frame])];
CFRunLoopRunInMode(kCFRunLoopDefaultMode, keystrokeDelay, false);

return YES;
}

// We didn't find anything, so try the symbols pane
if (modifierKey) {
[keyboardView tapAtPoint:CGPointCenteredInRect([modifierKey frame])];
CFRunLoopRunInMode(kCFRunLoopDefaultMode, keystrokeDelay, false);

// If we're back at a place we've been before, and we still have things to explore in the previous
id /*UIKBKeyplane*/ newKeyplane = [keyboardView valueForKey:@"keyplane"];
id /*UIKBKeyplane*/ previousKeyplane = [history valueForKey:@"previousKeyplane"];

if (newKeyplane == previousKeyplane) {
// Come back to the keyplane that we just tested so that we can try the other modifiers
NSMutableArray *previousKeyplaneHistory = [history objectForKey:[NSValue valueWithNonretainedObject:newKeyplane]];
[previousKeyplaneHistory insertObject:[history valueForKey:@"lastModifierRepresentedString"] atIndex:0];
} else {
[history setValue:keyplane forKey:@"previousKeyplane"];
[history setValue:selectedModifierRepresentedString forKey:@"lastModifierRepresentedString"];
}

return [self _enterCharacter:characterString history:history];
}

return NO;
}

+ (BOOL)_enterCustomKeyboardCharacter:(NSString *)characterString;
{
const NSTimeInterval keystrokeDelay = 0.05f;

if (!characterString.length) {
return YES;
}

characterString = [self _representedKeyboardStringForCharacter:characterString];

// For custom keyboards, use the classic methods of looking up views based on accessibility labels
UIWindow *keyboardWindow = [[UIApplication sharedApplication] keyboardWindow];

UIAccessibilityElement *element = [keyboardWindow accessibilityElementWithLabel:characterString];
if (!element) {
return NO;
}

UIView *view = [UIAccessibilityElement viewContainingAccessibilityElement:element];
CGRect keyFrame = [view.window convertRect:[element accessibilityFrame] toView:view];
[view tapAtPoint:CGPointCenteredInRect(keyFrame)];
CFRunLoopRunInMode(kCFRunLoopDefaultMode, keystrokeDelay, false);

return YES;
}

+ (UIAccessibilityElement *)_accessibilityElementWithLabel:(NSString *)label accessibilityValue:(NSString *)value tappable:(BOOL)mustBeTappable traits:(UIAccessibilityTraits)traits error:(out NSError **)error;
{
Expand Down
11 changes: 11 additions & 0 deletions Classes/KIFTypist.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// KIFTypist.h
// KIF
//
// Created by Pete Hodgson on 8/12/12.
//
//

@interface KIFTypist : NSObject
+ (BOOL)enterCharacter:(NSString *)characterString;
@end
165 changes: 165 additions & 0 deletions Classes/KIFTypist.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//
// KIFTypist.m
// KIF
//
// Created by Pete Hodgson on 8/12/12.
//
//

#import "KIFTypist.h"
#import "UIApplication-KIFAdditions.h"
#import "UIView-KIFAdditions.h"
#import "CGGeometry-KIFAdditions.h"
#import "UIAccessibilityElement-KIFAdditions.h"

@interface KIFTypist()
+ (NSString *)_representedKeyboardStringForCharacter:(NSString *)characterString;
+ (BOOL)_enterCharacter:(NSString *)characterString history:(NSMutableDictionary *)history;
+ (BOOL)_enterCustomKeyboardCharacter:(NSString *)characterString;
@end

@implementation KIFTypist

+ (NSString *)_representedKeyboardStringForCharacter:(NSString *)characterString;
{
// Interpret control characters appropriately
if ([characterString isEqual:@"\b"]) {
characterString = @"Delete";
}

return characterString;
}

+ (BOOL)enterCharacter:(NSString *)characterString;
{
return [self _enterCharacter:characterString history:[NSMutableDictionary dictionary]];
}

+ (BOOL)_enterCharacter:(NSString *)characterString history:(NSMutableDictionary *)history;
{
const NSTimeInterval keystrokeDelay = 0.05f;

// Each key on the keyboard does not have its own view, so we have to ask for the list of keys,
// find the appropriate one, and tap inside the frame of that key on the main keyboard view.
if (!characterString.length) {
return YES;
}

UIWindow *keyboardWindow = [[UIApplication sharedApplication] keyboardWindow];
UIView *keyboardView = [[keyboardWindow subviewsWithClassNamePrefix:@"UIKBKeyplaneView"] lastObject];

// If we didn't find the standard keyboard view, then we may have a custom keyboard
if (!keyboardView) {
return [self _enterCustomKeyboardCharacter:characterString];
}
id /*UIKBKeyplane*/ keyplane = [keyboardView valueForKey:@"keyplane"];
BOOL isShiftKeyplane = [[keyplane valueForKey:@"isShiftKeyplane"] boolValue];

NSMutableArray *unvisitedForKeyplane = [history objectForKey:[NSValue valueWithNonretainedObject:keyplane]];
if (!unvisitedForKeyplane) {
unvisitedForKeyplane = [NSMutableArray arrayWithObjects:@"More", @"International", nil];
if (!isShiftKeyplane) {
[unvisitedForKeyplane insertObject:@"Shift" atIndex:0];
}
[history setObject:unvisitedForKeyplane forKey:[NSValue valueWithNonretainedObject:keyplane]];
}

NSArray *keys = [keyplane valueForKey:@"keys"];

// Interpret control characters appropriately
characterString = [self _representedKeyboardStringForCharacter:characterString];

id keyToTap = nil;
id modifierKey = nil;
NSString *selectedModifierRepresentedString = nil;

while (YES) {
for (id/*UIKBKey*/ key in keys) {
NSString *representedString = [key valueForKey:@"representedString"];

// Find the key based on the key's represented string
if ([representedString isEqual:characterString]) {
keyToTap = key;
}

if (!modifierKey && unvisitedForKeyplane.count && [[unvisitedForKeyplane objectAtIndex:0] isEqual:representedString]) {
modifierKey = key;
selectedModifierRepresentedString = representedString;
[unvisitedForKeyplane removeObjectAtIndex:0];
}
}

if (keyToTap) {
break;
}

if (modifierKey) {
break;
}

if (!unvisitedForKeyplane.count) {
return NO;
}

// If we didn't find the key or the modifier, then this modifier must not exist on this keyboard. Remove it.
[unvisitedForKeyplane removeObjectAtIndex:0];
}

if (keyToTap) {
[keyboardView tapAtPoint:CGPointCenteredInRect([keyToTap frame])];
CFRunLoopRunInMode(kCFRunLoopDefaultMode, keystrokeDelay, false);

return YES;
}

// We didn't find anything, so try the symbols pane
if (modifierKey) {
[keyboardView tapAtPoint:CGPointCenteredInRect([modifierKey frame])];
CFRunLoopRunInMode(kCFRunLoopDefaultMode, keystrokeDelay, false);

// If we're back at a place we've been before, and we still have things to explore in the previous
id /*UIKBKeyplane*/ newKeyplane = [keyboardView valueForKey:@"keyplane"];
id /*UIKBKeyplane*/ previousKeyplane = [history valueForKey:@"previousKeyplane"];

if (newKeyplane == previousKeyplane) {
// Come back to the keyplane that we just tested so that we can try the other modifiers
NSMutableArray *previousKeyplaneHistory = [history objectForKey:[NSValue valueWithNonretainedObject:newKeyplane]];
[previousKeyplaneHistory insertObject:[history valueForKey:@"lastModifierRepresentedString"] atIndex:0];
} else {
[history setValue:keyplane forKey:@"previousKeyplane"];
[history setValue:selectedModifierRepresentedString forKey:@"lastModifierRepresentedString"];
}

return [self _enterCharacter:characterString history:history];
}

return NO;
}

+ (BOOL)_enterCustomKeyboardCharacter:(NSString *)characterString;
{
const NSTimeInterval keystrokeDelay = 0.05f;

if (!characterString.length) {
return YES;
}

characterString = [self _representedKeyboardStringForCharacter:characterString];

// For custom keyboards, use the classic methods of looking up views based on accessibility labels
UIWindow *keyboardWindow = [[UIApplication sharedApplication] keyboardWindow];

UIAccessibilityElement *element = [keyboardWindow accessibilityElementWithLabel:characterString];
if (!element) {
return NO;
}

UIView *view = [UIAccessibilityElement viewContainingAccessibilityElement:element];
CGRect keyFrame = [view.window convertRect:[element accessibilityFrame] toView:view];
[view tapAtPoint:CGPointCenteredInRect(keyFrame)];
CFRunLoopRunInMode(kCFRunLoopDefaultMode, keystrokeDelay, false);

return YES;
}

@end
8 changes: 8 additions & 0 deletions KIF.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
AAB072B213971AB2008AF393 /* UIWindow-KIFAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = AAB072A213971AB2008AF393 /* UIWindow-KIFAdditions.h */; };
AAB072B313971AB2008AF393 /* UIWindow-KIFAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = AAB072A313971AB2008AF393 /* UIWindow-KIFAdditions.m */; };
AAB072B513971AEA008AF393 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AAB072B413971AEA008AF393 /* UIKit.framework */; };
C194255815D83DE9004FC314 /* KIFTypist.h in Headers */ = {isa = PBXBuildFile; fileRef = C194255615D83DE9004FC314 /* KIFTypist.h */; };
C194255915D83DE9004FC314 /* KIFTypist.m in Sources */ = {isa = PBXBuildFile; fileRef = C194255715D83DE9004FC314 /* KIFTypist.m */; };
CDFD8E86139728B4008D299F /* NSFileManager-KIFAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = CDFD8E84139728B4008D299F /* NSFileManager-KIFAdditions.h */; };
CDFD8E87139728B4008D299F /* NSFileManager-KIFAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CDFD8E85139728B4008D299F /* NSFileManager-KIFAdditions.m */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -62,6 +64,8 @@
AAB072A213971AB2008AF393 /* UIWindow-KIFAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIWindow-KIFAdditions.h"; sourceTree = "<group>"; };
AAB072A313971AB2008AF393 /* UIWindow-KIFAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIWindow-KIFAdditions.m"; sourceTree = "<group>"; };
AAB072B413971AEA008AF393 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
C194255615D83DE9004FC314 /* KIFTypist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KIFTypist.h; sourceTree = "<group>"; };
C194255715D83DE9004FC314 /* KIFTypist.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KIFTypist.m; sourceTree = "<group>"; };
CDFD8E84139728B4008D299F /* NSFileManager-KIFAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSFileManager-KIFAdditions.h"; sourceTree = "<group>"; };
CDFD8E85139728B4008D299F /* NSFileManager-KIFAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSFileManager-KIFAdditions.m"; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -126,6 +130,8 @@
AAB0728913971A98008AF393 /* KIFTestScenario.m */,
AAB0728A13971A98008AF393 /* KIFTestStep.h */,
AAB0728B13971A98008AF393 /* KIFTestStep.m */,
C194255615D83DE9004FC314 /* KIFTypist.h */,
C194255715D83DE9004FC314 /* KIFTypist.m */,
);
path = Classes;
sourceTree = "<group>";
Expand Down Expand Up @@ -174,6 +180,7 @@
AAB072B213971AB2008AF393 /* UIWindow-KIFAdditions.h in Headers */,
CDFD8E86139728B4008D299F /* NSFileManager-KIFAdditions.h in Headers */,
39160B1113D1E6BB00311E38 /* LoadableCategory.h in Headers */,
C194255815D83DE9004FC314 /* KIFTypist.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -238,6 +245,7 @@
AAB072B113971AB2008AF393 /* UIView-KIFAdditions.m in Sources */,
AAB072B313971AB2008AF393 /* UIWindow-KIFAdditions.m in Sources */,
CDFD8E87139728B4008D299F /* NSFileManager-KIFAdditions.m in Sources */,
C194255915D83DE9004FC314 /* KIFTypist.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down