agent-device 0.20.8 → 0.20.10

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 (220) hide show
  1. package/README.md +94 -31
  2. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.8.apk → agent-device-android-ime-helper-0.20.10.apk} +0 -0
  3. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.10.apk.sha256 +1 -0
  4. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.8.manifest.json → agent-device-android-ime-helper-0.20.10.manifest.json} +4 -4
  5. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.8.apk → agent-device-android-snapshot-helper-0.20.10.apk} +0 -0
  6. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.10.apk.sha256 +1 -0
  7. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.8.manifest.json → agent-device-android-snapshot-helper-0.20.10.manifest.json} +6 -6
  8. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +3 -0
  9. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +13 -7
  10. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +10 -117
  11. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +4 -4
  12. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +103 -2
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +22 -23
  14. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +3 -23
  15. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +10 -11
  16. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+PrivateAXPresentation.swift +108 -0
  17. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +8 -5
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +179 -73
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +13 -12
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotPresentation.swift +183 -0
  21. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotPresentationTests.swift +0 -0
  22. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +20 -18
  23. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputCandidatePolicy.swift +19 -0
  24. package/dist/src/agent-browser-tool.js +1 -1
  25. package/dist/src/agent-device-client.js +2 -2
  26. package/dist/src/ai-sdk.d.ts +61 -0
  27. package/dist/src/ai-sdk.js +1 -0
  28. package/dist/src/android-adb.d.ts +6 -6
  29. package/dist/src/android-adb.js +1 -1
  30. package/dist/src/android.js +1 -1
  31. package/dist/src/app-helpers.js +1 -1
  32. package/dist/src/app-lifecycle.js +6 -7
  33. package/dist/src/app-lifecycle2.js +2 -2
  34. package/dist/src/app-log-reconnect.js +1 -1
  35. package/dist/src/app-log-runtime.js +3 -3
  36. package/dist/src/app-resolution.js +2 -2
  37. package/dist/src/app-state.js +2 -0
  38. package/dist/src/appearance.js +1 -0
  39. package/dist/src/apps.js +5 -5
  40. package/dist/src/artifacts.d.ts +1 -1
  41. package/dist/src/audio-probe.js +1 -1
  42. package/dist/src/auth-session.js +1 -1
  43. package/dist/src/backend-snapshot-options.js +1 -0
  44. package/dist/src/boot-diagnostics.js +2 -2
  45. package/dist/src/bounds.js +1 -0
  46. package/dist/src/byte-limit-stream.js +1 -0
  47. package/dist/src/cli-config.js +1 -1
  48. package/dist/src/cli-help.js +42 -30
  49. package/dist/src/cli-runner.js +1 -1
  50. package/dist/src/cli.js +7 -7
  51. package/dist/src/client-metro.js +1 -1
  52. package/dist/src/client-types.d.ts +1582 -0
  53. package/dist/src/command-catalog.js +1 -0
  54. package/dist/src/command-metadata.js +1 -1
  55. package/dist/src/command-schema.js +1 -1
  56. package/dist/src/command-surface.js +1 -1
  57. package/dist/src/command-tools.js +2 -0
  58. package/dist/src/config.js +1 -0
  59. package/dist/src/connection-runtime.js +1 -1
  60. package/dist/src/connection.js +3 -3
  61. package/dist/src/contracts.d.ts +1 -1
  62. package/dist/src/contracts.js +1 -1
  63. package/dist/src/daemon-shutdown-report.js +1 -1
  64. package/dist/src/daemon.js +1 -1
  65. package/dist/src/debug-symbols.js +3 -3
  66. package/dist/src/device-claim-inspection.js +1 -1
  67. package/dist/src/device-input-state.js +1 -1
  68. package/dist/src/device-ready.js +1 -1
  69. package/dist/src/device2.js +2 -2
  70. package/dist/src/dispatch.js +1 -1
  71. package/dist/src/entities.js +1 -1
  72. package/dist/src/find.js +1 -1
  73. package/dist/src/finders.js +1 -1
  74. package/dist/src/generic-settle.js +1 -1
  75. package/dist/src/generic.js +1 -1
  76. package/dist/src/harmonyos.js +1 -1
  77. package/dist/src/hdc.js +1 -1
  78. package/dist/src/helper-package-install.js +1 -0
  79. package/dist/src/helper.js +2 -2
  80. package/dist/src/host-process.js +1 -1
  81. package/dist/src/ime-lifecycle.js +2 -9
  82. package/dist/src/ime-recovery-marker.js +1 -0
  83. package/dist/src/index.d.ts +2 -1569
  84. package/dist/src/index.js +1 -1
  85. package/dist/src/input-actions.js +4 -4
  86. package/dist/src/input-actions2.js +1 -1
  87. package/dist/src/install-artifact.js +1 -1
  88. package/dist/src/install-artifact2.js +1 -1
  89. package/dist/src/install-source2.js +1 -1
  90. package/dist/src/interaction-common.js +1 -0
  91. package/dist/src/interaction-runtime.js +1 -1
  92. package/dist/src/interaction-snapshot.js +1 -1
  93. package/dist/src/interaction.js +1 -1
  94. package/dist/src/interaction2.js +1 -0
  95. package/dist/src/interactor.js +2 -2
  96. package/dist/src/interactor2.js +1 -1
  97. package/dist/src/interactors.js +1 -0
  98. package/dist/src/internal/bin.js +2 -2
  99. package/dist/src/internal/daemon.js +4 -7
  100. package/dist/src/internal/png-worker.js +1 -1
  101. package/dist/src/inventory.js +1 -1
  102. package/dist/src/keyed-lock.js +1 -0
  103. package/dist/src/lease-context.js +1 -1
  104. package/dist/src/lease.js +1 -1
  105. package/dist/src/limrun-runtime-dependencies.js +1 -1
  106. package/dist/src/limrun.d.ts +13 -3
  107. package/dist/src/limrun.js +1 -1
  108. package/dist/src/linux.js +1 -1
  109. package/dist/src/location-coordinates.js +1 -0
  110. package/dist/src/logcat.js +1 -1
  111. package/dist/src/manifest.js +1 -1
  112. package/dist/src/mobile-snapshot-semantics.js +1 -0
  113. package/dist/src/observability.js +1 -0
  114. package/dist/src/owner-identity.js +1 -1
  115. package/dist/src/perf.js +4 -0
  116. package/dist/src/physical-device-control.js +1 -1
  117. package/dist/src/platform-runtime-app-log-process.js +1 -1
  118. package/dist/src/platform-runtime-host.js +1 -1
  119. package/dist/src/platform-runtime-network-web-transport.js +1 -1
  120. package/dist/src/platform-runtime-open-target.js +1 -0
  121. package/dist/src/platform-runtime-operation-host.js +4 -4
  122. package/dist/src/platform-runtime-runtime-hints.js +2 -0
  123. package/dist/src/platform-runtime-screen-recording-apple-runner-host.js +1 -1
  124. package/dist/src/platform-runtime-screen-recording-apple-runner-transport.js +1 -1
  125. package/dist/src/platform-runtime-screen-recording-apple-transport.js +1 -1
  126. package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
  127. package/dist/src/platform-runtime-screen-recording-web-host.js +1 -1
  128. package/dist/src/platform-runtime.js +1 -1
  129. package/dist/src/platform-runtime2.js +1 -0
  130. package/dist/src/plist.js +1 -1
  131. package/dist/src/png-resize.js +1 -0
  132. package/dist/src/png-worker-contract.js +1 -1
  133. package/dist/src/process-lock.js +1 -1
  134. package/dist/src/project-runtime.js +1 -0
  135. package/dist/src/provider-webdriver.js +1 -1
  136. package/dist/src/provider.js +1 -0
  137. package/dist/src/proxy.js +1 -1
  138. package/dist/src/react-native.js +1 -1
  139. package/dist/src/record-runtime.js +1 -1
  140. package/dist/src/record-trace.js +1 -1
  141. package/dist/src/recording-export-quality.js +1 -0
  142. package/dist/src/registry.js +29 -26
  143. package/dist/src/remote-config2.js +1 -1
  144. package/dist/src/replay.js +2 -2
  145. package/dist/src/reporting.js +5 -5
  146. package/dist/src/rolldown-runtime.js +1 -0
  147. package/dist/src/runner-client.js +1 -0
  148. package/dist/src/runner-disposal.js +1 -1
  149. package/dist/src/runner-lease.js +1 -1
  150. package/dist/src/runner-sequence.js +1 -1
  151. package/dist/src/runtime-transport.js +1 -1
  152. package/dist/src/runtime.js +3 -3
  153. package/dist/src/runtime2.js +4 -3
  154. package/dist/src/runtime3.js +1 -3
  155. package/dist/src/runtime4.js +3 -1
  156. package/dist/src/runtime5.js +1 -1
  157. package/dist/src/runtime6.js +1 -1
  158. package/dist/src/runtime7.js +1 -1
  159. package/dist/src/runtime8.js +1 -0
  160. package/dist/src/runtime9.js +1 -0
  161. package/dist/src/scoped-provider.js +1 -0
  162. package/dist/src/screenshot-density.js +1 -0
  163. package/dist/src/screenshot-result.js +4 -4
  164. package/dist/src/screenshot.js +1 -1
  165. package/dist/src/script-source-bundle.js +16 -0
  166. package/dist/src/sdk-android-adb.d.ts +44 -26
  167. package/dist/src/sdk-android-adb.js +4 -4
  168. package/dist/src/sdk-batch-runner.js +3 -3
  169. package/dist/src/sdk-contracts.d.ts +125 -88
  170. package/dist/src/sdk-contracts.js +2 -2
  171. package/dist/src/sdk-device.js +1 -1
  172. package/dist/src/sdk-finders.d.ts +1 -1
  173. package/dist/src/sdk-finders.js +1 -1
  174. package/dist/src/sdk-io.js +1 -1
  175. package/dist/src/sdk-remote-config.js +1 -1
  176. package/dist/src/sdk-selectors.d.ts +107 -95
  177. package/dist/src/sdk-selectors.js +1 -1
  178. package/dist/src/sdk.js +1 -0
  179. package/dist/src/selector-runtime.js +1 -1
  180. package/dist/src/selectors.d.ts +1 -1
  181. package/dist/src/selectors.js +1 -1
  182. package/dist/src/server.js +10 -3
  183. package/dist/src/session-target-evidence.js +1 -1
  184. package/dist/src/session.js +1 -30
  185. package/dist/src/session2.js +27 -0
  186. package/dist/src/setting-state.js +1 -1
  187. package/dist/src/shared.js +1 -1
  188. package/dist/src/simctl.js +1 -0
  189. package/dist/src/simulator-state.js +1 -0
  190. package/dist/src/simulator.js +1 -1
  191. package/dist/src/snapshot-runtime.js +1 -1
  192. package/dist/src/snapshot-scope.js +1 -0
  193. package/dist/src/snapshot-session.js +1 -1
  194. package/dist/src/snapshot.js +8 -1
  195. package/dist/src/snapshot2.js +1 -1
  196. package/dist/src/snapshot3.js +1 -0
  197. package/dist/src/src.js +1 -1
  198. package/dist/src/src2.js +1 -2
  199. package/dist/src/src3.js +2 -1
  200. package/dist/src/src4.js +1 -0
  201. package/dist/src/string-enum.js +1 -0
  202. package/dist/src/tool-provider.js +3 -3
  203. package/dist/src/tool-provider2.js +1 -1
  204. package/dist/src/tool-provider3.js +1 -1
  205. package/dist/src/update-check.js +1 -1
  206. package/dist/src/verdict.js +1 -0
  207. package/dist/src/verified-file.js +1 -0
  208. package/dist/src/version.js +1 -1
  209. package/dist/src/video.js +1 -1
  210. package/dist/src/web.js +1 -1
  211. package/package.json +27 -10
  212. package/server.json +3 -3
  213. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.8.apk.sha256 +0 -1
  214. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.8.apk.sha256 +0 -1
  215. package/dist/src/interaction-touch-reference-frame.js +0 -1
  216. package/dist/src/notifications.js +0 -1
  217. package/dist/src/platform-runtime-toolchain-host.js +0 -1
  218. package/dist/src/prepare-kind.js +0 -15
  219. package/dist/src/selector-vocabulary.js +0 -1
  220. package/dist/src/text-surface.js +0 -1
