agent-device 0.20.9 → 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 (140) hide show
  1. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.9.apk → agent-device-android-ime-helper-0.20.10.apk} +0 -0
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.10.apk.sha256 +1 -0
  3. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.9.manifest.json → agent-device-android-ime-helper-0.20.10.manifest.json} +4 -4
  4. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.9.apk → agent-device-android-snapshot-helper-0.20.10.apk} +0 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.10.apk.sha256 +1 -0
  6. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.9.manifest.json → agent-device-android-snapshot-helper-0.20.10.manifest.json} +6 -6
  7. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +6 -5
  8. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +4 -4
  9. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +94 -1
  10. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +22 -23
  11. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +3 -23
  12. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +10 -11
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+PrivateAXPresentation.swift +30 -12
  14. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +8 -5
  15. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +133 -63
  16. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +13 -12
  17. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotPresentation.swift +183 -0
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotPresentationTests.swift +0 -0
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +20 -18
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputCandidatePolicy.swift +19 -0
  21. package/dist/src/agent-device-client.js +2 -2
  22. package/dist/src/ai-sdk.d.ts +61 -0
  23. package/dist/src/ai-sdk.js +1 -0
  24. package/dist/src/android-adb.d.ts +1 -1
  25. package/dist/src/app-lifecycle.js +1 -1
  26. package/dist/src/app-log-reconnect.js +1 -1
  27. package/dist/src/app-log-runtime.js +3 -3
  28. package/dist/src/app-resolution.js +2 -2
  29. package/dist/src/apps.js +5 -5
  30. package/dist/src/audio-probe.js +1 -1
  31. package/dist/src/boot-diagnostics.js +2 -2
  32. package/dist/src/cli-config.js +1 -1
  33. package/dist/src/cli-help.js +30 -23
  34. package/dist/src/cli-runner.js +1 -1
  35. package/dist/src/cli.js +7 -7
  36. package/dist/src/client-metro.js +1 -1
  37. package/dist/src/client-types.d.ts +1582 -0
  38. package/dist/src/command-tools.js +2 -0
  39. package/dist/src/connection-runtime.js +1 -1
  40. package/dist/src/contracts.d.ts +1 -1
  41. package/dist/src/contracts.js +1 -1
  42. package/dist/src/daemon-shutdown-report.js +1 -1
  43. package/dist/src/daemon.js +1 -1
  44. package/dist/src/debug-symbols.js +1 -1
  45. package/dist/src/device-input-state.js +1 -1
  46. package/dist/src/dispatch.js +1 -1
  47. package/dist/src/find.js +1 -1
  48. package/dist/src/finders.js +1 -1
  49. package/dist/src/generic-settle.js +1 -1
  50. package/dist/src/generic.js +1 -1
  51. package/dist/src/helper-package-install.js +1 -1
  52. package/dist/src/helper.js +1 -1
  53. package/dist/src/ime-lifecycle.js +1 -1
  54. package/dist/src/ime-recovery-marker.js +1 -1
  55. package/dist/src/index.d.ts +2 -1578
  56. package/dist/src/index.js +1 -1
  57. package/dist/src/input-actions.js +1 -1
  58. package/dist/src/input-actions3.js +1 -1
  59. package/dist/src/install-source2.js +1 -1
  60. package/dist/src/interaction-common.js +1 -1
  61. package/dist/src/interaction-runtime.js +1 -1
  62. package/dist/src/interaction-snapshot.js +1 -1
  63. package/dist/src/interaction.js +1 -1
  64. package/dist/src/interaction2.js +1 -1
  65. package/dist/src/interactor.js +2 -2
  66. package/dist/src/interactor2.js +1 -1
  67. package/dist/src/interactors.js +1 -1
  68. package/dist/src/internal/bin.js +2 -2
  69. package/dist/src/internal/daemon.js +4 -4
  70. package/dist/src/internal/png-worker.js +1 -1
  71. package/dist/src/lease-context.js +1 -1
  72. package/dist/src/lease.js +1 -1
  73. package/dist/src/limrun.d.ts +10 -2
  74. package/dist/src/linux.js +1 -1
  75. package/dist/src/location-coordinates.js +1 -1
  76. package/dist/src/{screenshot-geometry.js → mobile-snapshot-semantics.js} +1 -1
  77. package/dist/src/perf.js +1 -1
  78. package/dist/src/platform-runtime-open-target.js +1 -1
  79. package/dist/src/platform-runtime-operation-host.js +4 -4
  80. package/dist/src/platform-runtime.js +1 -1
  81. package/dist/src/platform-runtime2.js +1 -1
  82. package/dist/src/png-resize.js +1 -1
  83. package/dist/src/provider.js +1 -1
  84. package/dist/src/proxy.js +1 -1
  85. package/dist/src/react-native.js +1 -1
  86. package/dist/src/record-runtime.js +1 -1
  87. package/dist/src/registry.js +27 -27
  88. package/dist/src/remote-config2.js +1 -1
  89. package/dist/src/replay.js +2 -2
  90. package/dist/src/reporting.js +5 -5
  91. package/dist/src/runner-client.js +1 -1
  92. package/dist/src/runner-disposal.js +1 -1
  93. package/dist/src/runner-sequence.js +1 -1
  94. package/dist/src/runtime.js +3 -3
  95. package/dist/src/runtime2.js +4 -4
  96. package/dist/src/runtime3.js +1 -1
  97. package/dist/src/runtime4.js +3 -3
  98. package/dist/src/runtime5.js +1 -1
  99. package/dist/src/runtime6.js +1 -1
  100. package/dist/src/runtime7.js +1 -1
  101. package/dist/src/runtime8.js +1 -1
  102. package/dist/src/runtime9.js +1 -1
  103. package/dist/src/screenshot-result.js +4 -4
  104. package/dist/src/screenshot.js +1 -1
  105. package/dist/src/script-source-bundle.js +16 -0
  106. package/dist/src/sdk-android-adb.d.ts +40 -40
  107. package/dist/src/sdk-android-adb.js +4 -4
  108. package/dist/src/sdk-batch-runner.js +3 -3
  109. package/dist/src/sdk-contracts.d.ts +122 -85
  110. package/dist/src/sdk-contracts.js +2 -2
  111. package/dist/src/sdk-finders.d.ts +1 -1
  112. package/dist/src/sdk-finders.js +1 -1
  113. package/dist/src/sdk-selectors.d.ts +89 -77
  114. package/dist/src/sdk-selectors.js +1 -1
  115. package/dist/src/selector-runtime.js +1 -1
  116. package/dist/src/selectors.js +1 -1
  117. package/dist/src/server.js +10 -3
  118. package/dist/src/session-target-evidence.js +1 -1
  119. package/dist/src/session2.js +7 -8
  120. package/dist/src/shared.js +1 -1
  121. package/dist/src/snapshot-runtime.js +1 -1
  122. package/dist/src/snapshot-scope.js +1 -0
  123. package/dist/src/snapshot.js +6 -6
  124. package/dist/src/snapshot2.js +1 -1
  125. package/dist/src/snapshot3.js +1 -1
  126. package/dist/src/src.js +1 -1
  127. package/dist/src/src2.js +1 -1
  128. package/dist/src/src3.js +1 -1
  129. package/dist/src/src4.js +1 -0
  130. package/dist/src/tool-provider2.js +1 -1
  131. package/dist/src/tool-provider3.js +1 -0
  132. package/dist/src/verified-file.js +1 -1
  133. package/dist/src/web.js +1 -1
  134. package/package.json +21 -8
  135. package/server.json +2 -2
  136. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.9.apk.sha256 +0 -1
  137. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.9.apk.sha256 +0 -1
  138. package/dist/src/cancel.js +0 -1
  139. package/dist/src/prepare-kind.js +0 -15
  140. package/dist/src/selector-vocabulary.js +0 -1
