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
@@ -11,21 +11,7 @@ extension RunnerTests {
11
11
  private static let rawSnapshotMaxNodes = 5_000
12
12
  private static let rawSnapshotTooLargeHint =
13
13
  "Raw iOS snapshot exceeded the runner payload guard. Use regular snapshot for visible UI, or scope/depth-limit raw snapshot when inspecting a large accessibility tree."
14
- private static let collapsedTabCandidateTypes: Set<XCUIElement.ElementType> = [
15
- .button,
16
- .link,
17
- .menuItem,
18
- .other,
19
- .staticText
20
- ]
21
- private static let scrollContainerTypes: Set<XCUIElement.ElementType> = [
22
- .collectionView,
23
- .scrollView,
24
- .table
25
- ]
26
- private static let flatInteractiveFallbackBudget: TimeInterval = 1.0
27
-
28
- private struct SnapshotTraversalContext {
14
+ struct SnapshotTraversalContext {
29
15
  let queryRoot: XCUIElement
30
16
  let rootSnapshot: XCUIElementSnapshot
31
17
  let viewport: CGRect
@@ -44,12 +30,6 @@ extension RunnerTests {
44
30
  let visible: Bool
45
31
  }
46
32
 
47
- private enum SnapshotTraversalCapture {
48
- case context(SnapshotTraversalContext)
49
- case fallback(DataPayload)
50
- case empty
51
- }
52
-
53
33
  struct SnapshotCaptureFailure: Error {
54
34
  let code: String
55
35
  let message: String
@@ -98,29 +78,45 @@ extension RunnerTests {
98
78
  }
99
79
  }
100
80
 
81
+ static let structuralOnlyNodeTypes: Set<String> = [
82
+ "Application",
83
+ "Window",
84
+ "Other",
85
+ "ScrollView"
86
+ ]
87
+
88
+ private static let collapsedTabCandidateTypes: Set<XCUIElement.ElementType> = [
89
+ .button,
90
+ .link,
91
+ .menuItem,
92
+ .other,
93
+ .staticText
94
+ ]
95
+
96
+ static let scrollContainerTypes: Set<XCUIElement.ElementType> = [
97
+ .collectionView,
98
+ .scrollView,
99
+ .table
100
+ ]
101
+
102
+ private static let flatInteractiveFallbackBudget: TimeInterval = 1.0
103
+
101
104
  func snapshotFast(app: XCUIApplication, options: SnapshotOptions) throws -> DataPayload {
102
- if options.interactiveOnly && options.compact {
103
- return snapshotFlatInteractive(app: app, options: options)
104
- }
105
105
  if let blocking = blockingSystemAlertSnapshot() {
106
106
  return blocking
107
107
  }
108
-
109
- let capture = try captureSnapshotTraversalContext(
108
+ return try runSnapshotCapturePlan(
109
+ Self.regularVisiblePlan,
110
110
  app: app,
111
111
  options: options,
112
- allowInteractiveUnavailableFallback: true
112
+ terminal: .sparseWithFatalOnAXFailure
113
113
  )
114
- let context: SnapshotTraversalContext
115
- switch capture {
116
- case .context(let traversalContext):
117
- context = traversalContext
118
- case .fallback(let fallback):
119
- return fallback
120
- case .empty:
121
- return DataPayload(nodes: [], truncated: false)
122
- }
114
+ }
123
115
 
116
+ func recursiveTreeSnapshotPayload(
117
+ context: SnapshotTraversalContext,
118
+ options: SnapshotOptions
119
+ ) -> DataPayload {
124
120
  var cachedDescendantElements: [XCUIElement]?
125
121
  func collapsedTabDescendants() -> [XCUIElement] {
126
122
  if let cachedDescendantElements {
@@ -253,22 +249,18 @@ extension RunnerTests {
253
249
  if let blocking = blockingSystemAlertSnapshot() {
254
250
  return blocking
255
251
  }
256
-
257
- let capture = try captureSnapshotTraversalContext(
252
+ return try runSnapshotCapturePlan(
253
+ Self.rawDiagnosticPlan,
258
254
  app: app,
259
255
  options: options,
260
- allowInteractiveUnavailableFallback: false
256
+ terminal: .throwOnAXFailure
261
257
  )
262
- let context: SnapshotTraversalContext
263
- switch capture {
264
- case .context(let traversalContext):
265
- context = traversalContext
266
- case .fallback(let fallback):
267
- return fallback
268
- case .empty:
269
- return DataPayload(nodes: [], truncated: false)
270
- }
258
+ }
271
259
 
260
+ func rawTreeSnapshotPayload(
261
+ context: SnapshotTraversalContext,
262
+ options: SnapshotOptions
263
+ ) throws -> DataPayload {
272
264
  var nodes: [SnapshotNode] = []
273
265
 
274
266
  func walk(_ snapshot: XCUIElementSnapshot, depth: Int, parentIndex: Int?) throws {
@@ -310,9 +302,9 @@ extension RunnerTests {
310
302
  return DataPayload(nodes: nodes, truncated: false)
311
303
  }
312
304
 
313
- private func snapshotFlatInteractive(app: XCUIApplication, options: SnapshotOptions) -> DataPayload {
305
+ func snapshotFlatInteractive(app: XCUIApplication, options: SnapshotOptions) -> DataPayload {
314
306
  var nodes: [SnapshotNode] = [
315
- compactInteractiveRootNode(rect: .zero)
307
+ interactiveRootNode(rect: .zero)
316
308
  ]
317
309
  if options.depth == 0 {
318
310
  return DataPayload(nodes: nodes, truncated: false)
@@ -356,7 +348,13 @@ extension RunnerTests {
356
348
  return left.type < right.type
357
349
  }
358
350
 
359
- nodes[0] = compactInteractiveRootNode(rect: compactInteractiveRootFrame(for: candidates))
351
+ // The synthetic root doubles as the daemon's viewport (find.ts prefers on-screen matches
352
+ // inside nodes[0].rect): use the real screen viewport when capture produced a finite one,
353
+ // so off-screen candidates can never inflate the root and masquerade as on-screen.
354
+ let rootRect = viewport.isInfinite || viewport.isNull || viewport.isEmpty
355
+ ? interactiveRootFrame(for: candidates)
356
+ : viewport
357
+ nodes[0] = interactiveRootNode(rect: rootRect)
360
358
  for candidate in candidates {
361
359
  nodes.append(
362
360
  SnapshotNode(
@@ -380,78 +378,26 @@ extension RunnerTests {
380
378
  return DataPayload(nodes: nodes, truncated: truncated)
381
379
  }
382
380
 
383
- private func snapshotAccessibilityUnavailable(failure: SnapshotCaptureFailure) -> DataPayload {
381
+ func snapshotAccessibilityUnavailable(failure: SnapshotCaptureFailure) -> DataPayload {
384
382
  NSLog("AGENT_DEVICE_RUNNER_SNAPSHOT_AX_UNAVAILABLE=%@", failure.message)
385
383
  invalidateCachedTarget(reason: Self.axSnapshotUnavailableReason)
384
+ // This is a planned terminal result, so it carries the structured verdict like every other
385
+ // planned snapshot — downstream sparse handling keys off the verdict, not node shapes.
386
386
  return sparseTruncatedSnapshotPayload(
387
387
  message: recoveredSnapshotMessage(failure),
388
+ snapshotQuality: SnapshotQuality(
389
+ state: "sparse",
390
+ backend: SnapshotBackendKind.recursiveTree.rawValue,
391
+ reason: failure.message,
392
+ reasonCode: "ax-rejected",
393
+ effectiveDepth: nil,
394
+ collapsedLeafIndexes: nil
395
+ ),
388
396
  runnerFatal: true,
389
397
  runnerFatalReason: Self.axSnapshotUnavailableReason
390
398
  )
391
399
  }
392
400
 
393
- private func captureSnapshotTraversalContext(
394
- app: XCUIApplication,
395
- options: SnapshotOptions,
396
- allowInteractiveUnavailableFallback: Bool
397
- ) throws -> SnapshotTraversalCapture {
398
- do {
399
- guard let context = try makeSnapshotTraversalContext(app: app, options: options) else {
400
- return .empty
401
- }
402
- return .context(context)
403
- } catch let failure as SnapshotCaptureFailure {
404
- if let fallback = snapshotDepthLimitedAccessibilityFallback(
405
- app: app,
406
- options: options,
407
- failure: failure
408
- ) {
409
- return .fallback(fallback)
410
- }
411
- if allowInteractiveUnavailableFallback && options.interactiveOnly {
412
- return .fallback(snapshotAccessibilityUnavailable(failure: failure))
413
- }
414
- throw failure
415
- }
416
- }
417
-
418
- private func snapshotDepthLimitedAccessibilityFallback(
419
- app: XCUIApplication,
420
- options: SnapshotOptions,
421
- failure: SnapshotCaptureFailure
422
- ) -> DataPayload? {
423
- guard let requestedDepth = options.depth else {
424
- return nil
425
- }
426
-
427
- NSLog(
428
- "AGENT_DEVICE_RUNNER_SNAPSHOT_DEPTH_FALLBACK=%@",
429
- failure.message
430
- )
431
-
432
- if requestedDepth <= 0 {
433
- return sparseTruncatedSnapshotPayload(message: recoveredSnapshotMessage(failure))
434
- }
435
-
436
- // Raw depth-limited recovery intentionally falls back to sparse interactive discovery because
437
- // the raw AX tree is the failed operation.
438
- let fallback = snapshotFlatInteractive(
439
- app: app,
440
- options: SnapshotOptions(
441
- interactiveOnly: true,
442
- compact: options.compact,
443
- depth: requestedDepth,
444
- scope: options.scope,
445
- raw: false
446
- )
447
- )
448
- return DataPayload(
449
- message: recoveredSnapshotMessage(failure),
450
- nodes: fallback.nodes,
451
- truncated: true
452
- )
453
- }
454
-
455
401
  private func recoveredSnapshotMessage(_ failure: SnapshotCaptureFailure) -> String {
456
402
  return "\(failure.message) Hint: \(failure.hint)"
457
403
  }
@@ -464,15 +410,17 @@ extension RunnerTests {
464
410
  )
465
411
  }
466
412
 
467
- private func sparseTruncatedSnapshotPayload(
468
- message: String,
413
+ func sparseTruncatedSnapshotPayload(
414
+ message: String? = nil,
415
+ snapshotQuality: SnapshotQuality? = nil,
469
416
  runnerFatal: Bool? = nil,
470
417
  runnerFatalReason: String? = nil
471
418
  ) -> DataPayload {
472
419
  return DataPayload(
473
420
  message: message,
474
- nodes: [compactInteractiveRootNode(rect: .zero)],
421
+ nodes: [interactiveRootNode(rect: .zero)],
475
422
  truncated: true,
423
+ snapshotQuality: snapshotQuality,
476
424
  runnerFatal: runnerFatal,
477
425
  runnerFatalReason: runnerFatalReason
478
426
  )
@@ -521,40 +469,7 @@ extension RunnerTests {
521
469
  XCTAssertEqual(failure.hint, Self.rawSnapshotTooLargeHint)
522
470
  }
523
471
 
524
- func testDepthLimitedSnapshotFailureReturnsNonFatalFallback() {
525
- currentApp = app
526
- currentBundleId = "com.example.app"
527
-
528
- let payload = snapshotDepthLimitedAccessibilityFallback(
529
- app: app,
530
- options: SnapshotOptions(
531
- interactiveOnly: false,
532
- compact: false,
533
- depth: 0,
534
- scope: nil,
535
- raw: false
536
- ),
537
- failure: SnapshotCaptureFailure(
538
- code: Self.axSnapshotErrorCode,
539
- message: "\(Self.axSnapshotFailureMessage) kAXErrorIllegalArgument.",
540
- hint: Self.axSnapshotHint
541
- )
542
- )
543
-
544
- XCTAssertEqual(
545
- payload?.message,
546
- "\(Self.axSnapshotFailureMessage) kAXErrorIllegalArgument. Hint: \(Self.axSnapshotHint)"
547
- )
548
- XCTAssertEqual(payload?.nodes?.count, 1)
549
- XCTAssertEqual(payload?.nodes?.first?.type, "Application")
550
- XCTAssertEqual(payload?.truncated, true)
551
- XCTAssertNil(payload?.runnerFatal)
552
- XCTAssertNil(payload?.runnerFatalReason)
553
- XCTAssertNotNil(currentApp)
554
- XCTAssertEqual(currentBundleId, "com.example.app")
555
- }
556
-
557
- private func compactInteractiveRootNode(rect: CGRect) -> SnapshotNode {
472
+ private func interactiveRootNode(rect: CGRect) -> SnapshotNode {
558
473
  SnapshotNode(
559
474
  index: 0,
560
475
  type: "Application",
@@ -573,7 +488,7 @@ extension RunnerTests {
573
488
  )
574
489
  }
575
490
 
576
- private func compactInteractiveRootFrame(for candidates: [SnapshotNode]) -> CGRect {
491
+ private func interactiveRootFrame(for candidates: [SnapshotNode]) -> CGRect {
577
492
  guard !candidates.isEmpty else {
578
493
  return .zero
579
494
  }
@@ -605,9 +520,6 @@ extension RunnerTests {
605
520
  ) -> Bool {
606
521
  let type = snapshot.elementType
607
522
  let hasContent = !label.isEmpty || !identifier.isEmpty || (valueText != nil)
608
- if options.compact && type == .other && !hasContent && !hittable {
609
- if snapshot.children.count <= 1 { return false }
610
- }
611
523
  if options.interactiveOnly {
612
524
  if isScrollableContainer(snapshot, visible: visible) { return true }
613
525
  #if os(macOS)
@@ -620,9 +532,6 @@ extension RunnerTests {
620
532
  if hasContent { return true }
621
533
  return false
622
534
  }
623
- if options.compact {
624
- return hasContent || hittable
625
- }
626
535
  if regularSnapshot {
627
536
  if type == .application || type == .window { return true }
628
537
  return visible
@@ -649,7 +558,7 @@ extension RunnerTests {
649
558
  return true
650
559
  }
651
560
 
652
- private func makeSnapshotTraversalContext(
561
+ func makeSnapshotTraversalContext(
653
562
  app: XCUIApplication,
654
563
  options: SnapshotOptions
655
564
  ) throws -> SnapshotTraversalContext? {
@@ -692,7 +601,7 @@ extension RunnerTests {
692
601
  return nil
693
602
  }
694
603
 
695
- private func safeSnapshotViewport(app: XCUIApplication) -> CGRect {
604
+ func safeSnapshotViewport(app: XCUIApplication) -> CGRect {
696
605
  safely("SNAPSHOT_VIEWPORT", CGRect.infinite) { snapshotViewport(app: app) }
697
606
  }
698
607
 
@@ -725,6 +634,10 @@ extension RunnerTests {
725
634
  || (normalized.contains("illegal argument") && normalized.contains("snapshot"))
726
635
  }
727
636
 
637
+ static func isAxSnapshotFailure(_ failure: SnapshotCaptureFailure) -> Bool {
638
+ failure.code == Self.axSnapshotErrorCode || isAxIllegalArgument(failure.message)
639
+ }
640
+
728
641
  private func evaluateSnapshot(
729
642
  _ snapshot: XCUIElementSnapshot,
730
643
  in context: SnapshotTraversalContext
@@ -826,15 +739,6 @@ extension RunnerTests {
826
739
  }
827
740
 
828
741
  private func snapshotViewport(app: XCUIApplication) -> CGRect {
829
- let windows = app.windows.allElementsBoundByIndex
830
- let windowFrames = windows
831
- .filter { $0.exists && !$0.frame.isNull && !$0.frame.isEmpty }
832
- .map(\.frame)
833
- if let largestWindowFrame = windowFrames.max(by: { left, right in
834
- left.width * left.height < right.width * right.height
835
- }) {
836
- return largestWindowFrame
837
- }
838
742
  let appFrame = app.frame
839
743
  if !appFrame.isNull && !appFrame.isEmpty {
840
744
  return appFrame
@@ -855,7 +759,7 @@ extension RunnerTests {
855
759
  return nil
856
760
  }
857
761
 
858
- private func isVisibleInViewport(_ rect: CGRect, _ viewport: CGRect) -> Bool {
762
+ func isVisibleInViewport(_ rect: CGRect, _ viewport: CGRect) -> Bool {
859
763
  if rect.isNull || rect.isEmpty { return false }
860
764
  return rect.intersects(viewport)
861
765
  }
@@ -1191,19 +1095,19 @@ extension RunnerTests {
1191
1095
  let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
1192
1096
  let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
1193
1097
  let valueText = snapshotValueText(element)
1194
- let hasContent = !label.isEmpty || !identifier.isEmpty || valueText != nil
1195
1098
  let elementType = element.elementType
1196
1099
  let enabled = element.isEnabled
1197
1100
  let hittable = visible && enabled && element.isHittable
1198
- if options.compact && !hasContent && !hittable && !interactiveTypes.contains(elementType) {
1101
+ let filterNode = FlatSnapshotFilterNode(
1102
+ isRoot: false,
1103
+ label: label,
1104
+ identifier: identifier,
1105
+ valueText: valueText,
1106
+ visible: visible
1107
+ )
1108
+ if !flatSnapshotFilterDecision(filterNode, options: options, insideMatchedScope: false).include {
1199
1109
  return
1200
1110
  }
1201
- if let scope = options.scope?.trimmingCharacters(in: .whitespacesAndNewlines), !scope.isEmpty {
1202
- let haystack = [label, identifier, valueText ?? ""].joined(separator: "\n")
1203
- if !haystack.localizedCaseInsensitiveContains(scope) {
1204
- return
1205
- }
1206
- }
1207
1111
 
1208
1112
  node = SnapshotNode(
1209
1113
  index: index,