@@ -30,6 +30,15 @@ extension RunnerTests {
30
30
  let visible: Bool
31
31
  }
32
32
 
33
+ private struct SnapshotTraversalEntry {
34
+ let snapshot: XCUIElementSnapshot
35
+ let depth: Int
36
+ let visibleDepth: Int
37
+ let parentIndex: Int?
38
+ let nearestScrollAnchor: (index: Int, rect: CGRect)?
39
+ let projectionCursor: FlatSnapshotProjectionCursor
40
+ }
41
+
33
42
  struct SnapshotCaptureFailure: Error {
34
43
  let code: String
35
44
  let message: String
@@ -108,7 +117,7 @@ extension RunnerTests {
108
117
  // `boundedBlockingSystemAlertSnapshot`'s probe closure (see `systemModalProbeOverrideForTesting`
109
118
  // in RunnerTests.swift), so reverting this entry point to bypass the bounded probe fails the
110
119
  // regression test.
111
- func snapshotFast(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
120
+ func snapshotFast(app: XCUIApplication, options: PresentationOptions) throws -> DataPayload {
112
121
  let deadline = Date().addingTimeInterval(Self.snapshotPlanBudget)
113
122
  if let blocking = boundedBlockingSystemAlertSnapshot(deadline: deadline) {
114
123
  return blocking
@@ -122,10 +131,10 @@ extension RunnerTests {
122
131
  )
123
132
  }
124
133
 
125
- func recursiveTreeSnapshotPayload(
134
+ func recursiveTreeSnapshotAcquisition(
126
135
  context: SnapshotTraversalContext,
127
- options: SnapshotOptions
128
- ) -> DataPayload {
136
+ options: PresentationOptions
137
+ ) -> SnapshotAcquisition {
129
138
  var cachedDescendantElements: [XCUIElement]?
130
139
  func collapsedTabDescendants() -> [XCUIElement] {
131
140
  if let cachedDescendantElements {
@@ -138,7 +147,7 @@ extension RunnerTests {
138
147
  return result.elements
139
148
  }
140
149
 
141
- var nodes: [SnapshotNode] = []
150
+ var nodes: [RawAXNode] = []
142
151
  var hiddenContentHintsByNodeIndex: [Int: (above: Bool, below: Bool)] = [:]
143
152
  let rootEvaluation = evaluateSnapshot(context.rootSnapshot, in: context)
144
153
  nodes.append(
@@ -166,35 +175,79 @@ extension RunnerTests {
166
175
  visible: rootEvaluation.visible,
167
176
  nodeIndex: 0
168
177
  )
169
- var stack: [(XCUIElementSnapshot, Int, Int, Int?, (index: Int, rect: CGRect)?)] =
170
- context.rootSnapshot.children.map {
171
- ($0, 1, 1, 0, rootScrollAnchor)
172
- }
178
+ #if os(iOS)
179
+ let rootProjection = flatSnapshotProjectionTransition(
180
+ frame: context.rootSnapshot.frame,
181
+ intersectsViewportAndScrollClip: rootEvaluation.visible,
182
+ elementType: context.rootSnapshot.elementType,
183
+ hasChildren: !context.rootSnapshot.children.isEmpty,
184
+ cursor: .root
185
+ )
186
+ let rootDescendantProjectionCursor = rootProjection.decision.descendants
187
+ #else
188
+ let rootDescendantProjectionCursor = FlatSnapshotProjectionCursor.root
189
+ #endif
190
+ var stack: [SnapshotTraversalEntry] = context.rootSnapshot.children.map {
191
+ SnapshotTraversalEntry(
192
+ snapshot: $0,
193
+ depth: 1,
194
+ visibleDepth: 1,
195
+ parentIndex: 0,
196
+ nearestScrollAnchor: rootScrollAnchor,
197
+ projectionCursor: rootDescendantProjectionCursor
198
+ )
199
+ }
173
200
 
174
- while let (snapshot, depth, visibleDepth, parentIndex, nearestScrollAnchor) = stack.popLast() {
201
+ while let entry = stack.popLast() {
202
+ let snapshot = entry.snapshot
203
+ let depth = entry.depth
204
+ let visibleDepth = entry.visibleDepth
205
+ let parentIndex = entry.parentIndex
206
+ let nearestScrollAnchor = entry.nearestScrollAnchor
207
+ let projectionCursor = entry.projectionCursor
175
208
  if let limit = options.depth, depth > limit { continue }
176
209
 
177
210
  let evaluation = evaluateSnapshot(snapshot, in: context)
178
- let regularVisible = isVisibleInRegularSnapshot(
211
+ let intersectsViewportAndScrollClip = isVisibleInRegularSnapshot(
179
212
  snapshot.frame,
180
213
  viewport: context.viewport,
181
214
  scrollContainerAnchor: nearestScrollAnchor
182
215
  )
183
- if !regularVisible, let nearestScrollAnchor {
216
+ #if os(iOS)
217
+ let projectionTransition = flatSnapshotProjectionTransition(
218
+ frame: snapshot.frame,
219
+ intersectsViewportAndScrollClip: intersectsViewportAndScrollClip,
220
+ elementType: snapshot.elementType,
221
+ hasChildren: !snapshot.children.isEmpty,
222
+ cursor: projectionCursor
223
+ )
224
+ let projection = projectionTransition.decision
225
+ #else
226
+ let projectionTransition = FlatSnapshotProjectionTransition(
227
+ decision: FlatSnapshotProjectionDecision(
228
+ presentationVisible: intersectsViewportAndScrollClip,
229
+ descendants: .root
230
+ ),
231
+ hiddenContentFrame: !intersectsViewportAndScrollClip ? snapshot.frame : nil
232
+ )
233
+ let projection = projectionTransition.decision
234
+ #endif
235
+ if let hiddenFrame = projectionTransition.hiddenContentFrame, let nearestScrollAnchor
236
+ {
184
237
  rememberHiddenContentHint(
185
- for: snapshot.frame,
238
+ for: hiddenFrame,
186
239
  relativeTo: nearestScrollAnchor,
187
240
  hints: &hiddenContentHintsByNodeIndex
188
241
  )
189
242
  }
190
- let include = shouldInclude(
243
+ let include = shouldAcquireSnapshotNode(
191
244
  snapshot: snapshot,
192
245
  label: evaluation.label,
193
246
  identifier: evaluation.identifier,
194
247
  valueText: evaluation.valueText,
195
248
  options: options,
196
249
  hittable: evaluation.hittable,
197
- visible: regularVisible,
250
+ visible: projection.presentationVisible,
198
251
  regularSnapshot: true
199
252
  )
200
253
 
@@ -217,7 +270,7 @@ extension RunnerTests {
217
270
  nextScrollContainerAnchor =
218
271
  scrollContainerAnchor(
219
272
  for: snapshot,
220
- visible: regularVisible,
273
+ visible: intersectsViewportAndScrollClip,
221
274
  nodeIndex: currentIndex
222
275
  )
223
276
  ?? nearestScrollAnchor
@@ -225,7 +278,16 @@ extension RunnerTests {
225
278
  nextScrollContainerAnchor = nearestScrollAnchor
226
279
  }
227
280
  for child in snapshot.children.reversed() {
228
- stack.append((child, depth + 1, nextVisibleDepth, currentIndex, nextScrollContainerAnchor))
281
+ stack.append(
282
+ SnapshotTraversalEntry(
283
+ snapshot: child,
284
+ depth: depth + 1,
285
+ visibleDepth: nextVisibleDepth,
286
+ parentIndex: currentIndex,
287
+ nearestScrollAnchor: nextScrollContainerAnchor,
288
+ projectionCursor: projection.descendants
289
+ )
290
+ )
229
291
  }
230
292
  }
231
293
 
@@ -253,14 +315,15 @@ extension RunnerTests {
253
315
 
254
316
  }
255
317
 
256
- return DataPayload(
318
+ return SnapshotAcquisition(
257
319
  nodes: applyHiddenContentHints(hiddenContentHintsByNodeIndex, to: nodes),
258
- truncated: false
320
+ truncated: false,
321
+ effectiveDepth: nil
259
322
  )
260
323
  }
261
324
 
262
325
  // See `snapshotFast` above: the single production entry point, no unit-test overload.
263
- func snapshotRaw(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
326
+ func snapshotRaw(app: XCUIApplication, options: PresentationOptions) throws -> DataPayload {
264
327
  let deadline = Date().addingTimeInterval(Self.snapshotPlanBudget)
265
328
  if let blocking = boundedBlockingSystemAlertSnapshot(deadline: deadline) {
266
329
  return blocking
@@ -351,17 +414,17 @@ extension RunnerTests {
351
414
  return min(budget, deadlineRemaining)
352
415
  }
353
416
 
354
- func rawTreeSnapshotPayload(
417
+ func rawTreeSnapshotAcquisition(
355
418
  context: SnapshotTraversalContext,
356
- options: SnapshotOptions
357
- ) throws -> DataPayload {
358
- var nodes: [SnapshotNode] = []
419
+ options: PresentationOptions
420
+ ) throws -> SnapshotAcquisition {
421
+ var nodes: [RawAXNode] = []
359
422
 
360
423
  func walk(_ snapshot: XCUIElementSnapshot, depth: Int, parentIndex: Int?) throws {
361
424
  if let limit = options.depth, depth > limit { return }
362
425
 
363
426
  let evaluation = evaluateSnapshot(snapshot, in: context)
364
- let include = shouldInclude(
427
+ let include = shouldAcquireSnapshotNode(
365
428
  snapshot: snapshot,
366
429
  label: evaluation.label,
367
430
  identifier: evaluation.identifier,
@@ -393,19 +456,19 @@ extension RunnerTests {
393
456
  }
394
457
 
395
458
  try walk(context.rootSnapshot, depth: 0, parentIndex: nil)
396
- return DataPayload(nodes: nodes, truncated: false)
459
+ return SnapshotAcquisition(nodes: nodes, truncated: false, effectiveDepth: nil)
397
460
  }
398
461
 
399
- func snapshotFlatInteractive(
462
+ func querySweepSnapshotAcquisition(
400
463
  app: XCUIApplication,
401
- options: SnapshotOptions,
464
+ options: PresentationOptions,
402
465
  planDeadline: Date = .distantFuture
403
- ) -> DataPayload {
404
- var nodes: [SnapshotNode] = [
466
+ ) -> SnapshotAcquisition {
467
+ var nodes: [RawAXNode] = [
405
468
  interactiveRootNode(rect: .zero)
406
469
  ]
407
470
  if options.depth == 0 {
408
- return DataPayload(nodes: nodes, truncated: false)
471
+ return SnapshotAcquisition(nodes: nodes, truncated: false, effectiveDepth: nil)
409
472
  }
410
473
 
411
474
  // Bounded by both its own sweep budget and the umbrella capture-plan deadline, so a
@@ -416,7 +479,7 @@ extension RunnerTests {
416
479
  let deadline = min(sweepDeadline, planDeadline)
417
480
  let viewport = safeSnapshotViewport(app: app)
418
481
  var seen = Set<String>()
419
- var candidates: [SnapshotNode] = []
482
+ var candidates: [RawAXNode] = []
420
483
  let flatElements = flatInteractiveElements(app: app, deadline: deadline)
421
484
  var truncated = flatElements.truncated
422
485
  for element in flatElements.elements {
@@ -458,7 +521,7 @@ extension RunnerTests {
458
521
  nodes[0] = interactiveRootNode(rect: rootRect)
459
522
  for candidate in candidates {
460
523
  nodes.append(
461
- SnapshotNode(
524
+ RawAXNode(
462
525
  index: nodes.count,
463
526
  type: candidate.type,
464
527
  label: candidate.label,
@@ -476,7 +539,7 @@ extension RunnerTests {
476
539
  )
477
540
  )
478
541
  }
479
- return DataPayload(nodes: nodes, truncated: truncated)
542
+ return SnapshotAcquisition(nodes: nodes, truncated: truncated, effectiveDepth: nil)
480
543
  }
481
544
 
482
545
  func snapshotAccessibilityUnavailable(failure: SnapshotCaptureFailure) -> DataPayload {
@@ -521,7 +584,7 @@ extension RunnerTests {
521
584
  ) -> DataPayload {
522
585
  return DataPayload(
523
586
  message: message,
524
- nodes: [interactiveRootNode(rect: .zero)],
587
+ nodes: [SnapshotPresentation.singleElementRead(interactiveRootNode(rect: .zero))],
525
588
  truncated: true,
526
589
  snapshotQuality: snapshotQuality,
527
590
  runnerFatal: runnerFatal,
@@ -530,8 +593,8 @@ extension RunnerTests {
530
593
  }
531
594
 
532
595
 
533
- private func interactiveRootNode(rect: CGRect) -> SnapshotNode {
534
- SnapshotNode(
596
+ private func interactiveRootNode(rect: CGRect) -> RawAXNode {
597
+ RawAXNode(
535
598
  index: 0,
536
599
  type: "Application",
537
600
  label: nil,
@@ -549,7 +612,7 @@ extension RunnerTests {
549
612
  )
550
613
  }
551
614
 
552
- private func interactiveRootFrame(for candidates: [SnapshotNode]) -> CGRect {
615
+ private func interactiveRootFrame(for candidates: [RawAXNode]) -> CGRect {
553
616
  guard !candidates.isEmpty else {
554
617
  return .zero
555
618
  }
@@ -569,34 +632,41 @@ extension RunnerTests {
569
632
 
570
633
  // MARK: - Snapshot Filtering
571
634
 
572
- private func shouldInclude(
635
+ /// Conservative acquisition gate. Regular capture keeps every node its visibility policy can
636
+ /// supply; `SnapshotPresentation` alone decides eligibility after the backend returns.
637
+ private func shouldAcquireSnapshotNode(
573
638
  snapshot: XCUIElementSnapshot,
574
639
  label: String,
575
640
  identifier: String,
576
641
  valueText: String?,
577
- options: SnapshotOptions,
642
+ options: PresentationOptions,
578
643
  hittable: Bool,
579
644
  visible: Bool,
580
645
  regularSnapshot: Bool = false
581
646
  ) -> Bool {
582
647
  let type = snapshot.elementType
583
648
  let hasContent = !label.isEmpty || !identifier.isEmpty || (valueText != nil)
584
- if options.interactiveOnly {
585
- if isScrollableContainer(snapshot, visible: visible) { return true }
649
+ #if os(iOS)
650
+ if regularSnapshot && !visible && type != .application && type != .window {
651
+ return false
652
+ }
653
+ #endif
654
+ if regularSnapshot {
586
655
  #if os(macOS)
587
- if !visible && type != .application {
656
+ if options.interactiveOnly && !visible && type != .application {
588
657
  return false
589
658
  }
590
659
  #endif
660
+ if type == .application || type == .window { return true }
661
+ return visible
662
+ }
663
+ if options.interactiveOnly {
664
+ if isScrollableContainer(snapshot, visible: visible) { return true }
591
665
  if interactiveTypes.contains(type) { return true }
592
666
  if hittable && type != .other { return true }
593
667
  if hasContent { return true }
594
668
  return false
595
669
  }
596
- if regularSnapshot {
597
- if type == .application || type == .window { return true }
598
- return visible
599
- }
600
670
  return true
601
671
  }
602
672
 
@@ -621,7 +691,7 @@ extension RunnerTests {
621
691
 
622
692
  func makeSnapshotTraversalContext(
623
693
  app: XCUIApplication,
624
- options: SnapshotOptions,
694
+ options: PresentationOptions,
625
695
  captureDeadline: Date = .distantFuture,
626
696
  treeCaptureSliceBudgetOverride: TimeInterval? = nil
627
697
  ) throws -> SnapshotTraversalContext? {
@@ -815,8 +885,8 @@ extension RunnerTests {
815
885
  depth: Int,
816
886
  index: Int,
817
887
  parentIndex: Int?
818
- ) -> SnapshotNode {
819
- return SnapshotNode(
888
+ ) -> RawAXNode {
889
+ return RawAXNode(
820
890
  index: index,
821
891
  type: elementTypeName(snapshot.elementType),
822
892
  label: evaluation.label.isEmpty ? nil : evaluation.label,
@@ -925,7 +995,7 @@ extension RunnerTests {
925
995
  return rect.intersects(viewport)
926
996
  }
927
997
 
928
- private func isVisibleInRegularSnapshot(
998
+ func isVisibleInRegularSnapshot(
929
999
  _ rect: CGRect,
930
1000
  viewport: CGRect,
931
1001
  scrollContainerAnchor: (index: Int, rect: CGRect)?
@@ -936,7 +1006,7 @@ extension RunnerTests {
936
1006
  }
937
1007
 
938
1008
  private func appendCollapsedTabFallbackNodes(
939
- to nodes: inout [SnapshotNode],
1009
+ to nodes: inout [RawAXNode],
940
1010
  containerSnapshot: XCUIElementSnapshot,
941
1011
  resolveElements: () -> [XCUIElement],
942
1012
  depth: Int,
@@ -956,13 +1026,33 @@ extension RunnerTests {
956
1026
  for snapshot: XCUIElementSnapshot,
957
1027
  visible: Bool,
958
1028
  nodeIndex: Int?
1029
+ ) -> (index: Int, rect: CGRect)? {
1030
+ return scrollContainerAnchor(
1031
+ for: snapshot.elementType,
1032
+ hasChildren: !snapshot.children.isEmpty,
1033
+ visible: visible,
1034
+ frame: snapshot.frame,
1035
+ nodeIndex: nodeIndex
1036
+ )
1037
+ }
1038
+
1039
+ func scrollContainerAnchor(
1040
+ for elementType: XCUIElement.ElementType,
1041
+ hasChildren: Bool,
1042
+ visible: Bool,
1043
+ frame: CGRect,
1044
+ nodeIndex: Int?
959
1045
  ) -> (index: Int, rect: CGRect)? {
960
1046
  guard let nodeIndex else { return nil }
961
- if !isScrollableContainer(snapshot, visible: visible) { return nil }
962
- return (nodeIndex, snapshot.frame)
1047
+ if !isScrollableContainer(
1048
+ elementType: elementType,
1049
+ hasChildren: hasChildren,
1050
+ visible: visible
1051
+ ) { return nil }
1052
+ return (nodeIndex, frame)
963
1053
  }
964
1054
 
965
- private func rememberHiddenContentHint(
1055
+ func rememberHiddenContentHint(
966
1056
  for frame: CGRect,
967
1057
  relativeTo scrollContainerAnchor: (index: Int, rect: CGRect),
968
1058
  hints: inout [Int: (above: Bool, below: Bool)]
@@ -979,16 +1069,16 @@ extension RunnerTests {
979
1069
  hints[scrollContainerAnchor.index] = hint
980
1070
  }
981
1071
 
982
- private func applyHiddenContentHints(
1072
+ func applyHiddenContentHints(
983
1073
  _ hints: [Int: (above: Bool, below: Bool)],
984
- to nodes: [SnapshotNode]
985
- ) -> [SnapshotNode] {
1074
+ to nodes: [RawAXNode]
1075
+ ) -> [RawAXNode] {
986
1076
  if hints.isEmpty { return nodes }
987
1077
  return nodes.map { node in
988
1078
  guard let hint = hints[node.index] else { return node }
989
1079
  let hiddenContentAbove: Bool? = (node.hiddenContentAbove == true || hint.above) ? true : nil
990
1080
  let hiddenContentBelow: Bool? = (node.hiddenContentBelow == true || hint.below) ? true : nil
991
- return SnapshotNode(
1081
+ return RawAXNode(
992
1082
  index: node.index,
993
1083
  type: node.type,
994
1084
  label: node.label,
@@ -1002,7 +1092,8 @@ extension RunnerTests {
1002
1092
  depth: node.depth,
1003
1093
  parentIndex: node.parentIndex,
1004
1094
  hiddenContentAbove: hiddenContentAbove,
1005
- hiddenContentBelow: hiddenContentBelow
1095
+ hiddenContentBelow: hiddenContentBelow,
1096
+ actions: node.actions
1006
1097
  )
1007
1098
  }
1008
1099
  }
@@ -1013,7 +1104,7 @@ extension RunnerTests {
1013
1104
  startingIndex: Int,
1014
1105
  depth: Int,
1015
1106
  parentIndex: Int
1016
- ) -> [SnapshotNode] {
1107
+ ) -> [RawAXNode] {
1017
1108
  if !containerSnapshot.children.isEmpty { return [] }
1018
1109
  guard shouldExpandCollapsedTabContainer(containerSnapshot) else { return [] }
1019
1110
  let containerFrame = containerSnapshot.frame
@@ -1052,7 +1143,7 @@ extension RunnerTests {
1052
1143
  if uniqueCandidates.count < 2 { return [] }
1053
1144
 
1054
1145
  return uniqueCandidates.enumerated().map { offset, node in
1055
- SnapshotNode(
1146
+ RawAXNode(
1056
1147
  index: startingIndex + offset,
1057
1148
  type: node.type,
1058
1149
  label: node.label,
@@ -1075,8 +1166,8 @@ extension RunnerTests {
1075
1166
  element: XCUIElement,
1076
1167
  containerSnapshot: XCUIElementSnapshot,
1077
1168
  containerFrame: CGRect
1078
- ) -> SnapshotNode? {
1079
- var node: SnapshotNode?
1169
+ ) -> RawAXNode? {
1170
+ var node: RawAXNode?
1080
1171
  let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
1081
1172
  if !element.exists { return }
1082
1173
  let elementType = element.elementType
@@ -1104,7 +1195,7 @@ extension RunnerTests {
1104
1195
  return
1105
1196
  }
1106
1197
 
1107
- node = SnapshotNode(
1198
+ node = RawAXNode(
1108
1199
  index: 0,
1109
1200
  type: elementTypeName(elementType),
1110
1201
  label: label.isEmpty ? nil : label,
@@ -1241,9 +1332,9 @@ extension RunnerTests {
1241
1332
  index: Int,
1242
1333
  parentIndex: Int?,
1243
1334
  viewport: CGRect,
1244
- options: SnapshotOptions
1245
- ) -> SnapshotNode? {
1246
- var node: SnapshotNode?
1335
+ options: PresentationOptions
1336
+ ) -> RawAXNode? {
1337
+ var node: RawAXNode?
1247
1338
  let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
1248
1339
  if !element.exists { return }
1249
1340
  let frame = element.frame
@@ -1266,11 +1357,16 @@ extension RunnerTests {
1266
1357
  valueText: valueText,
1267
1358
  visible: visible
1268
1359
  )
1269
- if !flatSnapshotFilterDecision(filterNode, options: options, insideMatchedScope: false).include {
1360
+ if !flatSnapshotFilterDecision(
1361
+ filterNode,
1362
+ options: options,
1363
+ visibilityPolicy: .interactiveOnly,
1364
+ insideMatchedScope: false
1365
+ ).include {
1270
1366
  return
1271
1367
  }
1272
1368
 
1273
- node = SnapshotNode(
1369
+ node = RawAXNode(
1274
1370
  index: index,
1275
1371
  type: elementTypeName(elementType),
1276
1372
  label: label.isEmpty ? nil : label,
@@ -1295,8 +1391,18 @@ extension RunnerTests {
1295
1391
  }
1296
1392
 
1297
1393
  private func isScrollableContainer(_ snapshot: XCUIElementSnapshot, visible: Bool) -> Bool {
1298
- if !visible { return false }
1299
- if !Self.scrollContainerTypes.contains(snapshot.elementType) { return false }
1300
- return !snapshot.children.isEmpty
1394
+ return isScrollableContainer(
1395
+ elementType: snapshot.elementType,
1396
+ hasChildren: !snapshot.children.isEmpty,
1397
+ visible: visible
1398
+ )
1399
+ }
1400
+
1401
+ private func isScrollableContainer(
1402
+ elementType: XCUIElement.ElementType,
1403
+ hasChildren: Bool,
1404
+ visible: Bool
1405
+ ) -> Bool {
1406
+ return visible && hasChildren && Self.scrollContainerTypes.contains(elementType)
1301
1407
  }
1302
1408
  }
@@ -250,7 +250,7 @@ extension RunnerTests {
250
250
  func runSnapshotCapturePlan(
251
251
  _ plan: [SnapshotBackendKind],
252
252
  app: XCUIApplication,
253
- options: SnapshotOptions,
253
+ options: PresentationOptions,
254
254
  terminal: SnapshotCaptureTerminalPolicy,
255
255
  deadline: Date? = nil
256
256
  ) throws -> DataPayload {
@@ -452,10 +452,11 @@ extension RunnerTests {
452
452
  private func captureWithBackend(
453
453
  _ kind: SnapshotBackendKind,
454
454
  app: XCUIApplication,
455
- options: SnapshotOptions,
455
+ options: PresentationOptions,
456
456
  deadline: Date,
457
457
  treeCaptureSliceBudgetOverride: TimeInterval?
458
458
  ) throws -> SnapshotBackendCapture? {
459
+ let acquisition: SnapshotAcquisition?
459
460
  switch kind {
460
461
  case .recursiveTree:
461
462
  guard
@@ -468,28 +469,28 @@ extension RunnerTests {
468
469
  else {
469
470
  return nil
470
471
  }
471
- let payload = try runMainThreadWork(
472
+ acquisition = try runMainThreadWork(
472
473
  command: nil,
473
474
  timeout: min(treeCaptureSliceBudget, max(0.5, deadline.timeIntervalSinceNow)),
474
475
  timeoutError: snapshotMainThreadTimeoutError("processing tree snapshot")
475
476
  ) {
476
477
  options.raw
477
- ? try self.rawTreeSnapshotPayload(context: context, options: options)
478
- : self.recursiveTreeSnapshotPayload(context: context, options: options)
478
+ ? try self.rawTreeSnapshotAcquisition(context: context, options: options)
479
+ : self.recursiveTreeSnapshotAcquisition(context: context, options: options)
479
480
  }
480
- return SnapshotBackendCapture(payload: payload, effectiveDepth: nil)
481
481
  case .querySweep:
482
- let payload = try runMainThreadWork(
482
+ acquisition = try runMainThreadWork(
483
483
  command: nil,
484
484
  timeout: min(Self.flatInteractiveFallbackBudget, max(0.1, deadline.timeIntervalSinceNow)),
485
485
  timeoutError: snapshotMainThreadTimeoutError("running query-sweep snapshot")
486
486
  ) {
487
- self.snapshotFlatInteractive(app: app, options: options, planDeadline: deadline)
487
+ self.querySweepSnapshotAcquisition(app: app, options: options, planDeadline: deadline)
488
488
  }
489
- return SnapshotBackendCapture(payload: payload, effectiveDepth: nil)
490
489
  case .privateAX:
491
- return privateAXSnapshotCapture(app: app, options: options, deadline: deadline)
490
+ acquisition = privateAXSnapshotAcquisition(app: app, options: options, deadline: deadline)
492
491
  }
492
+ guard let acquisition else { return nil }
493
+ return SnapshotPresentation.present(acquisition, options: options)
493
494
  }
494
495
 
495
496
  // MARK: Quality classifier (the single source of "is this snapshot degraded")
@@ -529,7 +530,7 @@ extension RunnerTests {
529
530
  }
530
531
  }
531
532
 
532
- static func isSparseApplicationWindowTree(_ nodes: [SnapshotNode]) -> Bool {
533
+ static func isSparseApplicationWindowTree(_ nodes: [PresentedNode]) -> Bool {
533
534
  guard !nodes.isEmpty else { return false }
534
535
  let rootRects = nodes.compactMap { node in
535
536
  node.type == "Application" || node.type == "Window" ? node.rect : nil
@@ -564,7 +565,7 @@ extension RunnerTests {
564
565
  /// A leaf whose label joins many short segments is a container marked as an accessibility
565
566
  /// element: the platform folds every descendant into one merged node. Nothing below it can
566
567
  /// be addressed — by automation or by assistive tech. This is app-side; no backend recovers it.
567
- static func collapsedLeafIndexes(_ nodes: [SnapshotNode]) -> [Int]? {
568
+ static func collapsedLeafIndexes(_ nodes: [PresentedNode]) -> [Int]? {
568
569
  let parents = Set(nodes.compactMap { $0.parentIndex })
569
570
  let collapsed = nodes.filter { node in
570
571
  guard !parents.contains(node.index) else { return false }