@@ -0,0 +1 @@
1
+ 37cfdc9733315068659a6a4c1da8e65cd403e382401194f989f510fb0f320ba5 agent-device-android-ime-helper-0.20.10.apk
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "android-ime-helper",
3
- "version": "0.20.9",
4
- "assetName": "agent-device-android-ime-helper-0.20.9.apk",
5
- "sha256": "f98ded51f104c10268a75b7eb77f2e479461b844e49f238593d44b821d13e294",
3
+ "version": "0.20.10",
4
+ "assetName": "agent-device-android-ime-helper-0.20.10.apk",
5
+ "sha256": "37cfdc9733315068659a6a4c1da8e65cd403e382401194f989f510fb0f320ba5",
6
6
  "packageName": "com.callstack.agentdevice.imehelper",
7
- "versionCode": 20009,
7
+ "versionCode": 20010,
8
8
  "serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
9
9
  "broadcastProtocol": "android-ime-helper-v1"
10
10
  }
@@ -0,0 +1 @@
1
+ a5e8c39ef8dc5ca6fc1959dfcd52b00bd7845ce584abc0930f8d4a517bd784cc agent-device-android-snapshot-helper-0.20.10.apk
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "android-snapshot-helper",
3
- "version": "0.20.9",
4
- "releaseTag": "v0.20.9",
5
- "assetName": "agent-device-android-snapshot-helper-0.20.9.apk",
3
+ "version": "0.20.10",
4
+ "releaseTag": "v0.20.10",
5
+ "assetName": "agent-device-android-snapshot-helper-0.20.10.apk",
6
6
  "apkUrl": null,
