diff --git a/companion/Sources/TextInput.swift b/companion/Sources/TextInput.swift new file mode 100644 index 0000000..5728936 --- /dev/null +++ b/companion/Sources/TextInput.swift @@ -0,0 +1,43 @@ +import XCTest + +// Types unicode text into the focused element through the public typeText API, +// avoiding the pasteboard entirely. With replace enabled, the current value +// length is consumed first by prepending that many delete keys so the whole +// replacement happens in one typeText call. +enum TextInput { + static func type(text: String, replace: Bool, bundleIdentifier: String) throws { + let application = XCUIApplication(bundleIdentifier: bundleIdentifier) + let focused = focusedElement(in: application) + + var payload = text + if replace { + let currentLength = currentValueLength(of: focused) + if currentLength > 0 { + let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentLength) + payload = deletes + text + } + } + + if let focused = focused { + focused.typeText(payload) + } else { + application.typeText(payload) + } + } + + private static func focusedElement(in application: XCUIApplication) -> XCUIElement? { + let predicate = NSPredicate(format: "hasKeyboardFocus == true") + let matches = application.descendants(matching: .any).matching(predicate) + if matches.count > 0 { + return matches.element(boundBy: 0) + } + return nil + } + + private static func currentValueLength(of element: XCUIElement?) -> Int { + guard let element = element, let value = element.value as? String else { + return 0 + } + return value.count + } +}