agent-device 0.15.2 → 0.16.1

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 (45) hide show
  1. package/README.md +51 -155
  2. package/android-multitouch-helper/README.md +41 -0
  3. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.1.apk +0 -0
  4. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.1.apk.sha256 +1 -0
  5. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.1.manifest.json +10 -0
  6. package/android-snapshot-helper/README.md +2 -0
  7. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.1.apk +0 -0
  8. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.1.apk.sha256 +1 -0
  9. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.15.2.manifest.json → agent-device-android-snapshot-helper-0.16.1.manifest.json} +6 -6
  10. package/dist/src/1231.js +1 -1
  11. package/dist/src/1769.js +7 -7
  12. package/dist/src/2099.js +1 -0
  13. package/dist/src/221.js +4 -4
  14. package/dist/src/3622.js +3 -0
  15. package/dist/src/7519.js +1 -0
  16. package/dist/src/7556.js +1 -1
  17. package/dist/src/89.js +1 -0
  18. package/dist/src/940.js +1 -1
  19. package/dist/src/9542.js +2 -2
  20. package/dist/src/9639.js +2 -2
  21. package/dist/src/989.js +1 -1
  22. package/dist/src/android-adb.d.ts +26 -0
  23. package/dist/src/android-adb.js +1 -1
  24. package/dist/src/android-snapshot-helper.d.ts +30 -0
  25. package/dist/src/batch.d.ts +9 -9
  26. package/dist/src/cli.js +495 -80
  27. package/dist/src/index.d.ts +47 -5
  28. package/dist/src/internal/daemon.js +69 -44
  29. package/dist/src/server.js +2 -2
  30. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
  31. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +19 -0
  32. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +297 -0
  33. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +144 -5
  34. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +328 -23
  35. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +3 -1
  36. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +8 -0
  37. package/package.json +9 -3
  38. package/server.json +2 -2
  39. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.15.2.apk +0 -0
  40. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.15.2.apk.sha256 +0 -1
  41. package/dist/src/1393.js +0 -1
  42. package/dist/src/2151.js +0 -438
  43. package/dist/src/3572.js +0 -1
  44. package/dist/src/7599.js +0 -3
  45. package/dist/src/9671.js +0 -1
