agent-device 0.18.3 → 0.19.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 (215) hide show
  1. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.apk → agent-device-android-multitouch-helper-0.19.0.apk} +0 -0
  2. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.0.apk.sha256 +1 -0
  3. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.manifest.json → agent-device-android-multitouch-helper-0.19.0.manifest.json} +4 -4
  4. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.apk → agent-device-android-snapshot-helper-0.19.0.apk} +0 -0
  5. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.0.apk.sha256 +1 -0
  6. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.manifest.json → agent-device-android-snapshot-helper-0.19.0.manifest.json} +6 -6
  7. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +28 -0
  8. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +9 -120
  9. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +132 -66
  10. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +155 -0
  11. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +67 -0
  12. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +20 -74
  13. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
  14. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +0 -177
  15. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +81 -48
  16. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +102 -129
  17. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +208 -46
  18. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +82 -11
  19. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +72 -0
  20. package/dist/src/agent-browser-tool.js +1 -0
  21. package/dist/src/android-adb.d.ts +64 -234
  22. package/dist/src/android-adb.js +1 -1
  23. package/dist/src/android.js +1 -5
  24. package/dist/src/app-lifecycle.js +7 -0
  25. package/dist/src/apple-runner-options.js +1 -0
  26. package/dist/src/apps.js +5 -11
  27. package/dist/src/artifacts.d.ts +4 -3
  28. package/dist/src/artifacts.js +1 -1
  29. package/dist/src/audio-probe.js +1 -1
  30. package/dist/src/batch.d.ts +2 -177
  31. package/dist/src/batch.js +1 -1
  32. package/dist/src/cli-help.js +58 -37
  33. package/dist/src/cli-output.js +1 -1
  34. package/dist/src/cli-runner.js +2 -0
  35. package/dist/src/cli.js +12 -41
  36. package/dist/src/client-metro-companion.js +1 -0
  37. package/dist/src/client.js +3 -0
  38. package/dist/src/command-metadata.js +1 -0
  39. package/dist/src/command-schema.js +22 -0
  40. package/dist/src/command-surface.js +1 -1
  41. package/dist/src/contracts.d.ts +2 -213
  42. package/dist/src/contracts.js +1 -1
  43. package/dist/src/debug-symbols.js +5 -3
  44. package/dist/src/device-input-state.js +2 -0
  45. package/dist/src/device-rotation.js +14 -0
  46. package/dist/src/devices.js +1 -1
  47. package/dist/src/devices2.js +1 -0
  48. package/dist/src/devices3.js +1 -0
  49. package/dist/src/find.js +1 -1
  50. package/dist/src/finders.d.ts +2 -81
  51. package/dist/src/finders.js +1 -1
  52. package/dist/src/generic.js +1 -1
  53. package/dist/src/handler-utils.js +1 -1
  54. package/dist/src/helper.js +2 -0
  55. package/dist/src/host-process.js +2 -0
  56. package/dist/src/index.d.ts +1265 -1786
  57. package/dist/src/index.js +1 -1
  58. package/dist/src/input-actions.js +3 -1
  59. package/dist/src/input-actions2.js +1 -0
  60. package/dist/src/install-source.d.ts +13 -14
  61. package/dist/src/install-source.js +1 -1
  62. package/dist/src/install-source2.js +1 -0
  63. package/dist/src/interaction-snapshot.js +1 -0
  64. package/dist/src/interaction.js +1 -1
  65. package/dist/src/interactor.js +2 -1
  66. package/dist/src/internal/bin.d.ts +1 -1
  67. package/dist/src/internal/bin.js +2 -5
  68. package/dist/src/internal/companion-tunnel.d.ts +1 -1
  69. package/dist/src/internal/companion-tunnel.js +1 -1
  70. package/dist/src/internal/daemon.d.ts +1 -1
  71. package/dist/src/internal/daemon.js +16 -1
  72. package/dist/src/internal/png-worker.d.ts +29 -26
  73. package/dist/src/internal/png-worker.js +1 -1
  74. package/dist/src/internal/update-check-entry.d.ts +1 -1
  75. package/dist/src/internal/update-check-entry.js +1 -1
  76. package/dist/src/io.d.ts +2 -85
  77. package/dist/src/io.js +1 -1
  78. package/dist/src/lease.js +1 -1
  79. package/dist/src/linux.js +1 -1
  80. package/dist/src/logcat.js +6 -0
  81. package/dist/src/manifest.js +1 -0
  82. package/dist/src/metro.d.ts +70 -113
  83. package/dist/src/metro.js +1 -1
  84. package/dist/src/metro2.js +1 -0
  85. package/dist/src/notifications.js +1 -1
  86. package/dist/src/output-path.js +1 -0
  87. package/dist/src/path-resolution.js +1 -0
  88. package/dist/src/png-worker-contract.js +1 -0
  89. package/dist/src/points.js +4 -0
  90. package/dist/src/project-runtime.js +1 -0
  91. package/dist/src/provider.js +1 -1
  92. package/dist/src/providers.js +1 -0
  93. package/dist/src/react-native.js +1 -1
  94. package/dist/src/record-trace-recording.js +2 -5
  95. package/dist/src/record-trace.js +1 -1
  96. package/dist/src/recording-provider.js +1 -1
  97. package/dist/src/registry.js +37 -0
  98. package/dist/src/remote-config.d.ts +2 -83
  99. package/dist/src/reporting.js +9 -0
  100. package/dist/src/rolldown-runtime.js +1 -0
  101. package/dist/src/runner-client.js +1 -0
  102. package/dist/src/runner-provider.js +1 -0
  103. package/dist/src/runner-sequence.js +1 -1
  104. package/dist/src/runtime-transport.js +1 -0
  105. package/dist/src/runtime.js +3 -0
  106. package/dist/src/scoped-provider.js +1 -0
  107. package/dist/src/sdk-android-adb.d.ts +144 -0
  108. package/dist/src/sdk-android-adb.js +4 -0
  109. package/dist/src/sdk-app-inventory.d.ts +4 -0
  110. package/dist/src/sdk-app-inventory.js +1 -0
  111. package/dist/src/sdk-batch-runner.d.ts +40 -0
  112. package/dist/src/sdk-batch-runner.js +2 -0
  113. package/dist/src/sdk-batch.js +2 -0
  114. package/dist/src/sdk-contracts.d.ts +386 -0
  115. package/dist/src/sdk-contracts.js +2 -0
  116. package/dist/src/sdk-finders.d.ts +61 -0
  117. package/dist/src/sdk-finders.js +1 -0
  118. package/dist/src/sdk-io.d.ts +79 -0
  119. package/dist/src/sdk-io.js +1 -0
  120. package/dist/src/sdk-remote-config.d.ts +140 -0
  121. package/dist/src/sdk-remote-config.js +1 -0
  122. package/dist/src/sdk-selectors.js +1 -0
  123. package/dist/src/selector-runtime.js +1 -1
  124. package/dist/src/selectors.d.ts +39 -137
  125. package/dist/src/selectors.js +1 -1
  126. package/dist/src/selectors2.js +1 -0
  127. package/dist/src/server.js +3 -2
  128. package/dist/src/session.js +10 -11
  129. package/dist/src/setting-state.js +1 -0
  130. package/dist/src/simctl.js +1 -0
  131. package/dist/src/simulator.js +1 -2
  132. package/dist/src/snapshot-diagnostics.js +3 -0
  133. package/dist/src/snapshot-helper.js +5 -0
  134. package/dist/src/snapshot-quality.js +1 -0
  135. package/dist/src/snapshot.js +1 -2
  136. package/dist/src/snapshot2.js +2 -0
  137. package/dist/src/{5560.js → timeouts.js} +1 -1
  138. package/dist/src/tool-provider.js +3 -0
  139. package/dist/src/tool-provider2.js +1 -0
  140. package/dist/src/ttl-memo.js +1 -0
  141. package/dist/src/update-check.js +1 -0
  142. package/dist/src/url.js +1 -0
  143. package/dist/src/version.js +1 -0
  144. package/dist/src/web.js +1 -1
  145. package/dist/src/xml.js +2 -0
  146. package/package.json +12 -19
  147. package/server.json +2 -2
  148. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.18.3.apk.sha256 +0 -1
  149. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.18.3.apk.sha256 +0 -1
  150. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +0 -443
  151. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -169
  152. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +0 -220
  153. package/dist/src/1404.js +0 -1
  154. package/dist/src/1986.js +0 -1
  155. package/dist/src/2748.js +0 -1
  156. package/dist/src/2948.js +0 -30
  157. package/dist/src/3006.js +0 -1
  158. package/dist/src/314.js +0 -1
  159. package/dist/src/3340.js +0 -8
  160. package/dist/src/4418.js +0 -1
  161. package/dist/src/4428.js +0 -3
  162. package/dist/src/485.js +0 -1
  163. package/dist/src/4917.js +0 -1
  164. package/dist/src/495.js +0 -5
  165. package/dist/src/5448.js +0 -3
  166. package/dist/src/5484.js +0 -1
  167. package/dist/src/5643.js +0 -1
  168. package/dist/src/587.js +0 -1
  169. package/dist/src/5913.js +0 -4
  170. package/dist/src/6849.js +0 -1
  171. package/dist/src/6918.js +0 -1
  172. package/dist/src/6967.js +0 -1
  173. package/dist/src/760.js +0 -1
  174. package/dist/src/7871.js +0 -1
  175. package/dist/src/8184.js +0 -1
  176. package/dist/src/8225.js +0 -1
  177. package/dist/src/8391.js +0 -1
  178. package/dist/src/8404.js +0 -2
  179. package/dist/src/8414.js +0 -2
  180. package/dist/src/8875.js +0 -4
  181. package/dist/src/893.js +0 -1
  182. package/dist/src/8936.js +0 -1
  183. package/dist/src/9414.js +0 -1
  184. package/dist/src/9581.js +0 -22
  185. package/dist/src/9612.js +0 -1
  186. package/dist/src/9624.js +0 -1
  187. package/dist/src/9722.js +0 -39
  188. package/dist/src/devices~1.js +0 -1
  189. package/dist/src/devices~2.js +0 -1
  190. package/dist/src/input-actions~1.js +0 -1
  191. package/dist/src/rslib-runtime.js +0 -1
  192. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
  193. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
  194. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  195. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
  196. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
  197. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
  198. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +0 -0
  199. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +0 -0
  200. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
  201. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
  202. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -0
  203. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -0
  204. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +0 -0
  205. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
  206. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
  207. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +0 -0
  208. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +0 -0
  209. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
  210. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +0 -0
  211. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
  212. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
  213. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
  214. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
  215. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
