agent-device 0.19.3 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -70
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.1.apk +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.1.apk.sha256 +1 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.1.manifest.json +10 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.1.apk +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.1.apk.sha256 +1 -0
- package/{android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.3.manifest.json → android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.1.manifest.json} +6 -6
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +2 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +5 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +5 -10
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +74 -174
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +113 -19
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+BlockingSystemModalResolution.swift +82 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +325 -123
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +2 -2
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +149 -105
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +56 -2
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+LifecycleCacheTests.swift +73 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +51 -26
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +2 -2
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +2 -107
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +130 -13
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +51 -8
- package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotTraversalIdentityTests.swift +24 -0
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +1 -1
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +43 -35
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +1 -1
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +16 -1
- package/dist/src/agent-browser-tool.js +1 -1
- package/dist/src/agent-device-client.js +3 -0
- package/dist/src/android-adb.d.ts +1 -1
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-method-overlays.js +1 -0
- package/dist/src/android-input-ownership.js +1 -6
- package/dist/src/android-system-chrome.js +1 -0
- package/dist/src/android.js +1 -1
- package/dist/src/app-lifecycle.js +5 -5
- package/dist/src/app-log-request-scope.js +1 -0
- package/dist/src/app-resolution.js +1 -0
- package/dist/src/apple-multitouch-support.js +1 -0
- package/dist/src/apps.js +5 -5
- package/dist/src/boot-diagnostics.js +2 -0
- package/dist/src/cli-config.js +1 -0
- package/dist/src/cli-help.js +131 -40
- package/dist/src/cli-runner.js +1 -1
- package/dist/src/cli.js +15 -11
- package/dist/src/command-metadata.js +1 -1
- package/dist/src/command-schema.js +3 -3
- package/dist/src/compatibility-policy.js +1 -0
- package/dist/src/context.js +1 -0
- package/dist/src/contracts.d.ts +2 -2
- package/dist/src/debug-symbols.js +1 -1
- package/dist/src/device-input-state.js +1 -1
- package/dist/src/devices2.js +1 -1
- package/dist/src/devices4.js +1 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/helper.js +1 -1
- package/dist/src/index.d.ts +533 -279
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +4 -3
- package/dist/src/install-artifact.js +1 -0
- package/dist/src/install-source.d.ts +5 -1
- package/dist/src/install-source.js +1 -1
- package/dist/src/install-source2.js +1 -1
- package/dist/src/interaction-snapshot.js +1 -1
- package/dist/src/interaction.d.ts +230 -0
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -2
- package/dist/src/interactor2.js +1 -0
- package/dist/src/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +42 -20
- package/dist/src/internal/png-worker.d.ts +22 -0
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/launch-console.js +1 -0
- package/dist/src/lease-context.js +1 -0
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/logcat.js +5 -0
- package/dist/src/method-scope.js +1 -0
- package/dist/src/metro-types.d.ts +52 -0
- package/dist/src/metro.d.ts +3 -3
- package/dist/src/notifications.js +1 -1
- package/dist/src/png-worker-contract.js +1 -1
- package/dist/src/process-lock.js +1 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace.js +1 -1
- package/dist/src/recording-export-quality.js +15 -0
- package/dist/src/register-builtins.js +1 -0
- package/dist/src/registry.js +24 -24
- package/dist/src/remote-config.d.ts +1 -1
- package/dist/src/reporting.js +8 -7
- package/dist/src/runner-client.js +1 -1
- package/dist/src/runner-disposal.js +2 -0
- package/dist/src/runner-lease.js +1 -0
- package/dist/src/runner-provider.js +1 -1
- package/dist/src/runner-sequence.js +1 -1
- package/dist/src/runtime.js +3 -3
- package/dist/src/runtime2.js +1 -0
- package/dist/src/sdk-android-adb.d.ts +97 -33
- package/dist/src/sdk-android-adb.js +4 -4
- package/dist/src/sdk-batch-runner.d.ts +2 -1
- package/dist/src/sdk-batch-runner.js +3 -2
- package/dist/src/sdk-contracts.d.ts +70 -149
- package/dist/src/sdk-contracts.js +1 -1
- package/dist/src/sdk-device.js +1 -1
- package/dist/src/sdk-finders.d.ts +5 -4
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-io.d.ts +1 -1
- package/dist/src/sdk-remote-config.d.ts +19 -96
- package/dist/src/sdk-remote-config.js +1 -1
- package/dist/src/sdk-selectors.js +1 -1
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selector-vocabulary.js +1 -0
- package/dist/src/selectors.d.ts +12 -3
- package/dist/src/selectors.js +1 -1
- package/dist/src/server.js +3 -3
- package/dist/src/session-target-evidence.js +1 -0
- package/dist/src/session.js +8 -10
- package/dist/src/simctl.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-diagnostics.js +5 -2
- package/dist/src/snapshot-quality.js +1 -1
- package/dist/src/snapshot.js +1 -1
- package/dist/src/snapshot2.js +1 -2
- package/dist/src/tool-provider3.js +1 -0
- package/dist/src/unsupported-interactor.js +1 -0
- package/dist/src/url.js +1 -1
- package/dist/src/web.js +1 -1
- package/package.json +34 -27
- package/server.json +2 -2
- package/skills/agent-device/SKILL.md +5 -3
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.3.apk +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.3.apk.sha256 +0 -1
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.3.manifest.json +0 -10
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.3.apk +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.3.apk.sha256 +0 -1
- package/dist/src/apple-runner-options.js +0 -1
- package/dist/src/cli-output.js +0 -1
- package/dist/src/client.js +0 -3
- package/dist/src/handler-utils.js +0 -1
- package/dist/src/output-path.js +0 -1
- package/dist/src/points.js +0 -4
- package/dist/src/provider.js +0 -1
- package/dist/src/providers.js +0 -1
- package/dist/src/record-trace-recording.js +0 -23
- package/dist/src/recording-provider.js +0 -1
- package/dist/src/sdk-batch.js +0 -2
- package/dist/src/selectors2.js +0 -1
- package/dist/src/ttl-memo.js +0 -1
- package/dist/src/tv-remote.js +0 -14
- package/dist/src/xml.js +0 -2
- /package/{macos-helper → apple/macos-helper}/Package.swift +0 -0
- /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/AudioProbe.swift +0 -0
- /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +0 -0
- /package/{macos-helper → apple/macos-helper}/Sources/AgentDeviceMacOSHelper/main.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
- /package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
- /package/{src/platforms/linux → linux}/atspi-dump.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import XCTest
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
extension RunnerTests {
|
|
4
5
|
// MARK: - Main Thread Dispatch
|
|
5
6
|
|
|
@@ -58,7 +59,7 @@ extension RunnerTests {
|
|
|
58
59
|
|
|
59
60
|
/// Runs a gesture action with uniform timing capture. Touch gestures pass `idleTimeout: true`
|
|
60
61
|
/// (the default) to run inside the scroll idle-timeout + quiescence-skip wrapper; synthesis
|
|
61
|
-
/// gestures
|
|
62
|
+
/// pointer-plan gestures pass `false` because RunnerSynthesizedGesture governs their
|
|
62
63
|
/// own timing. Returns the captured timing and the action's outcome.
|
|
63
64
|
///
|
|
64
65
|
/// NOTE: a new SYNTHESIS gesture must pass `idleTimeout: false` — the default `true` would wrap
|
|
@@ -85,7 +86,8 @@ extension RunnerTests {
|
|
|
85
86
|
message: String,
|
|
86
87
|
timing: (gestureStartUptimeMs: Double, gestureEndUptimeMs: Double),
|
|
87
88
|
frame: GestureFrame = .none,
|
|
88
|
-
fallback: GestureFallback? = nil
|
|
89
|
+
fallback: GestureFallback? = nil,
|
|
90
|
+
maestroNonHittableCoordinateFallbackUsed: Bool? = nil
|
|
89
91
|
) -> Response {
|
|
90
92
|
let data: DataPayload
|
|
91
93
|
switch frame {
|
|
@@ -96,7 +98,8 @@ extension RunnerTests {
|
|
|
96
98
|
gestureEndUptimeMs: timing.gestureEndUptimeMs,
|
|
97
99
|
gestureFallback: fallback?.strategy,
|
|
98
100
|
gestureFallbackMessage: fallback?.message,
|
|
99
|
-
gestureFallbackHint: fallback?.hint
|
|
101
|
+
gestureFallbackHint: fallback?.hint,
|
|
102
|
+
maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed
|
|
100
103
|
)
|
|
101
104
|
case .touch(let f):
|
|
102
105
|
data = DataPayload(
|
|
@@ -109,7 +112,8 @@ extension RunnerTests {
|
|
|
109
112
|
referenceHeight: f?.referenceHeight,
|
|
110
113
|
gestureFallback: fallback?.strategy,
|
|
111
114
|
gestureFallbackMessage: fallback?.message,
|
|
112
|
-
gestureFallbackHint: fallback?.hint
|
|
115
|
+
gestureFallbackHint: fallback?.hint,
|
|
116
|
+
maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed
|
|
113
117
|
)
|
|
114
118
|
case .drag(let f):
|
|
115
119
|
data = DataPayload(
|
|
@@ -130,6 +134,24 @@ extension RunnerTests {
|
|
|
130
134
|
return Response(ok: true, data: data)
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
/// Gesture plans already return canonical centroid endpoints from the portable runtime.
|
|
138
|
+
/// Keep runner timing/fallback diagnostics, but do not leak the coordinate-drag adapter's
|
|
139
|
+
/// visualization frame into only the fast-fling response shape.
|
|
140
|
+
private func canonicalPlannedGestureResponse(_ response: Response) -> Response {
|
|
141
|
+
guard response.ok, let data = response.data else { return response }
|
|
142
|
+
return Response(
|
|
143
|
+
ok: true,
|
|
144
|
+
data: DataPayload(
|
|
145
|
+
message: data.message,
|
|
146
|
+
gestureStartUptimeMs: data.gestureStartUptimeMs,
|
|
147
|
+
gestureEndUptimeMs: data.gestureEndUptimeMs,
|
|
148
|
+
gestureFallback: data.gestureFallback,
|
|
149
|
+
gestureFallbackMessage: data.gestureFallbackMessage,
|
|
150
|
+
gestureFallbackHint: data.gestureFallbackHint
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
133
155
|
|
|
134
156
|
func execute(command: Command) throws -> Response {
|
|
135
157
|
if command.command == .status {
|
|
@@ -252,31 +274,71 @@ extension RunnerTests {
|
|
|
252
274
|
)
|
|
253
275
|
}
|
|
254
276
|
|
|
255
|
-
private func
|
|
256
|
-
// XCTest work cannot be cancelled mid-flight: once the watchdog abandons a main-queue
|
|
257
|
-
// block, queueing more main-thread commands behind it only buries the runner deeper.
|
|
258
|
-
// Refuse fast instead so the daemon backs off while the abandoned work drains; past the
|
|
259
|
-
// wedge threshold, escalate so the daemon recycles this runner (#1105).
|
|
277
|
+
private func runnerUnavailableResponse(command: Command) -> Response? {
|
|
260
278
|
switch currentMainThreadBusyState() {
|
|
261
279
|
case .idle:
|
|
262
|
-
|
|
280
|
+
return nil
|
|
263
281
|
case .busy(let abandonedForSeconds):
|
|
264
282
|
return runnerBusyResponse(command: command, abandonedForSeconds: abandonedForSeconds)
|
|
265
283
|
case .wedged(let abandonedForSeconds):
|
|
266
284
|
return runnerWedgedResponse(command: command, abandonedForSeconds: abandonedForSeconds)
|
|
267
285
|
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private func executeDispatched(command: Command) throws -> Response {
|
|
289
|
+
// XCTest work cannot be cancelled mid-flight: once the watchdog abandons a main-queue
|
|
290
|
+
// block, queueing more main-thread commands behind it only buries the runner deeper.
|
|
291
|
+
// Refuse fast instead so the daemon backs off while the abandoned work drains; past the
|
|
292
|
+
// wedge threshold, escalate so the daemon recycles this runner (#1105).
|
|
293
|
+
if let unavailable = runnerUnavailableResponse(command: command) {
|
|
294
|
+
return unavailable
|
|
295
|
+
}
|
|
296
|
+
let alertDeadline = command.command == .alert
|
|
297
|
+
? Date().addingTimeInterval(Self.alertCommandTimeout(timeoutMs: command.timeoutMs))
|
|
298
|
+
: nil
|
|
268
299
|
if Thread.isMainThread {
|
|
269
|
-
|
|
300
|
+
let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(
|
|
301
|
+
command,
|
|
302
|
+
deadline: alertDeadline
|
|
303
|
+
)
|
|
304
|
+
return try executeOnMainSafely(
|
|
305
|
+
command: command,
|
|
306
|
+
alertDeadline: alertDeadline,
|
|
307
|
+
routeToSpringboard: routeToSpringboard
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
// Resolve this before the command's outer main-thread block. If the bounded probe abandons
|
|
311
|
+
// slow XCTest enumeration, return the established recoverable response instead of queueing
|
|
312
|
+
// command preparation behind work that may outlive the 30-second command watchdog.
|
|
313
|
+
let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(
|
|
314
|
+
command,
|
|
315
|
+
deadline: alertDeadline
|
|
316
|
+
)
|
|
317
|
+
if let unavailable = runnerUnavailableResponse(command: command) {
|
|
318
|
+
return unavailable
|
|
270
319
|
}
|
|
271
320
|
if command.command == .snapshot {
|
|
272
321
|
return try executeSnapshotDispatched(command: command)
|
|
273
322
|
}
|
|
323
|
+
if command.command == .alert, let deadline = alertDeadline {
|
|
324
|
+
return try runMainThreadWork(
|
|
325
|
+
command: command,
|
|
326
|
+
timeout: max(0.001, deadline.timeIntervalSinceNow),
|
|
327
|
+
timeoutError: mainThreadExecutionTimeoutError
|
|
328
|
+
) {
|
|
329
|
+
try self.executeOnMainSafely(
|
|
330
|
+
command: command,
|
|
331
|
+
alertDeadline: deadline,
|
|
332
|
+
routeToSpringboard: routeToSpringboard
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
274
336
|
return try runMainThreadWork(
|
|
275
337
|
command: command,
|
|
276
338
|
timeout: mainThreadExecutionTimeout,
|
|
277
339
|
timeoutError: mainThreadExecutionTimeoutError
|
|
278
340
|
) {
|
|
279
|
-
try self.executeOnMainSafely(command: command)
|
|
341
|
+
try self.executeOnMainSafely(command: command, routeToSpringboard: routeToSpringboard)
|
|
280
342
|
}
|
|
281
343
|
}
|
|
282
344
|
|
|
@@ -354,7 +416,11 @@ extension RunnerTests {
|
|
|
354
416
|
|
|
355
417
|
// MARK: - Command Handling
|
|
356
418
|
|
|
357
|
-
private func executeOnMainSafely(
|
|
419
|
+
private func executeOnMainSafely(
|
|
420
|
+
command: Command,
|
|
421
|
+
alertDeadline: Date? = nil,
|
|
422
|
+
routeToSpringboard: Bool
|
|
423
|
+
) throws -> Response {
|
|
358
424
|
var hasRetried = false
|
|
359
425
|
while true {
|
|
360
426
|
var response: Response?
|
|
@@ -362,7 +428,11 @@ extension RunnerTests {
|
|
|
362
428
|
let failureCountBefore = currentXCTestFailureCount()
|
|
363
429
|
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
364
430
|
do {
|
|
365
|
-
response = try self.executeOnMain(
|
|
431
|
+
response = try self.executeOnMain(
|
|
432
|
+
command: command,
|
|
433
|
+
alertDeadline: alertDeadline,
|
|
434
|
+
routeToSpringboard: routeToSpringboard
|
|
435
|
+
)
|
|
366
436
|
} catch {
|
|
367
437
|
swiftError = error
|
|
368
438
|
}
|
|
@@ -416,6 +486,18 @@ extension RunnerTests {
|
|
|
416
486
|
}
|
|
417
487
|
|
|
418
488
|
private func executeSnapshotDispatched(command: Command) throws -> Response {
|
|
489
|
+
try executeDispatchedWithRecovery(command: command) {
|
|
490
|
+
try self.executeSnapshotDispatchedOnce(command: command)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/// The dispatched snapshot recovery loop: read-only retry + XCTest-recorded-failure invalidation,
|
|
495
|
+
/// matching what `executeOnMainSafely` gives the generic path. `perform` runs the capture and its
|
|
496
|
+
/// own bounded main-thread work.
|
|
497
|
+
func executeDispatchedWithRecovery(
|
|
498
|
+
command: Command,
|
|
499
|
+
perform: () throws -> Response
|
|
500
|
+
) throws -> Response {
|
|
419
501
|
var hasRetried = false
|
|
420
502
|
while true {
|
|
421
503
|
let failureCountBefore = try runMainThreadWork(
|
|
@@ -425,7 +507,16 @@ extension RunnerTests {
|
|
|
425
507
|
) {
|
|
426
508
|
self.currentXCTestFailureCount()
|
|
427
509
|
}
|
|
428
|
-
let response = try
|
|
510
|
+
let response = try perform()
|
|
511
|
+
// Recovered independently — re-entering main for bookkeeping would queue behind the still-
|
|
512
|
+
// abandoned XCTest query and re-stall the command (#1244), so skip it until that work drains.
|
|
513
|
+
if hasAbandonedTreeCapture() {
|
|
514
|
+
NSLog(
|
|
515
|
+
"AGENT_DEVICE_RUNNER_DISPATCH_RECOVERY_SKIPPED_XCTEST_OCCUPIED command=%@",
|
|
516
|
+
command.command.rawValue
|
|
517
|
+
)
|
|
518
|
+
return response
|
|
519
|
+
}
|
|
429
520
|
let recordedFailureResponse = try runMainThreadWork(
|
|
430
521
|
command: command,
|
|
431
522
|
timeout: mainThreadExecutionTimeout,
|
|
@@ -471,7 +562,7 @@ extension RunnerTests {
|
|
|
471
562
|
timeout: mainThreadExecutionTimeout,
|
|
472
563
|
timeoutError: mainThreadExecutionTimeoutError
|
|
473
564
|
) {
|
|
474
|
-
try self.prepareActiveCommandContextSafely(command: command)
|
|
565
|
+
try self.prepareActiveCommandContextSafely(command: command, routeToSpringboard: false)
|
|
475
566
|
}
|
|
476
567
|
switch preparation {
|
|
477
568
|
case .response(let response):
|
|
@@ -550,10 +641,16 @@ extension RunnerTests {
|
|
|
550
641
|
}
|
|
551
642
|
}
|
|
552
643
|
|
|
553
|
-
private func prepareActiveCommandContextSafely(
|
|
644
|
+
private func prepareActiveCommandContextSafely(
|
|
645
|
+
command: Command,
|
|
646
|
+
routeToSpringboard: Bool
|
|
647
|
+
) throws -> ActiveCommandPreparation {
|
|
554
648
|
var preparation: ActiveCommandPreparation?
|
|
555
649
|
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
556
|
-
preparation = self.prepareActiveCommandContext(
|
|
650
|
+
preparation = self.prepareActiveCommandContext(
|
|
651
|
+
command: command,
|
|
652
|
+
routeToSpringboard: routeToSpringboard
|
|
653
|
+
)
|
|
557
654
|
})
|
|
558
655
|
if let exceptionMessage {
|
|
559
656
|
throw NSError(
|
|
@@ -572,8 +669,15 @@ extension RunnerTests {
|
|
|
572
669
|
return preparation
|
|
573
670
|
}
|
|
574
671
|
|
|
575
|
-
private func executeOnMain(
|
|
576
|
-
|
|
672
|
+
private func executeOnMain(
|
|
673
|
+
command: Command,
|
|
674
|
+
alertDeadline: Date?,
|
|
675
|
+
routeToSpringboard: Bool
|
|
676
|
+
) throws -> Response {
|
|
677
|
+
let preparation = prepareActiveCommandContext(
|
|
678
|
+
command: command,
|
|
679
|
+
routeToSpringboard: routeToSpringboard
|
|
680
|
+
)
|
|
577
681
|
let activeApp: XCUIApplication
|
|
578
682
|
switch preparation {
|
|
579
683
|
case .response(let response):
|
|
@@ -585,6 +689,8 @@ extension RunnerTests {
|
|
|
585
689
|
switch command.command {
|
|
586
690
|
case .status:
|
|
587
691
|
return executeStatus(command: command)
|
|
692
|
+
case .targetReset:
|
|
693
|
+
return resetTargetAfterExternalRelaunch()
|
|
588
694
|
case .shutdown:
|
|
589
695
|
stopRecordingIfNeeded()
|
|
590
696
|
return Response(ok: true, data: DataPayload(message: "shutdown"))
|
|
@@ -649,15 +755,46 @@ extension RunnerTests {
|
|
|
649
755
|
}
|
|
650
756
|
case .uptime:
|
|
651
757
|
return executeUptime()
|
|
758
|
+
case .activate:
|
|
759
|
+
guard
|
|
760
|
+
let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
761
|
+
!bundleId.isEmpty
|
|
762
|
+
else {
|
|
763
|
+
return Response(ok: false, error: ErrorPayload(message: "activate requires appBundleId"))
|
|
764
|
+
}
|
|
765
|
+
// prepareActiveCommandContext already activated this bundle. Keep this case as the
|
|
766
|
+
// explicit acknowledgement after that preflight, not as a second activation.
|
|
767
|
+
return Response(ok: true, data: DataPayload(message: "app activated"))
|
|
768
|
+
case .terminate:
|
|
769
|
+
guard
|
|
770
|
+
let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
771
|
+
!bundleId.isEmpty
|
|
772
|
+
else {
|
|
773
|
+
return Response(ok: false, error: ErrorPayload(message: "terminate requires appBundleId"))
|
|
774
|
+
}
|
|
775
|
+
XCUIApplication(bundleIdentifier: bundleId).terminate()
|
|
776
|
+
if currentBundleId == bundleId {
|
|
777
|
+
invalidateCachedTarget(reason: "target_terminated")
|
|
778
|
+
}
|
|
779
|
+
return Response(ok: true, data: DataPayload(message: "app terminated"))
|
|
652
780
|
default:
|
|
653
781
|
break
|
|
654
782
|
}
|
|
655
|
-
return try executeOnMainPrepared(
|
|
783
|
+
return try executeOnMainPrepared(
|
|
784
|
+
command: command,
|
|
785
|
+
activeApp: activeApp,
|
|
786
|
+
alertDeadline: alertDeadline
|
|
787
|
+
)
|
|
656
788
|
}
|
|
657
789
|
|
|
658
|
-
private func prepareActiveCommandContext(
|
|
790
|
+
private func prepareActiveCommandContext(
|
|
791
|
+
command: Command,
|
|
792
|
+
routeToSpringboard: Bool = false
|
|
793
|
+
) -> ActiveCommandPreparation {
|
|
659
794
|
var activeApp = currentApp ?? app
|
|
660
|
-
if
|
|
795
|
+
if routeToSpringboard {
|
|
796
|
+
activeApp = springboard
|
|
797
|
+
} else if shouldSkipAppActivationPreflight(command) {
|
|
661
798
|
activeApp = resolveAppWithoutActivation(command: command)
|
|
662
799
|
} else if !isRunnerLifecycleCommand(command.command) {
|
|
663
800
|
let normalizedBundleId = command.appBundleId?
|
|
@@ -666,11 +803,12 @@ extension RunnerTests {
|
|
|
666
803
|
if let bundleId = requestedBundleId {
|
|
667
804
|
if currentBundleId != bundleId || currentApp == nil {
|
|
668
805
|
_ = activateTarget(bundleId: bundleId, reason: "bundle_changed")
|
|
806
|
+
} else {
|
|
807
|
+
refreshCachedTargetIfProcessChanged(bundleId: bundleId)
|
|
669
808
|
}
|
|
670
809
|
} else {
|
|
671
810
|
// Do not reuse stale bundle targets when the caller does not explicitly request one.
|
|
672
|
-
|
|
673
|
-
currentBundleId = nil
|
|
811
|
+
invalidateCachedTarget(reason: "missing_app_bundle")
|
|
674
812
|
}
|
|
675
813
|
|
|
676
814
|
activeApp = currentApp ?? app
|
|
@@ -721,10 +859,14 @@ extension RunnerTests {
|
|
|
721
859
|
return .context(ActiveCommandContext(app: activeApp))
|
|
722
860
|
}
|
|
723
861
|
|
|
724
|
-
private func executeOnMainPrepared(
|
|
862
|
+
private func executeOnMainPrepared(
|
|
863
|
+
command: Command,
|
|
864
|
+
activeApp: XCUIApplication,
|
|
865
|
+
alertDeadline: Date? = nil
|
|
866
|
+
) throws -> Response {
|
|
725
867
|
var activeApp = activeApp
|
|
726
868
|
switch command.command {
|
|
727
|
-
case .status, .shutdown, .recordStart, .recordStop, .uptime:
|
|
869
|
+
case .status, .activate, .terminate, .targetReset, .shutdown, .recordStart, .recordStop, .uptime:
|
|
728
870
|
return Response(
|
|
729
871
|
ok: false,
|
|
730
872
|
error: ErrorPayload(
|
|
@@ -734,11 +876,21 @@ extension RunnerTests {
|
|
|
734
876
|
)
|
|
735
877
|
case .tap:
|
|
736
878
|
if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
|
|
879
|
+
let expectedPoint: CGPoint?
|
|
880
|
+
if command.allowNonHittableCoordinateFallback == true,
|
|
881
|
+
let x = command.x,
|
|
882
|
+
let y = command.y
|
|
883
|
+
{
|
|
884
|
+
expectedPoint = CGPoint(x: x, y: y)
|
|
885
|
+
} else {
|
|
886
|
+
expectedPoint = nil
|
|
887
|
+
}
|
|
737
888
|
let match = findElement(
|
|
738
889
|
app: activeApp,
|
|
739
890
|
selectorKey: selectorKey,
|
|
740
891
|
selectorValue: selectorValue,
|
|
741
|
-
allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true
|
|
892
|
+
allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true,
|
|
893
|
+
expectedPoint: expectedPoint
|
|
742
894
|
)
|
|
743
895
|
if match.isAmbiguous {
|
|
744
896
|
return Response(ok: false, error: ErrorPayload(code: "AMBIGUOUS_MATCH", message: "selector matched multiple elements"))
|
|
@@ -764,15 +916,19 @@ extension RunnerTests {
|
|
|
764
916
|
message: "element resolved off-screen at (\(Int(frame.midX)), \(Int(frame.midY)))"))
|
|
765
917
|
}
|
|
766
918
|
let isTextEntry = isTextEntryElement(element)
|
|
767
|
-
let
|
|
768
|
-
|
|
769
|
-
|
|
919
|
+
let touchPoint = expectedPoint ?? CGPoint(x: frame.midX, y: frame.midY)
|
|
920
|
+
let touchFrame = resolvedTouchVisualizationFrame(
|
|
921
|
+
app: activeApp,
|
|
922
|
+
x: touchPoint.x,
|
|
923
|
+
y: touchPoint.y
|
|
924
|
+
)
|
|
770
925
|
let (timing, outcome) = performGesture(activeApp) {
|
|
771
|
-
if match.usedNonHittableFallback {
|
|
926
|
+
if expectedPoint != nil || match.usedNonHittableFallback {
|
|
772
927
|
// Maestro compatibility: RN E2E backdoor controls can be 1x1 and
|
|
773
|
-
// reported non-hittable by XCTest
|
|
774
|
-
//
|
|
775
|
-
|
|
928
|
+
// reported non-hittable by XCTest. Snapshot-resolved targets also
|
|
929
|
+
// need coordinate delivery because XCUIElement.activate() does not
|
|
930
|
+
// invoke every React Native accessibility wrapper.
|
|
931
|
+
return tapAt(app: activeApp, x: touchPoint.x, y: touchPoint.y)
|
|
776
932
|
}
|
|
777
933
|
return activateElement(app: activeApp, element: element, action: "tap by selector")
|
|
778
934
|
}
|
|
@@ -785,7 +941,11 @@ extension RunnerTests {
|
|
|
785
941
|
return gestureResponse(
|
|
786
942
|
message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
|
|
787
943
|
timing: timing,
|
|
788
|
-
frame: .touch(touchFrame)
|
|
944
|
+
frame: .touch(touchFrame),
|
|
945
|
+
maestroNonHittableCoordinateFallbackUsed:
|
|
946
|
+
command.allowNonHittableCoordinateFallback == true
|
|
947
|
+
? match.usedNonHittableFallback
|
|
948
|
+
: nil
|
|
789
949
|
)
|
|
790
950
|
}
|
|
791
951
|
return Response(ok: false, error: ErrorPayload(code: "ELEMENT_NOT_FOUND", message: "element not found"))
|
|
@@ -867,7 +1027,6 @@ extension RunnerTests {
|
|
|
867
1027
|
durationMs: command.durationMs,
|
|
868
1028
|
synthesized: command.synthesized == true,
|
|
869
1029
|
message: "dragged",
|
|
870
|
-
dragSemantics: command.dragSemantics,
|
|
871
1030
|
synthesizedPolicyKind: .synthesizedDrag
|
|
872
1031
|
)
|
|
873
1032
|
case .scroll:
|
|
@@ -1094,6 +1253,12 @@ extension RunnerTests {
|
|
|
1094
1253
|
guard let pngData = runnerPngData(for: screenshot.image) else {
|
|
1095
1254
|
return Response(ok: false, error: ErrorPayload(message: "Failed to encode screenshot as PNG"))
|
|
1096
1255
|
}
|
|
1256
|
+
if command.inlineScreenshot == true {
|
|
1257
|
+
return Response(
|
|
1258
|
+
ok: true,
|
|
1259
|
+
data: DataPayload(imageBase64: pngData.base64EncodedString())
|
|
1260
|
+
)
|
|
1261
|
+
}
|
|
1097
1262
|
let fileName = "screenshot-\(Int(Date().timeIntervalSince1970 * 1000)).png"
|
|
1098
1263
|
let filePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(fileName)
|
|
1099
1264
|
do {
|
|
@@ -1127,15 +1292,35 @@ extension RunnerTests {
|
|
|
1127
1292
|
else {
|
|
1128
1293
|
return Response(ok: false, error: ErrorPayload(message: "rotate requires orientation"))
|
|
1129
1294
|
}
|
|
1130
|
-
|
|
1295
|
+
let supportedOrientations = [
|
|
1296
|
+
"portrait",
|
|
1297
|
+
"portrait-upside-down",
|
|
1298
|
+
"landscape-left",
|
|
1299
|
+
"landscape-right",
|
|
1300
|
+
]
|
|
1301
|
+
guard supportedOrientations.contains(orientation) else {
|
|
1131
1302
|
return Response(
|
|
1132
|
-
ok:
|
|
1133
|
-
|
|
1303
|
+
ok: false,
|
|
1304
|
+
error: ErrorPayload(message: "unsupported rotate orientation: \(orientation)")
|
|
1305
|
+
)
|
|
1306
|
+
}
|
|
1307
|
+
guard let observedOrientation = rotateDevice(to: orientation) else {
|
|
1308
|
+
return Response(
|
|
1309
|
+
ok: false,
|
|
1310
|
+
error: ErrorPayload(message: "unable to observe device orientation after rotate")
|
|
1311
|
+
)
|
|
1312
|
+
}
|
|
1313
|
+
guard observedOrientation == orientation else {
|
|
1314
|
+
return Response(
|
|
1315
|
+
ok: false,
|
|
1316
|
+
error: ErrorPayload(
|
|
1317
|
+
message: "device orientation is \(observedOrientation), expected \(orientation)"
|
|
1318
|
+
)
|
|
1134
1319
|
)
|
|
1135
1320
|
}
|
|
1136
1321
|
return Response(
|
|
1137
|
-
ok:
|
|
1138
|
-
|
|
1322
|
+
ok: true,
|
|
1323
|
+
data: DataPayload(message: "rotate", orientation: observedOrientation)
|
|
1139
1324
|
)
|
|
1140
1325
|
case .appSwitcher:
|
|
1141
1326
|
performAppSwitcherGesture(app: activeApp)
|
|
@@ -1183,83 +1368,60 @@ extension RunnerTests {
|
|
|
1183
1368
|
)
|
|
1184
1369
|
case .alert:
|
|
1185
1370
|
let action = (command.action ?? "get").lowercased()
|
|
1186
|
-
|
|
1371
|
+
let deadline = alertDeadline ?? Date().addingTimeInterval(
|
|
1372
|
+
Self.alertCommandTimeout(timeoutMs: command.timeoutMs)
|
|
1373
|
+
)
|
|
1374
|
+
guard let alert = resolveAlert(app: activeApp, deadline: deadline) else {
|
|
1187
1375
|
return Response(ok: false, error: ErrorPayload(message: "alert not found"))
|
|
1188
1376
|
}
|
|
1189
|
-
return handleAlert(alert, action: action)
|
|
1190
|
-
case .
|
|
1191
|
-
guard let
|
|
1192
|
-
return Response(
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
|
|
1196
|
-
}
|
|
1197
|
-
if let response = unsupportedResponse(for: outcome) {
|
|
1198
|
-
return response
|
|
1199
|
-
}
|
|
1200
|
-
return gestureResponse(message: "pinched", timing: timing)
|
|
1201
|
-
case .sequence:
|
|
1202
|
-
return executeSequence(command: command, activeApp: activeApp)
|
|
1203
|
-
case .rotateGesture:
|
|
1204
|
-
guard let degrees = command.degrees, degrees.isFinite else {
|
|
1205
|
-
return Response(ok: false, error: ErrorPayload(message: "rotateGesture requires degrees"))
|
|
1206
|
-
}
|
|
1207
|
-
let velocity = command.velocity ?? (degrees >= 0 ? 1.0 : -1.0)
|
|
1208
|
-
guard velocity.isFinite && velocity != 0 else {
|
|
1209
|
-
return Response(ok: false, error: ErrorPayload(message: "rotateGesture velocity must be non-zero"))
|
|
1210
|
-
}
|
|
1211
|
-
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
1212
|
-
rotateGesture(
|
|
1213
|
-
app: activeApp,
|
|
1214
|
-
degrees: degrees,
|
|
1215
|
-
x: command.x,
|
|
1216
|
-
y: command.y,
|
|
1217
|
-
velocity: velocity
|
|
1377
|
+
return handleAlert(alert, action: action, deadline: deadline)
|
|
1378
|
+
case .gesture:
|
|
1379
|
+
guard let plan = command.gesturePlan else {
|
|
1380
|
+
return Response(
|
|
1381
|
+
ok: false,
|
|
1382
|
+
error: ErrorPayload(code: "INVALID_ARGS", message: "gesture requires gesturePlan")
|
|
1218
1383
|
)
|
|
1219
1384
|
}
|
|
1220
|
-
if let
|
|
1221
|
-
return
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
guard
|
|
1226
|
-
let x = command.x,
|
|
1227
|
-
let y = command.y,
|
|
1228
|
-
let dx = command.dx,
|
|
1229
|
-
let dy = command.dy,
|
|
1230
|
-
x.isFinite,
|
|
1231
|
-
y.isFinite,
|
|
1232
|
-
dx.isFinite,
|
|
1233
|
-
dy.isFinite
|
|
1234
|
-
else {
|
|
1235
|
-
return Response(ok: false, error: ErrorPayload(message: "transformGesture requires finite x y dx dy"))
|
|
1236
|
-
}
|
|
1237
|
-
guard let scale = command.scale, scale.isFinite, scale > 0 else {
|
|
1238
|
-
return Response(ok: false, error: ErrorPayload(message: "transformGesture requires scale > 0"))
|
|
1239
|
-
}
|
|
1240
|
-
guard let degrees = command.degrees, degrees.isFinite else {
|
|
1241
|
-
return Response(ok: false, error: ErrorPayload(message: "transformGesture requires finite degrees"))
|
|
1385
|
+
if let validationError = plannedGestureValidationError(plan) {
|
|
1386
|
+
return Response(
|
|
1387
|
+
ok: false,
|
|
1388
|
+
error: ErrorPayload(code: "INVALID_ARGS", message: validationError)
|
|
1389
|
+
)
|
|
1242
1390
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1391
|
+
if plannedGestureExecution(for: plan) == .fastSwipe {
|
|
1392
|
+
// Validation above guarantees a non-empty, single-pointer path for this execution kind.
|
|
1393
|
+
let first = plan.pointers[0].samples.first!.point
|
|
1394
|
+
let last = plan.pointers[0].samples.last!.point
|
|
1395
|
+
return canonicalPlannedGestureResponse(
|
|
1396
|
+
executeDragGesture(
|
|
1397
|
+
activeApp: activeApp,
|
|
1398
|
+
x: first.x,
|
|
1399
|
+
y: first.y,
|
|
1400
|
+
x2: last.x,
|
|
1401
|
+
y2: last.y,
|
|
1402
|
+
durationMs: plan.durationMs,
|
|
1403
|
+
synthesized: true,
|
|
1404
|
+
message: plan.intent,
|
|
1405
|
+
synthesizedPolicyKind: .synthesizedDrag,
|
|
1406
|
+
synthesizedProfile: .fastSwipe
|
|
1407
|
+
)
|
|
1408
|
+
)
|
|
1246
1409
|
}
|
|
1247
1410
|
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
1248
|
-
|
|
1249
|
-
app: activeApp,
|
|
1250
|
-
x: x,
|
|
1251
|
-
y: y,
|
|
1252
|
-
dx: dx,
|
|
1253
|
-
dy: dy,
|
|
1254
|
-
scale: scale,
|
|
1255
|
-
degrees: degrees,
|
|
1256
|
-
durationMs: durationMs
|
|
1257
|
-
)
|
|
1411
|
+
sampledPlannedGesture(app: activeApp, plan: plan)
|
|
1258
1412
|
}
|
|
1259
1413
|
if let response = unsupportedResponse(for: outcome) {
|
|
1260
1414
|
return response
|
|
1261
1415
|
}
|
|
1262
|
-
return gestureResponse(message:
|
|
1416
|
+
return gestureResponse(message: plan.intent, timing: timing)
|
|
1417
|
+
case .gestureViewport:
|
|
1418
|
+
let frame = resolvedTouchReferenceFrame(app: activeApp, appFrame: activeApp.frame)
|
|
1419
|
+
guard !frame.isNull, !frame.isInfinite, !frame.isEmpty else {
|
|
1420
|
+
return Response(ok: false, error: ErrorPayload(code: "COMMAND_FAILED", message: "Active app interaction viewport is unavailable"))
|
|
1421
|
+
}
|
|
1422
|
+
return Response(ok: true, data: DataPayload(message: "gestureViewport", x: frame.minX, y: frame.minY, x2: frame.width, y2: frame.height))
|
|
1423
|
+
case .sequence:
|
|
1424
|
+
return executeSequence(command: command, activeApp: activeApp)
|
|
1263
1425
|
}
|
|
1264
1426
|
}
|
|
1265
1427
|
|
|
@@ -1275,9 +1437,9 @@ extension RunnerTests {
|
|
|
1275
1437
|
durationMs: Double?,
|
|
1276
1438
|
synthesized: Bool,
|
|
1277
1439
|
message: String,
|
|
1278
|
-
dragSemantics: SynthesizedDragSemantics? = nil,
|
|
1279
1440
|
synthesizedContext: SynthesizedCoordinateContext? = nil,
|
|
1280
|
-
synthesizedPolicyKind: SynthesizedGesturePolicyKind
|
|
1441
|
+
synthesizedPolicyKind: SynthesizedGesturePolicyKind,
|
|
1442
|
+
synthesizedProfile: SynthesizedDragProfile = .continuous
|
|
1281
1443
|
) -> Response {
|
|
1282
1444
|
let commandName = dragCommandName(message: message)
|
|
1283
1445
|
guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite else {
|
|
@@ -1294,9 +1456,9 @@ extension RunnerTests {
|
|
|
1294
1456
|
y2: y2,
|
|
1295
1457
|
durationMs: durationMs,
|
|
1296
1458
|
message: message,
|
|
1297
|
-
dragSemantics: dragSemantics,
|
|
1298
1459
|
context: synthesizedContext,
|
|
1299
|
-
policyKind: synthesizedPolicyKind
|
|
1460
|
+
policyKind: synthesizedPolicyKind,
|
|
1461
|
+
profile: synthesizedProfile
|
|
1300
1462
|
) {
|
|
1301
1463
|
return synthesizedResponse
|
|
1302
1464
|
}
|
|
@@ -1320,7 +1482,7 @@ extension RunnerTests {
|
|
|
1320
1482
|
x2: dragPoints.x2,
|
|
1321
1483
|
y2: dragPoints.y2,
|
|
1322
1484
|
durationMs: durationMs,
|
|
1323
|
-
|
|
1485
|
+
profile: synthesizedProfile,
|
|
1324
1486
|
context: context
|
|
1325
1487
|
)
|
|
1326
1488
|
}
|
|
@@ -1361,9 +1523,9 @@ extension RunnerTests {
|
|
|
1361
1523
|
y2: Double,
|
|
1362
1524
|
durationMs: Double?,
|
|
1363
1525
|
message: String,
|
|
1364
|
-
dragSemantics: SynthesizedDragSemantics?,
|
|
1365
1526
|
context: SynthesizedCoordinateContext?,
|
|
1366
|
-
policyKind: SynthesizedGesturePolicyKind
|
|
1527
|
+
policyKind: SynthesizedGesturePolicyKind,
|
|
1528
|
+
profile: SynthesizedDragProfile
|
|
1367
1529
|
) -> Response? {
|
|
1368
1530
|
#if os(iOS)
|
|
1369
1531
|
let policy = synthesizedGesturePolicy(policyKind)
|
|
@@ -1415,7 +1577,7 @@ extension RunnerTests {
|
|
|
1415
1577
|
x2: plan.points.x2,
|
|
1416
1578
|
y2: plan.points.y2,
|
|
1417
1579
|
durationMs: durationMs,
|
|
1418
|
-
|
|
1580
|
+
profile: profile,
|
|
1419
1581
|
context: plan.context
|
|
1420
1582
|
)
|
|
1421
1583
|
}
|
|
@@ -1535,23 +1697,58 @@ extension RunnerTests {
|
|
|
1535
1697
|
|
|
1536
1698
|
private func shouldSkipAppActivationPreflight(_ command: Command) -> Bool {
|
|
1537
1699
|
#if os(iOS)
|
|
1538
|
-
|
|
1700
|
+
if command.command == .alert {
|
|
1701
|
+
return true
|
|
1702
|
+
}
|
|
1703
|
+
// Coordinate-only synthesized taps can run after an AX-fatal foreground screen because they do not
|
|
1539
1704
|
// need app activation, window lookup, keyboard lookup, or element resolution. Selector/text
|
|
1540
1705
|
// interactions intentionally stay on the normal AX path because they need an element query.
|
|
1541
1706
|
// Scroll/drag/sequence keep the normal foreground guard and stabilization path.
|
|
1542
1707
|
guard command.text == nil, command.selectorKey == nil else { return false }
|
|
1543
1708
|
guard hasCachedTargetForActivationSkip(command: command) else { return false }
|
|
1544
|
-
return command
|
|
1545
|
-
&& command.synthesized == true
|
|
1546
|
-
&& command.x != nil
|
|
1547
|
-
&& command.y != nil
|
|
1709
|
+
return isCoordinateOnlyTap(command)
|
|
1548
1710
|
#else
|
|
1549
1711
|
return false
|
|
1550
1712
|
#endif
|
|
1551
1713
|
}
|
|
1552
1714
|
|
|
1715
|
+
private func shouldRouteToSpringboardBlockingSystemModal(
|
|
1716
|
+
_ command: Command,
|
|
1717
|
+
deadline: Date? = nil
|
|
1718
|
+
) -> Bool {
|
|
1719
|
+
#if os(iOS)
|
|
1720
|
+
guard command.command == .alert || isCoordinateOnlyTap(command) else {
|
|
1721
|
+
return false
|
|
1722
|
+
}
|
|
1723
|
+
let budgetDeadline = Date().addingTimeInterval(systemModalProbeBudget)
|
|
1724
|
+
let probeDeadline = deadline.map { min($0, budgetDeadline) } ?? budgetDeadline
|
|
1725
|
+
// `runMainThreadWork` executes inline for a main-thread caller, so that path cannot use its
|
|
1726
|
+
// timeout machinery. Direct main-thread dispatch keeps the prior synchronous modal check;
|
|
1727
|
+
// normal off-main command dispatch uses the bounded probe and post-probe busy recovery.
|
|
1728
|
+
if Thread.isMainThread {
|
|
1729
|
+
return firstBlockingSystemModal(
|
|
1730
|
+
in: springboard,
|
|
1731
|
+
deadline: probeDeadline
|
|
1732
|
+
) != nil
|
|
1733
|
+
}
|
|
1734
|
+
return boundedBlockingSystemAlertSnapshot(
|
|
1735
|
+
deadline: probeDeadline
|
|
1736
|
+
) != nil
|
|
1737
|
+
#else
|
|
1738
|
+
return false
|
|
1739
|
+
#endif
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
private func isCoordinateOnlyTap(_ command: Command) -> Bool {
|
|
1743
|
+
return command.command == .tap
|
|
1744
|
+
&& command.text == nil
|
|
1745
|
+
&& command.selectorKey == nil
|
|
1746
|
+
&& command.x != nil
|
|
1747
|
+
&& command.y != nil
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1553
1750
|
private func hasCachedTargetForActivationSkip(command: Command) -> Bool {
|
|
1554
|
-
guard currentApp
|
|
1751
|
+
guard let currentApp, currentApp.state == .runningForeground else { return false }
|
|
1555
1752
|
guard let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
1556
1753
|
!bundleId.isEmpty
|
|
1557
1754
|
else {
|
|
@@ -1580,6 +1777,7 @@ extension RunnerTests {
|
|
|
1580
1777
|
let delaySeconds = Double(max(command.delayMs ?? 0, 0)) / 1000.0
|
|
1581
1778
|
let textEntryMode = resolveTextEntryMode(command)
|
|
1582
1779
|
let target: TextEntryTarget
|
|
1780
|
+
var maestroNonHittableCoordinateFallbackUsed: Bool?
|
|
1583
1781
|
let focusStartedAt = Date()
|
|
1584
1782
|
if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
|
|
1585
1783
|
let match = findElement(
|
|
@@ -1597,6 +1795,9 @@ extension RunnerTests {
|
|
|
1597
1795
|
guard isTextEntryElement(element) else {
|
|
1598
1796
|
return Response(ok: false, error: ErrorPayload(code: "INVALID_TARGET", message: "selector did not match a text input"))
|
|
1599
1797
|
}
|
|
1798
|
+
if command.allowNonHittableCoordinateFallback == true {
|
|
1799
|
+
maestroNonHittableCoordinateFallbackUsed = match.usedNonHittableFallback
|
|
1800
|
+
}
|
|
1600
1801
|
target = focusTextInputForTextEntry(app: activeApp, element: element)
|
|
1601
1802
|
} else {
|
|
1602
1803
|
target = focusTextInputForTextEntry(app: activeApp, x: command.x, y: command.y)
|
|
@@ -1645,7 +1846,8 @@ extension RunnerTests {
|
|
|
1645
1846
|
x: point.map { Double($0.x) },
|
|
1646
1847
|
y: point.map { Double($0.y) },
|
|
1647
1848
|
referenceWidth: frame.isEmpty ? nil : Double(frame.width),
|
|
1648
|
-
referenceHeight: frame.isEmpty ? nil : Double(frame.height)
|
|
1849
|
+
referenceHeight: frame.isEmpty ? nil : Double(frame.height),
|
|
1850
|
+
maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed
|
|
1649
1851
|
)
|
|
1650
1852
|
)
|
|
1651
1853
|
}
|