7
- "sha256": "a5169f62fb38bb1980cf7c7963542f2bc5bbaafd4d9dc7b5b23d3e41ee782725",
8
- "checksumName": "agent-device-android-snapshot-helper-0.20.9.apk.sha256",
7
+ "sha256": "a5e8c39ef8dc5ca6fc1959dfcd52b00bd7845ce584abc0930f8d4a517bd784cc",
8
+ "checksumName": "agent-device-android-snapshot-helper-0.20.10.apk.sha256",
9
9
  "packageName": "com.callstack.agentdevice.snapshothelper",
10
- "versionCode": 20009,
10
+ "versionCode": 20010,
11
11
  "instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
12
12
  "minSdk": 23,
13
13
  "targetSdk": 36,
@@ -103,11 +103,11 @@ extension RunnerTests {
103
103
  }
104
104
  }
105
105
 
106
- func privateAXSnapshotCapture(
106
+ func privateAXSnapshotAcquisition(
107
107
  app: XCUIApplication,
108
- options: SnapshotOptions,
108
+ options: PresentationOptions,
109
109
  deadline: Date = .distantFuture
110
- ) -> SnapshotBackendCapture? {
110
+ ) -> SnapshotAcquisition? {
111
111
  #if os(iOS) && targetEnvironment(simulator)
112
112
  let requestedDepth = options.depth ?? 64
113
113
  // An explicit --depth request is honored as asked: no accepted-depth
@@ -202,8 +202,9 @@ extension RunnerTests {
202
202
  effectiveDepth,
203
203
  deepExtension?[RunnerAXSnapshotDeepExtensionNodesAddedKey] as? Int ?? 0
204
204
  )
205
- return SnapshotBackendCapture(
206
- payload: DataPayload(nodes: nodes, truncated: (response["truncated"] as? Bool) == true),
205
+ return SnapshotAcquisition(
206
+ nodes: nodes,
207
+ truncated: (response["truncated"] as? Bool) == true,
207
208
  effectiveDepth: depthLimited ? effectiveDepth : nil,
208
209
  customActions: Self.privateAXCustomActionCoverage(
209
210
  response[RunnerAXSnapshotCustomActionsKey]
@@ -584,10 +584,10 @@ extension RunnerTests {
584
584
  }
585
585
 
586
586
  /// Pure command→options projection, extracted so the runner unit bundle can
587
- /// prove the decoded wire field actually reaches capture options (#1634 P2).
588
- static func snapshotOptions(from command: Command) -> SnapshotOptions {
587
+ /// prove the decoded wire field actually reaches presentation options (#1634 P2).
588
+ static func presentationOptions(from command: Command) -> PresentationOptions {
589
589
  let customActions = command.customActions ?? false
590
- return SnapshotOptions(
590
+ return PresentationOptions(
591
591
  interactiveOnly: command.interactiveOnly ?? false,
592
592
  depth: command.depth,
593
593
  scope: command.scope,
@@ -602,7 +602,7 @@ extension RunnerTests {
602
602
  }
603
603
 
604
604
  private func executeSnapshotPrepared(command: Command, activeApp: XCUIApplication) throws -> Response {
605
- let options = Self.snapshotOptions(from: command)
605
+ let options = Self.presentationOptions(from: command)
606
606
  do {
607
607
  let payload: DataPayload
608
608
  if options.raw {
@@ -23,10 +23,103 @@ enum FlatSnapshotVisibilityPolicy {
23
23
  case viewportProjected
24
24
  }
25
25
 
26
+ enum FlatSnapshotGeometry {
27
+ case geometryless
28
+ case framed(intersectsViewportAndScrollClip: Bool)
29
+ }
30
+
31
+ enum FlatSnapshotDescendantVisibility: Equatable {
32
+ case independent
33
+ case owned
34
+ }
35
+
36
+ struct FlatSnapshotProjectionCursor {
37
+ static let root = FlatSnapshotProjectionCursor(ancestorProjectedOut: false)
38
+
39
+ private let ancestorProjectedOut: Bool
40
+
41
+ var isProjectedOut: Bool { ancestorProjectedOut }
42
+
43
+ func project(
44
+ geometry: FlatSnapshotGeometry,
45
+ descendantVisibility: FlatSnapshotDescendantVisibility
46
+ ) -> FlatSnapshotProjectionDecision {
47
+ let nodeProjectedOut: Bool
48
+ switch geometry {
49
+ case .geometryless:
50
+ nodeProjectedOut = false
51
+ case .framed(let intersectsViewportAndScrollClip):
52
+ nodeProjectedOut = !intersectsViewportAndScrollClip
53
+ }
54
+ let presentationVisible = !ancestorProjectedOut && !nodeProjectedOut
55
+ let descendantsProjectedOut = ancestorProjectedOut
56
+ || (nodeProjectedOut && descendantVisibility == .owned)
57
+ return FlatSnapshotProjectionDecision(
58
+ presentationVisible: presentationVisible,
59
+ descendants: FlatSnapshotProjectionCursor(
60
+ ancestorProjectedOut: descendantsProjectedOut
61
+ )
62
+ )
63
+ }
64
+ }
65
+
66
+ struct FlatSnapshotProjectionDecision {
67
+ let presentationVisible: Bool
68
+ let descendants: FlatSnapshotProjectionCursor
69
+ }
70
+
71
+ struct FlatSnapshotProjectionTransition {
72
+ let decision: FlatSnapshotProjectionDecision
73
+ let hiddenContentFrame: CGRect?
74
+ }
75
+
26
76
  extension RunnerTests {
77
+ func flatSnapshotGeometry(
78
+ frame: CGRect,
79
+ intersectsViewportAndScrollClip: Bool
80
+ ) -> FlatSnapshotGeometry {
81
+ if frame.isNull || frame.isEmpty { return .geometryless }
82
+ return .framed(intersectsViewportAndScrollClip: intersectsViewportAndScrollClip)
83
+ }
84
+
85
+ func flatSnapshotDescendantVisibility(
86
+ elementType: XCUIElement.ElementType?,
87
+ hasChildren: Bool
88
+ ) -> FlatSnapshotDescendantVisibility {
89
+ guard hasChildren, let elementType else { return .independent }
90
+ if elementType == .cell || Self.scrollContainerTypes.contains(elementType) {
91
+ return .owned
92
+ }
93
+ return .independent
94
+ }
95
+
96
+ func flatSnapshotProjectionTransition(
97
+ frame: CGRect,
98
+ intersectsViewportAndScrollClip: Bool,
99
+ elementType: XCUIElement.ElementType?,
100
+ hasChildren: Bool,
101
+ cursor: FlatSnapshotProjectionCursor
102
+ ) -> FlatSnapshotProjectionTransition {
103
+ let hasFrame = !frame.isNull && !frame.isEmpty
104
+ return FlatSnapshotProjectionTransition(
105
+ decision: cursor.project(
106
+ geometry: flatSnapshotGeometry(
107
+ frame: frame,
108
+ intersectsViewportAndScrollClip: intersectsViewportAndScrollClip
109
+ ),
110
+ descendantVisibility: flatSnapshotDescendantVisibility(
111
+ elementType: elementType,
112
+ hasChildren: hasChildren
113
+ )
114
+ ),
115
+ hiddenContentFrame: !cursor.isProjectedOut && hasFrame
116
+ && !intersectsViewportAndScrollClip ? frame : nil
117
+ )
118
+ }
119
+
27
120
  func flatSnapshotFilterDecision(
28
121
  _ node: FlatSnapshotFilterNode,
29
- options: SnapshotOptions,
122
+ options: PresentationOptions,
30
123
  visibilityPolicy: FlatSnapshotVisibilityPolicy,
31
124
  insideMatchedScope: Bool
32
125
  ) -> FlatSnapshotFilterDecision {
@@ -229,21 +229,23 @@ extension RunnerTests {
229
229
  let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
230
230
  let valueText = String(describing: element.value ?? "")
231
231
  .trimmingCharacters(in: .whitespacesAndNewlines)
232
- let node = SnapshotNode(
233
- index: 0,
234
- type: elementTypeName(element.elementType),
235
- label: label.isEmpty ? nil : label,
236
- identifier: identifier.isEmpty ? nil : identifier,
237
- value: valueText.isEmpty ? nil : valueText,
238
- rect: snapshotRect(from: element.frame),
239
- enabled: element.isEnabled,
240
- focused: nil,
241
- selected: element.isSelected ? true : nil,
242
- hittable: element.isHittable,
243
- depth: 0,
244
- parentIndex: nil,
245
- hiddenContentAbove: nil,
246
- hiddenContentBelow: nil
232
+ let node = SnapshotPresentation.singleElementRead(
233
+ RawAXNode(
234
+ index: 0,
235
+ type: elementTypeName(element.elementType),
236
+ label: label.isEmpty ? nil : label,
237
+ identifier: identifier.isEmpty ? nil : identifier,
238
+ value: valueText.isEmpty ? nil : valueText,
239
+ rect: snapshotRect(from: element.frame),
240
+ enabled: element.isEnabled,
241
+ focused: nil,
242
+ selected: element.isSelected ? true : nil,
243
+ hittable: element.isHittable,
244
+ depth: 0,
245
+ parentIndex: nil,
246
+ hiddenContentAbove: nil,
247
+ hiddenContentBelow: nil
248
+ )
247
249
  )
248
250
  return Response(
249
251
  ok: true,
@@ -317,7 +319,11 @@ extension RunnerTests {
317
319
  .filter { element in
318
320
  guard element.exists else { return false }
319
321
  let frame = element.frame
320
- return !frame.isEmpty && frameContainsPoint(frame, point, tolerance: 2)
322
+ return isCoordinateTextInputCandidate(
323
+ enabled: element.isEnabled,
324
+ frame: frame,
325
+ point: point
326
+ )
321
327
  }
322
328
  .sorted { left, right in
323
329
  let leftArea = max(1, left.frame.width * left.frame.height)
@@ -336,13 +342,6 @@ extension RunnerTests {
336
342
  }
337
343
  }
338
344
 
339
- private func frameContainsPoint(_ frame: CGRect, _ point: CGPoint, tolerance: CGFloat) -> Bool {
340
- point.x >= frame.minX - tolerance
341
- && point.x <= frame.maxX + tolerance
342
- && point.y >= frame.minY - tolerance
343
- && point.y <= frame.maxY + tolerance
344
- }
345
-
346
345
  private func readableText(for element: XCUIElement) -> String? {
347
346
  let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
348
347
  let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -235,7 +235,7 @@ struct DataPayload: Codable {
235
235
  let text: String?
236
236
  let found: Bool?
237
237
  let items: [String]?
238
- let nodes: [SnapshotNode]?
238
+ let nodes: [PresentedNode]?
239
239
  let truncated: Bool?
240
240
  let snapshotQuality: SnapshotQuality?
241
241
  let gestureStartUptimeMs: Double?
@@ -277,7 +277,7 @@ struct DataPayload: Codable {
277
277
  text: String? = nil,
278
278
  found: Bool? = nil,
279
279
  items: [String]? = nil,
280
- nodes: [SnapshotNode]? = nil,
280
+ nodes: [PresentedNode]? = nil,
281
281
  truncated: Bool? = nil,
282
282
  snapshotQuality: SnapshotQuality? = nil,
283
283
  gestureStartUptimeMs: Double? = nil,
@@ -375,27 +375,7 @@ struct SnapshotRect: Codable {
375
375
  let height: Double
376
376
  }
377
377
 
378
- struct SnapshotNode: Codable {
379
- let index: Int
380
- let type: String
381
- let label: String?
382
- let identifier: String?
383
- let value: String?
384
- let rect: SnapshotRect
385
- let enabled: Bool
386
- let focused: Bool?
387
- let selected: Bool?
388
- let hittable: Bool
389
- let depth: Int
390
- let parentIndex: Int?
391
- let hiddenContentAbove: Bool?
392
- let hiddenContentBelow: Bool?
393
- /// Names of the element's UIAccessibilityCustomActions, present only when the
394
- /// capture asked for them and the element carries some.
395
- var actions: [String]? = nil
396
- }
397
-
398
- struct SnapshotOptions {
378
+ struct PresentationOptions {
399
379
  let interactiveOnly: Bool
400
380
  let depth: Int?
401
381
  let scope: String?
@@ -81,17 +81,16 @@ extension RunnerTests {
81
81
  return navigationBackKeywords.firstIndex { text.contains($0) }
82
82
  }
83
83
 
84
+ // isFinite/>0 alone don't reject CGRect.infinite — its origin (~-9e307) is finite.
85
+ static func isUsableNavigationFrame(_ frame: CGRect) -> Bool {
86
+ guard frame.width.isFinite, frame.height.isFinite, frame.width > 0, frame.height > 0 else {
87
+ return false
88
+ }
89
+ return !frame.isInfinite
90
+ }
91
+
84
92
  static func isTopNavigationControlFrame(_ candidate: CGRect, in window: CGRect) -> Bool {
85
- guard
86
- candidate.width.isFinite,
87
- candidate.height.isFinite,
88
- window.width.isFinite,
89
- window.height.isFinite,
90
- candidate.width > 0,
91
- candidate.height > 0,
92
- window.width > 0,
93
- window.height > 0
94
- else {
93
+ guard isUsableNavigationFrame(candidate), isUsableNavigationFrame(window) else {
95
94
  return false
96
95
  }
97
96
  // Accept the compact navigation/search header band without matching deep content controls.
@@ -100,7 +99,7 @@ extension RunnerTests {
100
99
  }
101
100
 
102
101
  static func topLeadingNavigationFallbackPoint(in frame: CGRect) -> CGPoint? {
103
- guard frame.width.isFinite, frame.height.isFinite, frame.width > 0, frame.height > 0 else {
102
+ guard isUsableNavigationFrame(frame) else {
104
103
  return nil
105
104
  }
106
105
  // Aim at the standard leading navigation slot, bounded for compact and tablet widths.
@@ -1,20 +1,23 @@
1
1
  import XCTest
2
2
 
3
3
  extension RunnerTests {
4
- func privateAXPresentation(rawRoot: [String: Any], options: SnapshotOptions, viewport: CGRect)
5
- -> [SnapshotNode]
4
+ private static let privateAXProjectionTolerance: CGFloat = 1
5
+
6
+ func privateAXPresentation(rawRoot: [String: Any], options: PresentationOptions, viewport: CGRect)
7
+ -> [RawAXNode]
6
8
  {
7
- var nodes: [SnapshotNode] = []
9
+ var nodes: [RawAXNode] = []
8
10
  var hints: [Int: (above: Bool, below: Bool)] = [:]
9
11
  appendPrivateAXNode(rawRoot, to: &nodes, hints: &hints, options: options, viewport: viewport,
10
- depth: 0, parentIndex: nil, insideMatchedScope: false, scrollContext: nil)
12
+ depth: 0, parentIndex: nil, insideMatchedScope: false, scrollContext: nil,
13
+ projectionCursor: .root)
11
14
  return applyHiddenContentHints(hints, to: nodes)
12
15
  }
13
16
 
14
- private func appendPrivateAXNode(_ raw: [String: Any], to nodes: inout [SnapshotNode],
15
- hints: inout [Int: (above: Bool, below: Bool)], options: SnapshotOptions, viewport: CGRect,
17
+ private func appendPrivateAXNode(_ raw: [String: Any], to nodes: inout [RawAXNode],
18
+ hints: inout [Int: (above: Bool, below: Bool)], options: PresentationOptions, viewport: CGRect,
16
19
  depth: Int, parentIndex: Int?, insideMatchedScope: Bool,
17
- scrollContext: (index: Int, rect: CGRect)?)
20
+ scrollContext: (index: Int, rect: CGRect)?, projectionCursor: FlatSnapshotProjectionCursor)
18
21
  {
19
22
  if let limit = options.depth, depth > limit { return }
20
23
  let rect = privateAXRect(raw["frame"])
@@ -24,15 +27,29 @@ extension RunnerTests {
24
27
  let rawType = privateAXPresentationInt(raw["type"]) ?? 0
25
28
  let enabled = privateAXPresentationBool(raw["enabled"]) ?? true
26
29
  let children = raw["children"] as? [[String: Any]] ?? []
30
+ let hasSemanticContent = [label, identifier, value].contains(where: { !$0.isEmpty })
27
31
  let elementType = flatSnapshotElementType(rawElementType: rawType)
28
32
  let hasFrame = !rect.isNull && !rect.isEmpty
33
+ let negligibleDecoration = parentIndex != nil
34
+ && !hasSemanticContent
35
+ && (!hasFrame
36
+ || rect.width <= Self.privateAXProjectionTolerance
37
+ || rect.height <= Self.privateAXProjectionTolerance)
29
38
  let onScreen = hasFrame
30
39
  && isVisibleInRegularSnapshot(
31
40
  rect,
32
41
  viewport: viewport,
33
42
  scrollContainerAnchor: scrollContext
34
43
  )
35
- let presentationVisible = !hasFrame || onScreen
44
+ let projectionTransition = flatSnapshotProjectionTransition(
45
+ frame: rect,
46
+ intersectsViewportAndScrollClip: onScreen,
47
+ elementType: elementType,
48
+ hasChildren: !children.isEmpty,
49
+ cursor: projectionCursor
50
+ )
51
+ let projection = projectionTransition.decision
52
+ let presentationVisible = projection.presentationVisible && !negligibleDecoration
36
53
  let decision = flatSnapshotFilterDecision(
37
54
  FlatSnapshotFilterNode(isRoot: parentIndex == nil, label: label, identifier: identifier,
38
55
  valueText: value.isEmpty ? nil : value, visible: presentationVisible),
@@ -40,15 +57,15 @@ extension RunnerTests {
40
57
  insideMatchedScope: insideMatchedScope)
41
58
  let include = decision.include
42
59
 
43
- if !presentationVisible, let scrollContext {
44
- rememberHiddenContentHint(for: rect, relativeTo: scrollContext, hints: &hints)
60
+ if let hiddenFrame = projectionTransition.hiddenContentFrame, let scrollContext {
61
+ rememberHiddenContentHint(for: hiddenFrame, relativeTo: scrollContext, hints: &hints)
45
62
  }
46
63
 
47
64
  let currentIndex: Int?
48
65
  if include {
49
66
  currentIndex = nodes.count
50
67
  let typeName = elementType.map(elementTypeName) ?? "Element(\(rawType))"
51
- nodes.append(SnapshotNode(index: nodes.count, type: typeName,
68
+ nodes.append(RawAXNode(index: nodes.count, type: typeName,
52
69
  label: label.isEmpty ? nil : label, identifier: identifier.isEmpty ? nil : identifier,
53
70
  value: value.isEmpty ? nil : value, rect: snapshotRect(from: rect), enabled: enabled,
54
71
  focused: privateAXPresentationBool(raw["focused"]) == true ? true : nil,
@@ -71,7 +88,8 @@ extension RunnerTests {
71
88
  for child in children {
72
89
  appendPrivateAXNode(child, to: &nodes, hints: &hints, options: options, viewport: viewport,
73
90
  depth: depth + 1, parentIndex: currentIndex,
74
- insideMatchedScope: decision.insideMatchedScope, scrollContext: nextScrollContext)
91
+ insideMatchedScope: decision.insideMatchedScope, scrollContext: nextScrollContext,
92
+ projectionCursor: projection.descendants)
75
93
  }
76
94
  }
77
95
 
@@ -3,10 +3,10 @@ import XCTest
3
3
  // Swift port of buildScrollGesturePlan from packages/contracts/src/scroll-gesture.ts.
4
4
  //
5
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
8
- // packages/contracts/src/scroll-gesture.test.ts —
9
- // if you change the math in either language, update the other and both vector sets.
6
+ // recording, and reported-pixels), and the runner places the gesture with this Swift copy. Both
7
+ // ports are asserted against the same table, contracts/fixtures/scroll-gesture.json (gated
8
+ // XCTest at the bottom of this file, vitest twin packages/contracts/src/scroll-gesture.test.ts) —
9
+ // if you change the math in either language, update the other and the table.
10
10
  //
11
11
  // All inputs here are positive (reference dims, travel, center), so Swift's `.rounded()`
12
12
  // (half away from zero) matches JS `Math.round` (half up) on every value computed below.
@@ -20,7 +20,10 @@ struct RunnerScrollGesturePlan {
20
20
  }
21
21
 
22
22
  private let runnerDefaultScrollAmount = 0.6
23
- private let runnerDefaultEdgePaddingFraction = 0.05
23
+ // Both constants are pinned by contracts/fixtures/scroll-gesture.json (`constants`). Scroll gestures
24
+ // stay out of the outer 10% of each axis so a saturated scroll never touches down inside the status
25
+ // bar / Dynamic Island band (#1781 A1).
26
+ private let runnerDefaultEdgePaddingFraction = 0.1
24
27
 
25
28
  func runnerScrollGesturePlan(
26
29
  direction: String,