@@ -27,6 +27,7 @@ extension RunnerTests {
27
27
  struct SelectorElementMatch {
28
28
  let element: XCUIElement?
29
29
  let isAmbiguous: Bool
30
+ let usedNonHittableFallback: Bool
30
31
  }
31
32
 
32
33
  enum TextTypingRepairMode {
@@ -177,10 +178,15 @@ extension RunnerTests {
177
178
  return element.exists ? element : nil
178
179
  }
179
180
 
180
- func findElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> SelectorElementMatch {
181
+ func findElement(
182
+ app: XCUIApplication,
183
+ selectorKey: String,
184
+ selectorValue: String,
185
+ allowNonHittableFallback: Bool = false
186
+ ) -> SelectorElementMatch {
181
187
  let value = selectorValue.trimmingCharacters(in: .whitespacesAndNewlines)
182
188
  guard !value.isEmpty else {
183
- return SelectorElementMatch(element: nil, isAmbiguous: false)
189
+ return SelectorElementMatch(element: nil, isAmbiguous: false, usedNonHittableFallback: false)
184
190
  }
185
191
  let predicate: NSPredicate
186
192
  switch selectorKey {
@@ -193,21 +199,47 @@ extension RunnerTests {
193
199
  case "text":
194
200
  predicate = NSPredicate(format: "label ==[c] %@ OR identifier ==[c] %@ OR value ==[c] %@", value, value, value)
195
201
  default:
196
- return SelectorElementMatch(element: nil, isAmbiguous: false)
202
+ return SelectorElementMatch(element: nil, isAmbiguous: false, usedNonHittableFallback: false)
197
203
  }
198
204
 
199
205
  var matchedElement: XCUIElement?
206
+ var nonHittableElement: XCUIElement?
200
207
  let matches = app.descendants(matching: .any).matching(predicate).allElementsBoundByIndex
201
208
  for element in matches where element.exists {
202
- guard element.isHittable else {
209
+ if !element.isHittable {
210
+ if allowNonHittableFallback && hasTappableFrame(app: app, element: element) {
211
+ guard nonHittableElement == nil else {
212
+ return SelectorElementMatch(element: nil, isAmbiguous: true, usedNonHittableFallback: false)
213
+ }
214
+ nonHittableElement = element
215
+ }
203
216
  continue
204
217
  }
205
218
  guard matchedElement == nil else {
206
- return SelectorElementMatch(element: nil, isAmbiguous: true)
219
+ return SelectorElementMatch(element: nil, isAmbiguous: true, usedNonHittableFallback: false)
207
220
  }
208
221
  matchedElement = element
209
222
  }
210
- return SelectorElementMatch(element: matchedElement, isAmbiguous: false)
223
+ if let matchedElement {
224
+ return SelectorElementMatch(element: matchedElement, isAmbiguous: false, usedNonHittableFallback: false)
225
+ }
226
+ return SelectorElementMatch(
227
+ element: nonHittableElement,
228
+ isAmbiguous: false,
229
+ usedNonHittableFallback: nonHittableElement != nil
230
+ )
231
+ }
232
+
233
+ private func hasTappableFrame(app: XCUIApplication, element: XCUIElement) -> Bool {
234
+ let frame = element.frame
235
+ if frame.isEmpty {
236
+ return false
237
+ }
238
+ let appFrame = app.frame
239
+ if appFrame.isEmpty {
240
+ return true
241
+ }
242
+ return appFrame.contains(CGPoint(x: frame.midX, y: frame.midY))
211
243
  }
212
244
 
213
245
  func queryElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> Response {
@@ -303,7 +335,7 @@ extension RunnerTests {
303
335
  switch element.elementType {
304
336
  case .textField, .secureTextField, .searchField, .textView:
305
337
  let frame = element.frame
306
- return !frame.isEmpty && frame.contains(point)
338
+ return !frame.isEmpty && frameContainsPoint(frame, point, tolerance: 2)
307
339
  default:
308
340
  return false
309
341
  }
@@ -334,20 +366,33 @@ extension RunnerTests {
334
366
  return matched
335
367
  }
336
368
 
369
+ private func frameContainsPoint(_ frame: CGRect, _ point: CGPoint, tolerance: CGFloat) -> Bool {
370
+ point.x >= frame.minX - tolerance
371
+ && point.x <= frame.maxX + tolerance
372
+ && point.y >= frame.minY - tolerance
373
+ && point.y <= frame.maxY + tolerance
374
+ }
375
+
337
376
  func focusedTextInput(app: XCUIApplication) -> XCUIElement? {
377
+ #if os(iOS)
378
+ // iOS focus predicates can return stale or misleading text-input matches
379
+ // under XCUITest, so text entry readiness is driven by tap/keyboard state.
380
+ return nil
381
+ #else
338
382
  var focused: XCUIElement?
339
383
  let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
340
- let candidate = app
384
+ let candidates = app
341
385
  .descendants(matching: .any)
342
386
  .matching(NSPredicate(format: "hasKeyboardFocus == 1"))
343
- .firstMatch
344
- guard candidate.exists else { return }
345
-
346
- switch candidate.elementType {
347
- case .textField, .secureTextField, .searchField, .textView:
348
- focused = candidate
349
- default:
350
- return
387
+ .allElementsBoundByIndex
388
+ for candidate in candidates where candidate.exists {
389
+ switch candidate.elementType {
390
+ case .textField, .secureTextField, .searchField, .textView:
391
+ focused = candidate
392
+ return
393
+ default:
394
+ continue
395
+ }
351
396
  }
352
397
  })
353
398
  if let exceptionMessage {
@@ -358,6 +403,7 @@ extension RunnerTests {
358
403
  return nil
359
404
  }
360
405
  return focused
406
+ #endif
361
407
  }
362
408
 
363
409
  func stabilizeTextInputBeforeTyping(app: XCUIApplication, target: XCUIElement?) -> XCUIElement? {
@@ -417,6 +463,36 @@ extension RunnerTests {
417
463
  )
418
464
  }
419
465
 
466
+ func focusTextInputForTextEntry(app: XCUIApplication, element: XCUIElement) -> TextEntryTarget {
467
+ let point = textEntryRefreshPoint(for: element)
468
+ if let point {
469
+ _ = tapAt(app: app, x: point.x, y: point.y)
470
+ }
471
+ let stabilized = stabilizeTextInputBeforeTyping(app: app, target: element)
472
+ let resolved = waitForTextEntryReadiness(
473
+ app: app,
474
+ target: TextEntryTarget(
475
+ element: stabilized ?? element,
476
+ refreshPoint: point,
477
+ prefersFocusedElement: false
478
+ )
479
+ ) ?? stabilized ?? element
480
+ return TextEntryTarget(
481
+ element: resolved,
482
+ refreshPoint: textEntryRefreshPoint(for: resolved) ?? point,
483
+ prefersFocusedElement: false
484
+ )
485
+ }
486
+
487
+ func isTextEntryElement(_ element: XCUIElement) -> Bool {
488
+ switch element.elementType {
489
+ case .textField, .secureTextField, .searchField, .textView:
490
+ return true
491
+ default:
492
+ return false
493
+ }
494
+ }
495
+
420
496
  func resolveTextEntryMode(_ command: Command) -> TextTypingRepairMode {
421
497
  switch command.textEntryMode {
422
498
  case "append":
@@ -597,7 +673,7 @@ extension RunnerTests {
597
673
  guard let observedText = editableTextValue(for: targetElement) else {
598
674
  return TextEntryResult(verified: nil, repaired: repaired, expectedText: expectedText, observedText: nil)
599
675
  }
600
- guard observedText == expectedText else {
676
+ guard textEntryValueMatchesExpected(targetElement, observedText: observedText, expectedText: expectedText) else {
601
677
  return TextEntryResult(
602
678
  verified: false,
603
679
  repaired: repaired,
@@ -613,7 +689,11 @@ extension RunnerTests {
613
689
  return TextEntryResult(verified: nil, repaired: repaired, expectedText: expectedText, observedText: nil)
614
690
  }
615
691
  latestObservedText = nextObservedText
616
- guard nextObservedText == expectedText else {
692
+ guard textEntryValueMatchesExpected(
693
+ resolveTextEntryElement(app: app, target: target),
694
+ observedText: nextObservedText,
695
+ expectedText: expectedText
696
+ ) else {
617
697
  return TextEntryResult(
618
698
  verified: false,
619
699
  repaired: repaired,
@@ -630,6 +710,28 @@ extension RunnerTests {
630
710
  )
631
711
  }
632
712
 
713
+ private func textEntryValueMatchesExpected(
714
+ _ element: XCUIElement?,
715
+ observedText: String,
716
+ expectedText: String
717
+ ) -> Bool {
718
+ if observedText == expectedText {
719
+ return true
720
+ }
721
+ guard hasTextEntrySubmitSuffix(expectedText), element?.elementType != .textView else {
722
+ return false
723
+ }
724
+ var submittedText = expectedText
725
+ while hasTextEntrySubmitSuffix(submittedText) {
726
+ submittedText.removeLast()
727
+ }
728
+ return observedText == submittedText
729
+ }
730
+
731
+ private func hasTextEntrySubmitSuffix(_ text: String) -> Bool {
732
+ text.hasSuffix("\n") || text.hasSuffix("\r")
733
+ }
734
+
633
735
  private func expectedTextEntryValue(
634
736
  typedText: String,
635
737
  mode: TextTypingRepairMode,
@@ -661,7 +763,11 @@ extension RunnerTests {
661
763
  guard let observedText = editableTextValue(for: resolveTextEntryElement(app: app, target: target)) else {
662
764
  return false
663
765
  }
664
- if observedText == expectedText {
766
+ if textEntryValueMatchesExpected(
767
+ resolveTextEntryElement(app: app, target: target),
768
+ observedText: observedText,
769
+ expectedText: expectedText
770
+ ) {
665
771
  return false
666
772
  }
667
773
  latestObservedText = observedText
@@ -678,7 +784,11 @@ extension RunnerTests {
678
784
  guard let latestObservedText else {
679
785
  return false
680
786
  }
681
- guard latestObservedText != expectedText else {
787
+ guard !textEntryValueMatchesExpected(
788
+ resolveTextEntryElement(app: app, target: target),
789
+ observedText: latestObservedText,
790
+ expectedText: expectedText
791
+ ) else {
682
792
  return false
683
793
  }
684
794
  return isRepairableTextEntryMismatch(
@@ -780,6 +890,35 @@ extension RunnerTests {
780
890
  #endif
781
891
  }
782
892
 
893
+ func waitForTextEntryReadinessAfterTap(app: XCUIApplication, element: XCUIElement) {
894
+ #if os(iOS)
895
+ switch element.elementType {
896
+ case .textField, .secureTextField, .searchField, .textView:
897
+ if waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout) != nil {
898
+ return
899
+ }
900
+ let frame = element.frame
901
+ if !frame.isEmpty {
902
+ _ = tapAt(app: app, x: frame.midX, y: frame.midY)
903
+ _ = waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout)
904
+ }
905
+ default:
906
+ return
907
+ }
908
+ #endif
909
+ }
910
+
911
+ private func waitForFocusedTextInput(app: XCUIApplication, timeout: TimeInterval) -> XCUIElement? {
912
+ let deadline = Date().addingTimeInterval(timeout)
913
+ while Date() < deadline {
914
+ if let focused = focusedTextInput(app: app) {
915
+ return focused
916
+ }
917
+ sleepFor(TextEntryTiming.pollInterval)
918
+ }
919
+ return focusedTextInput(app: app)
920
+ }
921
+
783
922
  private func textEntryRefreshPoint(for element: XCUIElement?) -> CGPoint? {
784
923
  guard let element else {
785
924
  return nil
@@ -843,6 +982,85 @@ extension RunnerTests {
843
982
  #endif
844
983
  }
845
984
 
985
+ func pressKeyboardReturn(app: XCUIApplication) -> (wasVisible: Bool, pressed: Bool, visible: Bool) {
986
+ #if os(tvOS)
987
+ return (wasVisible: false, pressed: pressTvRemote(.select), visible: false)
988
+ #elseif os(iOS)
989
+ let wasVisible = isKeyboardVisible(app: app)
990
+ if tapKeyboardReturnControl(app: app) {
991
+ sleepFor(0.2)
992
+ return (wasVisible: wasVisible, pressed: true, visible: isKeyboardVisible(app: app))
993
+ }
994
+
995
+ var typed = false
996
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
997
+ app.typeText(XCUIKeyboardKey.return.rawValue)
998
+ typed = true
999
+ })
1000
+ if let exceptionMessage {
1001
+ NSLog(
1002
+ "AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_IGNORED_EXCEPTION=%@",
1003
+ exceptionMessage
1004
+ )
1005
+ if let singleTarget = singleTextEntryElement(app: app) {
1006
+ return pressKeyboardReturn(on: singleTarget, app: app, wasVisible: wasVisible)
1007
+ }
1008
+ return (wasVisible: wasVisible, pressed: false, visible: isKeyboardVisible(app: app))
1009
+ }
1010
+ sleepFor(0.2)
1011
+ return (wasVisible: wasVisible, pressed: typed, visible: isKeyboardVisible(app: app))
1012
+ #else
1013
+ return (wasVisible: false, pressed: false, visible: false)
1014
+ #endif
1015
+ }
1016
+
1017
+ private func pressKeyboardReturn(
1018
+ on element: XCUIElement,
1019
+ app: XCUIApplication,
1020
+ wasVisible: Bool
1021
+ ) -> (wasVisible: Bool, pressed: Bool, visible: Bool) {
1022
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
1023
+ element.tap()
1024
+ element.typeText(XCUIKeyboardKey.return.rawValue)
1025
+ })
1026
+ if let exceptionMessage {
1027
+ NSLog(
1028
+ "AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TARGET_IGNORED_EXCEPTION=%@",
1029
+ exceptionMessage
1030
+ )
1031
+ return (wasVisible: wasVisible, pressed: false, visible: isKeyboardVisible(app: app))
1032
+ }
1033
+ sleepFor(0.2)
1034
+ return (wasVisible: wasVisible, pressed: true, visible: isKeyboardVisible(app: app))
1035
+ }
1036
+
1037
+ private func singleTextEntryElement(app: XCUIApplication) -> XCUIElement? {
1038
+ #if os(iOS)
1039
+ var matches: [XCUIElement] = []
1040
+ let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
1041
+ matches = app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
1042
+ guard element.exists else { return false }
1043
+ switch element.elementType {
1044
+ case .textField, .secureTextField, .searchField, .textView:
1045
+ return true
1046
+ default:
1047
+ return false
1048
+ }
1049
+ }
1050
+ })
1051
+ if let exceptionMessage {
1052
+ NSLog(
1053
+ "AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TEXT_ENTRY_QUERY_IGNORED_EXCEPTION=%@",
1054
+ exceptionMessage
1055
+ )
1056
+ return nil
1057
+ }
1058
+ return matches.count == 1 ? matches[0] : nil
1059
+ #else
1060
+ return nil
1061
+ #endif
1062
+ }
1063
+
846
1064
  private func tapKeyboardDismissControl(app: XCUIApplication) -> Bool {
847
1065
  #if os(tvOS)
848
1066
  return false
@@ -880,6 +1098,22 @@ extension RunnerTests {
880
1098
  #endif
881
1099
  }
882
1100
 
1101
+ private func tapKeyboardReturnControl(app: XCUIApplication) -> Bool {
1102
+ #if os(iOS)
1103
+ for label in ["return", "Return", "Enter", "Go", "Search", "Next", "Done", "Send", "Join"] {
1104
+ let candidates = [
1105
+ app.keyboards.buttons[label],
1106
+ app.keyboards.keys[label],
1107
+ ]
1108
+ if let hittable = candidates.first(where: { $0.exists && $0.isHittable }) {
1109
+ hittable.tap()
1110
+ return true
1111
+ }
1112
+ }
1113
+ #endif
1114
+ return false
1115
+ }
1116
+
883
1117
  private func isKeyboardAccessoryControl(_ element: XCUIElement, keyboardFrame: CGRect) -> Bool {
884
1118
  let frame = element.frame
885
1119
  guard !frame.isEmpty && !keyboardFrame.isEmpty else {
@@ -942,11 +1176,24 @@ extension RunnerTests {
942
1176
  guard !normalizedValue.isEmpty else {
943
1177
  return false
944
1178
  }
945
- guard let placeholder = element.placeholderValue?.trimmingCharacters(in: .whitespacesAndNewlines),
946
- !placeholder.isEmpty else {
1179
+ let placeholder = element.placeholderValue?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
1180
+ if !placeholder.isEmpty && normalizedValue == placeholder {
1181
+ return true
1182
+ }
1183
+ if isGenericTextInputLabel(normalizedValue) {
1184
+ return true
1185
+ }
1186
+ let normalizedLabel = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
1187
+ return normalizedLabel == normalizedValue && isGenericTextInputLabel(normalizedLabel)
1188
+ }
1189
+
1190
+ private func isGenericTextInputLabel(_ value: String) -> Bool {
1191
+ switch value {
1192
+ case "Text input field":
1193
+ return true
1194
+ default:
947
1195
  return false
948
1196
  }
949
- return normalizedValue == placeholder
950
1197
  }
951
1198
 
952
1199
  private func readableText(for element: XCUIElement) -> String? {
@@ -1266,6 +1513,51 @@ extension RunnerTests {
1266
1513
  return performCoordinatePinch(app: app, scale: scale, x: x, y: y)
1267
1514
  }
1268
1515
 
1516
+ func rotateGesture(app: XCUIApplication, degrees: Double, x: Double?, y: Double?, velocity: Double) -> RunnerInteractionOutcome {
1517
+ return performCoordinateRotateGesture(app: app, degrees: degrees, x: x, y: y, velocity: velocity)
1518
+ }
1519
+
1520
+ func transformGesture(
1521
+ app: XCUIApplication,
1522
+ x: Double,
1523
+ y: Double,
1524
+ dx: Double,
1525
+ dy: Double,
1526
+ scale: Double,
1527
+ degrees: Double,
1528
+ durationMs: Double
1529
+ ) -> RunnerInteractionOutcome {
1530
+ #if os(iOS)
1531
+ let target = interactionRoot(app: app)
1532
+ if let message = RunnerSynthesizedGesture.synthesizeTransform(
1533
+ withApplication: app,
1534
+ x: x,
1535
+ y: y,
1536
+ dx: dx,
1537
+ dy: dy,
1538
+ scale: scale,
1539
+ degrees: degrees,
1540
+ radius: transformGestureRadius(frame: target.frame, scale: scale),
1541
+ durationMs: durationMs
1542
+ ) {
1543
+ return .unsupported(message)
1544
+ }
1545
+ return .performed
1546
+ #elseif os(tvOS)
1547
+ return .unsupported("transformGesture is not supported on tvOS")
1548
+ #else
1549
+ return .unsupported("transformGesture is not supported on macOS")
1550
+ #endif
1551
+ }
1552
+
1553
+ private func transformGestureRadius(frame: CGRect, scale: Double) -> Double {
1554
+ let shorterSide = Double(min(frame.width, frame.height))
1555
+ let frameRadius = shorterSide * 0.20
1556
+ let minimumEndRadius = shorterSide * 0.08
1557
+ let scaleAdjustedRadius = scale < 1.0 ? max(frameRadius, minimumEndRadius / scale) : frameRadius
1558
+ return min(max(scaleAdjustedRadius, 48.0), shorterSide * 0.35)
1559
+ }
1560
+
1269
1561
  private func performCoordinatePinch(app: XCUIApplication, scale: Double, x: Double?, y: Double?) -> RunnerInteractionOutcome {
1270
1562
  #if os(tvOS)
1271
1563
  return .unsupported("pinch is not supported on tvOS")
@@ -1304,6 +1596,19 @@ extension RunnerTests {
1304
1596
  #endif
1305
1597
  }
1306
1598
 
1599
+ private func performCoordinateRotateGesture(app: XCUIApplication, degrees: Double, x: Double?, y: Double?, velocity: Double) -> RunnerInteractionOutcome {
1600
+ #if os(iOS)
1601
+ let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
1602
+ let radians = CGFloat(degrees * .pi / 180.0)
1603
+ target.rotate(radians, withVelocity: CGFloat(velocity))
1604
+ return .performed
1605
+ #elseif os(tvOS)
1606
+ return .unsupported("rotate-gesture is not supported on tvOS")
1607
+ #else
1608
+ return .unsupported("rotate-gesture is not supported on macOS")
1609
+ #endif
1610
+ }
1611
+
1307
1612
  private func interactionRoot(app: XCUIApplication) -> XCUIElement {
1308
1613
  let windows = app.windows.allElementsBoundByIndex
1309
1614
  if let window = windows.first(where: { $0.exists && !$0.frame.isEmpty }) {
@@ -248,7 +248,9 @@ extension RunnerTests {
248
248
  .rotate,
249
249
  .appSwitcher,
250
250
  .keyboardDismiss,
251
- .pinch:
251
+ .pinch,
252
+ .rotateGesture,
253
+ .transformGesture:
252
254
  return true
253
255
  default:
254
256
  return false
@@ -23,8 +23,11 @@ enum CommandType: String, Codable {
23
23
  case rotate
24
24
  case appSwitcher
25
25
  case keyboardDismiss
26
+ case keyboardReturn
26
27
  case alert
27
28
  case pinch
29
+ case rotateGesture
30
+ case transformGesture
28
31
  case recordStart
29
32
  case recordStop
30
33
  case uptime
@@ -37,6 +40,7 @@ struct Command: Codable {
37
40
  let text: String?
38
41
  let selectorKey: String?
39
42
  let selectorValue: String?
43
+ let allowNonHittableCoordinateFallback: Bool?
40
44
  let delayMs: Int?
41
45
  let textEntryMode: String?
42
46
  let clearFirst: Bool?
@@ -52,10 +56,14 @@ struct Command: Codable {
52
56
  let pattern: String?
53
57
  let x2: Double?
54
58
  let y2: Double?
59
+ let dx: Double?
60
+ let dy: Double?
55
61
  let durationMs: Double?
56
62
  let direction: String?
57
63
  let orientation: String?
58
64
  let scale: Double?
65
+ let degrees: Double?
66
+ let velocity: Double?
59
67
  let outPath: String?
60
68
  let fps: Int?
61
69
  let quality: Int?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-device",
3
- "version": "0.15.2",
3
+ "version": "0.16.1",
4
4
  "description": "Agent-native CLI for AI mobile testing and app automation across iOS, Android, tvOS, Android TV, macOS, and Linux.",
5
5
  "mcpName": "io.github.callstackincubator/agent-device",
6
6
  "license": "MIT",
@@ -90,6 +90,9 @@
90
90
  "build:android-snapshot-helper": "sh ./scripts/build-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper",
91
91
  "package:android-snapshot-helper": "sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper",
92
92
  "package:android-snapshot-helper:npm": "rm -rf android-snapshot-helper/dist && sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") android-snapshot-helper/dist",
93
+ "build:android-multitouch-helper": "sh ./scripts/build-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper",
94
+ "package:android-multitouch-helper": "sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper",
95
+ "package:android-multitouch-helper:npm": "rm -rf android-multitouch-helper/dist && sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") android-multitouch-helper/dist",
93
96
  "build:macos-helper": "swift build -c release --package-path macos-helper",
94
97
  "build:all": "pnpm build:node && pnpm build:xcuitest",
95
98
  "ad": "node bin/agent-device.mjs",
@@ -104,7 +107,7 @@
104
107
  "check:tooling": "pnpm lint && pnpm typecheck && pnpm check:mcp-metadata && pnpm build",
105
108
  "check:unit": "pnpm test:unit && pnpm test:smoke",
106
109
  "check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit",
107
- "prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:android-snapshot-helper:npm",
110
+ "prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm",
108
111
  "typecheck": "tsc -p tsconfig.json",
109
112
  "test-app:install": "pnpm install --dir examples/test-app --ignore-workspace",
110
113
  "test-app:start": "pnpm --dir examples/test-app start",
@@ -122,7 +125,8 @@
122
125
  "test:integration:provider": "vitest run --project provider-integration",
123
126
  "test:integration:progress": "node scripts/integration-progress.mjs",
124
127
  "test:integration:progress:check": "node scripts/integration-progress.mjs --check",
125
- "test:skillgym": "pnpm build && skillgym run ./test/skillgym/suites/agent-device-smoke-suite.ts --config ./test/skillgym/skillgym.config.ts",
128
+ "test:skillgym": "node test/skillgym/runner-environment.ts && pnpm build && skillgym run ./test/skillgym/suites/agent-device-smoke-suite.ts --config ./test/skillgym/skillgym.config.ts",
129
+ "test:skillgym:case": "node test/skillgym/runner-environment.ts && pnpm build && skillgym run ./test/skillgym/suites/agent-device-smoke-suite.ts --config ./test/skillgym/skillgym.config.ts --case",
126
130
  "test:smoke": "node --test test/integration/smoke-*.test.ts",
127
131
  "test:integration:node": "node --test test/integration/*.test.ts",
128
132
  "test:integration": "pnpm test:integration:node && pnpm test:integration:provider",
@@ -144,6 +148,8 @@
144
148
  "!macos-helper/**/.build",
145
149
  "android-snapshot-helper/dist",
146
150
  "!android-snapshot-helper/dist/*.idsig",
151
+ "android-multitouch-helper/dist",
152
+ "!android-multitouch-helper/dist/*.idsig",
147
153
  "src/platforms/linux/atspi-dump.py",
148
154
  "skills",
149
155
  "server.json",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/callstackincubator/agent-device",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.15.2",
10
+ "version": "0.16.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "agent-device",
15
- "version": "0.15.2",
15
+ "version": "0.16.1",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -1 +0,0 @@
1
- 56421142cf1ac84bb0313d993da7eb408109149178873225741570ca96dd7aa0 agent-device-android-snapshot-helper-0.15.2.apk
package/dist/src/1393.js DELETED
@@ -1 +0,0 @@
1
- import{AppError as e}from"./9152.js";let r="user-installed";function t(e){return e??r}function o(r){if(void 0===r)throw new e("INVALID_ARGS","appsFilter must be resolved before executing the apps command");return r}export{r as DEFAULT_APPS_FILTER,o as assertResolvedAppsFilter,t as resolveAppsFilter};