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.
- package/README.md +51 -155
- package/android-multitouch-helper/README.md +41 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.1.apk +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.1.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.1.manifest.json +10 -0
- package/android-snapshot-helper/README.md +2 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.1.apk +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.1.apk.sha256 +1 -0
- 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
- package/dist/src/1231.js +1 -1
- package/dist/src/1769.js +7 -7
- package/dist/src/2099.js +1 -0
- package/dist/src/221.js +4 -4
- package/dist/src/3622.js +3 -0
- package/dist/src/7519.js +1 -0
- package/dist/src/7556.js +1 -1
- package/dist/src/89.js +1 -0
- package/dist/src/940.js +1 -1
- package/dist/src/9542.js +2 -2
- package/dist/src/9639.js +2 -2
- package/dist/src/989.js +1 -1
- package/dist/src/android-adb.d.ts +26 -0
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-snapshot-helper.d.ts +30 -0
- package/dist/src/batch.d.ts +9 -9
- package/dist/src/cli.js +495 -80
- package/dist/src/index.d.ts +47 -5
- package/dist/src/internal/daemon.js +69 -44
- package/dist/src/server.js +2 -2
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +1 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +19 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +297 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +144 -5
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +328 -23
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +3 -1
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +8 -0
- package/package.json +9 -3
- package/server.json +2 -2
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.15.2.apk +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.15.2.apk.sha256 +0 -1
- package/dist/src/1393.js +0 -1
- package/dist/src/2151.js +0 -438
- package/dist/src/3572.js +0 -1
- package/dist/src/7599.js +0 -3
- 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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
384
|
+
let candidates = app
|
|
341
385
|
.descendants(matching: .any)
|
|
342
386
|
.matching(NSPredicate(format: "hasKeyboardFocus == 1"))
|
|
343
|
-
.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
946
|
-
|
|
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 }) {
|
|
@@ -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.
|
|
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.
|
|
10
|
+
"version": "0.16.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "agent-device",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.16.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
Binary file
|
|
@@ -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};
|