feat(companion): synthesize timestamped touch gestures

This commit is contained in:
pj committed 2026-06-07 15:41:16 +05:30
1 parent cd2865bbd5
commit f45d25d1bb
1 file changed
+89
+89
View File
@@ -0,0 +1,89 @@
import XCTest
import CoreGraphics
// Synthesizes a single timestamped touch event from a list of primitive events.
// A running offset (seconds) is advanced as events are consumed so sequential
// taps collapse into one pointer path with press/lift/press/lift at exact
// offsets, honoring the inter-tap gap to the millisecond.
enum Gesture {
enum GestureError: Error {
case missingField(String)
case unknownKind(String)
case synthesisTimeout
case synthesisFailed(String)
}
static func perform(events: [[String: Any]]) throws {
var offset = 0.0
var path: XCPointerEventPath?
func point(_ event: [String: Any], _ xKey: String, _ yKey: String) throws -> CGPoint {
guard let x = event[xKey] as? Double, let y = event[yKey] as? Double else {
throw GestureError.missingField("\(xKey)/\(yKey)")
}
return CGPoint(x: x, y: y)
}
func ensurePath(at location: CGPoint) -> XCPointerEventPath {
if let existing = path {
existing.move(to: location, atOffset: offset)
return existing
}
let created = XCPointerEventPath(forTouchAt: location, offset: offset)
path = created
return created
}
for event in events {
guard let kind = event["kind"] as? String else {
throw GestureError.missingField("kind")
}
switch kind {
case "touchDown":
let location = try point(event, "x", "y")
ensurePath(at: location).pressDown(atOffset: offset)
case "touchUp":
let location = try point(event, "x", "y")
ensurePath(at: location).liftUp(atOffset: offset)
case "delay":
guard let milliseconds = event["milliseconds"] as? Double else {
throw GestureError.missingField("milliseconds")
}
offset += milliseconds / 1000.0
case "swipe":
let from = try point(event, "fromX", "fromY")
let to = try point(event, "toX", "toY")
guard let seconds = event["seconds"] as? Double else {
throw GestureError.missingField("seconds")
}
let activePath = ensurePath(at: from)
activePath.pressDown(atOffset: offset)
activePath.move(to: to, atOffset: offset + seconds)
activePath.liftUp(atOffset: offset + seconds)
offset += seconds
default:
throw GestureError.unknownKind(kind)
}
}
guard let path = path else {
throw GestureError.missingField("events")
}
let record = XCSynthesizedEventRecord(name: "companion", interfaceOrientation: 0)
record.add(path)
let semaphore = DispatchSemaphore(value: 0)
var synthesisError: Error?
XCTRunnerDaemonSession.shared().synthesize(event: record) { error in
synthesisError = error
semaphore.signal()
}
if semaphore.wait(timeout: .now() + 30) == .timedOut {
throw GestureError.synthesisTimeout
}
if let synthesisError = synthesisError {
throw GestureError.synthesisFailed("\(synthesisError)")
}
}
}