Skip to content

Commit

Permalink
chore: Update keyboard typing implementation (#832)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Jan 6, 2024
1 parent 766be59 commit 06cfb3b
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 97 deletions.
1 change: 1 addition & 0 deletions PrivateHeaders/XCTest/XCSynthesizedEventRecord.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#if !TARGET_OS_TV
- (id)initWithName:(NSString *)arg1 interfaceOrientation:(UIInterfaceOrientation)arg2;
#endif
- (id)initWithName:(id)arg1;
- (id)init;
- (BOOL)synthesizeWithError:(NSError **)arg1;

Expand Down
10 changes: 10 additions & 0 deletions WebDriverAgentLib/Categories/XCUIElement+FBTyping.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@

NS_ASSUME_NONNULL_BEGIN

/**
Types a text into the currently focused element.
@param text text that should be typed
@param typingSpeed Frequency of typing (letters per sec)
@param error If there is an error, upon return contains an NSError object that describes the problem.
@return YES if the operation succeeds, otherwise NO.
*/
BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error);

@interface XCUIElement (FBTyping)

/**
Expand Down
46 changes: 31 additions & 15 deletions WebDriverAgentLib/Categories/XCUIElement+FBTyping.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,34 @@

#import "FBConfiguration.h"
#import "FBErrorBuilder.h"
#import "FBKeyboard.h"
#import "NSString+FBVisualLength.h"
#import "FBXCElementSnapshotWrapper.h"
#import "FBXCElementSnapshotWrapper+Helpers.h"
#import "FBXCodeCompatibility.h"
#import "FBXCTestDaemonsProxy.h"
#import "NSString+FBVisualLength.h"
#import "XCUIDevice+FBHelpers.h"
#import "XCUIElement+FBCaching.h"
#import "XCUIElement+FBUtilities.h"
#import "FBXCodeCompatibility.h"
#import "XCSynthesizedEventRecord.h"
#import "XCPointerEventPath.h"

#define MAX_TEXT_ABBR_LEN 12
#define MAX_CLEAR_RETRIES 3

BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error)
{
NSString *name = text.length <= MAX_TEXT_ABBR_LEN
? [NSString stringWithFormat:@"Type '%@'", text]
: [NSString stringWithFormat:@"Type '%@…'", [text substringToIndex:MAX_TEXT_ABBR_LEN]];
XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:name];
XCPointerEventPath *ep = [[XCPointerEventPath alloc] initForTextInput];
[ep typeText:text
atOffset:0.0
typingSpeed:typingSpeed
shouldRedact:NO];
[eventRecord addPointerEventPath:ep];
return [FBXCTestDaemonsProxy synthesizeEventWithRecord:eventRecord error:error];
}

@interface NSString (FBRepeat)

Expand Down Expand Up @@ -96,13 +113,12 @@ - (BOOL)fb_typeText:(NSString *)text
id<FBXCElementSnapshot> snapshot = self.fb_isResolvedFromCache.boolValue
? self.lastSnapshot
: self.fb_takeSnapshot;
[self fb_prepareForTextInputWithSnapshot:[FBXCElementSnapshotWrapper ensureWrapped:snapshot]];
if (shouldClear && ![self fb_clearTextWithSnapshot:self.lastSnapshot
shouldPrepareForInput:NO
error:error]) {
FBXCElementSnapshotWrapper *wrapped = [FBXCElementSnapshotWrapper ensureWrapped:snapshot];
[self fb_prepareForTextInputWithSnapshot:wrapped];
if (shouldClear && ![self fb_clearTextWithSnapshot:wrapped shouldPrepareForInput:NO error:error]) {
return NO;
}
return [FBKeyboard typeText:text frequency:frequency error:error];
return FBTypeText(text, frequency, error);
}

- (BOOL)fb_clearTextWithError:(NSError **)error
Expand All @@ -122,15 +138,15 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot
id currentValue = snapshot.value;
if (nil != currentValue && ![currentValue isKindOfClass:NSString.class]) {
return [[[FBErrorBuilder builder]
withDescriptionFormat:@"The value of '%@' is not a string and thus cannot be edited", snapshot.fb_description]
buildError:error];
withDescriptionFormat:@"The value of '%@' is not a string and thus cannot be edited", snapshot.fb_description]
buildError:error];
}

if (nil == currentValue || 0 == [currentValue fb_visualLength]) {
// Short circuit if the content is not present
return YES;
}

static NSString *backspaceDeleteSequence;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Expand Down Expand Up @@ -160,8 +176,8 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot
} else if (retry >= MAX_CLEAR_RETRIES - 1) {
// Last chance retry. Tripple-tap the field to select its content
[self tapWithNumberOfTaps:3 numberOfTouches:1];
return [FBKeyboard typeText:backspaceDeleteSequence error:error];
} else if (![FBKeyboard typeText:backspacesToType error:error]) {
return FBTypeText(backspaceDeleteSequence, FBConfiguration.defaultTypingFrequency, error);
} else if (!FBTypeText(backspacesToType, FBConfiguration.defaultTypingFrequency, error)) {
// 2nd operation
return NO;
}
Expand All @@ -181,7 +197,7 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot
// kHIDPage_KeyboardOrKeypad did not work for tvOS's search field. (tvOS 17 at least)
// Tested XCUIElementTypeSearchField and XCUIElementTypeTextView whch were
// common search field and email/passowrd input in tvOS apps.
return [FBKeyboard typeText:backspacesToType error:error];
return FBTypeText(backspacesToType, FBConfiguration.defaultTypingFrequency, error);
#endif
}

Expand Down
2 changes: 1 addition & 1 deletion WebDriverAgentLib/Commands/FBElementCommands.m
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ + (NSArray *)routes
NSString *textToType = [request.arguments[@"value"] componentsJoinedByString:@""];
NSUInteger frequency = [request.arguments[@"frequency"] unsignedIntegerValue] ?: [FBConfiguration maxTypingFrequency];
NSError *error;
if (![FBKeyboard typeText:textToType frequency:frequency error:&error]) {
if (!FBTypeText(textToType, frequency, &error)) {
return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description
traceback:nil]);
}
Expand Down
1 change: 1 addition & 0 deletions WebDriverAgentLib/Utilities/FBConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ extern NSString *const FBSnapshotMaxDepthKey;
/* The maximum typing frequency for all typing activities */
+ (void)setMaxTypingFrequency:(NSUInteger)value;
+ (NSUInteger)maxTypingFrequency;
+ (NSUInteger)defaultTypingFrequency;

