agent-device 0.21.12 → 0.21.13

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.
Files changed (190) hide show
  1. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.12.apk → agent-device-android-ime-helper-0.21.13.apk} +0 -0
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.13.apk.sha256 +1 -0
  3. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.12.manifest.json → agent-device-android-ime-helper-0.21.13.manifest.json} +4 -4
  4. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.12.apk → agent-device-android-snapshot-helper-0.21.13.apk} +0 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.13.apk.sha256 +1 -0
  6. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.12.manifest.json → agent-device-android-snapshot-helper-0.21.13.manifest.json} +6 -6
  7. package/apple/macos-helper/Package.swift +4 -0
  8. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +3 -6
  9. package/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/ScreenshotResponseTests.swift +15 -0
  10. package/apple/snapshot-bridge/SnapshotBridgeRuntime.m +47 -1
  11. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +99 -42
  12. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner/Info.plist +23 -0
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +17 -0
  14. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAppScreenCapture.swift +21 -13
  15. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerScrollViewportPolicy.swift +1 -162
  16. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -9
  17. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -56
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSystemSurfaceHostPolicy.swift +0 -48
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +0 -50
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +22 -568
  21. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +21 -9
  22. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+BlockingSystemModalResolution.swift +0 -36
  23. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandDispatch.swift +630 -0
  24. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +18 -2236
  25. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +1 -338
  26. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -12
  27. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+GestureExecution.swift +165 -0
  28. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +0 -24
  29. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +0 -142
  30. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +90 -39
  31. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+MainThreadWork.swift +78 -106
  32. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +23 -2
  33. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +147 -95
  34. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+PrivateAXPresentation.swift +0 -209
  35. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +86 -10
  36. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollDragExecution.swift +287 -0
  37. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +1 -200
  38. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +0 -185
  39. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +75 -285
  40. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotAcquisition.swift +23 -10
  41. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotBackendCapabilities.swift +0 -78
  42. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +83 -578
  43. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCaptureTarget.swift +106 -0
  44. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotExecution.swift +142 -0
  45. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotTiming.swift +49 -78
  46. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedCommitDeadline.swift +0 -44
  47. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +27 -97
  48. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +1 -11
  49. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedTextEntry.swift +11 -87
  50. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +2 -10
  51. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +1 -1
  52. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntryFocus.swift +0 -35
  53. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputCandidatePolicy.swift +0 -31
  54. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift +2 -18
  55. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +34 -52
  56. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -22
  57. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TypeExecution.swift +142 -0
  58. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +22 -6
  59. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +140 -20
  60. package/dist/src/absence-observation-errors.js +1 -1
  61. package/dist/src/adapter.js +1 -1
  62. package/dist/src/agent-device-client.js +1 -1
  63. package/dist/src/alert-runtime.js +1 -1
  64. package/dist/src/android-adb.d.ts +1 -1
  65. package/dist/src/app-launch.js +2 -2
  66. package/dist/src/app-log-runtime2.js +3 -3
  67. package/dist/src/app-resolution.js +2 -2
  68. package/dist/src/back-mode.js +1 -1
  69. package/dist/src/capture-disclosure.js +1 -1
  70. package/dist/src/capture.js +1 -1
  71. package/dist/src/cli-config.js +1 -1
  72. package/dist/src/cli-help.js +6 -5
  73. package/dist/src/cli-runner.js +1 -1
  74. package/dist/src/cli.js +1 -1
  75. package/dist/src/client-types.d.ts +4 -5
  76. package/dist/src/code-signature.js +1 -1
  77. package/dist/src/command-schema.js +3 -3
  78. package/dist/src/command-tools.js +3 -3
  79. package/dist/src/connection.js +1 -1
  80. package/dist/src/daemon-client-lifecycle.js +1 -1
  81. package/dist/src/daemon-resolution.js +1 -1
  82. package/dist/src/device-claim-rule.js +2 -2
  83. package/dist/src/device-input-state.d.ts +1 -1
  84. package/dist/src/device-session.js +1 -1
  85. package/dist/src/devicectl.js +1 -1
  86. package/dist/src/display-inventory.js +1 -1
  87. package/dist/src/doctor-facade.js +1 -1
  88. package/dist/src/find.js +1 -1
  89. package/dist/src/gesture-plan-types.d.ts +38 -0
  90. package/dist/src/harmonyos.js +1 -1
  91. package/dist/src/helper.js +1 -1
  92. package/dist/src/host.js +1 -1
  93. package/dist/src/install-source-network-transport.js +1 -1
  94. package/dist/src/install-source2.js +1 -1
  95. package/dist/src/interaction-ref-policy.js +1 -1
  96. package/dist/src/interaction2.js +1 -1
  97. package/dist/src/interactor-operation-catalog.js +1 -1
  98. package/dist/src/interactor.js +4 -4
  99. package/dist/src/interactor2.js +1 -1
  100. package/dist/src/internal/bin.js +1 -1
  101. package/dist/src/internal/daemon.js +2 -2
  102. package/dist/src/inventory2.js +1 -1
  103. package/dist/src/ios-snapshot-acquisition.js +1 -1
  104. package/dist/src/ios-snapshot-runtime.js +1 -1
  105. package/dist/src/limrun-runtime-dependencies.js +1 -1
  106. package/dist/src/limrun.d.ts +435 -4
  107. package/dist/src/linux.js +1 -1
  108. package/dist/src/local-interactor-operation-set.js +1 -1
  109. package/dist/src/managed-device-scope.js +1 -1
  110. package/dist/src/mechanics.js +4 -4
  111. package/dist/src/mobile-snapshot-semantics.js +1 -1
  112. package/dist/src/perf-facade.js +1 -2
  113. package/dist/src/perf-runtime-plan.js +1 -1
  114. package/dist/src/perf-target.js +2 -0
  115. package/dist/src/physical-device-apps.js +1 -0
  116. package/dist/src/physical-device-control.js +1 -1
  117. package/dist/src/platform-request-scope.js +1 -1
  118. package/dist/src/platform-runtime-operation-host.js +1 -1
  119. package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
  120. package/dist/src/platform-runtime-unavailable.js +1 -1
  121. package/dist/src/platform-runtime.js +1 -1
  122. package/dist/src/platform-runtime2.js +1 -1
  123. package/dist/src/provider-webdriver.js +1 -1
  124. package/dist/src/proxy.js +1 -1
  125. package/dist/src/quality-warnings.js +1 -1
  126. package/dist/src/react-native.js +1 -1
  127. package/dist/src/record-runtime.js +1 -1
  128. package/dist/src/registry.js +35 -35
  129. package/dist/src/request-admission.js +1 -1
  130. package/dist/src/request-runtime-binding.js +1 -1
  131. package/dist/src/runner-client.js +1 -1
  132. package/dist/src/runner-dev-tools-security.js +1 -1
  133. package/dist/src/runner-device-readiness.js +1 -1
  134. package/dist/src/runner-disposal.js +1 -1
  135. package/dist/src/runner-error-classification.js +3 -0
  136. package/dist/src/runner-operations-facade.js +1 -1
  137. package/dist/src/runner-presentation.js +1 -1
  138. package/dist/src/runner.js +1 -1
  139. package/dist/src/runtime.js +3 -3
  140. package/dist/src/runtime10.js +1 -1
  141. package/dist/src/runtime2.js +2 -2
  142. package/dist/src/runtime4.js +5 -5
  143. package/dist/src/runtime5.js +1 -1
  144. package/dist/src/runtime6.js +1 -1
  145. package/dist/src/runtime7.js +1 -1
  146. package/dist/src/runtime8.js +1 -1
  147. package/dist/src/runtime9.js +1 -1
  148. package/dist/src/screen-recording-admission-ledger.js +1 -1
  149. package/dist/src/screen-recording-resource-recovery.js +1 -1
  150. package/dist/src/screen-recording-transport.js +1 -1
  151. package/dist/src/screenshot-runtime.js +1 -1
  152. package/dist/src/screenshot3.js +1 -1
  153. package/dist/src/script.js +1 -1
  154. package/dist/src/scroll-command.d.ts +377 -0
  155. package/dist/src/scroll-command.js +1 -1
  156. package/dist/src/sdk-batch.js +2 -2
  157. package/dist/src/sdk-contracts.d.ts +9 -0
  158. package/dist/src/sdk-device.js +1 -1
  159. package/dist/src/sdk-selectors.d.ts +23 -726
  160. package/dist/src/selection.js +0 -0
  161. package/dist/src/selector-runtime.js +1 -1
  162. package/dist/src/semantic-index.js +1 -1
  163. package/dist/src/server.js +1 -1
  164. package/dist/src/session-observation.js +1 -1
  165. package/dist/src/session-snapshot.js +1 -1
  166. package/dist/src/session2.js +2 -2
  167. package/dist/src/simctl-facade.js +1 -1
  168. package/dist/src/simctl.js +1 -1
  169. package/dist/src/simulator-state.js +1 -1
  170. package/dist/src/simulator.js +1 -1
  171. package/dist/src/snapshot-runtime-binding.js +1 -1
  172. package/dist/src/snapshot-runtime.js +1 -1
  173. package/dist/src/snapshot-state.js +1 -1
  174. package/dist/src/snapshot4.js +1 -1
  175. package/dist/src/src7.js +1 -1
  176. package/dist/src/text-entry-focus.js +1 -1
  177. package/dist/src/tool-provider.js +1 -1
  178. package/dist/src/touch-runtime.js +1 -1
  179. package/dist/src/tree.js +1 -1
  180. package/dist/src/update-check.js +1 -1
  181. package/dist/src/version.js +1 -1
  182. package/dist/src/web2.js +1 -1
  183. package/package.json +3 -2
  184. package/server.json +2 -2
  185. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.12.apk.sha256 +0 -1
  186. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.12.apk.sha256 +0 -1
  187. package/dist/src/app-inventory.d.ts +0 -4
  188. package/dist/src/apps-simctl.js +0 -1
  189. package/dist/src/cloud-artifacts.d.ts +0 -61
  190. package/dist/src/runner-contract.js +0 -3
