From 84e8274d09fc2b5f22158837199dd92711af7a9c Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 7 Jun 2026 15:41:16 +0530 Subject: [PATCH] feat(companion): type text with replace semantics --- companion/Sources/TextInput.swift | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 companion/Sources/TextInput.swift 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 + } +}