@@ -0,0 +1,67 @@
1
+ import XCTest
2
+
3
+ struct FlatSnapshotFilterNode {
4
+ let isRoot: Bool
5
+ let label: String
6
+ let identifier: String
7
+ let valueText: String?
8
+ let visible: Bool
9
+
10
+ func matchesScope(_ scope: String) -> Bool {
11
+ let haystack = [label, identifier, valueText ?? ""].joined(separator: "\n")
12
+ return haystack.localizedCaseInsensitiveContains(scope)
13
+ }
14
+ }
15
+
16
+ struct FlatSnapshotFilterDecision {
17
+ let include: Bool
18
+ let insideMatchedScope: Bool
19
+ }
20
+
21
+ extension RunnerTests {
22
+ func flatSnapshotFilterDecision(
23
+ _ node: FlatSnapshotFilterNode,
24
+ options: SnapshotOptions,
25
+ insideMatchedScope: Bool
26
+ ) -> FlatSnapshotFilterDecision {
27
+ let scope = options.scope?.trimmingCharacters(in: .whitespacesAndNewlines)
28
+ let scopeActive = scope?.isEmpty == false
29
+ let matchesScope: Bool
30
+ if scopeActive, let scope {
31
+ matchesScope = node.matchesScope(scope)
32
+ } else {
33
+ matchesScope = false
34
+ }
35
+ let nowInsideScope = insideMatchedScope || matchesScope
36
+
37
+ let include: Bool
38
+ if node.isRoot {
39
+ include = true
40
+ } else if scopeActive && !nowInsideScope {
41
+ include = false
42
+ } else if options.interactiveOnly && !node.visible {
43
+ include = false
44
+ } else {
45
+ include = true
46
+ }
47
+
48
+ return FlatSnapshotFilterDecision(include: include, insideMatchedScope: nowInsideScope)
49
+ }
50
+
51
+ func privateAXInteractiveCandidate(rawElementType: Int) -> Bool {
52
+ guard let type = flatSnapshotElementType(rawElementType: rawElementType) else {
53
+ return false
54
+ }
55
+ return interactiveTypes.contains(type) || Self.scrollContainerTypes.contains(type)
56
+ }
57
+
58
+ func flatSnapshotElementType(rawElementType: Int) -> XCUIElement.ElementType? {
59
+ guard let raw = UInt(exactly: rawElementType),
60
+ let type = XCUIElement.ElementType(rawValue: raw)
61
+ else {
62
+ return nil
63
+ }
64
+ return type
65
+ }
66
+
67
+ }
@@ -200,16 +200,32 @@ extension RunnerTests {
200
200
  )