@@ -1,1629 +1,7 @@
1
1
  import XCTest
2
2
  import AgentDeviceSnapshotPresentation
3
3
 
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
- extension RunnerTests {
31
-
32
-
33
- private func currentUptimeMs() -> Double {
34
- ProcessInfo.processInfo.systemUptime * 1000
35
- }
36
-
37
- private func measureGesture(_ action: () -> Void) -> (gestureStartUptimeMs: Double, gestureEndUptimeMs: Double) {
38
- let gestureStartUptimeMs = currentUptimeMs()
39
- action()
40
- return (gestureStartUptimeMs, currentUptimeMs())
41
- }
42
-
43
- func synthesizedSwipeFallbackHoldDuration(durationMs: Double) -> TimeInterval {
44
- min(max((durationMs / 5.0) / 1000.0, 0.016), 0.120)
45
- }
46
-
47
- func coordinateDragHoldDuration() -> TimeInterval {
48
- 0.050
49
- }
50
-
51
- func unsupportedResponse(for outcome: RunnerInteractionOutcome) -> Response? {
52
- switch outcome {
53
- case .performed:
54
- return nil
55
- case .unsupported(let message, let hint):
56
- return Response(
57
- ok: false,
58
- error: ErrorPayload(code: "UNSUPPORTED_OPERATION", message: message, hint: hint)
59
- )
60
- }
61
- }
62
-
63
-
64
- enum GestureFrame {
65
- case none
66
- case touch(TouchVisualizationFrame?)
67
- case drag(DragVisualizationFrame)
68
- }
69
-
70
- struct GestureFallback {
71
- let strategy: String
72
- let message: String
73
- let hint: String?
74
- }
75
-
76
- private func gestureFallback(strategy: String, from outcome: RunnerInteractionOutcome) -> GestureFallback? {
77
- switch outcome {
78
- case .performed:
79
- return nil
80
- case .unsupported(let message, let hint):
81
- return GestureFallback(strategy: strategy, message: message, hint: hint)
82
- }
83
- }
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
- func performGesture(
94
- _ app: XCUIApplication,
95
- idleTimeout: Bool = true,
96
- _ action: () -> RunnerInteractionOutcome
97
- ) -> (timing: (gestureStartUptimeMs: Double, gestureEndUptimeMs: Double), outcome: RunnerInteractionOutcome) {
98
- var outcome = RunnerInteractionOutcome.performed
99
- let timing = measureGesture {
100
- if idleTimeout {
101
- withBoundedInteractionIdleTimeoutIfSupported(app, waits: .bothSkipped) {
102
- outcome = action()
103
- }
104
- } else {
105
- outcome = action()
106
- }
107
- }
108
- return (timing, outcome)
109
- }
110
-
111
-
112
-
113
- private func gestureResponse(
114
- message: String,
115
- timing: (gestureStartUptimeMs: Double, gestureEndUptimeMs: Double),
116
- frame: GestureFrame = .none,
117
- fallback: GestureFallback? = nil,
118
- maestroNonHittableCoordinateFallbackUsed: Bool? = nil
119
- ) -> Response {
120
- let data: DataPayload
121
- switch frame {
122
- case .none:
123
- data = DataPayload(
124
- message: message,
125
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
126
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
127
- gestureFallback: fallback?.strategy,
128
- gestureFallbackMessage: fallback?.message,
129
- gestureFallbackHint: fallback?.hint,
130
- maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed
131
- )
132
- case .touch(let f):
133
- data = DataPayload(
134
- message: message,
135
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
136
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
137
- x: f?.x,
138
- y: f?.y,
139
- referenceWidth: f?.referenceWidth,
140
- referenceHeight: f?.referenceHeight,
141
- gestureFallback: fallback?.strategy,
142
- gestureFallbackMessage: fallback?.message,
143
- gestureFallbackHint: fallback?.hint,
144
- maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed
145
- )
146
- case .drag(let f):
147
- data = DataPayload(
148
- message: message,
149
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
150
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
151
- x: f.x,
152
- y: f.y,
153
- x2: f.x2,
154
- y2: f.y2,
155
- referenceWidth: f.referenceWidth,
156
- referenceHeight: f.referenceHeight,
157
- gestureFallback: fallback?.strategy,
158
- gestureFallbackMessage: fallback?.message,
159
- gestureFallbackHint: fallback?.hint
160
- )
161
- }
162
- return Response(ok: true, data: data)
163
- }
164
-
165
-
166
-
167
-
168
- private func canonicalPlannedGestureResponse(_ response: Response) -> Response {
169
- guard response.ok, let data = response.data else { return response }
170
- return Response(
171
- ok: true,
172
- data: DataPayload(
173
- message: data.message,
174
- gestureStartUptimeMs: data.gestureStartUptimeMs,
175
- gestureEndUptimeMs: data.gestureEndUptimeMs,
176
- gestureFallback: data.gestureFallback,
177
- gestureFallbackMessage: data.gestureFallbackMessage,
178
- gestureFallbackHint: data.gestureFallbackHint
179
- )
180
- )
181
- }
182
-
183
- private func plannedGestureResponse(
184
- plan: RunnerGesturePlan,
185
- timing: (gestureStartUptimeMs: Double, gestureEndUptimeMs: Double),
186
- outcome: RunnerInteractionOutcome
187
- ) -> Response {
188
- if let response = unsupportedResponse(for: outcome) {
189
- return response
190
- }
191
- return gestureResponse(message: plan.intent, timing: timing)
192
- }
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
-
441
-
442
-
443
-
444
-
445
-
446
-
447
-
448
-
449
-
450
-
451
-
452
-
453
-
454
-
455
-
456
-
457
-
458
-
459
-
460
-
461
-
462
-
463
-
464
-
465
-
466
-
467
-
468
-
469
-
470
-
471
-
472
-
473
-
474
-
475
-
476
-
477
-
478
-
479
-
480
-
481
-
482
-
483
-
484
-
485
-
486
-
487
-
488
-
489
-
490
-
491
-
492
-
493
-
494
-
495
-
496
-
497
-
498
-
499
-
500
-
501
-
502
-
503
-
504
-
505
-
506
-
507
-
508
-
509
-
510
-
511
-
512
-
513
-
514
-
515
-
516
-
517
-
518
-
519
-
520
-
521
-
522
-
523
-
524
-
525
-
526
-
527
-
528
-
529
-
530
-
531
-
532
-
533
-
534
-
535
-
536
-
537
-
538
-
539
-
540
-
541
-
542
-
543
-
544
-
545
-
546
-
547
-
548
-
549
-
550
-
551
-
552
-
553
-
554
-
555
-
556
-
557
-
558
-
559
-
560
-
561
-
562
-
563
-
564
-
565
-
566
-
567
-
568
-
569
-
570
-
571
-
572
-
573
-
574
-
575
-
576
-
577
-
578
-
579
-
580
-
581
-
582
-
583
-
584
-
585
-
586
-
587
-
588
-
589
-
590
-
591
-
592
-
593
-
594
-
595
-
596
-
597
-
598
-
599
-
600
-
601
-
602
-
603
-
604
-
605
-
606
-
607
-
608
-
609
-
610
-
611
-
612
-
613
-
614
-
615
-
616
-
617
-
618
-
619
-
620
-
621
-
622
-
623
-
624
-
625
-
626
-
627
-
628
-
629
-
630
-
631
-
632
-
633
-
634
-
635
-
636
-
637
-
638
-
639
-
640
-
641
-
642
-
643
-
644
-
645
-
646
-
647
-
648
-
649
-
650
-
651
-
652
-
653
-
654
-
655
-
656
-
657
-
658
-
659
-
660
-
661
-
662
-
663
-
664
-
665
-
666
-
667
-
668
-
669
-
670
-
671
-
672
-
673
-
674
-
675
-
676
-
677
-
678
-
679
-
680
-
681
-
682
-
683
-
684
-
685
-
686
-
687
-
688
-
689
-
690
-
691
-
692
-
693
-
694
-
695
-
696
-
697
-
698
-
699
-
700
-
701
-
702
-
703
-
704
-
705
-
706
-
707
-
708
-
709
-
710
-
711
-
712
-
713
-
714
-
715
-
716
-
717
-
718
-
719
-
720
-
721
-
722
-
723
-
724
-
725
-
726
-
727
-
728
-
729
-
730
-
731
-
732
-
733
-
734
-
735
-
736
-
737
-
738
-
739
-
740
-
741
-
742
-
743
-
744
-
745
-
746
-
747
-
748
-
749
-
750
-
751
-
752
-
753
-
754
-
755
-
756
-
757
-
758
-
759
-
760
-
761
-
762
-
763
-
764
-
765
-
766
-
767
-
768
-
769
-
770
-
771
-
772
-
773
-
774
-
775
-
776
-
777
-
778
-
779
-
780
-
781
-
782
-
783
-
784
-
785
-
786
-
787
-
788
-
789
-
790
-
791
-
792
-
793
-
794
-
795
-
796
-
797
-
798
-
799
-
800
-
801
-
802
-
803
-
804
-
805
-
806
-
807
-
808
-
809
-
810
-
811
-
812
-
813
-
814
-
815
-
816
-
817
-
818
-
819
-
820
-
821
-
822
-
823
-
824
-
825
-
826
-
827
-
828
-
829
-
830
-
831
-
832
-
833
-
834
-
835
-
836
-
837
-
838
-
839
-
840
-
841
-
842
-
843
-
844
-
845
-
846
-
847
-
848
-
849
-
850
-
851
-
852
-
853
-
854
-
855
-
856
-
857
-
858
-
859
-
860
-
861
-
862
-
863
-
864
-
865
-
866
-
867
-
868
-
869
-
870
-
871
-
872
-
873
-
874
-
875
-
876
-
877
-
878
-
879
-
880
-
881
-
882
-
883
-
884
-
885
-
886
-
887
-
888
-
889
-
890
-
891
-
892
-
893
-
894
-
895
-
896
-
897
-
898
-
899
-
900
-
901
-
902
-
903
-
904
-
905
-
906
-
907
-
908
-
909
-
910
-
911
-
912
-
913
-
914
-
915
-
916
-
917
-
918
-
919
-
920
-
921
-
922
-
923
-
924
-
925
-
926
-
927
-
928
-
929
-
930
-
931
-
932
-
933
-
934
-
935
-
936
-
937
-
938
-
939
-
940
-
941
-
942
-
943
-
944
-
945
-
946
-
947
-
948
-
949
-
950
-
951
- func executeAccepted(command: Command) throws -> Response {
952
- commandJournal.start(command: command)
953
- pendingTargetActivation = nil
954
- do {
955
- let response = try executeDispatched(command: command)
956
- commandJournal.finish(command: command, response: response)
957
- guard let fact = pendingTargetActivation else { return response }
958
-
959
-
960
-
961
- pendingTargetActivation = nil
962
- return response.stampingTargetActivation(fact)
963
- } catch {
964
- pendingTargetActivation = nil
965
- commandJournal.fail(command: command, error: error)
966
- throw error
967
- }
968
- }
969
-
970
- func executeStatus(command: Command) -> Response {
971
- guard
972
- let statusCommandId = command.statusCommandId?.trimmedNonEmpty
973
- else {
974
- return Response(
975
- ok: false,
976
- error: ErrorPayload(
977
- code: "INVALID_ARGS",
978
- message: "status requires statusCommandId",
979
- hint: "Set statusCommandId to the commandId of the runner command to inspect."
980
- )
981
- )
982
- }
983
- return Response(ok: true, data: commandJournal.status(normalizedCommandId: statusCommandId))
984
- }
985
-
986
- func executeUptime() -> Response {
987
-
988
-
989
- Response(
990
- ok: true,
991
- data: DataPayload(currentUptimeMs: currentUptimeMs())
992
- )
993
- }
994
-
995
- struct ActiveCommandContext {
996
- let app: XCUIApplication
997
-
998
- var systemSurface: SystemSurfaceHost? = nil
999
- }
1000
-
1001
- enum ActiveCommandPreparation {
1002
- case response(Response)
1003
- case context(ActiveCommandContext)
1004
- }
1005
-
1006
- private func runnerBusyResponse(command: Command, abandonedForSeconds: TimeInterval) -> Response {
1007
- NSLog(
1008
- "AGENT_DEVICE_RUNNER_BUSY command=%@ commandId=%@ abandonedForSeconds=%.1f",
1009
- command.command.rawValue,
1010
- command.commandId ?? "",
1011
- abandonedForSeconds
1012
- )
1013
- return Response(
1014
- ok: false,
1015
- error: ErrorPayload(
1016
- code: "RUNNER_BUSY",
1017
- message:
1018
- "The iOS runner is still finishing a previous command that exceeded its execution watchdog (usually an accessibility capture on a heavy or animating screen).",
1019
- hint:
1020
- "Wait a few seconds and retry. If snapshots keep failing on this screen, use screenshot as visual truth and interact by coordinates, or navigate to another screen."
1021
- )
1022
- )
1023
- }
1024
-
1025
- private func runnerWedgedResponse(command: Command, abandonedForSeconds: TimeInterval) -> Response {
1026
- NSLog(
1027
- "AGENT_DEVICE_RUNNER_WEDGED command=%@ commandId=%@ abandonedForSeconds=%.1f",
1028
- command.command.rawValue,
1029
- command.commandId ?? "",
1030
- abandonedForSeconds
1031
- )
1032
- return Response(
1033
- ok: false,
1034
- error: ErrorPayload(
1035
- code: "RUNNER_WEDGED",
1036
- message:
1037
- "The iOS runner main thread has been stuck in abandoned work for \(Int(abandonedForSeconds)) seconds and cannot recover on its own.",
1038
- hint:
1039
- "The runner session will be restarted. Retry the command after the restart; if this screen keeps wedging captures, use screenshot as visual truth and interact by coordinates."
1040
- )
1041
- )
1042
- }
1043
-
1044
- private func runnerUnavailableResponse(command: Command) -> Response? {
1045
- switch currentMainThreadBusyState() {
1046
- case .idle:
1047
- return nil
1048
- case .busy(let abandonedForSeconds):
1049
- return runnerBusyResponse(command: command, abandonedForSeconds: abandonedForSeconds)
1050
- case .wedged(let abandonedForSeconds):
1051
- return runnerWedgedResponse(command: command, abandonedForSeconds: abandonedForSeconds)
1052
- }
1053
- }
1054
-
1055
- private func executeDispatched(command: Command) throws -> Response {
1056
-
1057
-
1058
-
1059
-
1060
- if let unavailable = runnerUnavailableResponse(command: command) {
1061
- return unavailable
1062
- }
1063
- let alertDeadline = command.command == .alert
1064
- ? Date().addingTimeInterval(Self.alertCommandTimeout(timeoutMs: command.timeoutMs))
1065
- : nil
1066
- if Thread.isMainThread {
1067
- let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(command)
1068
- return try executeOnMainSafely(
1069
- command: command,
1070
- alertDeadline: alertDeadline,
1071
- routeToSpringboard: routeToSpringboard
1072
- )
1073
- }
1074
-
1075
-
1076
-
1077
- let routeToSpringboard = shouldRouteToSpringboardBlockingSystemModal(command)
1078
- if let unavailable = runnerUnavailableResponse(command: command) {
1079
- return unavailable
1080
- }
1081
- if command.command == .snapshot {
1082
- return try executeSnapshotDispatched(command: command)
1083
- }
1084
- if command.command == .alert, let deadline = alertDeadline {
1085
- return try runMainThreadWork(
1086
- "command_execution",
1087
- timeout: max(0.001, deadline.timeIntervalSinceNow),
1088
- timeoutError: mainThreadExecutionTimeoutError
1089
- ) {
1090
- try self.executeOnMainSafely(
1091
- command: command,
1092
- alertDeadline: deadline,
1093
- routeToSpringboard: routeToSpringboard
1094
- )
1095
- }
1096
- }
1097
- return try runMainThreadWork(
1098
- "command_execution",
1099
- timeout: mainThreadExecutionTimeout,
1100
- timeoutError: mainThreadExecutionTimeoutError
1101
- ) {
1102
- try self.executeOnMainSafely(command: command, routeToSpringboard: routeToSpringboard)
1103
- }
1104
- }
1105
-
1106
-
1107
-
1108
- private func executeOnMainSafely(
1109
- command: Command,
1110
- alertDeadline: Date? = nil,
1111
- routeToSpringboard: Bool
1112
- ) throws -> Response {
1113
- var hasRetried = false
1114
- while true {
1115
- var response: Response?
1116
- var swiftError: Error?
1117
- let failureCountBefore = currentXCTestFailureCount()
1118
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
1119
- do {
1120
- response = try self.executeOnMain(
1121
- command: command,
1122
- alertDeadline: alertDeadline,
1123
- routeToSpringboard: routeToSpringboard
1124
- )
1125
- } catch {
1126
- swiftError = error
1127
- }
1128
- })
1129
-
1130
- if let exceptionMessage {
1131
- invalidateCachedTarget(reason: "objc_exception")
1132
- if !hasRetried, shouldRetryException(command, message: exceptionMessage) {
1133
- NSLog(
1134
- "AGENT_DEVICE_RUNNER_RETRY command=%@ reason=objc_exception",
1135
- command.command.rawValue
1136
- )
1137
- hasRetried = true
1138
- sleepFor(retryCooldown)
1139
- continue
1140
- }
1141
- throw NSError(
1142
- domain: RunnerErrorDomain.exception,
1143
- code: RunnerErrorCode.objcException,
1144
- userInfo: [NSLocalizedDescriptionKey: exceptionMessage]
1145
- )
1146
- }
1147
- if let swiftError {
1148
- throw swiftError
1149
- }
1150
- guard let response else {
1151
- throw NSError(
1152
- domain: RunnerErrorDomain.general,
1153
- code: RunnerErrorCode.commandReturnedNoResponse,
1154
- userInfo: [NSLocalizedDescriptionKey: "command returned no response"]
1155
- )
1156
- }
1157
-
1158
-
1159
-
1160
-
1161
-
1162
-
1163
-
1164
-
1165
-
1166
-
1167
-
1168
-
1169
-
1170
-
1171
- if didRecordXCTestFailure(since: failureCountBefore),
1172
- let failureResponse = xctestRecordedFailureResponse(command: command, response: response)
1173
- {
1174
- invalidateCachedTarget(reason: "xctest_recorded_failure")
1175
- return failureResponse
1176
- }
1177
- if !hasRetried, shouldRetryCommand(command), shouldRetryResponse(response) {
1178
- NSLog(
1179
- "AGENT_DEVICE_RUNNER_RETRY command=%@ reason=response_unavailable",
1180
- command.command.rawValue
1181
- )
1182
- hasRetried = true
1183
- invalidateCachedTarget(reason: "response_unavailable")
1184
- sleepFor(retryCooldown)
1185
- continue
1186
- }
1187
- return response
1188
- }
1189
- }
1190
-
1191
- private func executeSnapshotDispatched(command: Command) throws -> Response {
1192
- try executeDispatchedWithRecovery(command: command) {
1193
- try self.executeSnapshotDispatchedOnce(command: command)
1194
- }
1195
- }
1196
-
1197
-
1198
-
1199
-
1200
- func executeDispatchedWithRecovery(
1201
- command: Command,
1202
- perform: () throws -> Response
1203
- ) throws -> Response {
1204
- var hasRetried = false
1205
- while true {
1206
- let failureCountBefore = try runMainThreadWork(
1207
- "recorded_failure_count",
1208
- timeout: mainThreadExecutionTimeout,
1209
- timeoutError: mainThreadExecutionTimeoutError
1210
- ) {
1211
- self.currentXCTestFailureCount()
1212
- }
1213
- let response = try perform()
1214
-
1215
-
1216
- if hasAbandonedMainThreadWork() {
1217
- NSLog(
1218
- "AGENT_DEVICE_RUNNER_DISPATCH_RECOVERY_SKIPPED_XCTEST_OCCUPIED command=%@",
1219
- command.command.rawValue
1220
- )
1221
- return response
1222
- }
1223
- let recordedFailureResponse = try runMainThreadWork(
1224
- "recorded_failure_count",
1225
- timeout: mainThreadExecutionTimeout,
1226
- timeoutError: mainThreadExecutionTimeoutError
1227
- ) {
1228
- self.didRecordXCTestFailure(since: failureCountBefore)
1229
- ? self.xctestRecordedFailureResponse(command: command, response: response)
1230
- : nil
1231
- }
1232
- if let recordedFailureResponse {
1233
- try runMainThreadWork(
1234
- "target_invalidation",
1235
- timeout: mainThreadExecutionTimeout,
1236
- timeoutError: mainThreadExecutionTimeoutError
1237
- ) {
1238
- self.invalidateCachedTarget(reason: "xctest_recorded_failure")
1239
- }
1240
- return recordedFailureResponse
1241
- }
1242
- if !hasRetried, shouldRetryCommand(command), shouldRetryResponse(response) {
1243
- NSLog(
1244
- "AGENT_DEVICE_RUNNER_RETRY command=%@ reason=response_unavailable",
1245
- command.command.rawValue
1246
- )
1247
- hasRetried = true
1248
- try runMainThreadWork(
1249
- "target_invalidation",
1250
- timeout: mainThreadExecutionTimeout,
1251
- timeoutError: mainThreadExecutionTimeoutError
1252
- ) {
1253
- self.invalidateCachedTarget(reason: "response_unavailable")
1254
- self.sleepFor(self.retryCooldown)
1255
- }
1256
- continue
1257
- }
1258
- return response
1259
- }
1260
- }
1261
-
1262
- private func executeSnapshotDispatchedOnce(command: Command) throws -> Response {
1263
- let preparation = try runMainThreadWork(
1264
- "command_preparation",
1265
- timeout: mainThreadExecutionTimeout,
1266
- timeoutError: mainThreadExecutionTimeoutError
1267
- ) {
1268
- try self.prepareActiveCommandContextSafely(command: command, routeToSpringboard: false)
1269
- }
1270
- switch preparation {
1271
- case .response(let response):
1272
- return response
1273
- case .context(let context):
1274
- return try executeSnapshotPrepared(
1275
- command: command,
1276
- activeApp: context.app,
1277
- systemSurface: context.systemSurface
1278
- )
1279
- }
1280
- }
1281
-
1282
-
1283
-
1284
- static func presentationOptions(from command: Command) -> PresentationOptions {
1285
- let customActions = command.customActions ?? false
1286
- return PresentationOptions(
1287
- interactiveOnly: command.interactiveOnly ?? false,
1288
- depth: command.depth,
1289
- scope: command.scope,
1290
- raw: command.raw ?? false,
1291
-
1292
-
1293
-
1294
- preferredBackend: command.preferredBackend
1295
- ?? (customActions ? SnapshotBackendKind.privateAX.rawValue : nil),
1296
- customActions: customActions
1297
- )
1298
- }
1299
-
1300
- private func executeSnapshotPrepared(
1301
- command: Command,
1302
- activeApp: XCUIApplication,
1303
- systemSurface: SystemSurfaceHost? = nil
1304
- ) throws -> Response {
1305
- let options = Self.presentationOptions(from: command)
1306
- do {
1307
- var payload: DataPayload
1308
- if options.raw {
1309
- payload = try snapshotRaw(app: activeApp, options: options)
1310
- } else {
1311
- payload = try snapshotFast(app: activeApp, options: options)
1312
- }
1313
- if let systemSurface {
1314
- payload.systemSurface = SystemSurfaceProvenancePayload(
1315
- bundleId: systemSurface.bundleId,
1316
- kind: systemSurface.kind.rawValue
1317
- )
1318
- }
1319
- setNeedsPostSnapshotInteractionDelay()
1320
- return Response(ok: true, data: payload)
1321
- } catch let failure as SnapshotCaptureFailure {
1322
- invalidateCachedTargetAfterSnapshotFailure()
1323
- return Response(
1324
- ok: false,
1325
- error: ErrorPayload(
1326
- code: failure.code,
1327
- message: failure.message,
1328
- hint: failure.hint
1329
- )
1330
- )
1331
- }
1332
- }
1333
-
1334
- private func setNeedsPostSnapshotInteractionDelay() {
1335
- if Thread.isMainThread {
1336
- needsPostSnapshotInteractionDelay = true
1337
- return
1338
- }
1339
- guard !hasAbandonedMainThreadWork() else {
1340
- NSLog("AGENT_DEVICE_RUNNER_POST_SNAPSHOT_DELAY_MARK_SKIPPED_XCTEST_OCCUPIED")
1341
- return
1342
- }
1343
- do {
1344
- try runMainThreadWork(
1345
- "post_snapshot_delay_mark",
1346
- timeout: 1,
1347
- timeoutError: mainThreadExecutionTimeoutError
1348
- ) {
1349
- self.needsPostSnapshotInteractionDelay = true
1350
- }
1351
- } catch {
1352
- NSLog("AGENT_DEVICE_RUNNER_POST_SNAPSHOT_DELAY_MARK_FAILED=%@", String(describing: error))
1353
- }
1354
- }
1355
-
1356
- private func invalidateCachedTargetAfterSnapshotFailure() {
1357
- if Thread.isMainThread {
1358
- invalidateCachedTarget(reason: "ax_snapshot_failure")
1359
- return
1360
- }
1361
-
1362
-
1363
- guard !hasAbandonedMainThreadWork() else {
1364
- NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_INVALIDATION_DEFERRED_XCTEST_OCCUPIED")
1365
- DispatchQueue.main.async {
1366
- self.invalidateCachedTarget(reason: "ax_snapshot_failure")
1367
- }
1368
- return
1369
- }
1370
- do {
1371
- try runMainThreadWork(
1372
- "target_invalidation",
1373
- timeout: 1,
1374
- timeoutError: mainThreadExecutionTimeoutError
1375
- ) {
1376
- self.invalidateCachedTarget(reason: "ax_snapshot_failure")
1377
- }
1378
- } catch {
1379
- NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_INVALIDATION_FAILED=%@", String(describing: error))
1380
- }
1381
- }
1382
-
1383
- private func prepareActiveCommandContextSafely(
1384
- command: Command,
1385
- routeToSpringboard: Bool
1386
- ) throws -> ActiveCommandPreparation {
1387
- var preparation: ActiveCommandPreparation?
1388
- let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
1389
- preparation = self.prepareActiveCommandContext(
1390
- command: command,
1391
- routeToSpringboard: routeToSpringboard
1392
- )
1393
- })
1394
- if let exceptionMessage {
1395
- throw NSError(
1396
- domain: RunnerErrorDomain.exception,
1397
- code: RunnerErrorCode.objcException,
1398
- userInfo: [NSLocalizedDescriptionKey: exceptionMessage]
1399
- )
1400
- }
1401
- guard let preparation else {
1402
- throw NSError(
1403
- domain: RunnerErrorDomain.general,
1404
- code: RunnerErrorCode.commandReturnedNoResponse,
1405
- userInfo: [NSLocalizedDescriptionKey: "snapshot preflight returned no response"]
1406
- )
1407
- }
1408
- return preparation
1409
- }
1410
-
1411
- private func executeOnMain(
1412
- command: Command,
1413
- alertDeadline: Date?,
1414
- routeToSpringboard: Bool
1415
- ) throws -> Response {
1416
- let preparation = prepareActiveCommandContext(
1417
- command: command,
1418
- routeToSpringboard: routeToSpringboard
1419
- )
1420
- let activeApp: XCUIApplication
1421
- switch preparation {
1422
- case .response(let response):
1423
- return response
1424
- case .context(let context):
1425
- activeApp = context.app
1426
- }
1427
-
1428
- switch command.command {
1429
- case .status:
1430
- return executeStatus(command: command)
1431
- case .targetReset:
1432
- return resetTargetAfterExternalRelaunch()
1433
- case .shutdown:
1434
- stopRecordingIfNeeded()
1435
- return Response(ok: true, data: DataPayload(message: "shutdown"))
1436
- case .recordStart:
1437
- guard
1438
- let requestedOutPath = command.outPath?.trimmingCharacters(in: .whitespacesAndNewlines),
1439
- !requestedOutPath.isEmpty
1440
- else {
1441
- return Response(ok: false, error: ErrorPayload(message: "recordStart requires outPath"))
1442
- }
1443
- let hasAppBundleId = !(command.appBundleId?
1444
- .trimmingCharacters(in: .whitespacesAndNewlines)
1445
- .isEmpty ?? true)
1446
- guard hasAppBundleId else {
1447
- return Response(ok: false, error: ErrorPayload(message: "recordStart requires appBundleId"))
1448
- }
1449
- if activeRecording != nil {
1450
- return Response(ok: false, error: ErrorPayload(message: "recording already in progress"))
1451
- }
1452
- if let requestedFps = command.fps, (requestedFps < minRecordingFps || requestedFps > maxRecordingFps) {
1453
- return Response(ok: false, error: ErrorPayload(message: "recordStart fps must be between \(minRecordingFps) and \(maxRecordingFps)"))
1454
- }
1455
- do {
1456
- let resolvedOutPath = resolveRecordingOutPath(requestedOutPath)
1457
- let fpsLabel = command.fps.map(String.init) ?? String(RunnerTests.defaultRecordingFps)
1458
- NSLog(
1459
- "AGENT_DEVICE_RUNNER_RECORD_START requestedOutPath=%@ resolvedOutPath=%@ fps=%@",
1460
- requestedOutPath,
1461
- resolvedOutPath,
1462
- fpsLabel
1463
- )
1464
- let recorder = ScreenRecorder(
1465
- outputPath: resolvedOutPath,
1466
- fps: command.fps.map { Int32($0) }
1467
- )
1468
- try recorder.start { [weak self] in
1469
- return self?.captureRunnerFrame(app: activeApp)
1470
- }
1471
- activeRecording = recorder
1472
- return Response(ok: true, data: DataPayload(message: "recording started"))
1473
- } catch {
1474
- activeRecording = nil
1475
- return Response(ok: false, error: ErrorPayload(message: "failed to start recording: \(error.localizedDescription)"))
1476
- }
1477
- case .recordStop:
1478
- guard let recorder = activeRecording else {
1479
-
1480
-
1481
-
1482
-
1483
- return Response(ok: true, data: DataPayload(message: "recording already stopped"))
1484
- }
1485
- do {
1486
- try recorder.stop()
1487
- activeRecording = nil
1488
- return Response(ok: true, data: DataPayload(message: "recording stopped"))
1489
- } catch {
1490
- activeRecording = nil
1491
- return Response(ok: false, error: ErrorPayload(message: "failed to stop recording: \(error.localizedDescription)"))
1492
- }
1493
- case .uptime:
1494
- return executeUptime()
1495
- case .activate:
1496
- guard
1497
- let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
1498
- !bundleId.isEmpty
1499
- else {
1500
- return Response(ok: false, error: ErrorPayload(message: "activate requires appBundleId"))
1501
- }
1502
-
1503
-
1504
- return Response(ok: true, data: DataPayload(message: "app activated"))
1505
- case .terminate:
1506
- guard
1507
- let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
1508
- !bundleId.isEmpty
1509
- else {
1510
- return Response(ok: false, error: ErrorPayload(message: "terminate requires appBundleId"))
1511
- }
1512
- XCUIApplication(bundleIdentifier: bundleId).terminate()
1513
- if currentBundleId == bundleId {
1514
- invalidateCachedTarget(reason: "target_terminated")
1515
- }
1516
- return Response(ok: true, data: DataPayload(message: "app terminated"))
1517
- default:
1518
- break
1519
- }
1520
- return try executeOnMainPrepared(
1521
- command: command,
1522
- activeApp: activeApp,
1523
- alertDeadline: alertDeadline
1524
- )
1525
- }
1526
-
1527
- private func prepareActiveCommandContext(
1528
- command: Command,
1529
- routeToSpringboard: Bool = false
1530
- ) -> ActiveCommandPreparation {
1531
- var activeApp = currentApp ?? app
1532
- var systemSurface: SystemSurfaceHost? = nil
1533
- if routeToSpringboard {
1534
- activeApp = springboard
1535
- } else if shouldSkipAppActivationPreflight(command) {
1536
- activeApp = resolveAppWithoutActivation(command: command)
1537
- } else if let presented = presentedSystemSurfaceHost() {
1538
-
1539
-
1540
-
1541
- activeApp = presented.app
1542
- systemSurface = presented.host
1543
- if isInteractionCommand(command.command) {
1544
- applyInteractionStabilizationIfNeeded()
1545
- }
1546
- } else if !isRunnerLifecycleCommand(command.command) {
1547
- let normalizedBundleId = command.appBundleId?
1548
- .trimmingCharacters(in: .whitespacesAndNewlines)
1549
- let requestedBundleId = (normalizedBundleId?.isEmpty == true) ? nil : normalizedBundleId
1550
- if let bundleId = requestedBundleId {
1551
- if currentBundleId != bundleId || currentApp == nil {
1552
- _ = activateTarget(bundleId: bundleId, reason: "bundle_changed")
1553
- } else {
1554
- refreshCachedTargetIfProcessChanged(bundleId: bundleId)
1555
- }
1556
- } else {
1557
-
1558
- invalidateCachedTarget(reason: "missing_app_bundle")
1559
- }
1560
-
1561
- activeApp = currentApp ?? app
1562
- if let bundleId = requestedBundleId, targetNeedsActivation(activeApp) {
1563
- activeApp = activateTarget(bundleId: bundleId, reason: "stale_target")
1564
- } else if requestedBundleId == nil, targetNeedsActivation(activeApp) {
1565
- ensureRunnerHostAppActive(reason: "missing_app_bundle")
1566
- activeApp = app
1567
- }
1568
-
1569
- let skipExistenceWait = canUseFastForegroundAppGuard(
1570
- activeApp: activeApp,
1571
- requestedBundleId: requestedBundleId,
1572
- command: command.command
1573
- )
1574
- if !skipExistenceWait && !activeApp.waitForExistence(timeout: appExistenceTimeout) {
1575
- if let bundleId = requestedBundleId {
1576
- activeApp = activateTarget(bundleId: bundleId, reason: "missing_after_wait")
1577
- guard activeApp.waitForExistence(timeout: appExistenceTimeout) else {
1578
- return .response(Response(ok: false, error: ErrorPayload(message: "app '\(bundleId)' is not available")))
1579
- }
1580
- } else {
1581
- return .response(Response(ok: false, error: ErrorPayload(message: "runner app is not available")))
1582
- }
1583
- }
1584
-
1585
- if isInteractionCommand(command.command) {
1586
- if let bundleId = requestedBundleId, activeApp.state != .runningForeground {
1587
- activeApp = activateTarget(bundleId: bundleId, reason: "interaction_foreground_guard")
1588
- } else if requestedBundleId == nil, activeApp.state != .runningForeground {
1589
- ensureRunnerHostAppActive(reason: "interaction_missing_app_bundle")
1590
- activeApp = app
1591
- }
1592
- let skipInteractionExistenceWait = canUseFastForegroundAppGuard(
1593
- activeApp: activeApp,
1594
- requestedBundleId: requestedBundleId,
1595
- command: command.command
1596
- )
1597
- if !skipInteractionExistenceWait && !activeApp.waitForExistence(timeout: 2) {
1598
- if let bundleId = requestedBundleId {
1599
- return .response(Response(ok: false, error: ErrorPayload(message: "app '\(bundleId)' is not available")))
1600
- }
1601
- return .response(Response(ok: false, error: ErrorPayload(message: "runner app is not available")))
1602
- }
1603
- applyInteractionStabilizationIfNeeded()
1604
- }
1605
- }
1606
- return .context(ActiveCommandContext(app: activeApp, systemSurface: systemSurface))
1607
- }
1608
-
1609
-
1610
-
1611
-
1612
-
1613
- private func presentedSystemSurfaceHost() -> (host: SystemSurfaceHost, app: XCUIApplication)? {
1614
- #if os(iOS)
1615
- for host in SystemSurfaceHostRegistry.hosts {
1616
- let candidate = XCUIApplication(bundleIdentifier: host.bundleId)
1617
- if candidate.state == .runningForeground {
1618
- return (host, candidate)
1619
- }
1620
- }
1621
- return nil
1622
- #else
1623
- return nil
1624
- #endif
1625
- }
1626
-
4
+ extension RunnerTests {
1627
5
  func executeOnMainPrepared(
1628
6
  command: Command,
1629
7
  activeApp: XCUIApplication,
@@ -1634,7 +12,8 @@ extension RunnerTests {
1634
12
  clearRememberedTextEntryTap()
1635
13
  }
1636
14
  switch command.command {
1637
- case .status, .activate, .terminate, .targetReset, .shutdown, .recordStart, .recordStop, .uptime:
15
+ case .status, .activate, .terminate, .targetReset, .shutdown, .recordStart, .recordStop, .uptime,
16
+ .snapshot:
1638
17
  return Response(
1639
18
  ok: false,
1640
19
  error: ErrorPayload(
@@ -1868,9 +247,7 @@ extension RunnerTests {
1868
247
  x2: x2,
1869
248
  y2: y2,
1870
249
  durationMs: defaults.durationMs,
1871
- synthesized: command.synthesized == true,
1872
- message: "dragged",
1873
- synthesizedPolicyKind: .synthesizedDrag
250
+ message: "dragged"
1874
251
  )
1875
252
  case .scroll:
1876
253
 
@@ -2052,8 +429,6 @@ extension RunnerTests {
2052
429
  return Response(ok: false, error: ErrorPayload(message: "readText did not resolve text"))
2053
430
  }
2054
431
  return Response(ok: true, data: DataPayload(text: text))
2055
- case .snapshot:
2056
- return try executeSnapshotPrepared(command: command, activeApp: activeApp)
2057
432
  case .screenshot:
2058
433
  #if os(macOS)
2059
434
 
@@ -2119,12 +494,20 @@ extension RunnerTests {
2119
494
  inlineScreenshot: command.inlineScreenshot == true
2120
495
  )
2121
496
  #endif
2122
- case .back, .backInApp:
2123
- if tapInAppBackControl(app: activeApp) {
2124
- let message = command.command == .back ? "back" : "backInApp"
2125
- return Response(ok: true, data: DataPayload(message: message))
497
+ case .backInApp:
498
+ switch tapInAppBackControl(app: activeApp) {
499
+ case .performed:
500
+ return Response(ok: true, data: DataPayload(message: "backInApp"))
501
+ case .unavailable:
502
+ return Response(
503
+ ok: false,
504
+ error: ErrorPayload(message: "in-app back control is not available")
505
+ )
506
+ case .unverified(let error):
507
+
508
+
509
+ return Response(ok: false, error: error)
2126
510
  }
2127
- return Response(ok: false, error: ErrorPayload(message: "in-app back control is not available"))
2128
511
  case .backSystem:
2129
512
  if performSystemBackAction(app: activeApp) {
2130
513
  return Response(ok: true, data: DataPayload(message: "backSystem"))
@@ -2232,10 +615,8 @@ extension RunnerTests {
2232
615
  x2: last.x,
2233
616
  y2: last.y,
2234
617
  durationMs: plan.durationMs,
2235
- synthesized: true,
2236
618
  message: plan.intent,
2237
- synthesizedPolicyKind: .synthesizedDrag,
2238
- synthesizedProfile: .fastSwipe
619
+ synthesized: (profile: .fastSwipe, policyKind: .synthesizedDrag)
2239
620
  )
2240
621
  )
2241
622
  case .sampled:
@@ -2254,603 +635,4 @@ extension RunnerTests {
2254
635
  return executeSequence(command: command, activeApp: activeApp)
2255
636
  }
2256
637
  }
2257
-
2258
- private func invalidScrollDirectionResponse(commandName: String) -> Response {
2259
- Response(
2260
- ok: false,
2261
- error: ErrorPayload(
2262
- code: "INVALID_ARGS",
2263
- message: "\(commandName) requires direction up|down|left|right"
2264
- )
2265
- )
2266
- }
2267
-
2268
- private func scrollDurationIsValid(_ durationMs: Double?) -> Bool {
2269
- guard let durationMs else { return true }
2270
- return durationMs.isFinite && durationMs >= 0 && durationMs <= 10000
2271
- }
2272
-
2273
- private func invalidScrollDurationResponse(commandName: String) -> Response {
2274
- return Response(
2275
- ok: false,
2276
- error: ErrorPayload(
2277
- code: "INVALID_ARGS",
2278
- message: "\(commandName) durationMs must be between 0 and 10000"
2279
- )
2280
- )
2281
- }
2282
-
2283
- private func executeScrollDragGesture(
2284
- activeApp: XCUIApplication,
2285
- x: Double,
2286
- y: Double,
2287
- x2: Double,
2288
- y2: Double,
2289
- durationMs: Double,
2290
- message: String,
2291
- context: SynthesizedCoordinateContext,
2292
- releaseBehavior: ScrollReleaseBehavior?
2293
- ) -> Response {
2294
- #if os(iOS)
2295
- return executeDragGesture(
2296
- activeApp: activeApp,
2297
- x: x,
2298
- y: y,
2299
- x2: x2,
2300
- y2: y2,
2301
- durationMs: durationMs,
2302
- synthesized: true,
2303
- message: message,
2304
- synthesizedContext: context,
2305
- synthesizedPolicyKind: .scroll,
2306
- synthesizedProfile: scrollDragProfile(releaseBehavior: releaseBehavior)
2307
- )
2308
- #else
2309
- return executeDragGesture(
2310
- activeApp: activeApp,
2311
- x: x,
2312
- y: y,
2313
- x2: x2,
2314
- y2: y2,
2315
- durationMs: durationMs,
2316
- synthesized: false,
2317
- message: message,
2318
- synthesizedPolicyKind: .scroll
2319
- )
2320
- #endif
2321
- }
2322
-
2323
-
2324
-
2325
- private func executeDragGesture(
2326
- activeApp: XCUIApplication,
2327
- x: Double,
2328
- y: Double,
2329
- x2: Double,
2330
- y2: Double,
2331
- durationMs: Double?,
2332
- synthesized: Bool,
2333
- message: String,
2334
- synthesizedContext: SynthesizedCoordinateContext? = nil,
2335
- synthesizedPolicyKind: SynthesizedGesturePolicyKind,
2336
- synthesizedProfile: SynthesizedDragProfile = .continuous
2337
- ) -> Response {
2338
- let durationMs = durationMs ?? runnerDefaultDragDurationMs
2339
- let commandName = dragCommandName(message: message)
2340
- guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite else {
2341
- return Response(
2342
- ok: false,
2343
- error: ErrorPayload(code: "INVALID_ARGS", message: "\(commandName) requires finite coordinates")
2344
- )
2345
- }
2346
- if synthesized, let synthesizedResponse = executeSynthesizedDragGesture(
2347
- activeApp: activeApp,
2348
- x: x,
2349
- y: y,
2350
- x2: x2,
2351
- y2: y2,
2352
- durationMs: durationMs,
2353
- message: message,
2354
- context: synthesizedContext,
2355
- policyKind: synthesizedPolicyKind,
2356
- profile: synthesizedProfile
2357
- ) {
2358
- return synthesizedResponse
2359
- }
2360
- let dragPoints = keyboardAvoidingDragPoints(app: activeApp, x: x, y: y, x2: x2, y2: y2)
2361
- let dragFrame = resolvedDragVisualizationFrame(
2362
- app: activeApp,
2363
- x: dragPoints.x,
2364
- y: dragPoints.y,
2365
- x2: dragPoints.x2,
2366
- y2: dragPoints.y2
2367
- )
2368
- var fallback: GestureFallback?
2369
- if synthesized {
2370
- let durationMs = min(max(durationMs, 16), 10000)
2371
- let context = synthesizedCoordinateContext(
2372
- app: activeApp,
2373
- policy: synthesizedGesturePolicy(synthesizedPolicyKind)
2374
- )
2375
- let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
2376
- synthesizedDragAt(
2377
- app: activeApp,
2378
- x: dragPoints.x,
2379
- y: dragPoints.y,
2380
- x2: dragPoints.x2,
2381
- y2: dragPoints.y2,
2382
- durationMs: durationMs,
2383
- profile: synthesizedProfile,
2384
- context: context
2385
- )
2386
- }
2387
- if case .performed = outcome {
2388
- return gestureResponse(message: message, timing: timing, frame: .drag(dragFrame))
2389
- }
2390
- fallback = gestureFallback(strategy: "xctest-coordinate-drag", from: outcome)
2391
- }
2392
- let holdDuration = synthesized
2393
- ? synthesizedSwipeFallbackHoldDuration(durationMs: durationMs)
2394
- : coordinateDragHoldDuration()
2395
- let (timing, outcome) = performGesture(activeApp) {
2396
- dragAt(
2397
- app: activeApp,
2398
- x: dragPoints.x,
2399
- y: dragPoints.y,
2400
- x2: dragPoints.x2,
2401
- y2: dragPoints.y2,
2402
- holdDuration: holdDuration
2403
- )
2404
- }
2405
- if let response = unsupportedResponse(for: outcome) {
2406
- return response
2407
- }
2408
- return gestureResponse(
2409
- message: message,
2410
- timing: timing,
2411
- frame: .drag(dragFrame),
2412
- fallback: fallback
2413
- )
2414
- }
2415
-
2416
- private func executeSynthesizedDragGesture(
2417
- activeApp: XCUIApplication,
2418
- x: Double,
2419
- y: Double,
2420
- x2: Double,
2421
- y2: Double,
2422
- durationMs: Double,
2423
- message: String,
2424
- context: SynthesizedCoordinateContext?,
2425
- policyKind: SynthesizedGesturePolicyKind,
2426
- profile: SynthesizedDragProfile
2427
- ) -> Response? {
2428
- #if os(iOS)
2429
- let policy = synthesizedGesturePolicy(policyKind)
2430
- let context = context ?? synthesizedCoordinateContext(app: activeApp, policy: policy)
2431
- guard let plan = axFreeSynthesizedDragPlan(
2432
- app: activeApp,
2433
- x: x,
2434
- y: y,
2435
- x2: x2,
2436
- y2: y2,
2437
- context: context
2438
- )
2439
- else {
2440
- if context?.allowsXCTestCoordinateFallback == true {
2441
- logSynthesizedGesturePolicyDecision(kind: policyKind, context: context, fallbackAttempted: true)
2442
- return executeCoordinateDragFallback(
2443
- activeApp: activeApp,
2444
- x: x,
2445
- y: y,
2446
- x2: x2,
2447
- y2: y2,
2448
- durationMs: durationMs,
2449
- message: message,
2450
- fallback: nil
2451
- )
2452
- }
2453
- logSynthesizedGesturePolicyDecision(kind: policyKind, context: context, fallbackAttempted: false)
2454
- return Response(
2455
- ok: false,
2456
- error: ErrorPayload(
2457
- code: "INVALID_ARGS",
2458
- message: "\(dragCommandName(message: message)) could not resolve a finite synthesized coordinate frame"
2459
- )
2460
- )
2461
- }
2462
- let durationMs = min(max(durationMs, 16), 10000)
2463
- let dragFrame = axFreeDragVisualizationFrame(
2464
- x: plan.points.x,
2465
- y: plan.points.y,
2466
- x2: plan.points.x2,
2467
- y2: plan.points.y2,
2468
- referenceFrame: plan.referenceFrame
2469
- )
2470
- let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
2471
- synthesizedDragAt(
2472
- app: activeApp,
2473
- x: plan.points.x,
2474
- y: plan.points.y,
2475
- x2: plan.points.x2,
2476
- y2: plan.points.y2,
2477
- durationMs: durationMs,
2478
- profile: profile,
2479
- context: plan.context
2480
- )
2481
- }
2482
- if case .performed = outcome {
2483
- logSynthesizedGesturePolicyDecision(kind: policyKind, context: plan.context, fallbackAttempted: false)
2484
- return gestureResponse(message: message, timing: timing, frame: .drag(dragFrame))
2485
- }
2486
- if plan.context.allowsXCTestCoordinateFallback {
2487
- logSynthesizedGesturePolicyDecision(kind: policyKind, context: plan.context, fallbackAttempted: true)
2488
- return executeCoordinateDragFallback(
2489
- activeApp: activeApp,
2490
- x: plan.points.x,
2491
- y: plan.points.y,
2492
- x2: plan.points.x2,
2493
- y2: plan.points.y2,
2494
- durationMs: durationMs,
2495
- message: message,
2496
- fallback: gestureFallback(strategy: "xctest-coordinate-drag", from: outcome)
2497
- )
2498
- }
2499
- logSynthesizedGesturePolicyDecision(kind: policyKind, context: plan.context, fallbackAttempted: false)
2500
- return unsupportedResponse(for: outcome)
2501
- #else
2502
- return nil
2503
- #endif
2504
- }
2505
-
2506
- private func executeCoordinateDragFallback(
2507
- activeApp: XCUIApplication,
2508
- x: Double,
2509
- y: Double,
2510
- x2: Double,
2511
- y2: Double,
2512
- durationMs: Double,
2513
- message: String,
2514
- fallback: GestureFallback?
2515
- ) -> Response {
2516
- let dragPoints = keyboardAvoidingDragPoints(app: activeApp, x: x, y: y, x2: x2, y2: y2)
2517
- let dragFrame = resolvedDragVisualizationFrame(
2518
- app: activeApp,
2519
- x: dragPoints.x,
2520
- y: dragPoints.y,
2521
- x2: dragPoints.x2,
2522
- y2: dragPoints.y2
2523
- )
2524
- let holdDuration = synthesizedSwipeFallbackHoldDuration(durationMs: durationMs)
2525
- let (timing, outcome) = performGesture(activeApp) {
2526
- dragAt(
2527
- app: activeApp,
2528
- x: dragPoints.x,
2529
- y: dragPoints.y,
2530
- x2: dragPoints.x2,
2531
- y2: dragPoints.y2,
2532
- holdDuration: holdDuration
2533
- )
2534
- }
2535
- if let response = unsupportedResponse(for: outcome) {
2536
- return response
2537
- }
2538
- return gestureResponse(
2539
- message: message,
2540
- timing: timing,
2541
- frame: .drag(dragFrame),
2542
- fallback: fallback
2543
- )
2544
- }
2545
-
2546
-
2547
-
2548
-
2549
-
2550
-
2551
-
2552
-
2553
- private func scrollKeyboardOccludedResponse(
2554
- direction: String,
2555
- keyboardMinY: Double,
2556
- visibleHeight: Double
2557
- ) -> Response {
2558
- return Response(
2559
- ok: false,
2560
- error: ErrorPayload(
2561
- code: ScrollViewportPolicy.occlusionRunnerCode,
2562
- message: String(
2563
- format:
2564
- "scroll %@ refused: the keyboard leaves %.0fpt of visible surface above it, too little to swipe",
2565
- direction,
2566
- visibleHeight
2567
- )
2568
- )
2569
- )
2570
- }
2571
-
2572
- private func dragCommandName(message: String) -> String {
2573
- return message == "scrolled" ? "scroll" : "drag"
2574
- }
2575
-
2576
- func currentXCTestFailureCount() -> Int {
2577
- return testRun?.failureCount ?? 0
2578
- }
2579
-
2580
- func didRecordXCTestFailure(since failureCountBefore: Int) -> Bool {
2581
- return currentXCTestFailureCount() > failureCountBefore
2582
- }
2583
-
2584
- private func xctestRecordedFailureResponse(command: Command, response: Response) -> Response? {
2585
- guard response.ok else { return nil }
2586
- if response.data?.runnerFatal == true {
2587
- return nil
2588
- }
2589
- guard !isReadOnlyCommand(command), !isRunnerLifecycleCommand(command.command) else {
2590
- return nil
2591
- }
2592
- return Response(
2593
- ok: false,
2594
- error: ErrorPayload(
2595
- code: "XCTEST_RECORDED_FAILURE",
2596
- message: "XCTest recorded a failure while executing \(command.command.rawValue); the action may not have been performed.",
2597
- hint: "The iOS runner session was invalidated. Re-observe with a fresh snapshot before retrying; if the accessibility tree is unavailable, use screenshot plus coordinate commands instead of retrying the tap blindly."
2598
- )
2599
- )
2600
- }
2601
-
2602
-
2603
-
2604
-
2605
-
2606
-
2607
-
2608
- private func shouldSkipAppActivationPreflight(_ command: Command) -> Bool {
2609
- #if os(iOS)
2610
- if command.command == .alert {
2611
- return true
2612
- }
2613
-
2614
-
2615
-
2616
-
2617
-
2618
- if command.command == .actionButton {
2619
- return true
2620
- }
2621
-
2622
-
2623
-
2624
-
2625
- guard command.text == nil, command.selectorKey == nil else { return false }
2626
- guard hasCachedTargetForActivationSkip(command: command) else { return false }
2627
- return isCoordinateOnlyTap(command)
2628
- #else
2629
- return false
2630
- #endif
2631
- }
2632
-
2633
- private func shouldRouteToSpringboardBlockingSystemModal(
2634
- _ command: Command
2635
- ) -> Bool {
2636
- #if os(iOS)
2637
- guard isCoordinateOnlyTap(command) else {
2638
- return false
2639
- }
2640
-
2641
-
2642
-
2643
-
2644
-
2645
- let probeDeadline = Date().addingTimeInterval(systemModalProbeBudget)
2646
-
2647
-
2648
-
2649
- if Thread.isMainThread {
2650
- return firstBlockingSystemModal(
2651
- in: springboard,
2652
- deadline: probeDeadline
2653
- ) != nil
2654
- }
2655
- return boundedBlockingSystemAlertSnapshot(
2656
- deadline: probeDeadline
2657
- ) != nil
2658
- #else
2659
- return false
2660
- #endif
2661
- }
2662
-
2663
- private func isCoordinateOnlyTap(_ command: Command) -> Bool {
2664
- return command.command == .tap
2665
- && command.text == nil
2666
- && command.selectorKey == nil
2667
- && command.x != nil
2668
- && command.y != nil
2669
- }
2670
-
2671
- private func hasCachedTargetForActivationSkip(command: Command) -> Bool {
2672
- guard let currentApp, currentApp.state == .runningForeground else { return false }
2673
- guard let bundleId = command.appBundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
2674
- !bundleId.isEmpty
2675
- else {
2676
- return true
2677
- }
2678
- return currentBundleId == bundleId
2679
- }
2680
-
2681
- private func resolveAppWithoutActivation(command: Command) -> XCUIApplication {
2682
- guard let bundleId = command.appBundleId?
2683
- .trimmingCharacters(in: .whitespacesAndNewlines),
2684
- !bundleId.isEmpty
2685
- else {
2686
- return currentApp ?? app
2687
- }
2688
- if currentBundleId == bundleId, let currentApp {
2689
- return currentApp
2690
- }
2691
- return XCUIApplication(bundleIdentifier: bundleId)
2692
- }
2693
-
2694
- func executeTypeCommand(activeApp: XCUIApplication, command: Command) -> Response {
2695
- guard let text = command.text else {
2696
- return Response(ok: false, error: ErrorPayload(message: "type requires text"))
2697
- }
2698
- let delaySeconds = Double(max(command.delayMs ?? 0, 0)) / 1000.0
2699
- let textEntryMode = resolveTextEntryMode(command)
2700
- let target: TextEntryTarget
2701
- var resolvedCoordinateContext: SynthesizedCoordinateContext?
2702
- var maestroNonHittableCoordinateFallbackUsed: Bool?
2703
- if command.allowNonHittableCoordinateFallback == true,
2704
- command.x != nil,
2705
- command.y != nil
2706
- {
2707
-
2708
-
2709
- maestroNonHittableCoordinateFallbackUsed = true
2710
- }
2711
- let focusStartedAt = Date()
2712
- #if os(iOS)
2713
- let xCTestChannelPenalized = isSnapshotXCTestChannelPenalized(bundleId: currentBundleId)
2714
- var resolvedCoordinateTarget: TextEntryTarget?
2715
- if Self.shouldUseResolvedCoordinateTextEntryRoute(
2716
- repairMode: textEntryMode,
2717
- hasX: command.x != nil,
2718
- hasY: command.y != nil,
2719
- xCTestChannelPenalized: xCTestChannelPenalized
2720
- ), let x = command.x, let y = command.y {
2721
- let policyKind = SynthesizedGesturePolicyKind.coordinateTap
2722
- let context = synthesizedCoordinateContext(
2723
- app: activeApp,
2724
- policy: synthesizedGesturePolicy(policyKind)
2725
- )
2726
- let (_, outcome) = performGesture(activeApp, idleTimeout: false) {
2727
- synthesizedTapAt(app: activeApp, x: x, y: y, context: context)
2728
- }
2729
- if Self.shouldFallbackFromSynthesizedTextEntryFocus(outcome) {
2730
- logSynthesizedGesturePolicyDecision(
2731
- kind: policyKind,
2732
- context: context,
2733
- fallbackAttempted: true
2734
- )
2735
- } else {
2736
- logSynthesizedGesturePolicyDecision(
2737
- kind: policyKind,
2738
- context: context,
2739
- fallbackAttempted: false
2740
- )
2741
- resolvedCoordinateContext = context
2742
- resolvedCoordinateTarget = TextEntryTarget(
2743
- element: nil,
2744
- refreshPoint: CGPoint(x: x, y: y),
2745
- prefersFocusedElement: false
2746
- )
2747
- }
2748
- }
2749
- #else
2750
- let xCTestChannelPenalized = false
2751
- let resolvedCoordinateTarget: TextEntryTarget? = nil
2752
- #endif
2753
- if let resolvedCoordinateTarget {
2754
- target = resolvedCoordinateTarget
2755
- } else if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
2756
-
2757
-
2758
- let match = findElement(
2759
- app: activeApp,
2760
- selectorKey: selectorKey,
2761
- selectorValue: selectorValue,
2762
- allowNonHittableFallback: command.allowNonHittableCoordinateFallback == true
2763
- )
2764
- if match.isAmbiguous {
2765
- return Response(ok: false, error: ErrorPayload(code: "AMBIGUOUS_MATCH", message: "selector matched multiple elements"))
2766
- }
2767
- guard let element = match.element else {
2768
- return Response(ok: false, error: ErrorPayload(code: "NO_MATCH", message: "selector did not match an element"))
2769
- }
2770
- guard isTextEntryElement(element) else {
2771
- return Response(ok: false, error: ErrorPayload(code: "INVALID_TARGET", message: "selector did not match a text input"))
2772
- }
2773
- if command.allowNonHittableCoordinateFallback == true {
2774
- maestroNonHittableCoordinateFallbackUsed = match.usedNonHittableFallback
2775
- }
2776
- target = focusTextInputForTextEntry(app: activeApp, element: element)
2777
- } else {
2778
- target = focusTextInputForTextEntry(app: activeApp, x: command.x, y: command.y)
2779
- }
2780
- NSLog(
2781
- "AGENT_DEVICE_RUNNER_TEXT_ENTRY_PHASE commandId=%@ phase=focus durationMs=%.1f chars=%d mode=%@",
2782
- command.commandId ?? "",
2783
- Date().timeIntervalSince(focusStartedAt) * 1000.0,
2784
- text.count,
2785
- textEntryModeName(textEntryMode)
2786
- )
2787
- if textEntryMode == .replacement {
2788
- #if os(iOS)
2789
- let canReplaceResolvedFirstResponder = Self.shouldUseSynthesizedFirstResponderReplacement(
2790
- hasResolvedElement: target.element != nil,
2791
- hasRefreshPoint: target.refreshPoint != nil,
2792
- xCTestChannelPenalized: xCTestChannelPenalized
2793
- )
2794
- #else
2795
- let canReplaceResolvedFirstResponder = false
2796
- #endif
2797
- guard target.element != nil || canReplaceResolvedFirstResponder else {
2798
- let message =
2799
- (command.x != nil && command.y != nil)
2800
- ? "no text input found at the provided coordinates to clear"
2801
- : "no focused text input to clear"
2802
- return Response(ok: false, error: ErrorPayload(message: message))
2803
- }
2804
- }
2805
- let textResult = typeTextReliably(
2806
- app: activeApp,
2807
- target: target,
2808
- text: text,
2809
- delaySeconds: delaySeconds,
2810
- repairMode: textEntryMode,
2811
- xCTestChannelPenalized: xCTestChannelPenalized,
2812
- synthesizer: PrivateXCTestTextEntrySynthesizer(),
2813
- commandId: command.commandId
2814
- )
2815
- if let failure = textResult.failure {
2816
- return Response(
2817
- ok: false,
2818
- error: ErrorPayload(code: failure.rawValue, message: failure.message, hint: failure.hint)
2819
- )
2820
- }
2821
- if textResult.verified == false {
2822
- let expected = textResult.expectedText ?? ""
2823
- let observed = textResult.observedText ?? ""
2824
- return Response(
2825
- ok: false,
2826
- error: ErrorPayload(
2827
- code: "TEXT_ENTRY_MISMATCH",
2828
- message: "text entry verification failed: expected \"\(expected)\", observed \"\(observed)\""
2829
- )
2830
- )
2831
- }
2832
- let point = target.refreshPoint
2833
- let frame: CGRect
2834
- if let resolvedCoordinateContext {
2835
- frame = resolvedCoordinateContext.referenceFrame
2836
- } else if point != nil {
2837
- frame = activeApp.frame
2838
- } else {
2839
-
2840
-
2841
- frame = .zero
2842
- }
2843
- return Response(
2844
- ok: true,
2845
- data: DataPayload(
2846
- message: textResult.repaired ? "typed after repair" : "typed",
2847
- x: point.map { Double($0.x) },
2848
- y: point.map { Double($0.y) },
2849
- referenceWidth: frame.isEmpty ? nil : Double(frame.width),
2850
- referenceHeight: frame.isEmpty ? nil : Double(frame.height),
2851
- maestroNonHittableCoordinateFallbackUsed: maestroNonHittableCoordinateFallbackUsed,
2852
- textEntryRoute: textResult.textEntryRoute
2853
- )
2854
- )
2855
- }
2856
638
  }