feat(companion): launch and terminate apps through the automation session

This commit is contained in:
pj committed 2026-06-07 16:54:33 +05:30
1 parent 4e02615aa4
commit 63de54ed4a
2 files changed
+60

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
import XCTest
// Launches and terminates apps through the automation session. Lifecycle
// performed outside the session leaves its cached app proxies bound to dead
// processes, after which snapshots hang and typing asserts.
enum AppLifecycle {
enum LifecycleError: Error {
case failed(String)
}
static func launch(bundleIdentifier: String, foregroundIfRunning: Bool) throws {
try onMainCatching {
let application = XCUIApplication(bundleIdentifier: bundleIdentifier)
if foregroundIfRunning {
// activate starts the app when needed and foregrounds it when
// already running, without a forced relaunch.
application.activate()
} else {
application.launch()
}
}
}
static func terminate(bundleIdentifier: String) throws {
try onMainCatching {
let application = XCUIApplication(bundleIdentifier: bundleIdentifier)
if application.state == .notRunning {
return
}
application.terminate()
}
}
// onMainCatching runs automation work on the main thread and converts a
// framework assertion into a thrown error so the server survives it.
private static func onMainCatching(_ work: @escaping () -> Void) throws {
var caughtError: NSError?
var completed = false
let run = {
completed = CompanionRunCatching(work, &caughtError)
}
if Thread.isMainThread {
run()
} else {
DispatchQueue.main.sync(execute: run)
}
if !completed {
throw LifecycleError.failed(caughtError?.localizedDescription ?? "unknown")
}
}
}
+9
View File
@@ -103,6 +103,15 @@ final class Server {
return ["ok": true]
case "gestureProbe":
return Gesture.probe()
case "launch":
let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier
let foregroundIfRunning = params["foregroundIfRunning"] as? Bool ?? false
try AppLifecycle.launch(bundleIdentifier: bundleIdentifier, foregroundIfRunning: foregroundIfRunning)
return ["ok": true]
case "terminate":
let bundleIdentifier = params["bundleId"] as? String ?? currentBundleIdentifier
try AppLifecycle.terminate(bundleIdentifier: bundleIdentifier)
return ["ok": true]
case "typeText":
let text = params["text"] as? String ?? ""
let replace = params["replace"] as? Bool ?? false