agent-device 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +6 -0
  2. package/dist/src/36.js +3 -0
  3. package/dist/src/bin.js +82 -66
  4. package/dist/src/daemon.js +40 -39
  5. package/dist/src/index.d.ts +567 -5
  6. package/dist/src/index.js +3 -1
  7. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +84 -16
  8. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +140 -50
  9. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +13 -2
  10. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +22 -9
  11. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +221 -0
  12. package/ios-runner/README.md +17 -2
  13. package/ios-runner/RUNNER_PROTOCOL.md +73 -0
  14. package/package.json +11 -6
  15. package/skills/agent-device/SKILL.md +38 -12
  16. package/skills/agent-device/references/bootstrap-install.md +55 -15
  17. package/skills/agent-device/references/exploration.md +65 -8
  18. package/skills/agent-device/references/macos-desktop.md +1 -2
  19. package/skills/agent-device/references/remote-tenancy.md +15 -2
  20. package/skills/agent-device/references/verification.md +7 -3
  21. package/dist/src/224.js +0 -2
  22. package/dist/src/331.js +0 -3
  23. package/dist/src/425.js +0 -1
  24. package/dist/src/bin.d.ts +0 -1
  25. package/dist/src/cli-client-commands.d.ts +0 -8
  26. package/dist/src/cli.d.ts +0 -6
  27. package/dist/src/client-metro.d.ts +0 -64
  28. package/dist/src/client-normalizers.d.ts +0 -20
  29. package/dist/src/client-shared.d.ts +0 -20
  30. package/dist/src/client-types.d.ts +0 -269
  31. package/dist/src/client.d.ts +0 -5
  32. package/dist/src/core/app-events.d.ts +0 -8
  33. package/dist/src/core/batch.d.ts +0 -17
  34. package/dist/src/core/capabilities.d.ts +0 -3
  35. package/dist/src/core/click-button.d.ts +0 -20
  36. package/dist/src/core/dispatch-payload.d.ts +0 -1
  37. package/dist/src/core/dispatch-resolve.d.ts +0 -29
  38. package/dist/src/core/dispatch-series.d.ts +0 -7
  39. package/dist/src/core/dispatch.d.ts +0 -37
  40. package/dist/src/core/open-target.d.ts +0 -4
  41. package/dist/src/core/session-surface.d.ts +0 -3
  42. package/dist/src/core/settings-contract.d.ts +0 -9
  43. package/dist/src/daemon/action-utils.d.ts +0 -3
  44. package/dist/src/daemon/android-system-dialog.d.ts +0 -11
  45. package/dist/src/daemon/app-log-android.d.ts +0 -4
  46. package/dist/src/daemon/app-log-ios.d.ts +0 -7
  47. package/dist/src/daemon/app-log-process.d.ts +0 -15
  48. package/dist/src/daemon/app-log-stream.d.ts +0 -19
  49. package/dist/src/daemon/app-log.d.ts +0 -28
  50. package/dist/src/daemon/artifact-archive.d.ts +0 -12
  51. package/dist/src/daemon/artifact-download.d.ts +0 -12
  52. package/dist/src/daemon/artifact-materialization.d.ts +0 -17
  53. package/dist/src/daemon/artifact-registry.d.ts +0 -12
  54. package/dist/src/daemon/config.d.ts +0 -16
  55. package/dist/src/daemon/context.d.ts +0 -25
  56. package/dist/src/daemon/device-ready.d.ts +0 -6
  57. package/dist/src/daemon/handlers/find.d.ts +0 -40
  58. package/dist/src/daemon/handlers/install-source.d.ts +0 -44
  59. package/dist/src/daemon/handlers/interaction-common.d.ts +0 -41
  60. package/dist/src/daemon/handlers/interaction-flags.d.ts +0 -4
  61. package/dist/src/daemon/handlers/interaction-get.d.ts +0 -3
  62. package/dist/src/daemon/handlers/interaction-is.d.ts +0 -3
  63. package/dist/src/daemon/handlers/interaction-read.d.ts +0 -14
  64. package/dist/src/daemon/handlers/interaction-scroll.d.ts +0 -3
  65. package/dist/src/daemon/handlers/interaction-selector.d.ts +0 -27
  66. package/dist/src/daemon/handlers/interaction-snapshot.d.ts +0 -8
  67. package/dist/src/daemon/handlers/interaction-targeting.d.ts +0 -28
  68. package/dist/src/daemon/handlers/interaction-touch.d.ts +0 -45
  69. package/dist/src/daemon/handlers/interaction.d.ts +0 -9
  70. package/dist/src/daemon/handlers/lease.d.ts +0 -8
  71. package/dist/src/daemon/handlers/parse-utils.d.ts +0 -3
  72. package/dist/src/daemon/handlers/record-trace-android.d.ts +0 -18
  73. package/dist/src/daemon/handlers/record-trace-ios.d.ts +0 -52
  74. package/dist/src/daemon/handlers/record-trace-recording.d.ts +0 -32
  75. package/dist/src/daemon/handlers/record-trace.d.ts +0 -10
  76. package/dist/src/daemon/handlers/session-batch.d.ts +0 -2
  77. package/dist/src/daemon/handlers/session-close.d.ts +0 -31
  78. package/dist/src/daemon/handlers/session-deploy.d.ts +0 -37
  79. package/dist/src/daemon/handlers/session-device-utils.d.ts +0 -26
  80. package/dist/src/daemon/handlers/session-open-target.d.ts +0 -3
  81. package/dist/src/daemon/handlers/session-open.d.ts +0 -22
  82. package/dist/src/daemon/handlers/session-perf.d.ts +0 -2
  83. package/dist/src/daemon/handlers/session-replay-heal.d.ts +0 -8
  84. package/dist/src/daemon/handlers/session-replay-script.d.ts +0 -3
  85. package/dist/src/daemon/handlers/session-runtime-command.d.ts +0 -9
  86. package/dist/src/daemon/handlers/session-runtime.d.ts +0 -36
  87. package/dist/src/daemon/handlers/session-startup-metrics.d.ts +0 -11
  88. package/dist/src/daemon/handlers/session.d.ts +0 -50
  89. package/dist/src/daemon/handlers/snapshot-alert.d.ts +0 -13
  90. package/dist/src/daemon/handlers/snapshot-capture.d.ts +0 -34
  91. package/dist/src/daemon/handlers/snapshot-session.d.ts +0 -15
  92. package/dist/src/daemon/handlers/snapshot-settings.d.ts +0 -24
  93. package/dist/src/daemon/handlers/snapshot-wait.d.ts +0 -37
  94. package/dist/src/daemon/handlers/snapshot.d.ts +0 -16
  95. package/dist/src/daemon/http-server.d.ts +0 -26
  96. package/dist/src/daemon/install-source-resolution.d.ts +0 -5
  97. package/dist/src/daemon/is-predicates.d.ts +0 -15
  98. package/dist/src/daemon/lease-context.d.ts +0 -9
  99. package/dist/src/daemon/lease-registry.d.ts +0 -63
  100. package/dist/src/daemon/materialized-path-registry.d.ts +0 -15
  101. package/dist/src/daemon/network-log.d.ts +0 -32
  102. package/dist/src/daemon/record-trace-errors.d.ts +0 -6
  103. package/dist/src/daemon/recording-gestures.d.ts +0 -3
  104. package/dist/src/daemon/recording-telemetry.d.ts +0 -20
  105. package/dist/src/daemon/recording-timing.d.ts +0 -24
  106. package/dist/src/daemon/request-cancel.d.ts +0 -9
  107. package/dist/src/daemon/request-lock-policy.d.ts +0 -2
  108. package/dist/src/daemon/request-router.d.ts +0 -23
  109. package/dist/src/daemon/runtime-hints.d.ts +0 -19
  110. package/dist/src/daemon/script-utils.d.ts +0 -28
  111. package/dist/src/daemon/scroll-planner.d.ts +0 -12
  112. package/dist/src/daemon/selectors-build.d.ts +0 -5
  113. package/dist/src/daemon/selectors-match.d.ts +0 -6
  114. package/dist/src/daemon/selectors-parse.d.ts +0 -29
  115. package/dist/src/daemon/selectors-resolve.d.ts +0 -33
  116. package/dist/src/daemon/selectors.d.ts +0 -5
  117. package/dist/src/daemon/server-lifecycle.d.ts +0 -23
  118. package/dist/src/daemon/session-open-script.d.ts +0 -7
  119. package/dist/src/daemon/session-routing.d.ts +0 -3
  120. package/dist/src/daemon/session-selector.d.ts +0 -10
  121. package/dist/src/daemon/session-store.d.ts +0 -33
  122. package/dist/src/daemon/snapshot-diff.d.ts +0 -20
  123. package/dist/src/daemon/snapshot-processing.d.ts +0 -10
  124. package/dist/src/daemon/touch-reference-frame.d.ts +0 -7
  125. package/dist/src/daemon/transport.d.ts +0 -6
  126. package/dist/src/daemon/types.d.ts +0 -173
  127. package/dist/src/daemon/upload-registry.d.ts +0 -7
  128. package/dist/src/daemon/upload.d.ts +0 -5
  129. package/dist/src/daemon-client.d.ts +0 -40
  130. package/dist/src/daemon.d.ts +0 -1
  131. package/dist/src/platforms/android/adb.d.ts +0 -5
  132. package/dist/src/platforms/android/app-lifecycle.d.ts +0 -31
  133. package/dist/src/platforms/android/device-input-state.d.ts +0 -19
  134. package/dist/src/platforms/android/devices.d.ts +0 -26
  135. package/dist/src/platforms/android/index.d.ts +0 -8
  136. package/dist/src/platforms/android/input-actions.d.ts +0 -17
  137. package/dist/src/platforms/android/install-artifact.d.ts +0 -11
  138. package/dist/src/platforms/android/manifest.d.ts +0 -1
  139. package/dist/src/platforms/android/notifications.d.ts +0 -11
  140. package/dist/src/platforms/android/open-target.d.ts +0 -4
  141. package/dist/src/platforms/android/screenshot.d.ts +0 -16
  142. package/dist/src/platforms/android/sdk.d.ts +0 -2
  143. package/dist/src/platforms/android/settings.d.ts +0 -3
  144. package/dist/src/platforms/android/snapshot.d.ts +0 -7
  145. package/dist/src/platforms/android/ui-hierarchy.d.ts +0 -21
  146. package/dist/src/platforms/appearance.d.ts +0 -2
  147. package/dist/src/platforms/boot-diagnostics.d.ts +0 -14
  148. package/dist/src/platforms/install-source.d.ts +0 -29
  149. package/dist/src/platforms/ios/app-filter.d.ts +0 -2
  150. package/dist/src/platforms/ios/apps.d.ts +0 -34
  151. package/dist/src/platforms/ios/config.d.ts +0 -10
  152. package/dist/src/platforms/ios/devicectl.d.ts +0 -13
  153. package/dist/src/platforms/ios/devices.d.ts +0 -40
  154. package/dist/src/platforms/ios/ensure-simulator.d.ts +0 -18
  155. package/dist/src/platforms/ios/index.d.ts +0 -3
  156. package/dist/src/platforms/ios/install-artifact.d.ts +0 -18
  157. package/dist/src/platforms/ios/launch-diagnostics.d.ts +0 -11
  158. package/dist/src/platforms/ios/macos-apps.d.ts +0 -12
  159. package/dist/src/platforms/ios/macos-helper.d.ts +0 -69
  160. package/dist/src/platforms/ios/plist.d.ts +0 -1
  161. package/dist/src/platforms/ios/runner-client.d.ts +0 -38
  162. package/dist/src/platforms/ios/runner-errors.d.ts +0 -20
  163. package/dist/src/platforms/ios/runner-macos-products.d.ts +0 -3
  164. package/dist/src/platforms/ios/runner-session.d.ts +0 -30
  165. package/dist/src/platforms/ios/runner-transport.d.ts +0 -10
  166. package/dist/src/platforms/ios/runner-xctestrun-products.d.ts +0 -2
  167. package/dist/src/platforms/ios/runner-xctestrun.d.ts +0 -38
  168. package/dist/src/platforms/ios/screenshot-status-bar.d.ts +0 -2
  169. package/dist/src/platforms/ios/screenshot.d.ts +0 -14
  170. package/dist/src/platforms/ios/simctl.d.ts +0 -7
  171. package/dist/src/platforms/ios/simulator.d.ts +0 -11
  172. package/dist/src/platforms/permission-utils.d.ts +0 -9
  173. package/dist/src/recording/overlay.d.ts +0 -10
  174. package/dist/src/upload-client.d.ts +0 -7
  175. package/dist/src/utils/args.d.ts +0 -27
  176. package/dist/src/utils/cli-config.d.ts +0 -10
  177. package/dist/src/utils/cli-option-schema.d.ts +0 -19
  178. package/dist/src/utils/cli-options.d.ts +0 -13
  179. package/dist/src/utils/command-schema.d.ts +0 -123
  180. package/dist/src/utils/device-isolation.d.ts +0 -3
  181. package/dist/src/utils/device.d.ts +0 -35
  182. package/dist/src/utils/diagnostics.d.ts +0 -30
  183. package/dist/src/utils/errors.d.ts +0 -26
  184. package/dist/src/utils/exec.d.ts +0 -32
  185. package/dist/src/utils/finders.d.ts +0 -12
  186. package/dist/src/utils/interactors.d.ts +0 -38
  187. package/dist/src/utils/json-input.d.ts +0 -1
  188. package/dist/src/utils/keyed-lock.d.ts +0 -1
  189. package/dist/src/utils/output.d.ts +0 -27
  190. package/dist/src/utils/path-resolution.d.ts +0 -8
  191. package/dist/src/utils/payload-input.d.ts +0 -12
  192. package/dist/src/utils/process-identity.d.ts +0 -11
  193. package/dist/src/utils/remote-config.d.ts +0 -15
  194. package/dist/src/utils/remote-open.d.ts +0 -9
  195. package/dist/src/utils/retry.d.ts +0 -54
  196. package/dist/src/utils/screenshot-diff.d.ts +0 -23
  197. package/dist/src/utils/session-binding.d.ts +0 -18
  198. package/dist/src/utils/snapshot-lines.d.ts +0 -15
  199. package/dist/src/utils/snapshot.d.ts +0 -49
  200. package/dist/src/utils/text-surface.d.ts +0 -19
  201. package/dist/src/utils/timeouts.d.ts +0 -3
  202. package/dist/src/utils/version.d.ts +0 -2
  203. package/dist/src/utils/video.d.ts +0 -9
