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

chore: Update keyboard typing implementation #832

Merged
merged 6 commits into from
Jan 6, 2024
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
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"];
Copy link
Member

Choose a reason for hiding this comment

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

👍

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