agent-device 0.19.2 → 0.20.0
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 +61 -70
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.0.apk +0 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.0.apk.sha256 +1 -0
- package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.0.manifest.json +10 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.0.apk +0 -0
- package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.0.apk.sha256 +1 -0
- package/{android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.2.manifest.json → android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.0.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 +12 -10
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +154 -185
- 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 +275 -113
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +2 -2
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +150 -104
- 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 +44 -14
- 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 -106
- package/dist/{apple-runner → apple/runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +126 -11
- 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.swift +14 -0
- 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/audio-probe.js +1 -1
- package/dist/src/boot-diagnostics.js +2 -0
- package/dist/src/cli-config.js +1 -0
- package/dist/src/cli-help.js +64 -30
- package/dist/src/cli-runner.js +1 -1
- package/dist/src/cli.js +15 -11
- package/dist/src/command-schema.js +3 -3
- package/dist/src/config.js +1 -0
- package/dist/src/context.js +1 -0
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/daemon-shutdown-report.js +1 -0
- 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/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 +412 -289
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +4 -3
- package/dist/src/input-actions2.js +1 -1
- 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/internal/bin.js +1 -1
- package/dist/src/internal/daemon.js +43 -17
- 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/metro.d.ts +2 -2
- 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/register-builtins.js +1 -0
- package/dist/src/registry.js +24 -24
- 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-sequence.js +1 -1
- package/dist/src/runtime-transport.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 +119 -33
- package/dist/src/sdk-android-adb.js +4 -4
- package/dist/src/sdk-app-inventory.js +1 -1
- package/dist/src/sdk-batch-runner.d.ts +2 -1
- package/dist/src/sdk-batch-runner.js +2 -2
- package/dist/src/sdk-batch.js +2 -2
- package/dist/src/sdk-contracts.d.ts +105 -98
- package/dist/src/sdk-contracts.js +2 -2
- package/dist/src/sdk-device.js +1 -0
- package/dist/src/sdk-finders.d.ts +3 -3
- package/dist/src/sdk-finders.js +1 -1
- package/dist/src/sdk-io.d.ts +1 -1
- package/dist/src/sdk-io.js +1 -1
- package/dist/src/sdk-remote-config.d.ts +9 -1
- 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/setting-state.js +1 -1
- package/dist/src/simulator.js +1 -1
- package/dist/src/snapshot-diagnostics.js +5 -2
- package/dist/src/snapshot.js +1 -1
- package/dist/src/snapshot2.js +1 -2
- package/dist/src/tool-provider.js +2 -2
- package/dist/src/tool-provider2.js +1 -1
- package/dist/src/tv-remote.js +13 -12
- package/dist/src/url.js +1 -1
- package/dist/src/web.js +1 -1
- package/package.json +33 -20
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.2.apk +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.2.apk.sha256 +0 -1
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.2.manifest.json +0 -10
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.2.apk +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.2.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/selectors2.js +0 -1
- package/dist/src/simctl.js +0 -1
- package/dist/src/snapshot-helper.js +0 -5
- package/dist/src/ttl-memo.js +0 -1
- 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+TextEntry.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"))
|
|
@@ -652,12 +758,21 @@ extension RunnerTests {
|
|
|
652
758
|
default:
|
|
653
759
|
break
|
|
654
760
|
}
|
|
655
|
-
return try executeOnMainPrepared(
|
|
761
|
+
return try executeOnMainPrepared(
|
|
762
|
+
command: command,
|
|
763
|
+
activeApp: activeApp,
|
|
764
|
+
alertDeadline: alertDeadline
|
|
765
|
+
)
|
|
656
766
|
}
|
|
657
767
|
|
|
658
|
-
private func prepareActiveCommandContext(
|
|
768
|
+
private func prepareActiveCommandContext(
|
|
769
|
+
command: Command,
|
|
770
|
+
routeToSpringboard: Bool = false
|
|
771
|
+
) -> ActiveCommandPreparation {
|
|
659
772
|
var activeApp = currentApp ?? app
|
|
660
|
-
if
|
|
773
|
+
if routeToSpringboard {
|
|
774
|
+
activeApp = springboard
|
|
775
|
+
} else if shouldSkipAppActivationPreflight(command) {
|
|
661
776
|
activeApp = resolveAppWithoutActivation(command: command)
|
|
662
777
|
} else if !isRunnerLifecycleCommand(command.command) {
|
|
663
778
|
let normalizedBundleId = command.appBundleId?
|
|
@@ -666,11 +781,12 @@ extension RunnerTests {
|
|
|
666
781
|
if let bundleId = requestedBundleId {
|
|
667
782
|
if currentBundleId != bundleId || currentApp == nil {
|
|
668
783
|
_ = activateTarget(bundleId: bundleId, reason: "bundle_changed")
|
|
784
|
+
} else {
|
|
785
|
+
refreshCachedTargetIfProcessChanged(bundleId: bundleId)
|
|
669
786
|
}
|
|
670
787
|
} else {
|
|
671
788
|
// Do not reuse stale bundle targets when the caller does not explicitly request one.
|
|
672
|
-
|
|
673
|
-
currentBundleId = nil
|
|
789
|
+
invalidateCachedTarget(reason: "missing_app_bundle")
|
|
674
790
|
}
|
|
675
791
|
|
|
676
792
|
activeApp = currentApp ?? app
|
|
@@ -721,10 +837,14 @@ extension RunnerTests {
|
|
|
721
837
|
return .context(ActiveCommandContext(app: activeApp))
|
|
722
838
|
}
|
|
723
839
|
|
|
724
|
-
private func executeOnMainPrepared(
|
|
840
|
+
private func executeOnMainPrepared(
|
|
841
|
+
command: Command,
|
|
842
|
+
activeApp: XCUIApplication,
|
|
843
|
+
alertDeadline: Date? = nil
|
|
844
|
+
) throws -> Response {
|
|
725
845
|
var activeApp = activeApp
|
|
726
846
|
switch command.command {
|
|
727
|
-
case .status, .shutdown, .recordStart, .recordStop, .uptime:
|
|
847
|
+
case .status, .targetReset, .shutdown, .recordStart, .recordStop, .uptime:
|
|
728
848
|
return Response(
|
|
729
849
|
ok: false,
|
|
730
850
|
error: ErrorPayload(
|
|
@@ -734,11 +854,21 @@ extension RunnerTests {
|
|
|
734
854
|
)
|
|
735
855
|
case .tap:
|
|
736
856
|
if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
|
|
857
|
+
let expectedPoint: CGPoint?
|
|
858
|
+
if command.allowNonHittableCoordinateFallback == true,
|
|
859
|
+
let x = command.x,
|
|
860
|
+
let y = command.y
|
|
861
|
+
{
|
|
862
|
+
expectedPoint = CGPoint(x: x, y: y)
|
|
863
|
+
} else {
|
|
864
|
+
expectedPoint = nil
|
|
865
|
+
}
|
|
737
866
|
let match = findElement(
|
|
738
867
|
app: activeApp,
|
|
739
868
|
selectorKey: selectorKey,
|
|
740
869
|
selectorValue: selectorValue,
|
|
741
|
-
allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true
|
|
870
|
+
allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true,
|
|
871
|
+
expectedPoint: expectedPoint
|
|
742
872
|
)
|
|
743
873
|
if match.isAmbiguous {
|
|
744
874
|
return Response(ok: false, error: ErrorPayload(code: "AMBIGUOUS_MATCH", message: "selector matched multiple elements"))
|
|
@@ -764,15 +894,19 @@ extension RunnerTests {
|
|
|
764
894
|
message: "element resolved off-screen at (\(Int(frame.midX)), \(Int(frame.midY)))"))
|
|
765
895
|
}
|
|
766
896
|
let isTextEntry = isTextEntryElement(element)
|
|
767
|
-
let
|
|
768
|
-
|
|
769
|
-
|
|
897
|
+
let touchPoint = expectedPoint ?? CGPoint(x: frame.midX, y: frame.midY)
|
|
898
|
+
let touchFrame = resolvedTouchVisualizationFrame(
|
|
899
|
+
app: activeApp,
|
|
900
|
+
x: touchPoint.x,
|
|
901
|
+
y: touchPoint.y
|
|
902
|
+
)
|
|
770
903
|
let (timing, outcome) = performGesture(activeApp) {
|
|
771
|
-
if match.usedNonHittableFallback {
|
|
904
|
+
if expectedPoint != nil || match.usedNonHittableFallback {
|
|
772
905
|
// Maestro compatibility: RN E2E backdoor controls can be 1x1 and
|
|
773
|
-
// reported non-hittable by XCTest
|
|
774
|
-
//
|
|
775
|
-
|
|
906
|
+
// reported non-hittable by XCTest. Snapshot-resolved targets also
|
|
907
|
+
// need coordinate delivery because XCUIElement.activate() does not
|
|
908
|
+
// invoke every React Native accessibility wrapper.
|
|
909
|
+
return tapAt(app: activeApp, x: touchPoint.x, y: touchPoint.y)
|
|
776
910
|
}
|
|
777
911
|
return activateElement(app: activeApp, element: element, action: "tap by selector")
|
|
778
912
|
}
|
|
@@ -785,7 +919,11 @@ extension RunnerTests {
|
|
|
785
919
|
return gestureResponse(
|
|
786
920
|
message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
|
|
787
921
|
timing: timing,
|
|
788
|
-
frame: .touch(touchFrame)
|
|
922
|
+
frame: .touch(touchFrame),
|
|
923
|
+
maestroNonHittableCoordinateFallbackUsed:
|
|
924
|
+
command.allowNonHittableCoordinateFallback == true
|
|
925
|
+
? match.usedNonHittableFallback
|
|
926
|
+
: nil
|
|
789
927
|
)
|
|
790
928
|
}
|
|
791
929
|
return Response(ok: false, error: ErrorPayload(code: "ELEMENT_NOT_FOUND", message: "element not found"))
|
|
@@ -1146,7 +1284,9 @@ extension RunnerTests {
|
|
|
1146
1284
|
ok: false,
|
|
1147
1285
|
error: ErrorPayload(
|
|
1148
1286
|
code: "UNSUPPORTED_OPERATION",
|
|
1149
|
-
message: "Unable to dismiss the iOS keyboard without a safe native dismiss control"
|
|
1287
|
+
message: "Unable to dismiss the iOS keyboard without a safe native dismiss control",
|
|
1288
|
+
hint:
|
|
1289
|
+
"The on-screen keyboard usually does not block agent-device interactions: press the next target directly instead of retrying dismiss. If that press fails or reports no visible effect, scroll the target into view, or use keyboard enter to press the return key when submission is wanted."
|
|
1150
1290
|
)
|
|
1151
1291
|
)
|
|
1152
1292
|
}
|
|
@@ -1180,83 +1320,60 @@ extension RunnerTests {
|
|
|
1180
1320
|
)
|
|
1181
1321
|
case .alert:
|
|
1182
1322
|
let action = (command.action ?? "get").lowercased()
|
|
1183
|
-
|
|
1323
|
+
let deadline = alertDeadline ?? Date().addingTimeInterval(
|
|
1324
|
+
Self.alertCommandTimeout(timeoutMs: command.timeoutMs)
|
|
1325
|
+
)
|
|
1326
|
+
guard let alert = resolveAlert(app: activeApp, deadline: deadline) else {
|
|
1184
1327
|
return Response(ok: false, error: ErrorPayload(message: "alert not found"))
|
|
1185
1328
|
}
|
|
1186
|
-
return handleAlert(alert, action: action)
|
|
1187
|
-
case .
|
|
1188
|
-
guard let
|
|
1189
|
-
return Response(
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
|
|
1193
|
-
}
|
|
1194
|
-
if let response = unsupportedResponse(for: outcome) {
|
|
1195
|
-
return response
|
|
1196
|
-
}
|
|
1197
|
-
return gestureResponse(message: "pinched", timing: timing)
|
|
1198
|
-
case .sequence:
|
|
1199
|
-
return executeSequence(command: command, activeApp: activeApp)
|
|
1200
|
-
case .rotateGesture:
|
|
1201
|
-
guard let degrees = command.degrees, degrees.isFinite else {
|
|
1202
|
-
return Response(ok: false, error: ErrorPayload(message: "rotateGesture requires degrees"))
|
|
1203
|
-
}
|
|
1204
|
-
let velocity = command.velocity ?? (degrees >= 0 ? 1.0 : -1.0)
|
|
1205
|
-
guard velocity.isFinite && velocity != 0 else {
|
|
1206
|
-
return Response(ok: false, error: ErrorPayload(message: "rotateGesture velocity must be non-zero"))
|
|
1207
|
-
}
|
|
1208
|
-
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
1209
|
-
rotateGesture(
|
|
1210
|
-
app: activeApp,
|
|
1211
|
-
degrees: degrees,
|
|
1212
|
-
x: command.x,
|
|
1213
|
-
y: command.y,
|
|
1214
|
-
velocity: velocity
|
|
1329
|
+
return handleAlert(alert, action: action, deadline: deadline)
|
|
1330
|
+
case .gesture:
|
|
1331
|
+
guard let plan = command.gesturePlan else {
|
|
1332
|
+
return Response(
|
|
1333
|
+
ok: false,
|
|
1334
|
+
error: ErrorPayload(code: "INVALID_ARGS", message: "gesture requires gesturePlan")
|
|
1215
1335
|
)
|
|
1216
1336
|
}
|
|
1217
|
-
if let
|
|
1218
|
-
return
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
guard
|
|
1223
|
-
let x = command.x,
|
|
1224
|
-
let y = command.y,
|
|
1225
|
-
let dx = command.dx,
|
|
1226
|
-
let dy = command.dy,
|
|
1227
|
-
x.isFinite,
|
|
1228
|
-
y.isFinite,
|
|
1229
|
-
dx.isFinite,
|
|
1230
|
-
dy.isFinite
|
|
1231
|
-
else {
|
|
1232
|
-
return Response(ok: false, error: ErrorPayload(message: "transformGesture requires finite x y dx dy"))
|
|
1233
|
-
}
|
|
1234
|
-
guard let scale = command.scale, scale.isFinite, scale > 0 else {
|
|
1235
|
-
return Response(ok: false, error: ErrorPayload(message: "transformGesture requires scale > 0"))
|
|
1236
|
-
}
|
|
1237
|
-
guard let degrees = command.degrees, degrees.isFinite else {
|
|
1238
|
-
return Response(ok: false, error: ErrorPayload(message: "transformGesture requires finite degrees"))
|
|
1337
|
+
if let validationError = plannedGestureValidationError(plan) {
|
|
1338
|
+
return Response(
|
|
1339
|
+
ok: false,
|
|
1340
|
+
error: ErrorPayload(code: "INVALID_ARGS", message: validationError)
|
|
1341
|
+
)
|
|
1239
1342
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1343
|
+
if plannedGestureExecution(for: plan) == .fastSwipe {
|
|
1344
|
+
// Validation above guarantees a non-empty, single-pointer path for this execution kind.
|
|
1345
|
+
let first = plan.pointers[0].samples.first!.point
|
|
1346
|
+
let last = plan.pointers[0].samples.last!.point
|
|
1347
|
+
return canonicalPlannedGestureResponse(
|
|
1348
|
+
executeDragGesture(
|
|
1349
|
+
activeApp: activeApp,
|
|
1350
|
+
x: first.x,
|
|
1351
|
+
y: first.y,
|
|
1352
|
+
x2: last.x,
|
|
1353
|
+
y2: last.y,
|
|
1354
|
+
durationMs: plan.durationMs,
|
|
1355
|
+
synthesized: true,
|
|
1356
|
+
message: plan.intent,
|
|
1357
|
+
synthesizedPolicyKind: .synthesizedDrag,
|
|
1358
|
+
synthesizedProfile: .fastSwipe
|
|
1359
|
+
)
|
|
1360
|
+
)
|
|
1243
1361
|
}
|
|
1244
1362
|
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
1245
|
-
|
|
1246
|
-
app: activeApp,
|
|
1247
|
-
x: x,
|
|
1248
|
-
y: y,
|
|
1249
|
-
dx: dx,
|
|
1250
|
-
dy: dy,
|
|
1251
|
-
scale: scale,
|
|
1252
|
-
degrees: degrees,
|
|
1253
|
-
durationMs: durationMs
|
|
1254
|
-
)
|
|
1363
|
+
sampledPlannedGesture(app: activeApp, plan: plan)
|
|
1255
1364
|
}
|
|
1256
1365
|
if let response = unsupportedResponse(for: outcome) {
|
|
1257
1366
|
return response
|
|
1258
1367
|
}
|
|
1259
|
-
return gestureResponse(message:
|
|
1368
|
+
return gestureResponse(message: plan.intent, timing: timing)
|
|
1369
|
+
case .gestureViewport:
|
|
1370
|
+
let frame = resolvedTouchReferenceFrame(app: activeApp, appFrame: activeApp.frame)
|
|
1371
|
+
guard !frame.isNull, !frame.isInfinite, !frame.isEmpty else {
|
|
1372
|
+
return Response(ok: false, error: ErrorPayload(code: "COMMAND_FAILED", message: "Active app interaction viewport is unavailable"))
|
|
1373
|
+
}
|
|
1374
|
+
return Response(ok: true, data: DataPayload(message: "gestureViewport", x: frame.minX, y: frame.minY, x2: frame.width, y2: frame.height))
|
|
1375
|
+
case .sequence:
|
|
1376
|
+
return executeSequence(command: command, activeApp: activeApp)
|
|
1260
1377
|
}
|
|
1261
1378
|
}
|
|
1262
1379
|
|
|
@@ -1273,7 +1390,8 @@ extension RunnerTests {
|
|
|
1273
1390
|
synthesized: Bool,
|
|
1274
1391
|
message: String,
|
|
1275
1392
|
synthesizedContext: SynthesizedCoordinateContext? = nil,
|
|
1276
|
-
synthesizedPolicyKind: SynthesizedGesturePolicyKind
|
|
1393
|
+
synthesizedPolicyKind: SynthesizedGesturePolicyKind,
|
|
1394
|
+
synthesizedProfile: SynthesizedDragProfile = .continuous
|
|
1277
1395
|
) -> Response {
|
|
1278
1396
|
let commandName = dragCommandName(message: message)
|
|
1279
1397
|
guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite else {
|
|
@@ -1291,7 +1409,8 @@ extension RunnerTests {
|
|
|
1291
1409
|
durationMs: durationMs,
|
|
1292
1410
|
message: message,
|
|
1293
1411
|
context: synthesizedContext,
|
|
1294
|
-
policyKind: synthesizedPolicyKind
|
|
1412
|
+
policyKind: synthesizedPolicyKind,
|
|
1413
|
+
profile: synthesizedProfile
|
|
1295
1414
|
) {
|
|
1296
1415
|
return synthesizedResponse
|
|
1297
1416
|
}
|
|
@@ -1315,6 +1434,7 @@ extension RunnerTests {
|
|
|
1315
1434
|
x2: dragPoints.x2,
|
|
1316
1435
|
y2: dragPoints.y2,
|
|
1317
1436
|
durationMs: durationMs,
|
|
1437
|
+
profile: synthesizedProfile,
|
|
1318
1438
|
context: context
|
|
1319
1439
|
)
|
|
1320
1440
|
}
|
|
@@ -1356,7 +1476,8 @@ extension RunnerTests {
|
|
|
1356
1476
|
durationMs: Double?,
|
|
1357
1477
|
message: String,
|
|
1358
1478
|
context: SynthesizedCoordinateContext?,
|
|
1359
|
-
policyKind: SynthesizedGesturePolicyKind
|
|
1479
|
+
policyKind: SynthesizedGesturePolicyKind,
|
|
1480
|
+
profile: SynthesizedDragProfile
|
|
1360
1481
|
) -> Response? {
|
|
1361
1482
|
#if os(iOS)
|
|
1362
1483
|
let policy = synthesizedGesturePolicy(policyKind)
|
|
@@ -1408,6 +1529,7 @@ extension RunnerTests {
|
|
|
1408
1529
|
x2: plan.points.x2,
|
|
1409
1530
|
y2: plan.points.y2,
|
|
1410
1531
|
durationMs: durationMs,
|
|
1532
|
+
profile: profile,
|
|
1411
1533
|
context: plan.context
|
|
1412
1534
|
)
|
|
1413
1535
|
}
|
|
@@ -1527,23 +1649,58 @@ extension RunnerTests {
|
|
|
1527
1649
|
|
|
1528
1650
|
private func shouldSkipAppActivationPreflight(_ command: Command) -> Bool {
|
|
1529
1651
|
#if os(iOS)
|
|
1530
|
-
|
|
1652
|
+
if command.command == .alert {
|
|
1653
|
+
return true
|
|
1654
|
+
}
|
|
1655
|
+
// Coordinate-only synthesized taps can run after an AX-fatal foreground screen because they do not
|
|
1531
1656
|
// need app activation, window lookup, keyboard lookup, or element resolution. Selector/text
|
|
1532
1657
|
// interactions intentionally stay on the normal AX path because they need an element query.
|
|
1533
1658
|
// Scroll/drag/sequence keep the normal foreground guard and stabilization path.
|
|
1534
1659
|
guard command.text == nil, command.selectorKey == nil else { return false }
|
|
1535
1660
|
guard hasCachedTargetForActivationSkip(command: command) else { return false }
|
|
1536
|
-
return command
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1661
|
+
return isCoordinateOnlyTap(command)
|
|
1662
|
+
#else
|
|
1663
|
+
return false
|
|
1664
|
+
#endif
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
private func shouldRouteToSpringboardBlockingSystemModal(
|
|
1668
|
+
_ command: Command,
|
|
1669
|
+
deadline: Date? = nil
|
|
1670
|
+
) -> Bool {
|
|
1671
|
+
#if os(iOS)
|
|
1672
|
+
guard command.command == .alert || isCoordinateOnlyTap(command) else {
|
|
1673
|
+
return false
|
|
1674
|
+
}
|
|
1675
|
+
let budgetDeadline = Date().addingTimeInterval(systemModalProbeBudget)
|
|
1676
|
+
let probeDeadline = deadline.map { min($0, budgetDeadline) } ?? budgetDeadline
|
|
1677
|
+
// `runMainThreadWork` executes inline for a main-thread caller, so that path cannot use its
|
|
1678
|
+
// timeout machinery. Direct main-thread dispatch keeps the prior synchronous modal check;
|
|
1679
|
+
// normal off-main command dispatch uses the bounded probe and post-probe busy recovery.
|
|
1680
|
+
if Thread.isMainThread {
|
|
1681
|
+
return firstBlockingSystemModal(
|
|
1682
|
+
in: springboard,
|
|
1683
|
+
deadline: probeDeadline
|
|
1684
|
+
) != nil
|
|
1685
|
+
}
|
|
1686
|
+
return boundedBlockingSystemAlertSnapshot(
|
|
1687
|
+
deadline: probeDeadline
|
|
1688
|
+
) != nil
|
|
1540
1689
|
#else
|
|
1541
1690
|
return false
|
|
1542
1691
|
#endif
|
|
1543
1692
|
}
|
|
1544
1693
|
|
|
1694
|
+
private func isCoordinateOnlyTap(_ command: Command) -> Bool {
|
|
1695
|
+
return command.command == .tap
|
|
1696
|
+
&& command.text == nil
|
|
1697
|
+
&& command.selectorKey == nil
|
|
1698
|
+
&& command.x != nil
|
|
1699
|
+
&& command.y != nil
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1545
1702
|
private func hasCachedTargetForActivationSkip(command: Command) -> Bool {
|
|
1546
|
-
guard currentApp
|
|
1703
|
+
guard let currentApp, currentApp.state == .runningForeground else { return false }
|
|
1547
1704
|
guard let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
1548
1705
|
!bundleId.isEmpty
|
|
1549
1706
|
else {
|
|
@@ -1572,6 +1729,7 @@ extension RunnerTests {
|
|
|
1572
1729
|
let delaySeconds = Double(max(command.delayMs ?? 0, 0)) / 1000.0
|
|
1573
1730
|
let textEntryMode = resolveTextEntryMode(command)
|
|
1574
1731
|
let target: TextEntryTarget
|
|
1732
|
+
var maestroNonHittableCoordinateFallbackUsed: Bool?
|
|
1575
1733
|
let focusStartedAt = Date()
|
|
1576
1734
|
if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
|
|
1577
1735
|
let match = findElement(
|
|
@@ -1589,6 +1747,9 @@ extension RunnerTests {
|
|
|
1589
1747
|
guard isTextEntryElement(element) else {
|
|
1590
1748
|
return Response(ok: false, error: ErrorPayload(code: "INVALID_TARGET", message: "selector did not match a text input"))
|
|
1591
1749
|
}
|
|
1750
|
+
if command.allowNonHittableCoordinateFallback == true {
|
|
1751
|
+
maestroNonHittableCoordinateFallbackUsed = match.usedNonHittableFallback
|
|
1752
|
+
}
|
|
1592
1753
|
target = focusTextInputForTextEntry(app: activeApp, element: element)
|
|
1593
1754
|
} else {
|
|
1594
1755
|
target = focusTextInputForTextEntry(app: activeApp, x: command.x, y: command.y)
|
|
@@ -1637,7 +1798,8 @@ extension RunnerTests {
|
|
|
1637
1798
|
x: point.map { Double($0.x) },
|
|
1638
1799
|
y: point.map { Double($0.y) },
|
|
1639
1800
|
referenceWidth: frame.isEmpty ? nil : Double(frame.width),
|
|
1640
|
-
referenceHeight: frame.isEmpty ? nil : Double(frame.height)
|
|
1801
|
+
referenceHeight: frame.isEmpty ? nil : Double(frame.height),
|
|
1802
|
+
maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed
|
|
1641
1803
|
)
|
|
1642
1804
|
)
|
|
1643
1805
|
}
|
|
@@ -146,8 +146,8 @@ final class RunnerCommandJournal {
|
|
|
146
146
|
case .tap, .mouseClick, .longPress, .drag,
|
|
147
147
|
.remotePress, .type, .swipe, .scroll, .desktopScroll, .findText, .querySelector, .readText, .back,
|
|
148
148
|
.backInApp, .backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn,
|
|
149
|
-
.alert, .
|
|
150
|
-
.status, .uptime, .shutdown:
|
|
149
|
+
.alert, .sequence, .gesture, .gestureViewport, .recordStart, .recordStop,
|
|
150
|
+
.status, .uptime, .targetReset, .shutdown:
|
|
151
151
|
return true
|
|
152
152
|
}
|
|
153
153
|
}
|