201
201
  }
202
202
 
203
+ // Maestro-compat gate for the non-hittable coordinate fallback: an element
204
+ // with no frame at all cannot be coordinate-tapped, otherwise the decision
205
+ // is the shared TapPointPolicy center-in-frame rule (golden parity table
206
+ // with the TS twin). app.frame is the frame source here — replay taps
207
+ // resolved bounds Maestro-style, so the union frame is intentional.
203
208
  private func hasTappableFrame(app: XCUIApplication, element: XCUIElement) -> Bool {
204
209
  let frame = element.frame
205
210
  if frame.isEmpty {
206
211
  return false
207
212
  }
208
- let appFrame = app.frame
209
- if appFrame.isEmpty {
210
- return true
213
+ return TapPointPolicy.isAllowed(elementFrame: frame, windowFrame: app.frame)
214
+ }
215
+
216
+ // The tappable on-screen viewport. app.frame is unsuitable: it unions
217
+ // transformed subtrees, so a closed drawer at negative x inflates it and
218
+ // out-of-window coordinates still pass containment. Falls back to app.frame
219
+ // when no window frame is readable.
220
+ func onScreenWindowFrame(app: XCUIApplication) -> CGRect {
221
+ let window = app.windows.element(boundBy: 0)
222
+ if window.exists {
223
+ let frame = window.frame
224
+ if !frame.isEmpty {
225
+ return frame
226
+ }
211
227
  }
212
- return appFrame.contains(CGPoint(x: frame.midX, y: frame.midY))
228
+ return app.frame
213
229
  }
214
230
 
215
231
  func queryElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> Response {
@@ -1232,74 +1248,4 @@ extension RunnerTests {
1232
1248
  return element.exists ? element : nil
1233
1249
  }
1234
1250
 
1235
- #if AGENT_DEVICE_RUNNER_UNIT_TESTS
1236
- // Identity in portrait/unknown, 90° per landscape, 180° upside-down.
1237
- func testNativeSynthesizedPointRotatesByInterfaceOrientation() {
1238
- let portrait = CGRect(x: 0, y: 0, width: 834, height: 1210)
1239
- let landscape = CGRect(x: 0, y: 0, width: 1210, height: 834)
1240
- let offsetLandscape = CGRect(x: 10, y: 20, width: 1210, height: 834)
1241
- // (frame, UIInterfaceOrientation, expected native point) for a tap at (170, 268).
1242
- let cases: [(CGRect, Int, CGPoint)] = [
1243
- (portrait, RunnerInterfaceOrientation.portrait, CGPoint(x: 170, y: 268)),
1244
- (landscape, RunnerInterfaceOrientation.landscapeRight, CGPoint(x: 566, y: 170)),
1245
- (landscape, RunnerInterfaceOrientation.landscapeLeft, CGPoint(x: 268, y: 1040)),
1246
- (portrait, RunnerInterfaceOrientation.portraitUpsideDown, CGPoint(x: 664, y: 942)),
1247
- (portrait, RunnerInterfaceOrientation.unknown, CGPoint(x: 170, y: 268)),
1248
- ]
1249
- for (frame, orientation, expected) in cases {
1250
- XCTAssertEqual(
1251
- nativeSynthesizedPoint(orientedX: 170, orientedY: 268, in: frame, interfaceOrientation: orientation),
1252
- expected,
1253
- "interfaceOrientation \(orientation)"
1254
- )
1255
- }
1256
- XCTAssertEqual(
1257
- nativeSynthesizedPoint(
1258
- orientedX: 180,
1259
- orientedY: 288,
1260
- in: offsetLandscape,
1261
- interfaceOrientation: RunnerInterfaceOrientation.landscapeLeft
1262
- ),
1263
- CGPoint(x: 268, y: 1040),
1264
- "non-zero frame origin is localized before rotation"
1265
- )
1266
- }
1267
-
1268
- func testNativeSynthesizedVectorRotatesByInterfaceOrientation() {
1269
- let cases: [(Int, CGVector)] = [
1270
- (RunnerInterfaceOrientation.portrait, CGVector(dx: 40, dy: -20)),
1271
- (RunnerInterfaceOrientation.landscapeRight, CGVector(dx: 20, dy: 40)),
1272
- (RunnerInterfaceOrientation.landscapeLeft, CGVector(dx: -20, dy: -40)),
1273
- (RunnerInterfaceOrientation.portraitUpsideDown, CGVector(dx: -40, dy: 20)),
1274
- (RunnerInterfaceOrientation.unknown, CGVector(dx: 40, dy: -20)),
1275
- ]
1276
- for (orientation, expected) in cases {
1277
- let vector = nativeSynthesizedVector(orientedDx: 40, orientedDy: -20, interfaceOrientation: orientation)
1278
- XCTAssertEqual(vector.dx, expected.dx, "dx interfaceOrientation \(orientation)")
1279
- XCTAssertEqual(vector.dy, expected.dy, "dy interfaceOrientation \(orientation)")
1280
- }
1281
- }
1282
-
1283
- func testDesktopScrollWheelDeltasMapDirections() throws {
1284
- XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "up", pixels: 120)).vertical, 120)
1285
- XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "down", pixels: 120)).vertical, -120)
1286
- XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "left", pixels: 120)).horizontal, 120)
1287
- XCTAssertEqual(try XCTUnwrap(desktopScrollWheelDeltas(direction: "right", pixels: 120)).horizontal, -120)
1288
- XCTAssertNil(desktopScrollWheelDeltas(direction: "diagonal", pixels: 120))
1289
- }
1290
-
1291
- func testDesktopScrollWheelDeltaEventsHonorDurationAndPreservePixels() throws {
1292
- let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 50))
1293
- XCTAssertEqual(events.count, 4)
1294
- XCTAssertEqual(events.map(\.vertical).reduce(0, +), -200)
1295
- XCTAssertEqual(events.map(\.horizontal).reduce(0, +), 0)
1296
- XCTAssertEqual(desktopScrollEventIntervalSeconds(durationMs: 50, eventCount: events.count), 0.05 / 3.0)
1297
- }
1298
-
1299
- func testDesktopScrollWheelDeltaEventsKeepInstantScrollSingleEvent() throws {
1300
- let events = try XCTUnwrap(desktopScrollWheelDeltaEvents(direction: "down", pixels: 200, durationMs: 0))
1301
- XCTAssertEqual(events.count, 1)
1302
- XCTAssertEqual(events.first?.vertical, -200)
1303
- }
1304
- #endif
1305
1251
  }
