agent-device 0.21.8 → 0.21.9

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 (171) hide show
  1. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.8.apk → agent-device-android-ime-helper-0.21.9.apk} +0 -0
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.9.apk.sha256 +1 -0
  3. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.21.8.manifest.json → agent-device-android-ime-helper-0.21.9.manifest.json} +4 -4
  4. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.8.apk → agent-device-android-snapshot-helper-0.21.9.apk} +0 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.9.apk.sha256 +1 -0
  6. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.21.8.manifest.json → agent-device-android-snapshot-helper-0.21.9.manifest.json} +6 -6
  7. package/apple/macos-helper/Sources/AgentDeviceMacOSDeviceHub/DeviceHubWindowInventory.swift +65 -0
  8. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubPose.swift +45 -140
  9. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubSidebar.swift +57 -0
  10. package/apple/macos-helper/Sources/AgentDeviceMacOSHelper/DeviceHubWindowDiscovery.swift +108 -0
  11. package/apple/macos-helper/Tests/AgentDeviceMacOSDeviceHubTests/DeviceHubWindowInventoryTests.swift +65 -0
  12. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAppScreenCapture.swift +175 -0
  14. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +10 -2
  15. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +42 -15
  16. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +8 -0
  17. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +45 -22
  18. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +39 -734
  19. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +20 -4
  20. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +63 -1
  21. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +13 -0
  22. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +25 -10
  23. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +5 -562
  24. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotAcquisition.swift +566 -0
  25. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +5 -1
  26. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedInteraction.swift +594 -0
  27. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.h +35 -1
  28. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerXCTestEventBridge.m +160 -0
  29. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/RecordingExportSupport.swift +3 -0
  30. package/dist/apple/runner/AgentDeviceRunner/RecordingScripts/recording-frames.swift +203 -0
  31. package/dist/apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotCoordinateSpace.swift +5 -0
  32. package/dist/src/absence-observation-errors.js +1 -1
  33. package/dist/src/adapter.js +1 -1
  34. package/dist/src/agent-browser-lifecycle.js +1 -1
  35. package/dist/src/agent-browser-process-record.js +1 -1
  36. package/dist/src/agent-device-client.js +1 -1
  37. package/dist/src/alert-runtime.js +1 -1
  38. package/dist/src/android-adb.js +1 -1
  39. package/dist/src/android-input-ownership.js +1 -1
  40. package/dist/src/android.js +1 -1
  41. package/dist/src/app-launch.js +1 -1
  42. package/dist/src/app-log-runtime2.js +3 -3
  43. package/dist/src/app-resolution.js +2 -2
  44. package/dist/src/apple-runner-platform.js +1 -1
  45. package/dist/src/application-lifecycle-interaction.js +1 -1
  46. package/dist/src/artifact-paths.js +1 -0
  47. package/dist/src/back-mode.js +1 -1
  48. package/dist/src/cli-config.js +1 -1
  49. package/dist/src/cli-help.js +8 -7
  50. package/dist/src/cli.js +4 -4
  51. package/dist/src/client-types.d.ts +23 -5
  52. package/dist/src/command-tools.js +3 -3
  53. package/dist/src/connection-runtime.js +1 -1
  54. package/dist/src/device-claim-inspection.js +1 -1
  55. package/dist/src/device-claim-rule.js +2 -2
  56. package/dist/src/device-input-state.d.ts +1 -1
  57. package/dist/src/device-selection-resolver.js +1 -1
  58. package/dist/src/device.js +1 -1
  59. package/dist/src/device2.js +1 -1
  60. package/dist/src/dispatch-resolve.js +1 -1
  61. package/dist/src/doctor-facade.js +1 -1
  62. package/dist/src/durable-resource.js +1 -1
  63. package/dist/src/find.js +1 -1
  64. package/dist/src/finders.js +1 -1
  65. package/dist/src/fold-runtime.js +1 -0
  66. package/dist/src/gesture-admission.js +1 -1
  67. package/dist/src/interaction-positionals.js +1 -1
  68. package/dist/src/interaction-ref-policy.js +1 -1
  69. package/dist/src/interaction.js +1 -1
  70. package/dist/src/interaction2.js +1 -1
  71. package/dist/src/interactor-operation-catalog.js +1 -1
  72. package/dist/src/interactor.js +4 -4
  73. package/dist/src/interactors.js +1 -1
  74. package/dist/src/internal/bin.js +1 -1
  75. package/dist/src/internal/daemon.js +2 -2
  76. package/dist/src/inventory2.js +1 -1
  77. package/dist/src/ios-snapshot-acquisition.js +1 -1
  78. package/dist/src/ios-snapshot-runtime.js +1 -1
  79. package/dist/src/lease.js +1 -1
  80. package/dist/src/local-interactor-operation-set.js +1 -1
  81. package/dist/src/location-coordinates.js +1 -1
  82. package/dist/src/managed-device-scope.js +1 -1
  83. package/dist/src/mechanics.js +17 -17
  84. package/dist/src/mobile-snapshot-semantics.js +1 -1
  85. package/dist/src/mp4-duration.js +1 -0
  86. package/dist/src/multitouch-support.js +1 -1
  87. package/dist/src/owner-identity.js +1 -1
  88. package/dist/src/parameterized-recorded-fill.js +1 -1
  89. package/dist/src/perf-facade.js +2 -2
  90. package/dist/src/perf-runtime-plan.js +1 -1
  91. package/dist/src/platform-request-scope.js +1 -1
  92. package/dist/src/platform-runtime-managed-owner.js +1 -1
  93. package/dist/src/platform-runtime-open-target.js +1 -1
  94. package/dist/src/platform-runtime-operation-host.js +2 -2
  95. package/dist/src/platform-runtime-runtime-hints.js +1 -1
  96. package/dist/src/platform-runtime-screen-recording-finalizer-host.js +1 -1
  97. package/dist/src/platform-runtime-unavailable.js +1 -1
  98. package/dist/src/platform-runtime.js +1 -1
  99. package/dist/src/platform-runtime2.js +1 -1
  100. package/dist/src/png-crop-bytes.js +1 -1
  101. package/dist/src/png-encode.js +1 -0
  102. package/dist/src/png-size.js +1 -0
  103. package/dist/src/provider-webdriver.js +1 -1
  104. package/dist/src/quality-warnings.js +1 -1
  105. package/dist/src/react-native.js +1 -1
  106. package/dist/src/record-runtime.js +1 -1
  107. package/dist/src/recording-export-quality.js +1 -1
  108. package/dist/src/recording-scope.js +1 -0
  109. package/dist/src/recording.js +3 -0
  110. package/dist/src/registry.js +35 -35
  111. package/dist/src/replay.js +2 -2
  112. package/dist/src/request-admission.js +1 -1
  113. package/dist/src/request-runtime-binding.js +1 -1
  114. package/dist/src/result-serialization.js +1 -1
  115. package/dist/src/runner-cache-metadata.js +2 -2
  116. package/dist/src/runner-client.js +1 -1
  117. package/dist/src/runner-contract.js +1 -1
  118. package/dist/src/runner-dev-tools-security.js +1 -1
  119. package/dist/src/runner-device-readiness.js +1 -1
  120. package/dist/src/runner-disposal.js +1 -1
  121. package/dist/src/runner-operations-facade.js +1 -1
  122. package/dist/src/runner-presentation.js +1 -1
  123. package/dist/src/runner.js +1 -1
  124. package/dist/src/runtime.js +2 -2
  125. package/dist/src/runtime10.js +2 -1
  126. package/dist/src/runtime2.js +4 -4
  127. package/dist/src/runtime4.js +5 -5
  128. package/dist/src/runtime5.js +1 -1
  129. package/dist/src/runtime6.js +1 -1
  130. package/dist/src/runtime7.js +1 -1
  131. package/dist/src/runtime8.js +1 -1
  132. package/dist/src/runtime9.js +1 -1
  133. package/dist/src/screen-recording-admission-ledger.js +1 -1
  134. package/dist/src/screen-recording-resource-recovery.js +1 -1
  135. package/dist/src/screen-recording-transport.js +1 -1
  136. package/dist/src/screenshot-density.js +1 -1
  137. package/dist/src/screenshot-overlay-draw.js +1 -0
  138. package/dist/src/screenshot-runtime.js +1 -1
  139. package/dist/src/script.js +1 -1
  140. package/dist/src/scroll-gesture.js +1 -1
  141. package/dist/src/sdk-batch.js +2 -2
  142. package/dist/src/sdk-contracts.d.ts +7 -5
  143. package/dist/src/sdk-device.js +1 -1
  144. package/dist/src/sdk-finders.js +1 -1
  145. package/dist/src/sdk-selectors.d.ts +44 -1
  146. package/dist/src/sdk-selectors.js +1 -1
  147. package/dist/src/selection.js +0 -0
  148. package/dist/src/selector-runtime.js +1 -1
  149. package/dist/src/selectors.js +1 -1
  150. package/dist/src/semantic-index.js +1 -1
  151. package/dist/src/session-routing.js +1 -1
  152. package/dist/src/session-snapshot.js +1 -1
  153. package/dist/src/session-store.js +3 -3
  154. package/dist/src/session2.js +3 -3
  155. package/dist/src/simctl.js +1 -1
  156. package/dist/src/simulator-boot.js +1 -1
  157. package/dist/src/snapshot-runtime-binding.js +1 -1
  158. package/dist/src/snapshot-runtime.js +1 -1
  159. package/dist/src/snapshot-state.js +1 -1
  160. package/dist/src/snapshot4.js +1 -1
  161. package/dist/src/src.js +1 -1
  162. package/dist/src/src7.js +1 -1
  163. package/dist/src/stop-sequence.js +1 -1
  164. package/dist/src/text-entry-focus.js +1 -1
  165. package/dist/src/tree.js +1 -1
  166. package/dist/src/type-text-runtime.js +1 -1
  167. package/dist/src/video.js +2 -2
  168. package/package.json +1 -1
  169. package/server.json +2 -2
  170. package/android/ime-helper/dist/agent-device-android-ime-helper-0.21.8.apk.sha256 +0 -1
  171. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.21.8.apk.sha256 +0 -1
