From 20b38bcd148f43c9ad1fb4e41edeacc691b37aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 5 Jul 2026 11:59:16 +0200 Subject: [PATCH] perf: speed up iOS simulator text entry --- ...AgentDeviceRunnerUITests-Bridging-Header.h | 1 + .../RunnerTests+TextEntry.swift | 25 +++++++-- .../RunnerTextEntry.h | 13 +++++ .../RunnerTextEntry.m | 51 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTextEntry.h create mode 100644 apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTextEntry.m diff --git a/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h b/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h index e888189c6..f2632e777 100644 --- a/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +++ b/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h @@ -1,3 +1,4 @@ #import "RunnerObjCExceptionCatcher.h" #import "RunnerAXSnapshotBridge.h" #import "RunnerSynthesizedGesture.h" +#import "RunnerTextEntry.h" diff --git a/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift b/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift index 69d979af8..5d373625b 100644 --- a/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +++ b/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift @@ -221,9 +221,9 @@ extension RunnerTests { } } - func typeIntoCurrentTarget(_ value: String) -> XCUIElement? { + func typeIntoCurrentTarget(_ value: String, allowBulkInput: Bool = true) -> XCUIElement? { if let currentTarget = resolveTextEntryElement(app: app, target: activeTarget) { - app.typeText(value) + typeTextIntoTarget(value, target: currentTarget, allowBulkInput: allowBulkInput) return currentTarget } else { app.typeText(value) @@ -249,7 +249,7 @@ extension RunnerTests { if delaySeconds > 0 && characters.count > 1 { var typedTarget: XCUIElement? for (index, character) in characters.enumerated() { - typedTarget = typeIntoCurrentTarget(String(character)) ?? typedTarget + typedTarget = typeIntoCurrentTarget(String(character), allowBulkInput: false) ?? typedTarget if index + 1 < characters.count { sleepFor(delaySeconds) } @@ -338,13 +338,30 @@ extension RunnerTests { observedText.count ) clearTextInput(repairTarget) - app.typeText(expectedText) + typeTextIntoTarget(expectedText, target: repairTarget) return verifyTextEntry(app: app, target: target, expectedText: expectedText, repaired: true) #else return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil) #endif } + private func typeTextIntoTarget( + _ value: String, + target: XCUIElement, + allowBulkInput: Bool = true + ) { +#if os(iOS) && targetEnvironment(simulator) + if allowBulkInput, value.count > 1 { + if let error = RunnerTextEntry.typeText(value, intoElement: target, typingSpeed: 120.0) { + NSLog("AGENT_DEVICE_RUNNER_FAST_TEXT_ENTRY_FALLBACK=%@", error) + } else { + return + } + } +#endif + target.typeText(value) + } + private func verifyTextEntry( app: XCUIApplication, target: TextEntryTarget, diff --git a/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTextEntry.h b/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTextEntry.h new file mode 100644 index 000000000..d459181bb --- /dev/null +++ b/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTextEntry.h @@ -0,0 +1,13 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RunnerTextEntry : NSObject + ++ (NSString * _Nullable)typeText:(NSString *)text + intoElement:(id)element + typingSpeed:(double)typingSpeed; + +@end + +NS_ASSUME_NONNULL_END diff --git a/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTextEntry.m b/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTextEntry.m new file mode 100644 index 000000000..8f734d16b --- /dev/null +++ b/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTextEntry.m @@ -0,0 +1,51 @@ +#import "RunnerTextEntry.h" + +#import +#import + +typedef void (*RunnerMsgSendTypeTextWithSpeed)(id, SEL, NSString *, NSUInteger, double, BOOL); + +@implementation RunnerTextEntry + ++ (NSString * _Nullable)typeText:(NSString *)text + intoElement:(id)element + typingSpeed:(double)typingSpeed { + SEL selector = NSSelectorFromString(@"typeText:atOffset:typingSpeed:shouldRedact:"); + if (element == nil || ![element respondsToSelector:selector]) { + return [NSString stringWithFormat: + @"private XCTest text entry unavailable: selector missing on %@ (%@)", + NSStringFromClass([element class]), + [self textEntrySelectorListForElement:element] + ]; + } + + @try { + ((RunnerMsgSendTypeTextWithSpeed)objc_msgSend)(element, selector, text, 0, typingSpeed, NO); + } @catch (NSException *exception) { + NSString *name = exception.name ?: @"NSException"; + NSString *reason = exception.reason ?: @"private XCTest text entry failed"; + return [NSString stringWithFormat:@"%@: %@", name, reason]; + } + return nil; +} + ++ (NSString *)textEntrySelectorListForElement:(id)element { + NSMutableArray *selectors = [NSMutableArray array]; + Class cls = [element class]; + while (cls != Nil) { + unsigned int count = 0; + Method *methods = class_copyMethodList(cls, &count); + for (unsigned int index = 0; index < count; index += 1) { + SEL selector = method_getName(methods[index]); + NSString *name = NSStringFromSelector(selector); + if ([name containsString:@"type"] || [name containsString:@"Text"] || [name containsString:@"keyboard"]) { + [selectors addObject:[NSString stringWithFormat:@"%@:%@", NSStringFromClass(cls), name]]; + } + } + free(methods); + cls = class_getSuperclass(cls); + } + return [selectors componentsJoinedByString:@", "]; +} + +@end