@@ -0,0 +1,64 @@
1
+ import XCTest
2
+
3
+ // Swift port of buildScrollGesturePlan from src/core/scroll-gesture.ts.
4
+ //
5
+ // This is a deliberate two-place invariant: the daemon keeps the TS implementation (for Android,
6
+ // recording, and reported-pixels), and the runner places the gesture with this Swift copy. The
7
+ // parity test vectors at the bottom of this file mirror src/core/__tests__/scroll-gesture.test.ts —
8
+ // if you change the math in either language, update the other and both vector sets.
9
+ //
10
+ // All inputs here are positive (reference dims, travel, center), so Swift's `.rounded()`
11
+ // (half away from zero) matches JS `Math.round` (half up) on every value computed below.
12
+
13
+ struct RunnerScrollGesturePlan {
14
+ let x1: Double
15
+ let y1: Double
16
+ let x2: Double
17
+ let y2: Double
18
+ let travelPixels: Double
19
+ }
20
+
21
+ private let runnerDefaultScrollAmount = 0.6
22
+ private let runnerDefaultEdgePaddingFraction = 0.05
23
+
24
+ func runnerScrollGesturePlan(
25
+ direction: String,
26
+ amount: Double?,
27
+ pixels: Double?,
28
+ referenceWidth: Double,
29
+ referenceHeight: Double
30
+ ) -> RunnerScrollGesturePlan? {
31
+ // Mirror the TS INVALID_ARGS contract: non-positive or non-finite amount/pixels are rejected
32
+ // rather than clamped into a journaled 1px scroll. The daemon validates before sending, so
33
+ // this only triggers for non-daemon wire clients.
34
+ if let amount, !(amount.isFinite && amount > 0) { return nil }
35
+ if let pixels, !(pixels.isFinite && pixels > 0) { return nil }
36
+ let axisLength = (direction == "up" || direction == "down") ? referenceHeight : referenceWidth
37
+ let requestedAmount = amount ?? runnerDefaultScrollAmount
38
+ let requestedPixels: Double =
39
+ pixels.map { max(1, $0.rounded()) } ?? (axisLength * requestedAmount).rounded()
40
+ let edgePadding = max(1, (axisLength * runnerDefaultEdgePaddingFraction).rounded())
41
+ let maxTravelPixels = max(1, axisLength - edgePadding * 2)
42
+ let travelPixels = max(1, min(requestedPixels, maxTravelPixels))
43
+ let halfTravel = (travelPixels / 2).rounded()
44
+ let centerX = (referenceWidth / 2).rounded()
45
+ let centerY = (referenceHeight / 2).rounded()
46
+
47
+ func plan(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double) -> RunnerScrollGesturePlan {
48
+ RunnerScrollGesturePlan(x1: x1, y1: y1, x2: x2, y2: y2, travelPixels: travelPixels)
49
+ }
50
+
51
+ switch direction {
52
+ case "up":
53
+ return plan(centerX, centerY - halfTravel, centerX, centerY + halfTravel)
54
+ case "down":
55
+ return plan(centerX, centerY + halfTravel, centerX, centerY - halfTravel)
56
+ case "left":
57
+ return plan(centerX - halfTravel, centerY, centerX + halfTravel, centerY)
58
+ case "right":
59
+ return plan(centerX + halfTravel, centerY, centerX - halfTravel, centerY)
60
+ default:
61
+ return nil
62
+ }
63
+ }
64
+
@@ -273,180 +273,3 @@ extension RunnerTests {
273
273
  }