/* Use singleton test manager proxy */
+ (void)setShouldUseSingletonTestManager:(BOOL)value;
Expand Down
20 changes: 16 additions & 4 deletions WebDriverAgentLib/Utilities/FBConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

// Session-specific settings
static BOOL FBShouldTerminateApp;
static NSUInteger FBMaxTypingFrequency;
static NSNumber* FBMaxTypingFrequency;
static NSUInteger FBScreenshotQuality;
static NSTimeInterval FBCustomSnapshotTimeout;
static BOOL FBShouldUseFirstMatch;
Expand All @@ -57,6 +57,13 @@

@implementation FBConfiguration

+ (NSUInteger)defaultTypingFrequency
{
NSInteger defaultFreq = [[NSUserDefaults standardUserDefaults]
integerForKey:@"com.apple.xctest.iOSMaximumTypingFrequency"];
return defaultFreq > 0 ? defaultFreq : 60;
}

+ (void)initialize
{
[FBConfiguration resetSessionSettings];
Expand Down Expand Up @@ -200,12 +207,17 @@ + (NSString *)elementResponseAttributes

+ (void)setMaxTypingFrequency:(NSUInteger)value
{
FBMaxTypingFrequency = value;
FBMaxTypingFrequency = @(value);
}

+ (NSUInteger)maxTypingFrequency
{
return FBMaxTypingFrequency;
if (nil == FBMaxTypingFrequency) {
return [self defaultTypingFrequency];
}
return FBMaxTypingFrequency.integerValue <= 0
? [self defaultTypingFrequency]
: FBMaxTypingFrequency.integerValue;
}

+ (void)setShouldUseSingletonTestManager:(BOOL)value
Expand Down Expand Up @@ -462,7 +474,7 @@ + (void)resetSessionSettings
FBShouldTerminateApp = YES;
FBShouldUseCompactResponses = YES;
FBElementResponseAttributes = @"type,label";
FBMaxTypingFrequency = 60;
FBMaxTypingFrequency = @([self defaultTypingFrequency]);
FBScreenshotQuality = 3;
FBCustomSnapshotTimeout = 15.;
FBShouldUseFirstMatch = NO;
Expand Down
27 changes: 0 additions & 27 deletions WebDriverAgentLib/Utilities/FBKeyboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,6 @@ NS_ASSUME_NONNULL_BEGIN
+ (nullable NSString *)keyValueForName:(NSString *)name;
#endif

/**
Types a string into active element. There must be element with keyboard focus; otherwise an
error is raised.
This API discards any modifiers set in the current context by +performWithKeyModifiers:block: so that
it strictly interprets the provided text. To input keys with modifier flags, use -typeKey:modifierFlags:.
@param text that should be typed
@param error If there is an error, upon return contains an NSError object that describes the problem.
@return YES if the operation succeeds, otherwise NO.
*/
+ (BOOL)typeText:(NSString *)text error:(NSError **)error;

/**
Waits until the keyboard is visible on the screen or a timeout happens
Expand All @@ -46,20 +33,6 @@ NS_ASSUME_NONNULL_BEGIN
*/
+ (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app timeout:(NSTimeInterval)timeout error:(NSError **)error;

/**
Types a string into active element. There must be element with keyboard focus; otherwise an
error is raised.
This API discards any modifiers set in the current context by +performWithKeyModifiers:block: so that
it strictly interprets the provided text. To input keys with modifier flags, use -typeKey:modifierFlags:.
@param text that should be typed
@param frequency Frequency of typing (letters per sec)
@param error If there is an error, upon return contains an NSError object that describes the problem.
@return YES if the operation succeeds, otherwise NO.
*/
+ (BOOL)typeText:(NSString *)text frequency:(NSUInteger)frequency error:(NSError **)error;

@end

NS_ASSUME_NONNULL_END
25 changes: 0 additions & 25 deletions WebDriverAgentLib/Utilities/FBKeyboard.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,6 @@

@implementation FBKeyboard

+ (BOOL)typeText:(NSString *)text error:(NSError **)error
{
return [self typeText:text frequency:[FBConfiguration maxTypingFrequency] error:error];
}

+ (BOOL)typeText:(NSString *)text frequency:(NSUInteger)frequency error:(NSError **)error
{
__block BOOL didSucceed = NO;
__block NSError *innerError;
[FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){
[[FBXCTestDaemonsProxy testRunnerProxy]
_XCT_sendString:text
maximumFrequency:frequency
completion:^(NSError *typingError){
didSucceed = (typingError == nil);
innerError = typingError;
completion();
}];
}];
if (error) {
*error = innerError;
}
return didSucceed;
}

+ (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app timeout:(NSTimeInterval)timeout error:(NSError **)error
{
BOOL (^isKeyboardVisible)(void) = ^BOOL(void) {
Expand Down
25 changes: 0 additions & 25 deletions WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,6 @@ - (void)setUp
[self goToAttributesPage];
}

- (void)testTextTyping
{
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
XCTSkip(@"Failed on Azure Pipeline. Local run succeeded.");
}
NSString *text = @"Happy typing";
XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"];
[textField tap];

if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) {
// A workaround until find out to clear tutorial on iOS 15
XCUIElement *textField = self.testedApplication.staticTexts[@"Continue"];
if (textField.hittable) {
[textField tap];
}
}

NSError *error;
XCTAssertTrue([FBKeyboard waitUntilVisibleForApplication:self.testedApplication timeout:1 error:&error]);
XCTAssertNil(error);
XCTAssertTrue([FBKeyboard typeText:text error:&error]);
XCTAssertNil(error);
XCTAssertEqualObjects(textField.value, text);
}

- (void)testKeyboardDismissal
{
XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"];
Expand Down
4 changes: 4 additions & 0 deletions WebDriverAgentTests/IntegrationTests/FBTypingTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ - (void)testTextClearing
XCTAssertTrue([textField fb_clearTextWithError:&error]);
XCTAssertNil(error);
XCTAssertEqualObjects(textField.value, @"");
XCTAssertTrue([textField fb_typeText:@"Happy typing" shouldClear:YES error:&error]);
XCTAssertTrue([textField fb_typeText:@"Happy typing 2" shouldClear:YES error:&error]);
XCTAssertEqualObjects(textField.value, @"Happy typing 2");
XCTAssertNil(error);
}

@end

0 comments on commit 06cfb3b

Please sign in to comment.