@@ -422,38 +422,81 @@ extension RunnerTests {
422
422
  guard let text = command.text else {
423
423
  return Response(ok: false, error: ErrorPayload(message: "type requires text"))
424
424
  }
425
+ let delaySeconds = Double(max(command.delayMs ?? 0, 0)) / 1000.0
426
+ let target: XCUIElement?
427
+ if let x = command.x, let y = command.y {
428
+ target = textInputAt(app: activeApp, x: x, y: y) ?? focusedTextInput(app: activeApp)
429
+ } else {
430
+ target = focusedTextInput(app: activeApp)
431
+ }
432
+ func typeIntoTarget(_ value: String) {
433
+ if let focused = target {
434
+ focused.typeText(value)
435
+ } else {
436
+ activeApp.typeText(value)
437
+ }
438
+ }
425
439
  if command.clearFirst == true {
426
- guard let focused = focusedTextInput(app: activeApp) else {
427
- return Response(ok: false, error: ErrorPayload(message: "no focused text input to clear"))
440
+ guard let focused = target else {
441
+ let message =
442
+ (command.x != nil && command.y != nil)
443
+ ? "no text input found at the provided coordinates to clear"
444
+ : "no focused text input to clear"
445
+ return Response(ok: false, error: ErrorPayload(message: message))
428
446
  }
429
447
  clearTextInput(focused)
430
- focused.typeText(text)
431
- return Response(ok: true, data: DataPayload(message: "typed"))
432
448
  }
433
- if let focused = focusedTextInput(app: activeApp) {
434
- focused.typeText(text)
449
+ if delaySeconds > 0 && text.count > 1 {
450
+ let chunks = Array(text)
451
+ for (index, character) in chunks.enumerated() {
452
+ typeIntoTarget(String(character))
453
+ if index + 1 < chunks.count {
454
+ Thread.sleep(forTimeInterval: delaySeconds)
455
+ }
456
+ }
435
457
  } else {
436
- activeApp.typeText(text)
458
+ typeIntoTarget(text)
437
459
  }
438
460
  return Response(ok: true, data: DataPayload(message: "typed"))
461
+ case .interactionFrame:
462
+ let frame = resolvedTouchReferenceFrame(app: activeApp, appFrame: activeApp.frame)
463
+ return Response(
464
+ ok: true,
465
+ data: DataPayload(
466
+ x: frame.minX,
467
+ y: frame.minY,
468
+ referenceWidth: frame.width,
469
+ referenceHeight: frame.height
470
+ )
471
+ )
439
472
  case .swipe:
440
473
  guard let direction = command.direction else {
441
474
  return Response(ok: false, error: ErrorPayload(message: "swipe requires direction"))
442
475
  }
443
- let referenceFrame = resolvedGestureReferenceFrame(app: activeApp)
476
+ var executedFrame: DragVisualizationFrame?
444
477
  let timing = measureGesture {
445
478
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
446
- swipe(app: activeApp, direction: direction)
479
+ executedFrame = swipe(
480
+ app: activeApp,
481
+ direction: direction
482
+ )
447
483
  }
448
484
  }
485
+ guard let dragFrame = executedFrame else {
486
+ return Response(ok: false, error: ErrorPayload(message: "swipe is only supported on tvOS"))
487
+ }
449
488
  return Response(
450
489
  ok: true,
451
490
  data: DataPayload(
452
491
  message: "swiped",
453
492
  gestureStartUptimeMs: timing.gestureStartUptimeMs,
454
493
  gestureEndUptimeMs: timing.gestureEndUptimeMs,
455
- referenceWidth: referenceFrame.referenceWidth,
456
- referenceHeight: referenceFrame.referenceHeight
494
+ x: dragFrame.x,
495
+ y: dragFrame.y,
496
+ x2: dragFrame.x2,
497
+ y2: dragFrame.y2,
498
+ referenceWidth: dragFrame.referenceWidth,
499
+ referenceHeight: dragFrame.referenceHeight
457
500
  )
458
501
  )
459
502
  case .findText:
@@ -510,18 +553,43 @@ extension RunnerTests {
510
553
  // Return path relative to app container root (tmp/ maps to NSTemporaryDirectory)
511
554
  return Response(ok: true, data: DataPayload(message: "tmp/\(fileName)"))
512
555
  #endif
513
- case .back:
514
- if tapNavigationBack(app: activeApp) {
515
- return Response(ok: true, data: DataPayload(message: "back"))
556
+ case .back, .backInApp:
557
+ if tapInAppBackControl(app: activeApp) {
558
+ let message = command.command == .back ? "back" : "backInApp"
559
+ return Response(ok: true, data: DataPayload(message: message))
560
+ }
561
+ return Response(ok: false, error: ErrorPayload(message: "in-app back control is not available"))
562
+ case .backSystem:
563
+ if performSystemBackAction(app: activeApp) {
564
+ return Response(ok: true, data: DataPayload(message: "backSystem"))
516
565
  }
517
- performBackGesture(app: activeApp)
518
- return Response(ok: true, data: DataPayload(message: "back"))
566
+ return Response(ok: false, error: ErrorPayload(message: "system back is not available"))
519
567
  case .home:
520
568
  pressHomeButton()
521
569
  return Response(ok: true, data: DataPayload(message: "home"))
522
570
  case .appSwitcher:
523
571
  performAppSwitcherGesture(app: activeApp)
524
572
  return Response(ok: true, data: DataPayload(message: "appSwitcher"))
573
+ case .keyboardDismiss:
574
+ let result = dismissKeyboard(app: activeApp)
575
+ if result.wasVisible && !result.dismissed {
576
+ return Response(
577
+ ok: false,
578
+ error: ErrorPayload(
579
+ code: "UNSUPPORTED_OPERATION",
580
+ message: "Unable to dismiss the iOS keyboard without a native dismiss gesture or control"
581
+ )
582
+ )
583
+ }
584
+ return Response(
585
+ ok: true,
586
+ data: DataPayload(
587
+ message: "keyboardDismiss",
588
+ visible: result.visible,
589
+ wasVisible: result.wasVisible,
590
+ dismissed: result.dismissed
591
+ )
592
+ )
525
593
  case .alert:
526
594
  let action = (command.action ?? "get").lowercased()
527
595
  let alert = activeApp.alerts.firstMatch
@@ -17,14 +17,9 @@ extension RunnerTests {
17
17
  let referenceHeight: Double
18
18
  }
19
19
 
20
- struct GestureReferenceFrame {
21
- let referenceWidth: Double
22
- let referenceHeight: Double
23
- }
24
-
25
20
  // MARK: - Navigation Gestures
26
21
 
27
- func tapNavigationBack(app: XCUIApplication) -> Bool {
22
+ func tapInAppBackControl(app: XCUIApplication) -> Bool {
28
23
  #if os(macOS)
29
24
  if let back = macOSNavigationBackElement(app: app) {
30
25
  tapElementCenter(app: app, element: back)
@@ -37,7 +32,7 @@ extension RunnerTests {
37
32
  back.tap()
38
33
  return true
39
34
  }
40
- return pressTvRemoteMenuIfAvailable()
35
+ return false
41
36
  #endif
42
37
  }
43
38
 
@@ -51,6 +46,18 @@ extension RunnerTests {
51
46
  start.press(forDuration: 0.05, thenDragTo: end)
52
47
  }
53
48
 
49
+ func performSystemBackAction(app: XCUIApplication) -> Bool {
50
+ #if os(macOS)
51
+ return false
52
+ #else
53
+ if pressTvRemoteMenuIfAvailable() {
54
+ return true
55
+ }
56
+ performBackGesture(app: app)
57
+ return true
58
+ #endif
59
+ }
60
+
54
61
  func performAppSwitcherGesture(app: XCUIApplication) {
55
62
  if performTvRemoteAppSwitcherIfAvailable() {
56
63
  return
@@ -161,19 +168,114 @@ extension RunnerTests {
161
168
  element.typeText(deletes)
162
169
  }
163
170
 
164
- func focusedTextInput(app: XCUIApplication) -> XCUIElement? {
165
- let focused = app
166
- .descendants(matching: .any)
167
- .matching(NSPredicate(format: "hasKeyboardFocus == 1"))
168
- .firstMatch
169
- guard focused.exists else { return nil }
171
+ func textInputAt(app: XCUIApplication, x: Double, y: Double) -> XCUIElement? {
172
+ let point = CGPoint(x: x, y: y)
173
+ var matched: XCUIElement?
174
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
175
+ // Prefer the smallest matching field so nested editable controls win over large containers.
176
+ let candidates = app.descendants(matching: .any).allElementsBoundByIndex
177
+ .filter { element in
178
+ guard element.exists else { return false }
179
+ switch element.elementType {
180
+ case .textField, .secureTextField, .searchField, .textView:
181
+ let frame = element.frame
182
+ return !frame.isEmpty && frame.contains(point)
183
+ default:
184
+ return false
185
+ }
186
+ }
187
+ .sorted { left, right in
188
+ let leftArea = max(1, left.frame.width * left.frame.height)
189
+ let rightArea = max(1, right.frame.width * right.frame.height)
190
+ if leftArea != rightArea {
191
+ return leftArea < rightArea
192
+ }
193
+ if left.frame.minY != right.frame.minY {
194
+ return left.frame.minY < right.frame.minY
195
+ }
196
+ if left.frame.minX != right.frame.minX {
197
+ return left.frame.minX < right.frame.minX
198
+ }
199
+ return left.elementType.rawValue < right.elementType.rawValue
200
+ }
201
+ matched = candidates.first
202
+ })
203
+ if let exceptionMessage {
204
+ NSLog(
205
+ "AGENT_DEVICE_RUNNER_TEXT_INPUT_AT_POINT_IGNORED_EXCEPTION=%@",
206
+ exceptionMessage
207
+ )
208
+ return nil
209
+ }
210
+ return matched
211
+ }
170
212
 
171
- switch focused.elementType {
172
- case .textField, .secureTextField, .searchField, .textView:
173
- return focused
174
- default:
213
+ func focusedTextInput(app: XCUIApplication) -> XCUIElement? {
214
+ var focused: XCUIElement?
215
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
216
+ let candidate = app
217
+ .descendants(matching: .any)
218
+ .matching(NSPredicate(format: "hasKeyboardFocus == 1"))
219
+ .firstMatch
220
+ guard candidate.exists else { return }
221
+
222
+ switch candidate.elementType {
223
+ case .textField, .secureTextField, .searchField, .textView:
224
+ focused = candidate
225
+ default:
226
+ return
227
+ }
228
+ })
229
+ if let exceptionMessage {
230
+ NSLog(
231
+ "AGENT_DEVICE_RUNNER_FOCUSED_INPUT_QUERY_IGNORED_EXCEPTION=%@",
232
+ exceptionMessage
233
+ )
175
234
  return nil
176
235
  }
236
+ return focused
237
+ }
238
+
239
+ func isKeyboardVisible(app: XCUIApplication) -> Bool {
240
+ let keyboard = app.keyboards.firstMatch
241
+ return keyboard.exists && !keyboard.frame.isEmpty
242
+ }
243
+
244
+ func dismissKeyboard(app: XCUIApplication) -> (wasVisible: Bool, dismissed: Bool, visible: Bool) {
245
+ let wasVisible = isKeyboardVisible(app: app)
246
+ guard wasVisible else {
247
+ return (wasVisible: false, dismissed: false, visible: false)
248
+ }
249
+
250
+ let keyboard = app.keyboards.firstMatch
251
+ keyboard.swipeDown()
252
+ sleepFor(0.2)
253
+ if !isKeyboardVisible(app: app) {
254
+ return (wasVisible: true, dismissed: true, visible: false)
255
+ }
256
+
257
+ if tapKeyboardDismissControl(app: app) {
258
+ sleepFor(0.2)
259
+ let visible = isKeyboardVisible(app: app)
260
+ return (wasVisible: true, dismissed: !visible, visible: visible)
261
+ }
262
+
263
+ return (wasVisible: true, dismissed: false, visible: isKeyboardVisible(app: app))
264
+ }
265
+
266
+ private func tapKeyboardDismissControl(app: XCUIApplication) -> Bool {
267
+ for label in ["Hide keyboard", "Dismiss keyboard"] {
268
+ let candidates = [
269
+ app.keyboards.buttons[label],
270
+ app.keyboards.keys[label],
271
+ app.toolbars.buttons[label],
272
+ ]
273
+ if let hittable = candidates.first(where: { $0.exists && $0.isHittable }) {
274
+ hittable.tap()
275
+ return true
276
+ }
277
+ }
278
+ return false
177
279
  }
178
280
 
179
281
  private func moveCaretToEnd(element: XCUIElement) {
@@ -322,7 +424,7 @@ extension RunnerTests {
322
424
  )
323
425
  }
324
426
 
325
- private func resolvedTouchReferenceFrame(app: XCUIApplication, appFrame: CGRect) -> CGRect {
427
+ func resolvedTouchReferenceFrame(app: XCUIApplication, appFrame: CGRect) -> CGRect {
326
428
  let window = app.windows.firstMatch
327
429
  let windowFrame = window.frame
328
430
  if window.exists && !windowFrame.isEmpty {
@@ -334,14 +436,6 @@ extension RunnerTests {
334
436
  return CGRect(x: 0, y: 0, width: 0, height: 0)
335
437
  }
336
438
 
337
- func resolvedGestureReferenceFrame(app: XCUIApplication) -> GestureReferenceFrame {
338
- let frame = resolvedTouchReferenceFrame(app: app, appFrame: app.frame)
339
- return GestureReferenceFrame(
340
- referenceWidth: frame.width,
341
- referenceHeight: frame.height
342
- )
343
- }
344
-
345
439
  func runSeries(count: Int, pauseMs: Double, operation: (Int) -> Void) {
346
440
  let total = max(count, 1)
347
441
  let pause = max(pauseMs, 0)
@@ -353,39 +447,36 @@ extension RunnerTests {
353
447
  }
354
448
  }
355
449
 
356
- func swipe(app: XCUIApplication, direction: SwipeDirection) {
450
+ func swipe(app: XCUIApplication, direction: String) -> DragVisualizationFrame? {
357
451
  if performTvRemoteSwipeIfAvailable(direction: direction) {
358
- return
359
- }
360
- let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
361
- let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2))
362
- let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8))
363
- let left = target.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
364
- let right = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
365
-
366
- switch direction {
367
- case .up:
368
- end.press(forDuration: 0.1, thenDragTo: start)
369
- case .down:
370
- start.press(forDuration: 0.1, thenDragTo: end)
371
- case .left:
372
- right.press(forDuration: 0.1, thenDragTo: left)
373
- case .right:
374
- left.press(forDuration: 0.1, thenDragTo: right)
452
+ let frame = resolvedTouchReferenceFrame(app: app, appFrame: app.frame)
453
+ let midX = frame.midX
454
+ let midY = frame.midY
455
+ return DragVisualizationFrame(
456
+ x: midX,
457
+ y: midY,
458
+ x2: midX,
459
+ y2: midY,
460
+ referenceWidth: frame.width,
461
+ referenceHeight: frame.height
462
+ )
375
463
  }
464
+ return nil
376
465
  }
377
466
 
378
- private func performTvRemoteSwipeIfAvailable(direction: SwipeDirection) -> Bool {
467
+ private func performTvRemoteSwipeIfAvailable(direction: String) -> Bool {
379
468
  #if os(tvOS)
380
469
  switch direction {
381
- case .up:
470
+ case "up":
382
471
  XCUIRemote.shared.press(.up)
383
- case .down:
472
+ case "down":
384
473
  XCUIRemote.shared.press(.down)
385
- case .left:
474
+ case "left":
386
475
  XCUIRemote.shared.press(.left)
387
- case .right:
476
+ case "right":
388
477
  XCUIRemote.shared.press(.right)
478
+ default:
479
+ return false
389
480
  }
390
481
  return true
391
482
  #else
@@ -461,5 +552,4 @@ extension RunnerTests {
461
552
  let element = app.descendants(matching: .any).matching(predicate).firstMatch
462
553
  return element.exists ? element : nil
463
554
  }
464
-
465
555
  }
@@ -152,7 +152,7 @@ extension RunnerTests {
152
152
 
153
153
  func isReadOnlyCommand(_ command: Command) -> Bool {
154
154
  switch command.command {
155
- case .findText, .readText, .snapshot, .screenshot:
155
+ case .interactionFrame, .findText, .readText, .snapshot, .screenshot:
156
156
  return true
157
157
  case .alert:
158
158
  let action = (command.action ?? "get").lowercased()
@@ -170,7 +170,18 @@ extension RunnerTests {
170
170
 
171
171
  func isInteractionCommand(_ command: CommandType) -> Bool {
172
172
  switch command {
173
- case .tap, .longPress, .drag, .type, .swipe, .back, .appSwitcher, .pinch:
173
+ case
174
+ .tap,
175
+ .longPress,
176
+ .drag,
177
+ .type,
178
+ .swipe,
179
+ .back,
180
+ .backInApp,
181
+ .backSystem,
182
+ .appSwitcher,
183
+ .keyboardDismiss,
184
+ .pinch:
174
185
  return true
175
186
  default:
176
187
  return false
@@ -5,6 +5,7 @@ enum CommandType: String, Codable {
5
5
  case mouseClick
6
6
  case tapSeries
7
7
  case longPress
8
+ case interactionFrame
8
9
  case drag
9
10
  case dragSeries
10
11
  case type
@@ -14,8 +15,11 @@ enum CommandType: String, Codable {
14
15
  case snapshot
15
16
  case screenshot
16
17
  case back
18
+ case backInApp
19
+ case backSystem
17
20
  case home
18
21
  case appSwitcher
22
+ case keyboardDismiss
19
23
  case alert
20
24
  case pinch
21
25
  case recordStart
@@ -24,17 +28,11 @@ enum CommandType: String, Codable {
24
28
  case shutdown
25
29
  }
26
30
 
27
- enum SwipeDirection: String, Codable {
28
- case up
29
- case down
30
- case left
31
- case right
32
- }
33
-
34
31
  struct Command: Codable {
35
32
  let command: CommandType
36
33
  let appBundleId: String?
37
34
  let text: String?
35
+ let delayMs: Int?
38
36
  let clearFirst: Bool?
39
37
  let action: String?
40
38
  let x: Double?
@@ -48,7 +46,7 @@ struct Command: Codable {
48
46
  let x2: Double?
49
47
  let y2: Double?
50
48
  let durationMs: Double?
51
- let direction: SwipeDirection?
49
+ let direction: String?
52
50
  let scale: Double?
53
51
  let outPath: String?
54
52
  let fps: Int?
@@ -87,6 +85,9 @@ struct DataPayload: Codable {
87
85
  let referenceWidth: Double?
88
86
  let referenceHeight: Double?
89
87
  let currentUptimeMs: Double?
88
+ let visible: Bool?
89
+ let wasVisible: Bool?
90
+ let dismissed: Bool?
90
91
 
91
92
  init(
92
93
  message: String? = nil,
@@ -103,7 +104,10 @@ struct DataPayload: Codable {
103
104
  y2: Double? = nil,
104
105
  referenceWidth: Double? = nil,
105
106
  referenceHeight: Double? = nil,
106
- currentUptimeMs: Double? = nil
107
+ currentUptimeMs: Double? = nil,
108
+ visible: Bool? = nil,
109
+ wasVisible: Bool? = nil,
110
+ dismissed: Bool? = nil
107
111
  ) {
108
112
  self.message = message
109
113
  self.text = text
@@ -120,11 +124,20 @@ struct DataPayload: Codable {
120
124
  self.referenceWidth = referenceWidth
121
125
  self.referenceHeight = referenceHeight
122
126
  self.currentUptimeMs = currentUptimeMs
127
+ self.visible = visible
128
+ self.wasVisible = wasVisible
129
+ self.dismissed = dismissed
123
130
  }
124
131
  }
125
132
 
126
133
  struct ErrorPayload: Codable {
134
+ let code: String?
127
135
  let message: String
136
+
137
+ init(code: String? = nil, message: String) {
138
+ self.code = code
139
+ self.message = message
140
+ }
128
141
  }
129
142
 
130
143
  struct SnapshotRect: Codable {