@@ -0,0 +1,566 @@
1
+ import XCTest
2
+ import AgentDeviceSnapshotPresentation
3
+
4
+ extension RunnerTests {
5
+ func interactiveRootNode(rect: CGRect) -> RawAXNode {
6
+ RawAXNode(
7
+ index: 0,
8
+ type: "Application",
9
+ label: nil,
10
+ identifier: nil,
11
+ value: nil,
12
+ rect: SnapshotRect(rect),
13
+ enabled: true,
14
+ focused: nil,
15
+ selected: nil,
16
+ hittable: false,
17
+ depth: 0,
18
+ parentIndex: nil,
19
+ hiddenContentAbove: nil,
20
+ hiddenContentBelow: nil
21
+ )
22
+ }
23
+
24
+ func interactiveRootFrame(for candidates: [RawAXNode]) -> CGRect {
25
+ guard !candidates.isEmpty else {
26
+ return .zero
27
+ }
28
+ let maxX = candidates.map { CGFloat($0.rect.x + $0.rect.width) }.max() ?? 0
29
+ let maxY = candidates.map { CGFloat($0.rect.y + $0.rect.height) }.max() ?? 0
30
+ return CGRect(x: 0, y: 0, width: max(1, maxX), height: max(1, maxY))
31
+ }
32
+
33
+
34
+
35
+ func makeSnapshotTraversalContext(
36
+ app: XCUIApplication,
37
+ hint: CaptureHint,
38
+ captureDeadline: Date = .distantFuture,
39
+ treeCaptureSliceBudgetOverride: TimeInterval? = nil
40
+ ) throws -> SnapshotTraversalContext? {
41
+
42
+
43
+
44
+ let geometry = try runMainThreadWork(
45
+ "snapshot_viewport",
46
+ timeout: min(1.0, max(0.1, captureDeadline.timeIntervalSinceNow)),
47
+ timeoutError: snapshotMainThreadTimeoutError("preparing tree snapshot")
48
+ ) {
49
+ (
50
+ viewport: self.safeSnapshotViewport(app: app),
51
+ interfaceOrientation: self.capturedInterfaceOrientation(app: app)
52
+ )
53
+ }
54
+ let viewport = geometry.viewport
55
+ let interfaceOrientation = geometry.interfaceOrientation
56
+ let treeSliceBudget = treeCaptureSliceBudgetOverride ?? treeCaptureSliceBudget
57
+ let slice = min(treeSliceBudget, max(0.5, captureDeadline.timeIntervalSinceNow))
58
+ guard let rootSnapshot = try captureSnapshotRootBounded(app, sliceSeconds: slice) else {
59
+ return nil
60
+ }
61
+
62
+
63
+
64
+
65
+
66
+
67
+ let keyboardBand = captureKeyboardBandFact(app: app, deadline: captureDeadline)
68
+
69
+ return SnapshotTraversalContext(
70
+ queryRoot: app,
71
+ rootSnapshot: rootSnapshot,
72
+ viewport: viewport,
73
+ interfaceOrientation: interfaceOrientation,
74
+ keyboardBand: keyboardBand
75
+ )
76
+ }
77
+
78
+ static let xCTestSnapshotTimeoutCode = "IOS_TREE_CAPTURE_TIMEOUT"
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+ private func captureSnapshotRootBounded(
87
+ _ element: XCUIElement,
88
+ sliceSeconds: TimeInterval
89
+ ) throws -> XCUIElementSnapshot? {
90
+ if Thread.isMainThread {
91
+ return try captureSnapshotRoot(element)
92
+ }
93
+ return try runMainThreadWork(
94
+ "tree_capture",
95
+ timeout: sliceSeconds,
96
+ timeoutError: treeCaptureTimeoutError(sliceSeconds: sliceSeconds)
97
+ ) {
98
+ try self.captureSnapshotRoot(element)
99
+ }
100
+ }
101
+
102
+ private func treeCaptureTimeoutError(sliceSeconds: TimeInterval) -> () -> Error {
103
+ {
104
+ SnapshotCaptureFailure(
105
+ code: Self.xCTestSnapshotTimeoutCode,
106
+ message: "the XCTest tree capture exceeded its \(Int(sliceSeconds))s time slice",
107
+ hint: "The capture plan will avoid or tightly bound XCTest-backed snapshot tiers on this screen."
108
+ )
109
+ }
110
+ }
111
+
112
+ func snapshotMainThreadTimeoutError(_ operation: String) -> () -> Error {
113
+ {
114
+ SnapshotCaptureFailure(
115
+ code: Self.xCTestSnapshotTimeoutCode,
116
+ message: "timed out while \(operation) on the XCTest main thread",
117
+ hint: "The capture plan will skip XCTest-backed snapshot tiers while the previous main-thread work drains."
118
+ )
119
+ }
120
+ }
121
+
122
+ private func captureSnapshotRoot(_ element: XCUIElement) throws -> XCUIElementSnapshot? {
123
+ var rootSnapshot: XCUIElementSnapshot?
124
+ var swiftErrorMessage: String?
125
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
126
+ do {
127
+ rootSnapshot = try element.snapshot()
128
+ } catch {
129
+ swiftErrorMessage = describeSnapshotError(error)
130
+ }
131
+ })
132
+
133
+ if let rootSnapshot {
134
+ return rootSnapshot
135
+ }
136
+ let message = exceptionMessage ?? swiftErrorMessage ?? "snapshot returned no root"
137
+ if Self.isAxIllegalArgument(message) {
138
+ throw axSnapshotFailure(message)
139
+ }
140
+ return nil
141
+ }
142
+
143
+ func safeSnapshotViewport(app: XCUIApplication) -> CGRect {
144
+ safely("SNAPSHOT_VIEWPORT", CGRect.infinite) { snapshotViewport(app: app) }
145
+ }
146
+
147
+ private func describeSnapshotError(_ error: Error) -> String {
148
+ let localized = error.localizedDescription
149
+ let debug = String(describing: error)
150
+ if localized.isEmpty { return debug }
151
+ if debug == localized { return localized }
152
+ return "\(localized) (\(debug))"
153
+ }
154
+
155
+ private func axSnapshotFailure(_ message: String) -> SnapshotCaptureFailure {
156
+ let detail = message.trimmingCharacters(in: .whitespacesAndNewlines)
157
+ let failureMessage: String
158
+ if detail.isEmpty {
159
+ failureMessage = Self.axSnapshotFailureMessage
160
+ } else {
161
+ failureMessage = "\(Self.axSnapshotFailureMessage) \(detail)"
162
+ }
163
+ return SnapshotCaptureFailure(
164
+ code: Self.axSnapshotErrorCode,
165
+ message: failureMessage,
166
+ hint: Self.axSnapshotHint
167
+ )
168
+ }
169
+
170
+ private static func isAxIllegalArgument(_ message: String) -> Bool {
171
+ let normalized = message.lowercased()
172
+ return normalized.contains("kaxerrorillegalargument")
173
+ || (normalized.contains("illegal argument") && normalized.contains("snapshot"))
174
+ }
175
+
176
+ static func isAxSnapshotFailure(_ failure: SnapshotCaptureFailure) -> Bool {
177
+ failure.code == Self.axSnapshotErrorCode || isAxIllegalArgument(failure.message)
178
+ }
179
+
180
+ func evaluateSnapshot(_ snapshot: XCUIElementSnapshot) -> SnapshotEvaluation {
181
+ let label = aggregatedLabel(for: snapshot) ?? snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
182
+ let identifier = snapshot.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
183
+ let valueText = snapshotValueText(snapshot)
184
+ return SnapshotEvaluation(
185
+ label: label,
186
+ identifier: identifier,
187
+ valueText: valueText,
188
+ focused: snapshotHasFocus(snapshot),
189
+ selected: snapshotIsSelected(snapshot)
190
+ )
191
+ }
192
+
193
+ func makeSnapshotNode(
194
+ snapshot: XCUIElementSnapshot,
195
+ evaluation: SnapshotEvaluation,
196
+ depth: Int,
197
+ index: Int,
198
+ parentIndex: Int?
199
+ ) -> RawAXNode {
200
+
201
+
202
+ return RawAXNode(
203
+ index: index,
204
+ type: elementTypeName(snapshot.elementType),
205
+ label: evaluation.label.isEmpty ? nil : evaluation.label,
206
+ identifier: evaluation.identifier.isEmpty ? nil : evaluation.identifier,
207
+ value: evaluation.valueText,
208
+ rect: SnapshotRect(snapshot.frame),
209
+ enabled: snapshot.isEnabled,
210
+ focused: evaluation.focused ? true : nil,
211
+ selected: evaluation.selected ? true : nil,
212
+ hittable: false,
213
+ depth: depth,
214
+ parentIndex: parentIndex,
215
+ hiddenContentAbove: nil,
216
+ hiddenContentBelow: nil
217
+ )
218
+ }
219
+
220
+
221
+
222
+ func capturedInterfaceOrientation(app: XCUIApplication) -> Int {
223
+ safely("SNAPSHOT_INTERFACE_ORIENTATION", RunnerInterfaceOrientation.unknown) {
224
+ Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
225
+ }
226
+ }
227
+
228
+ private func snapshotValueText(_ snapshot: XCUIElementSnapshot) -> String? {
229
+ guard let value = snapshot.value else { return nil }
230
+ let text = String(describing: value).trimmingCharacters(in: .whitespacesAndNewlines)
231
+ return text.isEmpty ? nil : text
232
+ }
233
+
234
+ private func snapshotViewport(app: XCUIApplication) -> CGRect {
235
+ #if os(iOS)
236
+ let appFrame = onScreenWindowFrame(app: app)
237
+ #else
238
+ let appFrame = app.frame
239
+ #endif
240
+ if !appFrame.isNull && !appFrame.isEmpty {
241
+ return appFrame
242
+ }
243
+ return .infinite
244
+ }
245
+
246
+ static func snapshotTraversalIdentity(
247
+ elementType: XCUIElement.ElementType,
248
+ label: String,
249
+ identifier: String,
250
+ frame: CGRect
251
+ ) -> String {
252
+ #if os(iOS)
253
+ "\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)-\(frame.width)-\(frame.height)"
254
+ #else
255
+ return "\(elementType)-\(label)-\(identifier)-\(frame.origin.x)-\(frame.origin.y)"
256
+ #endif
257
+ }
258
+
259
+ private func aggregatedLabel(for snapshot: XCUIElementSnapshot, depth: Int = 0) -> String? {
260
+ if depth > 4 { return nil }
261
+ let text = snapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
262
+ if !text.isEmpty { return text }
263
+ if let valueText = snapshotValueText(snapshot) { return valueText }
264
+ for child in snapshot.children {
265
+ if let childLabel = aggregatedLabel(for: child, depth: depth + 1) {
266
+ return childLabel
267
+ }
268
+ }
269
+ return nil
270
+ }
271
+
272
+ func appendCollapsedTabFallbackNodes(
273
+ to nodes: inout [RawAXNode],
274
+ containerSnapshot: XCUIElementSnapshot,
275
+ resolveElements: () -> [XCUIElement],
276
+ depth: Int,
277
+ parentIndex: Int
278
+ ) {
279
+ let fallbackNodes = collapsedTabFallbackNodes(
280
+ for: containerSnapshot,
281
+ resolveElements: resolveElements,
282
+ startingIndex: nodes.count,
283
+ depth: depth,
284
+ parentIndex: parentIndex
285
+ )
286
+ nodes.append(contentsOf: fallbackNodes)
287
+ }
288
+
289
+ private func collapsedTabFallbackNodes(
290
+ for containerSnapshot: XCUIElementSnapshot,
291
+ resolveElements: () -> [XCUIElement],
292
+ startingIndex: Int,
293
+ depth: Int,
294
+ parentIndex: Int
295
+ ) -> [RawAXNode] {
296
+ if !containerSnapshot.children.isEmpty { return [] }
297
+
298
+
299
+
300
+
301
+ guard shouldExpandCollapsedTabContainer(containerSnapshot) else { return [] }
302
+ let containerFrame = containerSnapshot.frame
303
+ if containerFrame.isNull || containerFrame.isEmpty { return [] }
304
+
305
+
306
+
307
+ let elements = resolveElements()
308
+ let candidates = elements.compactMap { element in
309
+ collapsedTabCandidateNode(
310
+ element: element,
311
+ containerSnapshot: containerSnapshot,
312
+ containerFrame: containerFrame
313
+ )
314
+ }
315
+ .sorted { left, right in
316
+ if left.rect.x != right.rect.x {
317
+ return left.rect.x < right.rect.x
318
+ }
319
+ return left.rect.y < right.rect.y
320
+ }
321
+
322
+ if candidates.count < 2 { return [] }
323
+ let rowMidpoints = candidates.map { $0.rect.y + ($0.rect.height / 2) }
324
+ let rowSpread = (rowMidpoints.max() ?? 0) - (rowMidpoints.min() ?? 0)
325
+
326
+ if rowSpread > max(24.0, Double(containerFrame.height) * 0.6) { return [] }
327
+
328
+ var seen = Set<String>()
329
+ let uniqueCandidates = candidates.filter { node in
330
+ let key = "\(node.type)-\(node.label ?? "")-\(node.identifier ?? "")-\(node.value ?? "")-\(node.rect.x)-\(node.rect.y)-\(node.rect.width)-\(node.rect.height)"
331
+ if seen.contains(key) { return false }
332
+ seen.insert(key)
333
+ return true
334
+ }
335
+ if uniqueCandidates.count < 2 { return [] }
336
+
337
+ return uniqueCandidates.enumerated().map { offset, node in
338
+ RawAXNode(
339
+ index: startingIndex + offset,
340
+ type: node.type,
341
+ label: node.label,
342
+ identifier: node.identifier,
343
+ value: node.value,
344
+ rect: node.rect,
345
+ enabled: node.enabled,
346
+ focused: node.focused,
347
+ selected: node.selected,
348
+ hittable: node.hittable,
349
+ depth: depth,
350
+ parentIndex: parentIndex,
351
+ hiddenContentAbove: nil,
352
+ hiddenContentBelow: nil
353
+ )
354
+ }
355
+ }
356
+
357
+ private func collapsedTabCandidateNode(
358
+ element: XCUIElement,
359
+ containerSnapshot: XCUIElementSnapshot,
360
+ containerFrame: CGRect
361
+ ) -> RawAXNode? {
362
+ var node: RawAXNode?
363
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
364
+ if !element.exists { return }
365
+ let elementType = element.elementType
366
+ if !Self.collapsedTabCandidateTypes.contains(elementType) { return }
367
+ let frame = element.frame
368
+ if frame.isNull || frame.isEmpty { return }
369
+ if frame.equalTo(containerFrame) { return }
370
+ let area = max(CGFloat(1), frame.width * frame.height)
371
+ let containerArea = max(CGFloat(1), containerFrame.width * containerFrame.height)
372
+ if area >= containerArea * 0.9 { return }
373
+ let center = CGPoint(x: frame.midX, y: frame.midY)
374
+ if !containerFrame.contains(center) { return }
375
+
376
+ let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
377
+ let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
378
+ let valueText = snapshotValueText(element)
379
+ let hasContent = !label.isEmpty || !identifier.isEmpty || valueText != nil
380
+ if !hasContent { return }
381
+ if sameSemanticElement(
382
+ containerSnapshot: containerSnapshot,
383
+ elementType: elementType,
384
+ label: label,
385
+ identifier: identifier
386
+ ) {
387
+ return
388
+ }
389
+
390
+
391
+
392
+ node = RawAXNode(
393
+ index: 0,
394
+ type: elementTypeName(elementType),
395
+ label: label.isEmpty ? nil : label,
396
+ identifier: identifier.isEmpty ? nil : identifier,
397
+ value: valueText,
398
+ rect: SnapshotRect(frame),
399
+ enabled: element.isEnabled,
400
+ focused: elementHasFocus(element) ? true : nil,
401
+ selected: element.isSelected ? true : nil,
402
+ hittable: false,
403
+ depth: 0,
404
+ parentIndex: nil,
405
+ hiddenContentAbove: nil,
406
+ hiddenContentBelow: nil
407
+ )
408
+ })
409
+ if let exceptionMessage {
410
+ NSLog(
411
+ "AGENT_DEVICE_RUNNER_SNAPSHOT_TAB_FALLBACK_IGNORED_EXCEPTION=%@",
412
+ exceptionMessage
413
+ )
414
+ return nil
415
+ }
416
+ return node
417
+ }
418
+
419
+ private func snapshotHasFocus(_ snapshot: XCUIElementSnapshot) -> Bool {
420
+ var focused = false
421
+ _ = RunnerObjCExceptionCatcher.catchException({
422
+ if let value = (snapshot as! NSObject).value(forKey: "hasFocus") as? Bool {
423
+ focused = value
424
+ }
425
+ })
426
+ return focused
427
+ }
428
+
429
+ private func snapshotIsSelected(_ snapshot: XCUIElementSnapshot) -> Bool {
430
+ return snapshot.isSelected
431
+ }
432
+
433
+ private func shouldExpandCollapsedTabContainer(_ snapshot: XCUIElementSnapshot) -> Bool {
434
+ let frame = snapshot.frame
435
+ if frame.isNull || frame.isEmpty { return false }
436
+ if frame.width < max(CGFloat(160), frame.height * 1.75) { return false }
437
+ switch snapshot.elementType {
438
+ case .tabBar, .segmentedControl, .slider:
439
+ return true
440
+ default:
441
+ return false
442
+ }
443
+ }
444
+
445
+ private func snapshotValueText(_ element: XCUIElement) -> String? {
446
+ let text = String(describing: element.value ?? "")
447
+ .trimmingCharacters(in: .whitespacesAndNewlines)
448
+ return text.isEmpty ? nil : text
449
+ }
450
+
451
+ private func sameSemanticElement(
452
+ containerSnapshot: XCUIElementSnapshot,
453
+ elementType: XCUIElement.ElementType,
454
+ label: String,
455
+ identifier: String
456
+ ) -> Bool {
457
+ if containerSnapshot.elementType != elementType { return false }
458
+ let containerLabel = containerSnapshot.label.trimmingCharacters(in: .whitespacesAndNewlines)
459
+ let containerIdentifier = containerSnapshot.identifier
460
+ .trimmingCharacters(in: .whitespacesAndNewlines)
461
+ return containerLabel == label && containerIdentifier == identifier
462
+ }
463
+
464
+ func flatInteractiveElements(
465
+ app: XCUIApplication,
466
+ deadline: Date
467
+ ) -> (elements: [XCUIElement], truncated: Bool) {
468
+ let queries: [XCUIElementQuery] = [
469
+ app.buttons,
470
+ app.links,
471
+ app.textFields,
472
+ app.secureTextFields,
473
+ app.searchFields,
474
+ app.textViews,
475
+ app.switches,
476
+ app.sliders,
477
+ app.segmentedControls,
478
+ app.cells,
479
+ app.collectionViews,
480
+ app.tables,
481
+ app.scrollViews,
482
+ app.pickers,
483
+ app.steppers,
484
+ app.tabBars,
485
+ app.menuItems,
486
+ app.staticTexts,
487
+ app.images
488
+ ]
489
+
490
+ var elements: [XCUIElement] = []
491
+ var truncated = false
492
+ for query in queries {
493
+ if Date() >= deadline {
494
+ NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_FALLBACK_DEADLINE")
495
+ truncated = true
496
+ break
497
+ }
498
+ let result = snapshotElementsQuery {
499
+ query.allElementsBoundByIndex
500
+ }
501
+ elements.append(contentsOf: result.elements)
502
+ if result.axUnavailable {
503
+ break
504
+ }
505
+ }
506
+ return (elements, truncated)
507
+ }
508
+
509
+ func snapshotElementsQuery(
510
+ _ fetch: () -> [XCUIElement]
511
+ ) -> (elements: [XCUIElement], axUnavailable: Bool) {
512
+ let (elements, exceptionMessage) = catchingObjCException(fallback: [], fetch)
513
+ guard let exceptionMessage else {
514
+ return (elements, false)
515
+ }
516
+ NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_QUERY_IGNORED_EXCEPTION=%@", exceptionMessage)
517
+ if Self.isAxIllegalArgument(exceptionMessage) {
518
+ invalidateCachedTarget(reason: "ax_snapshot_query_unavailable")
519
+ return ([], true)
520
+ }
521
+ return ([], false)
522
+ }
523
+
524
+ func flatSnapshotNode(
525
+ element: XCUIElement,
526
+ index: Int,
527
+ parentIndex: Int?
528
+ ) -> RawAXNode? {
529
+ var node: RawAXNode?
530
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
531
+ if !element.exists { return }
532
+
533
+
534
+ let frame = element.frame
535
+ if frame.isNull || frame.isEmpty { return }
536
+ let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
537
+ let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
538
+ let valueText = snapshotValueText(element)
539
+ let elementType = element.elementType
540
+ let enabled = element.isEnabled
541
+
542
+ node = RawAXNode(
543
+ index: index,
544
+ type: elementTypeName(elementType),
545
+ label: label.isEmpty ? nil : label,
546
+ identifier: identifier.isEmpty ? nil : identifier,
547
+ value: valueText,
548
+ rect: SnapshotRect(frame),
549
+ enabled: enabled,
550
+ focused: elementHasFocus(element) ? true : nil,
551
+ selected: element.isSelected ? true : nil,
552
+ hittable: false,
553
+ depth: 1,
554
+ parentIndex: parentIndex,
555
+ hiddenContentAbove: nil,
556
+ hiddenContentBelow: nil
557
+ )
558
+ })
559
+ if let exceptionMessage {
560
+ NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_FLAT_IGNORED_EXCEPTION=%@", exceptionMessage)
561
+ return nil
562
+ }
563
+ return node
564
+ }
565
+
566
+ }
@@ -59,6 +59,9 @@ struct SynthesizedGesturePolicy: Equatable, Hashable {
59
59
 
60
60
  struct SynthesizedCoordinateContext {
61
61
  let referenceFrame: CGRect
62
+
63
+
64
+ let resolvedWindow: XCUIElement
62
65
  let keyboardPolicy: SynthesizedKeyboardPolicy
63
66
  let fallbackPolicy: SynthesizedFallbackPolicy
64
67
  let accessibilityHealth: RunnerAccessibilityHealth
@@ -66,6 +69,7 @@ struct SynthesizedCoordinateContext {
66
69
  func withReferenceFrame(_ frame: CGRect) -> SynthesizedCoordinateContext {
67
70
  SynthesizedCoordinateContext(
68
71
  referenceFrame: frame,
72
+ resolvedWindow: resolvedWindow,
69
73
  keyboardPolicy: keyboardPolicy,
70
74
  fallbackPolicy: fallbackPolicy,
71
75
  accessibilityHealth: accessibilityHealth
@@ -144,7 +148,7 @@ extension RunnerTests {
144
148
  return
145
149
  }
146
150
  NSLog(
147
- "AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=%@ axHealth=%@ frameSource=screenshot keyboardPolicy=%@ fallbackPolicy=%@ fallbackAllowed=%@ fallbackAttempted=%@",
151
+ "AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=%@ axHealth=%@ frameSource=window keyboardPolicy=%@ fallbackPolicy=%@ fallbackAllowed=%@ fallbackAttempted=%@",
148
152
  kind.rawValue,
149
153
  context.accessibilityHealth.rawValue,
150
154
  context.keyboardPolicy.rawValue,