274
274
  }
275
275
 
276
- #if AGENT_DEVICE_RUNNER_UNIT_TESTS
277
- // MARK: - In-bundle unit tests (device-free)
278
-
279
- extension RunnerTests {
280
- func testSequenceDecodesStepsFromWire() throws {
281
- let json = """
282
- {"command":"sequence","commandId":"seq-1","steps":[
283
- {"kind":"tap","x":100,"y":200},
284
- {"kind":"doubleTap","x":101,"y":200},
285
- {"kind":"longPress","x":102,"y":200,"durationMs":300},
286
- {"kind":"drag","x":10,"y":600,"x2":10,"y2":200,"durationMs":250,"pauseMs":50}
287
- ]}
288
- """
289
- let command = try JSONDecoder().decode(Command.self, from: Data(json.utf8))
290
- XCTAssertEqual(command.command, .sequence)
291
- XCTAssertEqual(command.steps?.count, 4)
292
- XCTAssertEqual(command.steps?[0].kind, "tap")
293
- XCTAssertEqual(command.steps?[1].kind, "doubleTap")
294
- XCTAssertEqual(command.steps?[3].x2, 10)
295
- XCTAssertEqual(command.steps?[3].pauseMs, 50)
296
- }
297
-
298
- func testSequenceAcceptsDoubleTapKind() {
299
- // A doubleTap step missing coords must fail on the coords check, not the kind allowlist —
300
- // proving "doubleTap" passes validateSequenceStep without needing a device to execute on.
301
- let response = executeSequenceForTest(steps: [
302
- sequenceStep(kind: "doubleTap", x: nil)
303
- ])
304
- XCTAssertEqual(response.ok, false)
305
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
306
- XCTAssertTrue(response.error?.message.contains("requires finite x and y") ?? false)
307
- XCTAssertFalse(response.error?.message.contains("unsupported kind") ?? true)
308
- }
309
-
310
- func testSequenceRejectsUnknownKind() throws {
311
- let response = executeSequenceForTest(steps: [
312
- sequenceStep(kind: "tap", x: 1, y: 2),
313
- sequenceStep(kind: "pinch", x: 3, y: 4),
314
- ])
315
- XCTAssertEqual(response.ok, false)
316
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
317
- XCTAssertTrue(response.error?.message.contains("step 1") ?? false)
318
- XCTAssertTrue(response.error?.message.contains("pinch") ?? false)
319
- }
320
-
321
- func testSequenceRejectsEmpty() {
322
- let response = executeSequenceForTest(steps: [])
323
- XCTAssertEqual(response.ok, false)
324
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
325
- }
326
-
327
- func testSequenceRejectsTooManySteps() {
328
- let steps = (0..<21).map { _ in sequenceStep(kind: "tap", x: 1, y: 2) }
329
- let response = executeSequenceForTest(steps: steps)
330
- XCTAssertEqual(response.ok, false)
331
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
332
- XCTAssertTrue(response.error?.message.contains("at most 20") ?? false)
333
- }
334
-
335
- func testSequenceRejectsDragMissingSecondPoint() {
336
- let response = executeSequenceForTest(steps: [
337
- sequenceStep(kind: "tap", x: 1, y: 2),
338
- sequenceStep(kind: "drag", x: 3, y: 4),
339
- ])
340
- XCTAssertEqual(response.ok, false)
341
- XCTAssertEqual(response.error?.code, "INVALID_ARGS")
342
- XCTAssertTrue(response.error?.message.contains("step 1") ?? false)
343
- }
344
-
345
- func testAssembleSequencePreservesOrderOnSuccess() {
346
- let steps = [
347
- sequenceStep(kind: "tap", x: 1, y: 1),
348
- sequenceStep(kind: "longPress", x: 2, y: 2),
349
- sequenceStep(kind: "tap", x: 3, y: 3),
350
- ]
351
- var calls: [Int] = []
352
- let execution = assembleSequenceExecution(steps: steps) { index, _ in
353
- calls.append(index)
354
- return SequenceStepOutcome(
355
- outcome: .performed,
356
- gestureStartUptimeMs: Double(index * 10),
357
- gestureEndUptimeMs: Double(index * 10 + 5)
358
- )
359
- }
360
- XCTAssertEqual(calls, [0, 1, 2])
361
- XCTAssertEqual(execution.completedSteps, 3)
362
- XCTAssertNil(execution.failedStepIndex)
363
- XCTAssertEqual(execution.results.map { $0.kind }, ["tap", "longPress", "tap"])
364
- XCTAssertEqual(execution.gestureStartUptimeMs, 0)
365
- XCTAssertEqual(execution.gestureEndUptimeMs, 25)
366
- }
367
-
368
- func testAssembleSequenceStopsAtFirstFailure() {
369
- let steps = [
370
- sequenceStep(kind: "tap", x: 1, y: 1),
371
- sequenceStep(kind: "drag", x: 2, y: 2),
372
- sequenceStep(kind: "tap", x: 3, y: 3),
373
- ]
374
- var calls: [Int] = []
375
- let execution = assembleSequenceExecution(steps: steps) { index, _ in
376
- calls.append(index)
377
- if index == 1 {
378
- return SequenceStepOutcome(
379
- outcome: .unsupported(message: "drag unsupported", hint: nil),
380
- gestureStartUptimeMs: 10,
381
- gestureEndUptimeMs: 15
382
- )
383
- }
384
- return SequenceStepOutcome(outcome: .performed, gestureStartUptimeMs: 0, gestureEndUptimeMs: 5)
385
- }
386
- // Step 2 is never invoked.
387
- XCTAssertEqual(calls, [0, 1])
388
- XCTAssertEqual(execution.completedSteps, 1)
389
- XCTAssertEqual(execution.failedStepIndex, 1)
390
- // results.count == completedSteps + 1 (the failed step).
391
- XCTAssertEqual(execution.results.count, 2)
392
- XCTAssertEqual(execution.results[1].ok, false)
393
- XCTAssertEqual(execution.results[1].errorCode, "UNSUPPORTED_OPERATION")
394
- XCTAssertEqual(execution.results[1].errorMessage, "drag unsupported")
395
- }
396
-
397
- func testSequenceWorstCaseResponseStaysUnderJournalCap() throws {
398
- let longMessage = String(repeating: "e", count: 200)
399
- let results = (0..<20).map { index in
400
- SequenceStepResult(
401
- ok: index < 19,
402
- kind: "drag",
403
- errorCode: index < 19 ? nil : "UNSUPPORTED_OPERATION",
404
- errorMessage: index < 19 ? nil : longMessage,
405
- gestureStartUptimeMs: 123456.789,
406
- gestureEndUptimeMs: 123466.789
407
- )
408
- }
409
- let response = Response(
410
- ok: true,
411
- data: DataPayload(
412
- message: "sequence",
413
- completedSteps: 19,
414
- failedStepIndex: 19,
415
- sequenceResults: results
416
- )
417
- )
418
- let encoded = try JSONEncoder().encode(response)
419
- XCTAssertLessThan(encoded.count, 16 * 1024)
420
- }
421
-
422
- private func sequenceStep(
423
- kind: String,
424
- x: Double?,
425
- y: Double? = nil,
426
- x2: Double? = nil,
427
- y2: Double? = nil
428
- ) -> SequenceStep {
429
- SequenceStep(
430
- kind: kind, x: x, y: y, x2: x2, y2: y2, durationMs: nil, pauseMs: nil, synthesized: nil)
431
- }
432
-
433
- /// Validation runs before any executor call, so the INVALID_ARGS paths are exercised without
434
- /// reaching the device executor (which is never invoked when validation rejects).
435
- private func executeSequenceForTest(steps: [SequenceStep]) -> Response {
436
- let command = makeSequenceCommand(steps: steps)
437
- return executeSequence(command: command, activeApp: app)
438
- }
439
-
440
- /// Build a sequence Command via JSON so the test does not depend on the memberwise init's
441
- /// parameter order.
442
- private func makeSequenceCommand(steps: [SequenceStep]) -> Command {
443
- struct SequenceCommandFixture: Encodable {
444
- let command = "sequence"
445
- let commandId = "seq-test"
446
- let steps: [SequenceStep]
447
- }
448
- let data = try! JSONEncoder().encode(SequenceCommandFixture(steps: steps))
449
- return try! JSONDecoder().decode(Command.self, from: data)
450
- }
451
- }
452
- #endif
@@ -302,7 +302,11 @@ extension RunnerTests {
302
302
  return DataPayload(nodes: nodes, truncated: false)
303
303
  }
