feat(companion): add appState, eraseText, pressKey runner handlers

The Go runner transport already calls these methods; the in-device runner
implemented them only latently. They become load-bearing on the device
path, where the hybrid's legacy-companion fallback is absent. Backward
compatible: the simulator hybrid never calls them.
This commit is contained in:
pj committed 2026-06-08 23:06:06 +05:30
1 parent e04631d4c9
commit 99ae7c9294
3 files changed
+70

No files matched your search

+23
View File
@@ -31,6 +31,29 @@ enum AppLifecycle {
} }
} }
// state reports the app's run state in the vocabulary the Go transport maps
// to a process state: foreground, background, or notRunning. On the
// simulator the hybrid never calls it; on device it backs ForegroundApp.
static func state(bundleIdentifier: String) -> String {
var result = "notRunning"
let collect = {
switch XCUIApplication(bundleIdentifier: bundleIdentifier).state {
case .runningForeground:
result = "foreground"
case .runningBackground, .runningBackgroundSuspended:
result = "background"
default:
result = "notRunning"
}
}
if Thread.isMainThread {
collect()
} else {
DispatchQueue.main.sync(execute: collect)
}
return result
}
// onMainCatching runs automation work on the main thread and converts a // onMainCatching runs automation work on the main thread and converts a
// framework assertion into a thrown error so the server survives it. // framework assertion into a thrown error so the server survives it.
private static func onMainCatching(_ work: @escaping () -> Void) throws { private static func onMainCatching(_ work: @escaping () -> Void) throws {
+13
View File
@@ -122,6 +122,19 @@ final class Server {
let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier
try TextInput.type(text: text, replace: replace, bundleIdentifier: bundleIdentifier) try TextInput.type(text: text, replace: replace, bundleIdentifier: bundleIdentifier)
return ["ok": true] return ["ok": true]
case "eraseText":
let count = params["count"] as? Int ?? 0
let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier
try TextInput.erase(count: count, bundleIdentifier: bundleIdentifier)
return ["ok": true]
case "pressKey":
let key = params["key"] as? String ?? ""
let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier
try TextInput.pressKey(key: key, bundleIdentifier: bundleIdentifier)
return ["ok": true]
case "appState":
let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier
return ["state": AppLifecycle.state(bundleIdentifier: bundleIdentifier)]
case "screenshot": case "screenshot":
return try screenshot() return try screenshot()
default: default:
+34
View File
@@ -37,6 +37,40 @@ enum TextInput {
} }
} }
// erase deletes count characters from the focused field by typing that many
// delete keys. Used on device, where EraseText routes to the runner instead
// of the legacy companion's HID stream.
static func erase(count: Int, bundleIdentifier: String) throws {
guard count > 0 else { return }
let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: count)
try typeOnFocus(deletes, bundleIdentifier: bundleIdentifier)
}
// pressKey types a single logical key into the focused field. Only return is
// supported, matching the simulator companion's key surface.
static func pressKey(key: String, bundleIdentifier: String) throws {
switch key {
case "return", "enter", "Return", "Enter":
try typeOnFocus(XCUIKeyboardKey.return.rawValue, bundleIdentifier: bundleIdentifier)
default:
throw TextInputError.typingFailed("unsupported key \(key)")
}
}
private static func typeOnFocus(_ payload: String, bundleIdentifier: String) throws {
let application = XCUIApplication(bundleIdentifier: bundleIdentifier)
var caughtError: NSError?
var completed = false
runOnMain {
completed = CompanionRunCatching({
application.typeText(payload)
}, &caughtError)
}
if !completed {
throw TextInputError.typingFailed(caughtError?.localizedDescription ?? "unknown")
}
}
private static func runOnMain(_ work: () -> Void) { private static func runOnMain(_ work: () -> Void) {
if Thread.isMainThread { if Thread.isMainThread {
work() work()