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
|
@@ -24,23 +24,19 @@ enum CommandType: String, Codable {
|
|
|
24
24
|
case keyboardDismiss
|
|
25
25
|
case keyboardReturn
|
|
26
26
|
case alert
|
|
27
|
-
case pinch
|
|
28
27
|
case sequence
|
|
29
|
-
case
|
|
30
|
-
case
|
|
28
|
+
case gesture
|
|
29
|
+
case gestureViewport
|
|
31
30
|
case recordStart
|
|
32
31
|
case recordStop
|
|
33
32
|
case status
|
|
34
33
|
case uptime
|
|
34
|
+
case activate
|
|
35
|
+
case terminate
|
|
36
|
+
case targetReset
|
|
35
37
|
case shutdown
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
enum SynthesizedDragSemantics: String, Codable {
|
|
39
|
-
case swipe
|
|
40
|
-
case pan
|
|
41
|
-
case fling
|
|
42
|
-
}
|
|
43
|
-
|
|
44
40
|
/// Runner command traits — see CONTEXT.md ("Runner command traits").
|
|
45
41
|
///
|
|
46
42
|
/// Single source of truth for how the runner classifies a command across three
|
|
@@ -80,11 +76,11 @@ extension CommandType {
|
|
|
80
76
|
// .sequence is the fused multi-step gesture batch.
|
|
81
77
|
case .tap, .longPress, .drag, .remotePress, .type, .swipe, .scroll, .desktopScroll,
|
|
82
78
|
.back, .backInApp, .backSystem, .rotate, .appSwitcher,
|
|
83
|
-
.keyboardDismiss, .keyboardReturn, .
|
|
79
|
+
.keyboardDismiss, .keyboardReturn, .sequence, .gesture:
|
|
84
80
|
return CommandTraits(isInteraction: true, readOnly: .never, isLifecycle: false)
|
|
85
81
|
|
|
86
82
|
// Read-only reads: eligible for the session-invalidating retry.
|
|
87
|
-
case .findText, .readText, .snapshot:
|
|
83
|
+
case .findText, .readText, .snapshot, .gestureViewport:
|
|
88
84
|
return CommandTraits(isInteraction: false, readOnly: .always, isLifecycle: false)
|
|
89
85
|
|
|
90
86
|
// Screenshot is both a read and a runner-lifecycle command (skips app-activation preflight).
|
|
@@ -96,7 +92,7 @@ extension CommandType {
|
|
|
96
92
|
return CommandTraits(isInteraction: false, readOnly: .conditional, isLifecycle: false)
|
|
97
93
|
|
|
98
94
|
// Runner-lifecycle commands: skip the app-activation preflight.
|
|
99
|
-
case .recordStop, .uptime, .shutdown:
|
|
95
|
+
case .recordStop, .uptime, .terminate, .targetReset, .shutdown:
|
|
100
96
|
return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: true)
|
|
101
97
|
|
|
102
98
|
case .status:
|
|
@@ -107,7 +103,7 @@ extension CommandType {
|
|
|
107
103
|
// guard interacts with bespoke macOS activation, so classifying it needs a macOS smoke
|
|
108
104
|
// check first (tracked as a follow-up). Also preserved: querySelector is NOT read-only;
|
|
109
105
|
// recordStart is NOT a lifecycle command; home/alert remain non-interaction by design.
|
|
110
|
-
case .mouseClick, .querySelector, .home, .recordStart:
|
|
106
|
+
case .mouseClick, .querySelector, .home, .recordStart, .activate:
|
|
111
107
|
return CommandTraits(isInteraction: false, readOnly: .never, isLifecycle: false)
|
|
112
108
|
}
|
|
113
109
|
}
|
|
@@ -124,7 +120,6 @@ struct Command: Codable {
|
|
|
124
120
|
let allowNonHittableCoordinateFallback: Bool?
|
|
125
121
|
let delayMs: Int?
|
|
126
122
|
let textEntryMode: String?
|
|
127
|
-
let clearFirst: Bool?
|
|
128
123
|
let action: String?
|
|
129
124
|
let x: Double?
|
|
130
125
|
let y: Double?
|
|
@@ -132,16 +127,13 @@ struct Command: Codable {
|
|
|
132
127
|
let remoteButton: String?
|
|
133
128
|
let x2: Double?
|
|
134
129
|
let y2: Double?
|
|
135
|
-
let dx: Double?
|
|
136
|
-
let dy: Double?
|
|
137
130
|
let durationMs: Double?
|
|
131
|
+
let timeoutMs: Double?
|
|
138
132
|
let direction: String?
|
|
139
133
|
let amount: Double?
|
|
140
134
|
let pixels: Double?
|
|
141
135
|
let orientation: String?
|
|
142
|
-
let
|
|
143
|
-
let degrees: Double?
|
|
144
|
-
let velocity: Double?
|
|
136
|
+
let gesturePlan: RunnerGesturePlan?
|
|
145
137
|
let outPath: String?
|
|
146
138
|
let fps: Int?
|
|
147
139
|
let maxSize: Int?
|
|
@@ -150,12 +142,43 @@ struct Command: Codable {
|
|
|
150
142
|
let scope: String?
|
|
151
143
|
let raw: Bool?
|
|
152
144
|
let fullscreen: Bool?
|
|
145
|
+
let inlineScreenshot: Bool?
|
|
153
146
|
let synthesized: Bool?
|
|
154
|
-
/// Preserves the public gesture's timing semantics after it is lowered to runner `drag`.
|
|
155
|
-
let dragSemantics: SynthesizedDragSemantics?
|
|
156
147
|
let steps: [SequenceStep]?
|
|
157
148
|
}
|
|
158
149
|
|
|
150
|
+
/// Canonical one- or two-pointer plan produced by the portable TypeScript planner.
|
|
151
|
+
struct RunnerGesturePlan: Codable {
|
|
152
|
+
let topology: String
|
|
153
|
+
let intent: String
|
|
154
|
+
let executionProfile: String?
|
|
155
|
+
let durationMs: Double
|
|
156
|
+
let viewport: RunnerGestureViewport
|
|
157
|
+
let pointers: [RunnerGesturePointer]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
struct RunnerGestureViewport: Codable {
|
|
161
|
+
let x: Double
|
|
162
|
+
let y: Double
|
|
163
|
+
let width: Double
|
|
164
|
+
let height: Double
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
struct RunnerGesturePointer: Codable {
|
|
168
|
+
let pointerId: Int
|
|
169
|
+
let samples: [RunnerGestureSample]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
struct RunnerGestureSample: Codable {
|
|
173
|
+
let offsetMs: Double
|
|
174
|
+
let point: RunnerGesturePoint
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
struct RunnerGesturePoint: Codable {
|
|
178
|
+
let x: Double
|
|
179
|
+
let y: Double
|
|
180
|
+
}
|
|
181
|
+
|
|
159
182
|
/// One allowlisted coordinate gesture step inside a fused `sequence` command.
|
|
160
183
|
/// `kind` is decoded as a raw String (not an enum) so the runner can return a clear
|
|
161
184
|
/// INVALID_ARGS for an unknown kind instead of a generic decode failure.
|
|
@@ -163,15 +186,11 @@ struct SequenceStep: Codable {
|
|
|
163
186
|
let kind: String
|
|
164
187
|
let x: Double?
|
|
165
188
|
let y: Double?
|
|
166
|
-
let x2: Double?
|
|
167
|
-
let y2: Double?
|
|
168
189
|
let durationMs: Double?
|
|
169
190
|
let pauseMs: Double?
|
|
170
|
-
/// For `tap
|
|
191
|
+
/// For `tap` steps on iOS non-tv: use the synthesized HID fast path instead of the
|
|
171
192
|
/// drag-based XCUICoordinate path, matching the individual command behavior.
|
|
172
193
|
let synthesized: Bool?
|
|
173
|
-
/// Preserves one-shot swipe timing when repeated swipes are fused into a sequence.
|
|
174
|
-
let dragSemantics: SynthesizedDragSemantics?
|
|
175
194
|
}
|
|
176
195
|
|
|
177
196
|
/// Per-step result for a `sequence` response. `ok:false` carries the failing step's
|
|
@@ -211,6 +230,7 @@ extension Response {
|
|
|
211
230
|
|
|
212
231
|
struct DataPayload: Codable {
|
|
213
232
|
let message: String?
|
|
233
|
+
let imageBase64: String?
|
|
214
234
|
let text: String?
|
|
215
235
|
let found: Bool?
|
|
216
236
|
let items: [String]?
|
|
@@ -241,6 +261,7 @@ struct DataPayload: Codable {
|
|
|
241
261
|
let gestureFallback: String?
|
|
242
262
|
let gestureFallbackMessage: String?
|
|
243
263
|
let gestureFallbackHint: String?
|
|
264
|
+
let maestroNonHittableCoordinateFallbackUsed: Bool?
|
|
244
265
|
let runnerFatal: Bool?
|
|
245
266
|
let runnerFatalReason: String?
|
|
246
267
|
let completedSteps: Int?
|
|
@@ -249,6 +270,7 @@ struct DataPayload: Codable {
|
|
|
249
270
|
|
|
250
271
|
init(
|
|
251
272
|
message: String? = nil,
|
|
273
|
+
imageBase64: String? = nil,
|
|
252
274
|
text: String? = nil,
|
|
253
275
|
found: Bool? = nil,
|
|
254
276
|
items: [String]? = nil,
|
|
@@ -279,6 +301,7 @@ struct DataPayload: Codable {
|
|
|
279
301
|
gestureFallback: String? = nil,
|
|
280
302
|
gestureFallbackMessage: String? = nil,
|
|
281
303
|
gestureFallbackHint: String? = nil,
|
|
304
|
+
maestroNonHittableCoordinateFallbackUsed: Bool? = nil,
|
|
282
305
|
runnerFatal: Bool? = nil,
|
|
283
306
|
runnerFatalReason: String? = nil,
|
|
284
307
|
completedSteps: Int? = nil,
|
|
@@ -286,6 +309,7 @@ struct DataPayload: Codable {
|
|
|
286
309
|
sequenceResults: [SequenceStepResult]? = nil
|
|
287
310
|
) {
|
|
288
311
|
self.message = message
|
|
312
|
+
self.imageBase64 = imageBase64
|
|
289
313
|
self.text = text
|
|
290
314
|
self.found = found
|
|
291
315
|
self.items = items
|
|
@@ -316,6 +340,7 @@ struct DataPayload: Codable {
|
|
|
316
340
|
self.gestureFallback = gestureFallback
|
|
317
341
|
self.gestureFallbackMessage = gestureFallbackMessage
|
|
318
342
|
self.gestureFallbackHint = gestureFallbackHint
|
|
343
|
+
self.maestroNonHittableCoordinateFallbackUsed = maestroNonHittableCoordinateFallbackUsed
|
|
319
344
|
self.runnerFatal = runnerFatal
|
|
320
345
|
self.runnerFatalReason = runnerFatalReason
|
|
321
346
|
self.completedSteps = completedSteps
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import XCTest
|
|
2
2
|
|
|
3
|
-
// Swift port of buildScrollGesturePlan from src/
|
|
3
|
+
// Swift port of buildScrollGesturePlan from src/contracts/scroll-gesture.ts.
|
|
4
4
|
//
|
|
5
5
|
// This is a deliberate two-place invariant: the daemon keeps the TS implementation (for Android,
|
|
6
6
|
// recording, and reported-pixels), and the runner places the gesture with this Swift copy. The
|
|
7
|
-
// parity test vectors at the bottom of this file mirror src/
|
|
7
|
+
// parity test vectors at the bottom of this file mirror src/contracts/scroll-gesture.test.ts —
|
|
8
8
|
// if you change the math in either language, update the other and both vector sets.
|
|
9
9
|
//
|
|
10
10
|
// All inputs here are positive (reference dims, travel, center), so Swift's `.rounded()`
|
|
@@ -9,7 +9,7 @@ extension RunnerTests {
|
|
|
9
9
|
|
|
10
10
|
/// Allowlisted step kinds. Validated on both sides so an unsupported kind is rejected with a
|
|
11
11
|
/// clear INVALID_ARGS naming the step index, executing nothing.
|
|
12
|
-
private var sequenceableStepKinds: Set<String> { ["tap", "doubleTap", "longPress"
|
|
12
|
+
private var sequenceableStepKinds: Set<String> { ["tap", "doubleTap", "longPress"] }
|
|
13
13
|
|
|
14
14
|
/// Per-step outcome carried by `assembleSequenceExecution`. The timing is captured by the
|
|
15
15
|
/// executor closure (via performGesture) so ordering/stop-on-failure stay device-free testable.
|
|
@@ -127,17 +127,12 @@ extension RunnerTests {
|
|
|
127
127
|
private func validateSequenceStep(_ step: SequenceStep, index: Int) -> Response? {
|
|
128
128
|
guard sequenceableStepKinds.contains(step.kind) else {
|
|
129
129
|
return sequenceInvalidArgs(
|
|
130
|
-
"sequence step \(index) has unsupported kind \"\(step.kind)\"; allowed: tap, doubleTap, longPress
|
|
130
|
+
"sequence step \(index) has unsupported kind \"\(step.kind)\"; allowed: tap, doubleTap, longPress"
|
|
131
131
|
)
|
|
132
132
|
}
|
|
133
133
|
guard let x = step.x, let y = step.y, x.isFinite, y.isFinite else {
|
|
134
134
|
return sequenceInvalidArgs("sequence step \(index) (\(step.kind)) requires finite x and y")
|
|
135
135
|
}
|
|
136
|
-
if step.kind == "drag" {
|
|
137
|
-
guard let x2 = step.x2, let y2 = step.y2, x2.isFinite, y2.isFinite else {
|
|
138
|
-
return sequenceInvalidArgs("sequence step \(index) (drag) requires finite x2 and y2")
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
136
|
return nil
|
|
142
137
|
}
|
|
143
138
|
|
|
@@ -194,92 +189,6 @@ extension RunnerTests {
|
|
|
194
189
|
#endif
|
|
195
190
|
// Synthesis unsupported (e.g. macOS) — fall through to the drag-based tapAt below.
|
|
196
191
|
}
|
|
197
|
-
if step.kind == "drag", step.synthesized == true {
|
|
198
|
-
let policyKind = SynthesizedGesturePolicyKind.synthesizedDrag
|
|
199
|
-
let dragPoints: DragPoints
|
|
200
|
-
let dragContext: SynthesizedCoordinateContext?
|
|
201
|
-
#if os(iOS)
|
|
202
|
-
guard let dragPlan = axFreeSynthesizedDragPlan(
|
|
203
|
-
app: activeApp,
|
|
204
|
-
x: x,
|
|
205
|
-
y: y,
|
|
206
|
-
x2: step.x2 ?? x,
|
|
207
|
-
y2: step.y2 ?? y,
|
|
208
|
-
context: synthesizedContext
|
|
209
|
-
) else {
|
|
210
|
-
let nowMs = ProcessInfo.processInfo.systemUptime * 1000
|
|
211
|
-
logSynthesizedGesturePolicyDecision(kind: policyKind, context: synthesizedContext, fallbackAttempted: false)
|
|
212
|
-
return SequenceStepOutcome(
|
|
213
|
-
outcome: .unsupported(
|
|
214
|
-
message: "synthesized coordinate drag could not resolve a finite coordinate frame",
|
|
215
|
-
hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
|
|
216
|
-
),
|
|
217
|
-
gestureStartUptimeMs: nowMs,
|
|
218
|
-
gestureEndUptimeMs: nowMs
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
dragPoints = dragPlan.points
|
|
222
|
-
dragContext = dragPlan.context
|
|
223
|
-
#else
|
|
224
|
-
dragPoints = keyboardAvoidingDragPoints(
|
|
225
|
-
app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
|
|
226
|
-
dragContext = nil
|
|
227
|
-
#endif
|
|
228
|
-
let durationMs = min(max(step.durationMs ?? 250, 16), 10000)
|
|
229
|
-
let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
|
|
230
|
-
synthesizedDragAt(
|
|
231
|
-
app: activeApp,
|
|
232
|
-
x: dragPoints.x,
|
|
233
|
-
y: dragPoints.y,
|
|
234
|
-
x2: dragPoints.x2,
|
|
235
|
-
y2: dragPoints.y2,
|
|
236
|
-
durationMs: durationMs,
|
|
237
|
-
semantics: step.dragSemantics,
|
|
238
|
-
context: dragContext
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
if case .performed = outcome {
|
|
242
|
-
logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: false)
|
|
243
|
-
if let pauseMs = step.pauseMs, pauseMs > 0 {
|
|
244
|
-
sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
|
|
245
|
-
}
|
|
246
|
-
return SequenceStepOutcome(
|
|
247
|
-
outcome: outcome,
|
|
248
|
-
gestureStartUptimeMs: timing.gestureStartUptimeMs,
|
|
249
|
-
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
250
|
-
)
|
|
251
|
-
}
|
|
252
|
-
#if os(iOS)
|
|
253
|
-
guard dragContext?.allowsXCTestCoordinateFallback == true else {
|
|
254
|
-
logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: false)
|
|
255
|
-
return SequenceStepOutcome(
|
|
256
|
-
outcome: outcome,
|
|
257
|
-
gestureStartUptimeMs: timing.gestureStartUptimeMs,
|
|
258
|
-
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
#endif
|
|
262
|
-
logSynthesizedGesturePolicyDecision(kind: policyKind, context: dragContext, fallbackAttempted: true)
|
|
263
|
-
let fallbackHoldDuration = synthesizedSwipeFallbackHoldDuration(durationMs: step.durationMs ?? 250)
|
|
264
|
-
let (fallbackTiming, fallbackOutcome) = performGesture(activeApp) {
|
|
265
|
-
dragAt(
|
|
266
|
-
app: activeApp,
|
|
267
|
-
x: dragPoints.x,
|
|
268
|
-
y: dragPoints.y,
|
|
269
|
-
x2: dragPoints.x2,
|
|
270
|
-
y2: dragPoints.y2,
|
|
271
|
-
holdDuration: fallbackHoldDuration
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
if case .performed = fallbackOutcome, let pauseMs = step.pauseMs, pauseMs > 0 {
|
|
275
|
-
sleepFor(min(max(pauseMs, 0), 10000) / 1000.0)
|
|
276
|
-
}
|
|
277
|
-
return SequenceStepOutcome(
|
|
278
|
-
outcome: fallbackOutcome,
|
|
279
|
-
gestureStartUptimeMs: fallbackTiming.gestureStartUptimeMs,
|
|
280
|
-
gestureEndUptimeMs: fallbackTiming.gestureEndUptimeMs
|
|
281
|
-
)
|
|
282
|
-
}
|
|
283
192
|
let (timing, outcome) = performGesture(activeApp) {
|
|
284
193
|
switch step.kind {
|
|
285
194
|
case "doubleTap":
|
|
@@ -288,20 +197,6 @@ extension RunnerTests {
|
|
|
288
197
|
case "longPress":
|
|
289
198
|
let duration = min(max(step.durationMs ?? 800, 16), 10000) / 1000.0
|
|
290
199
|
return longPressAt(app: activeApp, x: x, y: y, duration: duration)
|
|
291
|
-
case "drag":
|
|
292
|
-
// Route through keyboardAvoidingDragPoints for parity with the individual `.drag` command.
|
|
293
|
-
// The non-synthesized coordinate-drag path ignores durationMs, matching that command's
|
|
294
|
-
// non-synthesized branch.
|
|
295
|
-
let dragPoints = keyboardAvoidingDragPoints(
|
|
296
|
-
app: activeApp, x: x, y: y, x2: step.x2 ?? x, y2: step.y2 ?? y)
|
|
297
|
-
return dragAt(
|
|
298
|
-
app: activeApp,
|
|
299
|
-
x: dragPoints.x,
|
|
300
|
-
y: dragPoints.y,
|
|
301
|
-
x2: dragPoints.x2,
|
|
302
|
-
y2: dragPoints.y2,
|
|
303
|
-
holdDuration: coordinateDragHoldDuration()
|
|
304
|
-
)
|
|
305
200
|
default:
|
|
306
201
|
return tapAt(app: activeApp, x: x, y: y)
|
|
307
202
|
}
|
|
@@ -57,12 +57,16 @@ extension RunnerTests {
|
|
|
57
57
|
case .collectionView: return "CollectionView"
|
|
58
58
|
case .table: return "Table"
|
|
59
59
|
case .scrollView: return "ScrollView"
|
|
60
|
+
case .toolbar: return "Toolbar"
|
|
60
61
|
case .searchField: return "SearchField"
|
|
61
62
|
case .segmentedControl: return "SegmentedControl"
|
|
62
63
|
case .stepper: return "Stepper"
|
|
63
64
|
case .picker: return "Picker"
|
|
65
|
+
case .activityIndicator: return "ActivityIndicator"
|
|
66
|
+
case .progressIndicator: return "ProgressIndicator"
|
|
64
67
|
case .checkBox: return "CheckBox"
|
|
65
68
|
case .menuItem: return "MenuItem"
|
|
69
|
+
case .webView: return "WebView"
|
|
66
70
|
case .other: return "Other"
|
|
67
71
|
default:
|
|
68
72
|
switch type.rawValue {
|
|
@@ -70,8 +74,6 @@ extension RunnerTests {
|
|
|
70
74
|
return "Keyboard"
|
|
71
75
|
case 20:
|
|
72
76
|
return "Key"
|
|
73
|
-
case 24:
|
|
74
|
-
return "SearchField"
|
|
75
77
|
default:
|
|
76
78
|
return "Element(\(type.rawValue))"
|
|
77
79
|
}
|
|
@@ -101,15 +103,22 @@ extension RunnerTests {
|
|
|
101
103
|
|
|
102
104
|
static let flatInteractiveFallbackBudget: TimeInterval = 1.0
|
|
103
105
|
|
|
106
|
+
// The single production entry point -- always compiled, no unit-test overload. A unit test
|
|
107
|
+
// exercises this exact function; the only injectable seam lives inside
|
|
108
|
+
// `boundedBlockingSystemAlertSnapshot`'s probe closure (see `systemModalProbeOverrideForTesting`
|
|
109
|
+
// in RunnerTests.swift), so reverting this entry point to bypass the bounded probe fails the
|
|
110
|
+
// regression test.
|
|
104
111
|
func snapshotFast(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
|
|
105
|
-
|
|
112
|
+
let deadline = Date().addingTimeInterval(Self.snapshotPlanBudget)
|
|
113
|
+
if let blocking = boundedBlockingSystemAlertSnapshot(deadline: deadline) {
|
|
106
114
|
return blocking
|
|
107
115
|
}
|
|
108
116
|
return try runSnapshotCapturePlan(
|
|
109
117
|
Self.regularVisiblePlan,
|
|
110
118
|
app: app,
|
|
111
119
|
options: options,
|
|
112
|
-
terminal: .sparseWithFatalOnAXFailure
|
|
120
|
+
terminal: .sparseWithFatalOnAXFailure,
|
|
121
|
+
deadline: deadline
|
|
113
122
|
)
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -189,7 +198,12 @@ extension RunnerTests {
|
|
|
189
198
|
regularSnapshot: true
|
|
190
199
|
)
|
|
191
200
|
|
|
192
|
-
let key =
|
|
201
|
+
let key = Self.snapshotTraversalIdentity(
|
|
202
|
+
elementType: snapshot.elementType,
|
|
203
|
+
label: evaluation.label,
|
|
204
|
+
identifier: evaluation.identifier,
|
|
205
|
+
frame: snapshot.frame
|
|
206
|
+
)
|
|
193
207
|
let isDuplicate = seen.contains(key)
|
|
194
208
|
if !isDuplicate {
|
|
195
209
|
seen.insert(key)
|
|
@@ -245,18 +259,98 @@ extension RunnerTests {
|
|
|
245
259
|
)
|
|
246
260
|
}
|
|
247
261
|
|
|
262
|
+
// See `snapshotFast` above: the single production entry point, no unit-test overload.
|
|
248
263
|
func snapshotRaw(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
|
|
249
|
-
|
|
264
|
+
let deadline = Date().addingTimeInterval(Self.snapshotPlanBudget)
|
|
265
|
+
if let blocking = boundedBlockingSystemAlertSnapshot(deadline: deadline) {
|
|
250
266
|
return blocking
|
|
251
267
|
}
|
|
252
268
|
return try runSnapshotCapturePlan(
|
|
253
269
|
Self.rawDiagnosticPlan,
|
|
254
270
|
app: app,
|
|
255
271
|
options: options,
|
|
256
|
-
terminal: .throwOnAXFailure
|
|
272
|
+
terminal: .throwOnAXFailure,
|
|
273
|
+
deadline: deadline
|
|
257
274
|
)
|
|
258
275
|
}
|
|
259
276
|
|
|
277
|
+
/// Runs the pre-plan SpringBoard system-modal probe as a bounded capture tier sharing the plan
|
|
278
|
+
/// deadline, so a slow alert enumeration cannot bypass the snapshot timeout and stall (#1244).
|
|
279
|
+
func boundedBlockingSystemAlertSnapshot(deadline: Date) -> DataPayload? {
|
|
280
|
+
boundedBlockingSystemAlertSnapshotBody(deadline: deadline) { probeDeadline in
|
|
281
|
+
return self.blockingSystemAlertSnapshot(deadline: probeDeadline)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// The real bounding/hook machinery used by `boundedBlockingSystemAlertSnapshot` above: the
|
|
286
|
+
/// probe closure it's given always calls `self.blockingSystemAlertSnapshot` in production, and
|
|
287
|
+
/// in unit-test builds may first consult `systemModalProbeOverrideForTesting`. Keeping this in
|
|
288
|
+
/// one place means the `runMainThreadWork` wrap and the `onAbandoned`/`onDrained` hooks can
|
|
289
|
+
/// never drift between what production runs and what the unit tests exercise.
|
|
290
|
+
private func boundedBlockingSystemAlertSnapshotBody(
|
|
291
|
+
deadline: Date,
|
|
292
|
+
probe: @escaping (Date) -> DataPayload?
|
|
293
|
+
) -> DataPayload? {
|
|
294
|
+
#if os(macOS)
|
|
295
|
+
return nil
|
|
296
|
+
#else
|
|
297
|
+
let slice = Self.systemModalProbeSlice(
|
|
298
|
+
budget: systemModalProbeBudget,
|
|
299
|
+
deadlineRemaining: deadline.timeIntervalSinceNow
|
|
300
|
+
)
|
|
301
|
+
guard slice > 0 else {
|
|
302
|
+
NSLog("AGENT_DEVICE_RUNNER_SYSTEM_MODAL_PROBE_SKIPPED reason=budget_exhausted")
|
|
303
|
+
return nil
|
|
304
|
+
}
|
|
305
|
+
let probeDeadline = Date().addingTimeInterval(slice)
|
|
306
|
+
let startedAt = Date()
|
|
307
|
+
do {
|
|
308
|
+
return try runMainThreadWork(
|
|
309
|
+
command: nil,
|
|
310
|
+
timeout: slice,
|
|
311
|
+
timeoutError: {
|
|
312
|
+
SnapshotCaptureFailure(
|
|
313
|
+
code: Self.xCTestSnapshotTimeoutCode,
|
|
314
|
+
message: "the system-modal probe exceeded its \(slice)s time slice",
|
|
315
|
+
hint: "The capture plan recovers through non-XCTest snapshot tiers while the modal probe drains."
|
|
316
|
+
)
|
|
317
|
+
},
|
|
318
|
+
onAbandoned: {
|
|
319
|
+
self.retainAbandonedXCTestChannelWork()
|
|
320
|
+
NSLog("AGENT_DEVICE_RUNNER_SYSTEM_MODAL_PROBE_TIMEOUT slice=%.1f", slice)
|
|
321
|
+
self.penalizeSnapshotXCTestChannel(
|
|
322
|
+
bundleId: self.currentBundleId,
|
|
323
|
+
reason: "system_modal_probe_timeout"
|
|
324
|
+
)
|
|
325
|
+
},
|
|
326
|
+
onDrained: {
|
|
327
|
+
self.releaseAbandonedXCTestChannelWork()
|
|
328
|
+
NSLog("AGENT_DEVICE_RUNNER_SYSTEM_MODAL_PROBE_DRAINED")
|
|
329
|
+
}
|
|
330
|
+
) {
|
|
331
|
+
probe(probeDeadline)
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
NSLog(
|
|
335
|
+
"AGENT_DEVICE_RUNNER_SYSTEM_MODAL_PROBE_ABORTED elapsedMs=%d error=%@",
|
|
336
|
+
Int(Date().timeIntervalSince(startedAt) * 1000),
|
|
337
|
+
String(describing: error)
|
|
338
|
+
)
|
|
339
|
+
return nil
|
|
340
|
+
}
|
|
341
|
+
#endif
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// The probe gets its own budget, clamped by whatever remains of the shared plan deadline, and
|
|
345
|
+
/// 0 (skip entirely) once that deadline is already spent.
|
|
346
|
+
static func systemModalProbeSlice(
|
|
347
|
+
budget: TimeInterval,
|
|
348
|
+
deadlineRemaining: TimeInterval
|
|
349
|
+
) -> TimeInterval {
|
|
350
|
+
guard deadlineRemaining > 0 else { return 0 }
|
|
351
|
+
return min(budget, deadlineRemaining)
|
|
352
|
+
}
|
|
353
|
+
|
|
260
354
|
func rawTreeSnapshotPayload(
|
|
261
355
|
context: SnapshotTraversalContext,
|
|
262
356
|
options: SnapshotOptions
|
|
@@ -566,6 +660,20 @@ extension RunnerTests {
|
|
|
566
660
|
return abandonedTreeCaptureCount > 0
|
|
567
661
|
}
|
|
568
662
|
|
|
663
|
+
/// The watchdog abandoned one unit of XCTest main-thread capture work that is still draining on
|
|
664
|
+
/// main; XCTest-backed snapshot tiers skip (`hasAbandonedTreeCapture`) until a matching release.
|
|
665
|
+
func retainAbandonedXCTestChannelWork() {
|
|
666
|
+
treeCaptureLock.lock()
|
|
667
|
+
abandonedTreeCaptureCount += 1
|
|
668
|
+
treeCaptureLock.unlock()
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
func releaseAbandonedXCTestChannelWork() {
|
|
672
|
+
treeCaptureLock.lock()
|
|
673
|
+
abandonedTreeCaptureCount -= 1
|
|
674
|
+
treeCaptureLock.unlock()
|
|
675
|
+
}
|
|
676
|
+
|
|
569
677
|
/// Runs the blocking tree-snapshot XPC on the main thread bounded by `sliceSeconds`. On
|
|
570
678
|
/// timeout the XPC keeps running on main (it cannot be cancelled); the capture is marked
|
|
571
679
|
/// abandoned so plans avoid XCTest-backed tiers until it drains, the tree backend is penalized
|
|
@@ -583,9 +691,7 @@ extension RunnerTests {
|
|
|
583
691
|
timeout: sliceSeconds,
|
|
584
692
|
timeoutError: treeCaptureTimeoutError(sliceSeconds: sliceSeconds),
|
|
585
693
|
onAbandoned: {
|
|
586
|
-
self.
|
|
587
|
-
self.abandonedTreeCaptureCount += 1
|
|
588
|
-
self.treeCaptureLock.unlock()
|
|
694
|
+
self.retainAbandonedXCTestChannelWork()
|
|
589
695
|
NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_SLICE_TIMEOUT slice=%.1f", sliceSeconds)
|
|
590
696
|
self.penalizeSnapshotXCTestChannel(
|
|
591
697
|
bundleId: self.currentBundleId,
|
|
@@ -593,9 +699,7 @@ extension RunnerTests {
|
|
|
593
699
|
)
|
|
594
700
|
},
|
|
595
701
|
onDrained: {
|
|
596
|
-
self.
|
|
597
|
-
self.abandonedTreeCaptureCount -= 1
|
|
598
|
-
self.treeCaptureLock.unlock()
|
|
702
|
+
self.releaseAbandonedXCTestChannelWork()
|
|
599
703
|
NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_DRAINED")
|
|
600
704
|
}
|
|
601
705
|
) {
|
|
@@ -789,6 +893,19 @@ extension RunnerTests {
|
|
|
789
893
|
return .infinite
|
|
790
894
|
}
|
|
791
895
|
|
|
896
|
+
static func snapshotTraversalIdentity(
|
|
897
|
+
elementType: XCUIElement.ElementType,
|
|
898
|
+
label: String,
|
|
899
|
+
identifier: String,
|
|
900
|
+
frame: CGRect
|
|
901
|
+
) -> String {
|
|
902
|
+
#if os(iOS)
|
|
903
|
+
"\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)-\(frame.width)-\(frame.height)"
|
|
904
|
+
#else
|
|
905
|
+
return "\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)"
|
|
906
|
+
#endif
|
|
907
|
+
}
|
|
908
|
+
|
|
792
909
|
private func aggregatedLabel(for snapshot: XCUIElementSnapshot, depth: Int = 0) -> String? {
|
|
793
910
|
if depth > 4 { return nil }
|
|
794
911
|
let text = snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
@@ -118,6 +118,17 @@ extension RunnerTests {
|
|
|
118
118
|
)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
func clearSnapshotXCTestChannelPenalty(reason: String) {
|
|
122
|
+
snapshotXCTestChannelPenaltyLock.lock()
|
|
123
|
+
let hadActivePenalty = Date() < snapshotXCTestChannelPenaltyUntil
|
|
124
|
+
snapshotXCTestChannelPenaltyBundleId = nil
|
|
125
|
+
snapshotXCTestChannelPenaltyUntil = Date.distantPast
|
|
126
|
+
snapshotXCTestChannelPenaltyLock.unlock()
|
|
127
|
+
if hadActivePenalty {
|
|
128
|
+
NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_XCTEST_CHANNEL_PENALTY_CLEARED reason=%@", reason)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
func isSnapshotXCTestChannelPenalized(bundleId: String?) -> Bool {
|
|
122
133
|
snapshotXCTestChannelPenaltyLock.lock()
|
|
123
134
|
defer { snapshotXCTestChannelPenaltyLock.unlock() }
|
|
@@ -127,6 +138,12 @@ extension RunnerTests {
|
|
|
127
138
|
return penalized == bundleId
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
func consumeSnapshotXCTestPenaltyWarmupExemption() -> Bool {
|
|
142
|
+
let pending = snapshotXCTestPenaltyWarmupExemptionPending
|
|
143
|
+
snapshotXCTestPenaltyWarmupExemptionPending = false
|
|
144
|
+
return pending
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
/// Pure plan-reorder rule: a penalized XCTest accessibility channel uses independent backends
|
|
131
148
|
/// when the platform has one, otherwise it keeps XCTest work on a short probe. The raw
|
|
132
149
|
/// diagnostic plan keeps tree-first errors, and unknown plans are left untouched.
|
|
@@ -168,12 +185,15 @@ extension RunnerTests {
|
|
|
168
185
|
_ plan: [SnapshotBackendKind],
|
|
169
186
|
app: XCUIApplication,
|
|
170
187
|
options: SnapshotOptions,
|
|
171
|
-
terminal: SnapshotCaptureTerminalPolicy
|
|
188
|
+
terminal: SnapshotCaptureTerminalPolicy,
|
|
189
|
+
deadline: Date? = nil
|
|
172
190
|
) throws -> DataPayload {
|
|
173
191
|
var best: (kind: SnapshotBackendKind, capture: SnapshotBackendCapture)?
|
|
174
192
|
var firstFailure: (reason: String, code: String)?
|
|
175
193
|
var axFailure: SnapshotCaptureFailure?
|
|
176
|
-
|
|
194
|
+
// A caller may share the pre-plan system-modal probe's deadline; otherwise own the full budget (#1244).
|
|
195
|
+
let deadline = deadline ?? Date().addingTimeInterval(Self.snapshotPlanBudget)
|
|
196
|
+
let suppressXCTestPenalty = consumeSnapshotXCTestPenaltyWarmupExemption()
|
|
177
197
|
|
|
178
198
|
// Reorder is iOS-only because hostile screens can make XCTest tree/query work grind while
|
|
179
199
|
// the app remains visually responsive. Simulators can avoid that channel through private AX;
|
|
@@ -237,14 +257,30 @@ extension RunnerTests {
|
|
|
237
257
|
treeCaptureSliceBudgetOverride: effective.treeCaptureSliceBudgetOverride
|
|
238
258
|
)
|
|
239
259
|
else {
|
|
240
|
-
recordSlowXCTestSnapshotBackendIfNeeded(
|
|
260
|
+
recordSlowXCTestSnapshotBackendIfNeeded(
|
|
261
|
+
kind,
|
|
262
|
+
startedAt: backendStartedAt,
|
|
263
|
+
penaltySuppressed: suppressXCTestPenalty
|
|
264
|
+
)
|
|
241
265
|
continue
|
|
242
266
|
}
|
|
243
267
|
capture = result
|
|
244
|
-
recordSlowXCTestSnapshotBackendIfNeeded(
|
|
268
|
+
recordSlowXCTestSnapshotBackendIfNeeded(
|
|
269
|
+
kind,
|
|
270
|
+
startedAt: backendStartedAt,
|
|
271
|
+
penaltySuppressed: suppressXCTestPenalty
|
|
272
|
+
)
|
|
245
273
|
} catch let failure as SnapshotCaptureFailure {
|
|
246
|
-
recordXCTestSnapshotBackendFailureIfNeeded(
|
|
247
|
-
|
|
274
|
+
recordXCTestSnapshotBackendFailureIfNeeded(
|
|
275
|
+
kind,
|
|
276
|
+
failure: failure,
|
|
277
|
+
penaltySuppressed: suppressXCTestPenalty
|
|
278
|
+
)
|
|
279
|
+
recordSlowXCTestSnapshotBackendIfNeeded(
|
|
280
|
+
kind,
|
|
281
|
+
startedAt: backendStartedAt,
|
|
282
|
+
penaltySuppressed: suppressXCTestPenalty
|
|
283
|
+
)
|
|
248
284
|
if Self.isAxSnapshotFailure(failure) { axFailure = failure }
|
|
249
285
|
if firstFailure == nil {
|
|
250
286
|
firstFailure = (failure.message, Self.isAxSnapshotFailure(failure) ? "ax-rejected" : "capture-failed")
|
|
@@ -313,7 +349,12 @@ extension RunnerTests {
|
|
|
313
349
|
|
|
314
350
|
/// Marks XCTest-backed snapshot tiers as penalized when one attempt ground past the slow-capture
|
|
315
351
|
/// threshold — even a successful one: the next capture of this screen must not re-grind.
|
|
316
|
-
private func recordSlowXCTestSnapshotBackendIfNeeded(
|
|
352
|
+
private func recordSlowXCTestSnapshotBackendIfNeeded(
|
|
353
|
+
_ kind: SnapshotBackendKind,
|
|
354
|
+
startedAt: Date,
|
|
355
|
+
penaltySuppressed: Bool
|
|
356
|
+
) {
|
|
357
|
+
guard !penaltySuppressed else { return }
|
|
317
358
|
guard kind.usesXCTestAccessibilityChannel else { return }
|
|
318
359
|
let elapsed = Date().timeIntervalSince(startedAt)
|
|
319
360
|
guard elapsed > snapshotXCTestSlowCaptureThreshold else { return }
|
|
@@ -325,8 +366,10 @@ extension RunnerTests {
|
|
|
325
366
|
|
|
326
367
|
private func recordXCTestSnapshotBackendFailureIfNeeded(
|
|
327
368
|
_ kind: SnapshotBackendKind,
|
|
328
|
-
failure: SnapshotCaptureFailure
|
|
369
|
+
failure: SnapshotCaptureFailure,
|
|
370
|
+
penaltySuppressed: Bool
|
|
329
371
|
) {
|
|
372
|
+
guard !penaltySuppressed else { return }
|
|
330
373
|
guard kind.usesXCTestAccessibilityChannel, failure.code == Self.xCTestSnapshotTimeoutCode else { return }
|
|
331
374
|
penalizeSnapshotXCTestChannel(
|
|
332
375
|
bundleId: currentBundleId,
|