304
304
 
305
- func snapshotFlatInteractive(app: XCUIApplication, options: SnapshotOptions) -> DataPayload {
305
+ func snapshotFlatInteractive(
306
+ app: XCUIApplication,
307
+ options: SnapshotOptions,
308
+ planDeadline: Date = .distantFuture
309
+ ) -> DataPayload {
306
310
  var nodes: [SnapshotNode] = [
307
311
  interactiveRootNode(rect: .zero)
308
312
  ]
@@ -310,9 +314,12 @@ extension RunnerTests {
310
314
  return DataPayload(nodes: nodes, truncated: false)
311
315
  }
312
316
 
313
- let deadline = options.interactiveOnly
317
+ // Bounded by both its own sweep budget and the umbrella capture-plan deadline, so a
318
+ // chained recovery tier can never push the plan past the main-thread watchdog (#1105).
319
+ let sweepDeadline = options.interactiveOnly
314
320
  ? Date().addingTimeInterval(Self.flatInteractiveFallbackBudget)
315
321
  : Date.distantFuture
322
+ let deadline = min(sweepDeadline, planDeadline)
316
323
  let viewport = safeSnapshotViewport(app: app)
317
324
  var seen = Set<String>()
318
325
  var candidates: [SnapshotNode] = []
@@ -426,50 +433,6 @@ extension RunnerTests {
426
433
  )
427
434
  }
428
435
 
429
- #if AGENT_DEVICE_RUNNER_UNIT_TESTS
430
- func testSnapshotAccessibilityUnavailableMarksSparseSnapshotRunnerFatal() {
431
- currentApp = app
432
- currentBundleId = "com.example.app"
433
-
434
- let payload = snapshotAccessibilityUnavailable(
435
- failure: SnapshotCaptureFailure(
436
- code: Self.axSnapshotErrorCode,
437
- message: Self.axSnapshotFailureMessage,
438
- hint: Self.axSnapshotHint
439
- )
440
- )
441
-
442
- XCTAssertEqual(payload.message, "\(Self.axSnapshotFailureMessage) Hint: \(Self.axSnapshotHint)")
443
- XCTAssertEqual(payload.nodes?.count, 1)
444
- XCTAssertEqual(payload.nodes?.first?.type, "Application")
445
- XCTAssertEqual(payload.truncated, true)
446
- XCTAssertEqual(payload.runnerFatal, true)
447
- XCTAssertEqual(payload.runnerFatalReason, Self.axSnapshotUnavailableReason)
448
- XCTAssertNil(currentApp)
449
- XCTAssertNil(currentBundleId)
450
- }
451
-
452
- func testRecoveredSnapshotMessagePreservesHint() {
453
- let message = recoveredSnapshotMessage(
454
- SnapshotCaptureFailure(
455
- code: Self.axSnapshotErrorCode,
456
- message: Self.axSnapshotFailureMessage,
457
- hint: Self.axSnapshotHint
458
- )
459
- )
460
-
461
- XCTAssertTrue(message.contains(Self.axSnapshotFailureMessage))
462
- XCTAssertTrue(message.contains(Self.axSnapshotHint))
463
- }
464
-
465
- func testRawSnapshotTooLargeFailureIsStructured() {
466
- let failure = rawSnapshotTooLargeFailure(nodeCount: Self.rawSnapshotMaxNodes + 1)
467
-
468
- XCTAssertEqual(failure.code, Self.rawSnapshotTooLargeCode)
469
- XCTAssertTrue(failure.message.contains("\(Self.rawSnapshotMaxNodes) nodes"))
470
- XCTAssertEqual(failure.hint, Self.rawSnapshotTooLargeHint)
471
- }
472
- #endif
473
436
 
474
437
  private func interactiveRootNode(rect: CGRect) -> SnapshotNode {
475
438
  SnapshotNode(
@@ -562,12 +525,14 @@ extension RunnerTests {
562
525
 
563
526
  func makeSnapshotTraversalContext(
564
527
  app: XCUIApplication,
565
- options: SnapshotOptions
528
+ options: SnapshotOptions,
529
+ captureDeadline: Date = .distantFuture
566
530
  ) throws -> SnapshotTraversalContext? {
567
531
  let viewport = safeSnapshotViewport(app: app)
568
532
  let queryRoot = options.scope.flatMap { findScopeElement(app: app, scope: $0) } ?? app
569
533
 
570
- guard let rootSnapshot = try captureSnapshotRoot(queryRoot) else {
534
+ let slice = min(treeCaptureSliceBudget, max(0.5, captureDeadline.timeIntervalSinceNow))
535
+ guard let rootSnapshot = try captureSnapshotRootBounded(queryRoot, sliceSeconds: slice) else {
571
536
  return nil
572
537
  }
573
538
 
@@ -582,6 +547,74 @@ extension RunnerTests {
582
547
  )
583
548
  }
584
549
 
550
+ static let treeCaptureTimeoutCode = "IOS_TREE_CAPTURE_TIMEOUT"
551
+
552
+ func hasAbandonedTreeCapture() -> Bool {
553
+ treeCaptureLock.lock()
554
+ defer { treeCaptureLock.unlock() }
555
+ return abandonedTreeCaptureCount > 0
556
+ }
557
+
558
+ /// Runs the blocking tree-snapshot XPC on a worker thread bounded by `sliceSeconds`. On
559
+ /// timeout the XPC keeps running on its worker (it cannot be cancelled); the capture is
560
+ /// marked abandoned so plans avoid XCTest-backed tiers until it drains, the tree backend is
561
+ /// penalized for this bundle, and the plan moves on to the private AX backend (#1105).
562
+ private func captureSnapshotRootBounded(
563
+ _ element: XCUIElement,
564
+ sliceSeconds: TimeInterval
565
+ ) throws -> XCUIElementSnapshot? {
566
+ final class TreeCaptureBox {
567
+ var abandoned = false
568
+ var outcome: Result<XCUIElementSnapshot?, Error>?
569
+ }
570
+ let box = TreeCaptureBox()
571
+ let semaphore = DispatchSemaphore(value: 0)
572
+ DispatchQueue.global(qos: .userInitiated).async {
573
+ var result: Result<XCUIElementSnapshot?, Error>
574
+ do {
575
+ result = .success(try self.captureSnapshotRoot(element))
576
+ } catch {
577
+ result = .failure(error)
578
+ }
579
+ self.treeCaptureLock.lock()
580
+ if box.abandoned {
581
+ self.abandonedTreeCaptureCount -= 1
582
+ self.treeCaptureLock.unlock()
583
+ NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_DRAINED")
584
+ } else {
585
+ box.outcome = result
586
+ self.treeCaptureLock.unlock()
587
+ }
588
+ semaphore.signal()
589
+ }
590
+ if semaphore.wait(timeout: .now() + sliceSeconds) == .timedOut {
591
+ treeCaptureLock.lock()
592
+ let timedOut = box.outcome == nil
593
+ if timedOut {
594
+ box.abandoned = true
595
+ abandonedTreeCaptureCount += 1
596
+ }
597
+ treeCaptureLock.unlock()
598
+ if timedOut {
599
+ NSLog("AGENT_DEVICE_RUNNER_TREE_CAPTURE_SLICE_TIMEOUT slice=%.1f", sliceSeconds)
600
+ penalizeSnapshotTreeBackend(bundleId: currentBundleId, reason: "tree_capture_slice_timeout")
601
+ throw SnapshotCaptureFailure(
602
+ code: Self.treeCaptureTimeoutCode,
603
+ message: "the XCTest tree capture exceeded its \(Int(sliceSeconds))s time slice",
604
+ hint: "The capture plan recovers through the private AX backend on this screen."
605
+ )
606
+ }
607
+ }
608
+ switch box.outcome {
609
+ case .success(let snapshot):
610
+ return snapshot
611
+ case .failure(let error):
612
+ throw error
613
+ case .none:
614
+ return nil
615
+ }
616
+ }
617
+
585
618
  private func captureSnapshotRoot(_ element: XCUIElement) throws -> XCUIElementSnapshot? {
586
619
  var rootSnapshot: XCUIElementSnapshot?
587
620
  var swiftErrorMessage: String?