agent-device 0.17.2 → 0.17.4

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 (86) hide show
  1. package/README.md +2 -2
  2. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.apk → agent-device-android-multitouch-helper-0.17.4.apk} +0 -0
  3. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.4.apk.sha256 +1 -0
  4. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.17.2.manifest.json → agent-device-android-multitouch-helper-0.17.4.manifest.json} +4 -4
  5. package/android-snapshot-helper/README.md +3 -0
  6. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.apk → agent-device-android-snapshot-helper-0.17.4.apk} +0 -0
  7. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.4.apk.sha256 +1 -0
  8. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.17.2.manifest.json → agent-device-android-snapshot-helper-0.17.4.manifest.json} +6 -6
  9. package/dist/src/123.js +1 -1
  10. package/dist/src/1231.js +1 -1
  11. package/dist/src/1352.js +1 -1
  12. package/dist/src/221.js +2 -2
  13. package/dist/src/2403.js +3 -3
  14. package/dist/src/2415.js +37 -29
  15. package/dist/src/4012.js +4 -0
  16. package/dist/src/5611.js +1 -0
  17. package/dist/src/5827.js +1 -0
  18. package/dist/src/6088.js +1 -0
  19. package/dist/src/6133.js +1 -0
  20. package/dist/src/7847.js +1 -1
  21. package/dist/src/9471.js +1 -1
  22. package/dist/src/9542.js +6 -3
  23. package/dist/src/9616.js +2 -1
  24. package/dist/src/9818.js +1 -1
  25. package/dist/src/android-snapshot-helper.d.ts +0 -1
  26. package/dist/src/android.js +5 -5
  27. package/dist/src/apple.js +1 -1
  28. package/dist/src/apps.js +10 -10
  29. package/dist/src/args.js +53 -14
  30. package/dist/src/batch.js +1 -1
  31. package/dist/src/cli-output.js +30 -0
  32. package/dist/src/cli.js +15 -16
  33. package/dist/src/command-surface.js +1 -1
  34. package/dist/src/contracts.d.ts +52 -0
  35. package/dist/src/contracts.js +1 -1
  36. package/dist/src/find.js +1 -1
  37. package/dist/src/generic.js +5 -5
  38. package/dist/src/index.d.ts +127 -8
  39. package/dist/src/index.js +1 -1
  40. package/dist/src/input-actions.js +1 -1
  41. package/dist/src/interaction.js +1 -1
  42. package/dist/src/react-native.js +1 -1
  43. package/dist/src/record-trace-recording.js +3 -3
  44. package/dist/src/selector-runtime.js +1 -1
  45. package/dist/src/selectors.d.ts +14 -0
  46. package/dist/src/selectors.js +1 -1
  47. package/dist/src/server.js +2 -2
  48. package/dist/src/session.js +8 -10
  49. package/dist/src/snapshot.js +2 -2
  50. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
  51. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +14 -0
  52. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +297 -0
  53. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +327 -0
  54. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +122 -173
  55. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +153 -5
  56. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +171 -0
  57. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +0 -10
  58. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +64 -18
  59. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +200 -0
  60. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +450 -0
  61. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +82 -178
  62. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +425 -0
  63. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +77 -6
  64. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +10 -1
  65. package/ios-runner/README.md +5 -5
  66. package/ios-runner/RUNNER_PROTOCOL.md +0 -1
  67. package/package.json +3 -3
  68. package/server.json +2 -2
  69. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.17.2.apk.sha256 +0 -1
  70. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.17.2.apk.sha256 +0 -1
  71. package/dist/src/1534.js +0 -1
  72. package/dist/src/1620.js +0 -1
  73. package/dist/src/1644.js +0 -2
  74. package/dist/src/2284.js +0 -1
  75. package/dist/src/2474.js +0 -1
  76. package/dist/src/2672.js +0 -1
  77. package/dist/src/3393.js +0 -1
  78. package/dist/src/5898.js +0 -1
  79. package/dist/src/695.js +0 -1
  80. package/dist/src/7455.js +0 -1
  81. package/dist/src/8173.js +0 -27
  82. package/dist/src/8407.js +0 -1
  83. package/dist/src/8699.js +0 -1
  84. package/dist/src/9238.js +0 -4
  85. package/dist/src/9673.js +0 -1
  86. package/dist/src/9974.js +0 -1
@@ -0,0 +1,327 @@
1
+ import XCTest
2
+
3
+ extension RunnerTests {
4
+ private static let privateAXSnapshotMaxNodes = 5_000
5
+ /// Deep React Native trees make the AX server reject bulk snapshot requests outright with
6
+ /// kAXErrorIllegalArgument once the requested depth crosses a tree-size-dependent limit
7
+ /// (observed between depth 56 and 64 on the Bluesky Home feed; the limit moves with live
8
+ /// content). Retrying the same request at a shallower depth succeeds, so on failure we walk
9
+ /// this ladder instead of giving up. Capped at 4 attempts to bound worst-case latency on
10
+ /// apps where the AX surface is genuinely unavailable.
11
+ static let privateAXSnapshotDepthLadder = [56, 40, 24, 12]
12
+
13
+ func privateAXSnapshotCapture(
14
+ app: XCUIApplication,
15
+ options: SnapshotOptions
16
+ ) -> SnapshotBackendCapture? {
17
+ #if os(iOS) && targetEnvironment(simulator)
18
+ let requestedDepth = options.depth ?? 64
19
+ var attemptDepths = [requestedDepth]
20
+ attemptDepths.append(
21
+ contentsOf: Self.privateAXSnapshotDepthLadder.filter { $0 < requestedDepth }
22
+ )
23
+ var response: [String: Any] = [:]
24
+ var effectiveDepth = requestedDepth
25
+ var lastError = "unknown private AX snapshot failure"
26
+ for depth in attemptDepths {
27
+ response = RunnerAXSnapshotBridge.snapshotTree(
28
+ for: app,
29
+ maxDepth: depth,
30
+ maxNodes: Self.privateAXSnapshotMaxNodes
31
+ )
32
+ if response["ok"] as? Bool == true {
33
+ effectiveDepth = depth
34
+ break
35
+ }
36
+ lastError = response["error"] as? String ?? lastError
37
+ NSLog(
38
+ "AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_DEPTH_RETRY depth=%ld error=%@",
39
+ depth,
40
+ lastError
41
+ )
42
+ }
43
+ guard response["ok"] as? Bool == true else {
44
+ NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=%@", lastError)
45
+ return nil
46
+ }
47
+ guard let root = response["root"] as? [String: Any] else {
48
+ NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_FAILED=missing root")
49
+ return nil
50
+ }
51
+
52
+ // If the app frame is unavailable, the private root's own frame is the reliable screen
53
+ // rect here. Avoid public window queries: stale transient windows can record XCTest
54
+ // failures after the runner already returned a successful command response.
55
+ var viewport = safeSnapshotViewport(app: app)
56
+ let rootFrame = privateAXRect(root["frame"])
57
+ if viewport.isInfinite || viewport.isNull || viewport.isEmpty, !rootFrame.isEmpty {
58
+ viewport = rootFrame
59
+ }
60
+ var nodes: [SnapshotNode] = []
61
+ appendPrivateAXNode(
62
+ root,
63
+ to: &nodes,
64
+ options: options,
65
+ viewport: viewport,
66
+ depth: 0,
67
+ parentIndex: nil,
68
+ insideMatchedScope: false
69
+ )
70
+ if nodes.count <= 1 {
71
+ NSLog("AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_SPARSE=%ld", nodes.count)
72
+ return nil
73
+ }
74
+
75
+ let depthLimited = effectiveDepth < requestedDepth
76
+ NSLog(
77
+ "AGENT_DEVICE_RUNNER_PRIVATE_AX_SNAPSHOT_USED nodes=%ld depth=%ld",
78
+ nodes.count,
79
+ effectiveDepth
80
+ )
81
+ return SnapshotBackendCapture(
82
+ payload: DataPayload(nodes: nodes, truncated: (response["truncated"] as? Bool) == true),
83
+ effectiveDepth: depthLimited ? effectiveDepth : nil
84
+ )
85
+ #else
86
+ return nil
87
+ #endif
88
+ }
89
+
90
+ private func appendPrivateAXNode(
91
+ _ rawNode: [String: Any],
92
+ to nodes: inout [SnapshotNode],
93
+ options: SnapshotOptions,
94
+ viewport: CGRect,
95
+ depth: Int,
96
+ parentIndex: Int?,
97
+ insideMatchedScope: Bool
98
+ ) {
99
+ if let limit = options.depth, depth > limit { return }
100
+
101
+ let rect = privateAXRect(rawNode["frame"])
102
+ let label = privateAXString(rawNode["label"])
103
+ let identifier = privateAXString(rawNode["identifier"])
104
+ let value = privateAXString(rawNode["value"])
105
+ let rawType = privateAXInt(rawNode["type"]) ?? 0
106
+ let typeName = elementTypeName(rawElementType: rawType)
107
+ let enabled = privateAXBool(rawNode["enabled"]) ?? true
108
+ let visible = isVisibleInViewport(rect, viewport)
109
+ let interactiveCandidate = privateAXInteractiveCandidate(rawElementType: rawType)
110
+ let filterDecision = flatSnapshotFilterDecision(
111
+ FlatSnapshotFilterNode(
112
+ isRoot: parentIndex == nil,
113
+ label: label,
114
+ identifier: identifier,
115
+ valueText: value.isEmpty ? nil : value,
116
+ visible: visible
117
+ ),
118
+ options: options,
119
+ insideMatchedScope: insideMatchedScope
120
+ )
121
+ let include = filterDecision.include
122
+ let nowInsideScope = filterDecision.insideMatchedScope
123
+
124
+ let currentIndex: Int?
125
+ if include {
126
+ currentIndex = nodes.count
127
+ nodes.append(
128
+ SnapshotNode(
129
+ index: nodes.count,
130
+ type: typeName,
131
+ label: label.isEmpty ? nil : label,
132
+ identifier: identifier.isEmpty ? nil : identifier,
133
+ value: value.isEmpty ? nil : value,
134
+ rect: snapshotRect(from: rect),
135
+ enabled: enabled,
136
+ focused: privateAXBool(rawNode["focused"]) == true ? true : nil,
137
+ selected: privateAXBool(rawNode["selected"]) == true ? true : nil,
138
+ hittable: visible && enabled && interactiveCandidate,
139
+ depth: depth,
140
+ parentIndex: parentIndex,
141
+ hiddenContentAbove: nil,
142
+ hiddenContentBelow: nil
143
+ )
144
+ )
145
+ } else {
146
+ currentIndex = parentIndex
147
+ }
148
+
149
+ guard let children = rawNode["children"] as? [[String: Any]] else {
150
+ return
151
+ }
152
+ for child in children {
153
+ appendPrivateAXNode(
154
+ child,
155
+ to: &nodes,
156
+ options: options,
157
+ viewport: viewport,
158
+ depth: depth + 1,
159
+ parentIndex: currentIndex,
160
+ insideMatchedScope: nowInsideScope
161
+ )
162
+ }
163
+ }
164
+
165
+ private func elementTypeName(rawElementType: Int) -> String {
166
+ if let type = flatSnapshotElementType(rawElementType: rawElementType) {
167
+ return elementTypeName(type)
168
+ }
169
+ return "Element(\(rawElementType))"
170
+ }
171
+
172
+ private func privateAXString(_ value: Any?) -> String {
173
+ guard let value else { return "" }
174
+ if let string = value as? String {
175
+ return string.trimmingCharacters(in: .whitespacesAndNewlines)
176
+ }
177
+ return String(describing: value).trimmingCharacters(in: .whitespacesAndNewlines)
178
+ }
179
+
180
+ private func privateAXInt(_ value: Any?) -> Int? {
181
+ if let value = value as? Int { return value }
182
+ if let value = value as? NSNumber { return value.intValue }
183
+ return nil
184
+ }
185
+
186
+ private func privateAXBool(_ value: Any?) -> Bool? {
187
+ if let value = value as? Bool { return value }
188
+ if let value = value as? NSNumber { return value.boolValue }
189
+ return nil
190
+ }
191
+
192
+ private func privateAXRect(_ value: Any?) -> CGRect {
193
+ guard let frame = value as? [String: Any] else {
194
+ return .zero
195
+ }
196
+ return CGRect(
197
+ x: privateAXDouble(frame["x"]) ?? 0,
198
+ y: privateAXDouble(frame["y"]) ?? 0,
199
+ width: privateAXDouble(frame["width"]) ?? 0,
200
+ height: privateAXDouble(frame["height"]) ?? 0
201
+ )
202
+ }
203
+
204
+ private func privateAXDouble(_ value: Any?) -> Double? {
205
+ if let value = value as? Double { return value }
206
+ if let value = value as? NSNumber { return value.doubleValue }
207
+ return nil
208
+ }
209
+ }
210
+
211
+ // MARK: - In-bundle unit tests
212
+
213
+ extension RunnerTests {
214
+ func testPrivateAXScopeSelectsSubtreeNotMatchingLabels() {
215
+ let tree: [String: Any] = [
216
+ "type": 1, "label": "App",
217
+ "children": [
218
+ [
219
+ "type": 9, "identifier": "homeScreen",
220
+ "children": [
221
+ ["type": 48, "label": "Post body without the scope text", "children": []]
222
+ ],
223
+ ],
224
+ ["type": 9, "label": "unrelated sibling", "children": []],
225
+ ],
226
+ ]
227
+ var nodes: [SnapshotNode] = []
228
+ appendPrivateAXNode(
229
+ tree,
230
+ to: &nodes,
231
+ options: SnapshotOptions(interactiveOnly: false, depth: nil, scope: "homeScreen", raw: false),
232
+ viewport: .infinite,
233
+ depth: 0,
234
+ parentIndex: nil,
235
+ insideMatchedScope: false
236
+ )
237
+
238
+ let labels = nodes.compactMap { $0.label ?? $0.identifier }
239
+ XCTAssertTrue(labels.contains("homeScreen"))
240
+ // Descendants of the matched scope are included even when they do not contain the text.
241
+ XCTAssertTrue(labels.contains("Post body without the scope text"))
242
+ XCTAssertFalse(labels.contains("unrelated sibling"))
243
+ }
244
+
245
+ func testPrivateAXInteractiveFiltersLoginLikeHiddenDrawer() {
246
+ let tree: [String: Any] = [
247
+ "type": Int(XCUIElement.ElementType.application.rawValue),
248
+ "label": "Blue Sky",
249
+ "frame": ["x": 0, "y": 0, "width": 390, "height": 844],
250
+ "children": [
251
+ [
252
+ "type": Int(XCUIElement.ElementType.scrollView.rawValue),
253
+ "frame": ["x": 0, "y": 0, "width": 390, "height": 844],
254
+ "children": [
255
+ [
256
+ "type": Int(XCUIElement.ElementType.image.rawValue),
257
+ "label": "Callstack",
258
+ "frame": ["x": 145, "y": 104, "width": 100, "height": 100],
259
+ "children": [],
260
+ ],
261
+ [
262
+ "type": Int(XCUIElement.ElementType.staticText.rawValue),
263
+ "label": "Welcome back",
264
+ "frame": ["x": 32, "y": 260, "width": 326, "height": 32],
265
+ "children": [],
266
+ ],
267
+ [
268
+ "type": Int(XCUIElement.ElementType.textField.rawValue),
269
+ "label": "Email",
270
+ "identifier": "login.email",
271
+ "frame": ["x": 32, "y": 348, "width": 326, "height": 48],
272
+ "children": [],
273
+ ],
274
+ [
275
+ "type": Int(XCUIElement.ElementType.secureTextField.rawValue),
276
+ "label": "Password",
277
+ "identifier": "login.password",
278
+ "frame": ["x": 32, "y": 412, "width": 326, "height": 48],
279
+ "children": [],
280
+ ],
281
+ [
282
+ "type": Int(XCUIElement.ElementType.button.rawValue),
283
+ "label": "Sign in",
284
+ "identifier": "login.submit",
285
+ "frame": ["x": 32, "y": 492, "width": 326, "height": 52],
286
+ "children": [],
287
+ ],
288
+ [
289
+ "type": Int(XCUIElement.ElementType.link.rawValue),
290
+ "label": "Forgot password?",
291
+ "frame": ["x": 128, "y": 568, "width": 134, "height": 32],
292
+ "children": [],
293
+ ],
294
+ [
295
+ "type": Int(XCUIElement.ElementType.button.rawValue),
296
+ "label": "Admin settings",
297
+ "frame": ["x": -260, "y": 184, "width": 220, "height": 44],
298
+ "children": [],
299
+ ],
300
+ [
301
+ "type": Int(XCUIElement.ElementType.other.rawValue),
302
+ "frame": ["x": 16, "y": 184, "width": 220, "height": 44],
303
+ "children": [],
304
+ ],
305
+ ],
306
+ ]
307
+ ],
308
+ ]
309
+ var nodes: [SnapshotNode] = []
310
+ appendPrivateAXNode(
311
+ tree,
312
+ to: &nodes,
313
+ options: SnapshotOptions(interactiveOnly: true, depth: nil, scope: nil, raw: false),
314
+ viewport: CGRect(x: 0, y: 0, width: 390, height: 844),
315
+ depth: 0,
316
+ parentIndex: nil,
317
+ insideMatchedScope: false
318
+ )
319
+
320
+ let labels = nodes.compactMap { $0.label }
321
+ XCTAssertEqual(
322
+ labels,
323
+ ["Blue Sky", "Callstack", "Welcome back", "Email", "Password", "Sign in", "Forgot password?"]
324
+ )
325
+ XCTAssertFalse(labels.contains("Admin settings"))
326
+ }
327
+ }