agent-device 0.14.7 → 0.14.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -9
- package/android-snapshot-helper/README.md +4 -2
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.14.7.apk → agent-device-android-snapshot-helper-0.14.9.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.14.9.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.14.7.manifest.json → agent-device-android-snapshot-helper-0.14.9.manifest.json} +6 -6
- package/dist/src/180.js +1 -1
- package/dist/src/208.js +1 -0
- package/dist/src/221.js +3 -3
- package/dist/src/6108.js +26 -0
- package/dist/src/7462.js +1 -0
- package/dist/src/7719.js +1 -0
- package/dist/src/9542.js +2 -2
- package/dist/src/9639.js +2 -2
- package/dist/src/9671.js +1 -0
- package/dist/src/9818.js +1 -1
- package/dist/src/android-adb.d.ts +11 -2
- package/dist/src/android-snapshot-helper.d.ts +12 -2
- package/dist/src/cli.js +82 -0
- package/dist/src/command-schema.js +382 -0
- package/dist/src/contracts.d.ts +1 -0
- package/dist/src/finders.d.ts +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +1 -1
- package/dist/src/internal/bin.js +2 -461
- package/dist/src/internal/daemon.js +20 -20
- package/dist/src/io.js +1 -1
- package/dist/src/remote-config.js +1 -1
- package/dist/src/selectors.d.ts +1 -0
- package/dist/src/server.js +20 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +86 -13
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +160 -93
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +1 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +3 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +15 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +1 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +185 -0
- package/package.json +33 -6
- package/server.json +21 -0
- package/skills/agent-device/SKILL.md +11 -1
- package/skills/dogfood/SKILL.md +3 -1
- package/smithery.yaml +1 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.14.7.apk.sha256 +0 -1
- package/dist/src/2007.js +0 -26
- package/skills/react-devtools/SKILL.md +0 -48
|
@@ -26,6 +26,9 @@ extension RunnerTests {
|
|
|
26
26
|
return true
|
|
27
27
|
}
|
|
28
28
|
return false
|
|
29
|
+
#elseif os(tvOS)
|
|
30
|
+
_ = pressTvRemote(.menu)
|
|
31
|
+
return true
|
|
29
32
|
#else
|
|
30
33
|
let buttons = app.navigationBars.buttons.allElementsBoundByIndex
|
|
31
34
|
if let back = buttons.first(where: { $0.isHittable }) {
|
|
@@ -37,20 +40,26 @@ extension RunnerTests {
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
func performBackGesture(app: XCUIApplication) {
|
|
40
|
-
if
|
|
43
|
+
if pressTvRemote(.menu) {
|
|
41
44
|
return
|
|
42
45
|
}
|
|
46
|
+
performCoordinateBackGesture(app: app)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private func performCoordinateBackGesture(app: XCUIApplication) {
|
|
50
|
+
#if !os(tvOS)
|
|
43
51
|
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
|
|
44
52
|
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.05, dy: 0.5))
|
|
45
53
|
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
|
|
46
54
|
start.press(forDuration: 0.05, thenDragTo: end)
|
|
55
|
+
#endif
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
func performSystemBackAction(app: XCUIApplication) -> Bool {
|
|
50
59
|
#if os(macOS)
|
|
51
60
|
return false
|
|
52
61
|
#else
|
|
53
|
-
if
|
|
62
|
+
if pressTvRemote(.menu) {
|
|
54
63
|
return true
|
|
55
64
|
}
|
|
56
65
|
performBackGesture(app: app)
|
|
@@ -59,20 +68,28 @@ extension RunnerTests {
|
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
func performAppSwitcherGesture(app: XCUIApplication) {
|
|
62
|
-
if
|
|
71
|
+
if pressTvRemote(.home) {
|
|
72
|
+
sleepFor(resolveTvRemoteDoublePressDelay())
|
|
73
|
+
_ = pressTvRemote(.home)
|
|
63
74
|
return
|
|
64
75
|
}
|
|
76
|
+
performCoordinateAppSwitcherGesture(app: app)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private func performCoordinateAppSwitcherGesture(app: XCUIApplication) {
|
|
80
|
+
#if !os(tvOS)
|
|
65
81
|
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
|
|
66
82
|
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99))
|
|
67
83
|
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.7))
|
|
68
84
|
start.press(forDuration: 0.6, thenDragTo: end)
|
|
85
|
+
#endif
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
func pressHomeButton() {
|
|
72
89
|
#if os(macOS)
|
|
73
90
|
return
|
|
74
91
|
#else
|
|
75
|
-
if
|
|
92
|
+
if pressTvRemote(.home) {
|
|
76
93
|
return
|
|
77
94
|
}
|
|
78
95
|
XCUIDevice.shared.press(.home)
|
|
@@ -80,7 +97,7 @@ extension RunnerTests {
|
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
func rotateDevice(to orientationName: String) -> Bool {
|
|
83
|
-
#if os(macOS)
|
|
100
|
+
#if os(macOS) || os(tvOS)
|
|
84
101
|
return false
|
|
85
102
|
#else
|
|
86
103
|
switch orientationName {
|
|
@@ -100,48 +117,6 @@ extension RunnerTests {
|
|
|
100
117
|
#endif
|
|
101
118
|
}
|
|
102
119
|
|
|
103
|
-
private func pressTvRemoteMenuIfAvailable() -> Bool {
|
|
104
|
-
#if os(tvOS)
|
|
105
|
-
XCUIRemote.shared.press(.menu)
|
|
106
|
-
return true
|
|
107
|
-
#else
|
|
108
|
-
return false
|
|
109
|
-
#endif
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private func pressTvRemoteHomeIfAvailable() -> Bool {
|
|
113
|
-
#if os(tvOS)
|
|
114
|
-
XCUIRemote.shared.press(.home)
|
|
115
|
-
return true
|
|
116
|
-
#else
|
|
117
|
-
return false
|
|
118
|
-
#endif
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private func performTvRemoteAppSwitcherIfAvailable() -> Bool {
|
|
122
|
-
#if os(tvOS)
|
|
123
|
-
XCUIRemote.shared.press(.home)
|
|
124
|
-
sleepFor(resolveTvRemoteDoublePressDelay())
|
|
125
|
-
XCUIRemote.shared.press(.home)
|
|
126
|
-
return true
|
|
127
|
-
#else
|
|
128
|
-
return false
|
|
129
|
-
#endif
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private func resolveTvRemoteDoublePressDelay() -> TimeInterval {
|
|
133
|
-
guard
|
|
134
|
-
let raw = ProcessInfo.processInfo.environment["AGENT_DEVICE_TV_REMOTE_DOUBLE_PRESS_DELAY_MS"],
|
|
135
|
-
!raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
136
|
-
else {
|
|
137
|
-
return tvRemoteDoublePressDelayDefault
|
|
138
|
-
}
|
|
139
|
-
guard let parsedMs = Double(raw), parsedMs >= 0 else {
|
|
140
|
-
return tvRemoteDoublePressDelayDefault
|
|
141
|
-
}
|
|
142
|
-
return min(parsedMs, 1000) / 1000.0
|
|
143
|
-
}
|
|
144
|
-
|
|
145
120
|
func findElement(app: XCUIApplication, text: String) -> XCUIElement? {
|
|
146
121
|
let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR identifier CONTAINS[c] %@ OR value CONTAINS[c] %@", text, text, text)
|
|
147
122
|
let element = app.descendants(matching: .any).matching(predicate).firstMatch
|
|
@@ -183,7 +158,9 @@ extension RunnerTests {
|
|
|
183
158
|
}
|
|
184
159
|
|
|
185
160
|
func clearTextInput(_ element: XCUIElement) {
|
|
161
|
+
#if !os(tvOS)
|
|
186
162
|
moveCaretToEnd(element: element)
|
|
163
|
+
#endif
|
|
187
164
|
let count = estimatedDeleteCount(for: element)
|
|
188
165
|
let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: count)
|
|
189
166
|
element.typeText(deletes)
|
|
@@ -268,6 +245,12 @@ extension RunnerTests {
|
|
|
268
245
|
return (wasVisible: false, dismissed: false, visible: false)
|
|
269
246
|
}
|
|
270
247
|
|
|
248
|
+
#if os(tvOS)
|
|
249
|
+
_ = pressTvRemote(.menu)
|
|
250
|
+
sleepFor(0.2)
|
|
251
|
+
let visible = isKeyboardVisible(app: app)
|
|
252
|
+
return (wasVisible: true, dismissed: !visible, visible: visible)
|
|
253
|
+
#else
|
|
271
254
|
let keyboard = app.keyboards.firstMatch
|
|
272
255
|
keyboard.swipeDown()
|
|
273
256
|
sleepFor(0.2)
|
|
@@ -282,9 +265,13 @@ extension RunnerTests {
|
|
|
282
265
|
}
|
|
283
266
|
|
|
284
267
|
return (wasVisible: true, dismissed: false, visible: isKeyboardVisible(app: app))
|
|
268
|
+
#endif
|
|
285
269
|
}
|
|
286
270
|
|
|
287
271
|
private func tapKeyboardDismissControl(app: XCUIApplication) -> Bool {
|
|
272
|
+
#if os(tvOS)
|
|
273
|
+
return false
|
|
274
|
+
#else
|
|
288
275
|
let keyboardFrame = app.keyboards.firstMatch.frame
|
|
289
276
|
for label in ["Hide keyboard", "Dismiss keyboard", "Done"] {
|
|
290
277
|
let candidates = [
|
|
@@ -313,6 +300,7 @@ extension RunnerTests {
|
|
|
313
300
|
}
|
|
314
301
|
}
|
|
315
302
|
return false
|
|
303
|
+
#endif
|
|
316
304
|
}
|
|
317
305
|
|
|
318
306
|
private func isKeyboardAccessoryControl(_ element: XCUIElement, keyboardFrame: CGRect) -> Bool {
|
|
@@ -324,6 +312,9 @@ extension RunnerTests {
|
|
|
324
312
|
}
|
|
325
313
|
|
|
326
314
|
private func moveCaretToEnd(element: XCUIElement) {
|
|
315
|
+
#if os(tvOS)
|
|
316
|
+
return
|
|
317
|
+
#else
|
|
327
318
|
let frame = element.frame
|
|
328
319
|
guard !frame.isEmpty else {
|
|
329
320
|
element.tap()
|
|
@@ -334,6 +325,7 @@ extension RunnerTests {
|
|
|
334
325
|
CGVector(dx: max(2, frame.width - 4), dy: max(2, frame.height / 2))
|
|
335
326
|
)
|
|
336
327
|
target.tap()
|
|
328
|
+
#endif
|
|
337
329
|
}
|
|
338
330
|
|
|
339
331
|
private func estimatedDeleteCount(for element: XCUIElement) -> Int {
|
|
@@ -379,49 +371,64 @@ extension RunnerTests {
|
|
|
379
371
|
return element.exists ? element : nil
|
|
380
372
|
}
|
|
381
373
|
|
|
382
|
-
func tapAt(app: XCUIApplication, x: Double, y: Double) {
|
|
383
|
-
let
|
|
384
|
-
|
|
374
|
+
func tapAt(app: XCUIApplication, x: Double, y: Double) -> RunnerInteractionOutcome {
|
|
375
|
+
if let outcome = selectFocusedTvElement(app: app, point: CGPoint(x: x, y: y), action: "tap") {
|
|
376
|
+
return outcome
|
|
377
|
+
}
|
|
378
|
+
return performCoordinateTap(app: app, x: x, y: y)
|
|
385
379
|
}
|
|
386
380
|
|
|
387
381
|
func mouseClickAt(app: XCUIApplication, x: Double, y: Double, button: String) throws {
|
|
382
|
+
#if os(macOS)
|
|
388
383
|
let coordinate = interactionCoordinate(app: app, x: x, y: y)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
default:
|
|
402
|
-
throw NSError(
|
|
403
|
-
domain: "AgentDeviceRunner",
|
|
404
|
-
code: 1,
|
|
405
|
-
userInfo: [NSLocalizedDescriptionKey: "unsupported mouse button: \(button)"]
|
|
406
|
-
)
|
|
407
|
-
}
|
|
408
|
-
#else
|
|
384
|
+
switch button {
|
|
385
|
+
case "primary":
|
|
386
|
+
coordinate.tap()
|
|
387
|
+
case "secondary":
|
|
388
|
+
coordinate.rightClick()
|
|
389
|
+
case "middle":
|
|
390
|
+
throw NSError(
|
|
391
|
+
domain: "AgentDeviceRunner",
|
|
392
|
+
code: 1,
|
|
393
|
+
userInfo: [NSLocalizedDescriptionKey: "middle mouse button is not supported"]
|
|
394
|
+
)
|
|
395
|
+
default:
|
|
409
396
|
throw NSError(
|
|
410
397
|
domain: "AgentDeviceRunner",
|
|
411
398
|
code: 1,
|
|
412
|
-
userInfo: [NSLocalizedDescriptionKey: "
|
|
399
|
+
userInfo: [NSLocalizedDescriptionKey: "unsupported mouse button: \(button)"]
|
|
413
400
|
)
|
|
414
|
-
|
|
401
|
+
}
|
|
402
|
+
#elseif os(tvOS)
|
|
403
|
+
throw NSError(
|
|
404
|
+
domain: "AgentDeviceRunner",
|
|
405
|
+
code: 1,
|
|
406
|
+
userInfo: [NSLocalizedDescriptionKey: "mouseClick is not supported on tvOS"]
|
|
407
|
+
)
|
|
408
|
+
#else
|
|
409
|
+
throw NSError(
|
|
410
|
+
domain: "AgentDeviceRunner",
|
|
411
|
+
code: 1,
|
|
412
|
+
userInfo: [NSLocalizedDescriptionKey: "mouseClick is only supported on macOS"]
|
|
413
|
+
)
|
|
414
|
+
#endif
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
-
func doubleTapAt(app: XCUIApplication, x: Double, y: Double) {
|
|
418
|
-
let
|
|
419
|
-
|
|
417
|
+
func doubleTapAt(app: XCUIApplication, x: Double, y: Double) -> RunnerInteractionOutcome {
|
|
418
|
+
if let outcome = selectFocusedTvElement(app: app, point: CGPoint(x: x, y: y), action: "double tap") {
|
|
419
|
+
guard case .performed = outcome else { return outcome }
|
|
420
|
+
sleepFor(0.1)
|
|
421
|
+
_ = pressTvRemote(.select)
|
|
422
|
+
return .performed
|
|
423
|
+
}
|
|
424
|
+
return performCoordinateDoubleTap(app: app, x: x, y: y)
|
|
420
425
|
}
|
|
421
426
|
|
|
422
|
-
func longPressAt(app: XCUIApplication, x: Double, y: Double, duration: TimeInterval) {
|
|
423
|
-
let
|
|
424
|
-
|
|
427
|
+
func longPressAt(app: XCUIApplication, x: Double, y: Double, duration: TimeInterval) -> RunnerInteractionOutcome {
|
|
428
|
+
if let outcome = longSelectFocusedTvElement(app: app, point: CGPoint(x: x, y: y), duration: duration) {
|
|
429
|
+
return outcome
|
|
430
|
+
}
|
|
431
|
+
return performCoordinateLongPress(app: app, x: x, y: y, duration: duration)
|
|
425
432
|
}
|
|
426
433
|
|
|
427
434
|
func dragAt(
|
|
@@ -431,10 +438,17 @@ extension RunnerTests {
|
|
|
431
438
|
x2: Double,
|
|
432
439
|
y2: Double,
|
|
433
440
|
holdDuration: TimeInterval
|
|
434
|
-
) {
|
|
435
|
-
|
|
436
|
-
let
|
|
437
|
-
|
|
441
|
+
) -> RunnerInteractionOutcome {
|
|
442
|
+
// tvOS has no coordinate drag. Preserve the direction as a focus move.
|
|
443
|
+
let dx = x2 - x
|
|
444
|
+
let dy = y2 - y
|
|
445
|
+
let button: TvRemoteButton = abs(dx) > abs(dy)
|
|
446
|
+
? (dx > 0 ? .right : .left)
|
|
447
|
+
: (dy > 0 ? .down : .up)
|
|
448
|
+
if pressTvRemote(button) {
|
|
449
|
+
return .performed
|
|
450
|
+
}
|
|
451
|
+
return performCoordinateDrag(app: app, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
|
|
438
452
|
}
|
|
439
453
|
|
|
440
454
|
func resolvedTouchVisualizationFrame(app: XCUIApplication, x: Double, y: Double) -> TouchVisualizationFrame {
|
|
@@ -510,26 +524,28 @@ extension RunnerTests {
|
|
|
510
524
|
}
|
|
511
525
|
|
|
512
526
|
private func performTvRemoteSwipeIfAvailable(direction: String) -> Bool {
|
|
513
|
-
#if os(tvOS)
|
|
514
527
|
switch direction {
|
|
515
528
|
case "up":
|
|
516
|
-
|
|
529
|
+
return pressTvRemote(.up)
|
|
517
530
|
case "down":
|
|
518
|
-
|
|
531
|
+
return pressTvRemote(.down)
|
|
519
532
|
case "left":
|
|
520
|
-
|
|
533
|
+
return pressTvRemote(.left)
|
|
521
534
|
case "right":
|
|
522
|
-
|
|
535
|
+
return pressTvRemote(.right)
|
|
523
536
|
default:
|
|
524
537
|
return false
|
|
525
538
|
}
|
|
526
|
-
return true
|
|
527
|
-
#else
|
|
528
|
-
return false
|
|
529
|
-
#endif
|
|
530
539
|
}
|
|
531
540
|
|
|
532
|
-
func pinch(app: XCUIApplication, scale: Double, x: Double?, y: Double?) {
|
|
541
|
+
func pinch(app: XCUIApplication, scale: Double, x: Double?, y: Double?) -> RunnerInteractionOutcome {
|
|
542
|
+
return performCoordinatePinch(app: app, scale: scale, x: x, y: y)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private func performCoordinatePinch(app: XCUIApplication, scale: Double, x: Double?, y: Double?) -> RunnerInteractionOutcome {
|
|
546
|
+
#if os(tvOS)
|
|
547
|
+
return .unsupported("pinch is not supported on tvOS")
|
|
548
|
+
#else
|
|
533
549
|
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
|
|
534
550
|
|
|
535
551
|
// Use double-tap + drag gesture for reliable map zoom
|
|
@@ -560,6 +576,8 @@ extension RunnerTests {
|
|
|
560
576
|
|
|
561
577
|
// Immediately press and drag (second tap + drag)
|
|
562
578
|
center.press(forDuration: 0.05, thenDragTo: endPoint)
|
|
579
|
+
return .performed
|
|
580
|
+
#endif
|
|
563
581
|
}
|
|
564
582
|
|
|
565
583
|
private func interactionRoot(app: XCUIApplication) -> XCUIElement {
|
|
@@ -570,6 +588,52 @@ extension RunnerTests {
|
|
|
570
588
|
return app
|
|
571
589
|
}
|
|
572
590
|
|
|
591
|
+
private func performCoordinateTap(app: XCUIApplication, x: Double, y: Double) -> RunnerInteractionOutcome {
|
|
592
|
+
#if os(tvOS)
|
|
593
|
+
return .unsupported("coordinate tap is not supported on tvOS; move focus with swipe or scroll, then select the focused element")
|
|
594
|
+
#else
|
|
595
|
+
interactionCoordinate(app: app, x: x, y: y).tap()
|
|
596
|
+
return .performed
|
|
597
|
+
#endif
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private func performCoordinateDoubleTap(app: XCUIApplication, x: Double, y: Double) -> RunnerInteractionOutcome {
|
|
601
|
+
#if os(tvOS)
|
|
602
|
+
return .unsupported("coordinate double tap is not supported on tvOS; move focus with swipe or scroll, then select the focused element")
|
|
603
|
+
#else
|
|
604
|
+
interactionCoordinate(app: app, x: x, y: y).doubleTap()
|
|
605
|
+
return .performed
|
|
606
|
+
#endif
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private func performCoordinateLongPress(app: XCUIApplication, x: Double, y: Double, duration: TimeInterval) -> RunnerInteractionOutcome {
|
|
610
|
+
#if os(tvOS)
|
|
611
|
+
return .unsupported("coordinate long press is not supported on tvOS; move focus with swipe or scroll, then long-select the focused element")
|
|
612
|
+
#else
|
|
613
|
+
interactionCoordinate(app: app, x: x, y: y).press(forDuration: duration)
|
|
614
|
+
return .performed
|
|
615
|
+
#endif
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private func performCoordinateDrag(
|
|
619
|
+
app: XCUIApplication,
|
|
620
|
+
x: Double,
|
|
621
|
+
y: Double,
|
|
622
|
+
x2: Double,
|
|
623
|
+
y2: Double,
|
|
624
|
+
holdDuration: TimeInterval
|
|
625
|
+
) -> RunnerInteractionOutcome {
|
|
626
|
+
#if os(tvOS)
|
|
627
|
+
return .unsupported("coordinate drag is not supported on tvOS")
|
|
628
|
+
#else
|
|
629
|
+
let start = interactionCoordinate(app: app, x: x, y: y)
|
|
630
|
+
let end = interactionCoordinate(app: app, x: x2, y: y2)
|
|
631
|
+
start.press(forDuration: holdDuration, thenDragTo: end)
|
|
632
|
+
return .performed
|
|
633
|
+
#endif
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#if !os(tvOS)
|
|
573
637
|
private func interactionCoordinate(app: XCUIApplication, x: Double, y: Double) -> XCUICoordinate {
|
|
574
638
|
let root = interactionRoot(app: app)
|
|
575
639
|
let origin = root.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
|
|
@@ -578,14 +642,17 @@ extension RunnerTests {
|
|
|
578
642
|
let offsetY = y - Double(rootFrame.origin.y)
|
|
579
643
|
return origin.withOffset(CGVector(dx: offsetX, dy: offsetY))
|
|
580
644
|
}
|
|
645
|
+
#endif
|
|
581
646
|
|
|
582
647
|
private func tapElementCenter(app: XCUIApplication, element: XCUIElement) {
|
|
583
648
|
let frame = element.frame
|
|
584
649
|
if !frame.isEmpty {
|
|
585
|
-
tapAt(app: app, x: frame.midX, y: frame.midY)
|
|
650
|
+
_ = tapAt(app: app, x: frame.midX, y: frame.midY)
|
|
586
651
|
return
|
|
587
652
|
}
|
|
653
|
+
#if !os(tvOS)
|
|
588
654
|
element.tap()
|
|
655
|
+
#endif
|
|
589
656
|
}
|
|
590
657
|
|
|
591
658
|
private func macOSNavigationBackElement(app: XCUIApplication) -> XCUIElement? {
|
|
@@ -8,6 +8,7 @@ enum CommandType: String, Codable {
|
|
|
8
8
|
case interactionFrame
|
|
9
9
|
case drag
|
|
10
10
|
case dragSeries
|
|
11
|
+
case remotePress
|
|
11
12
|
case type
|
|
12
13
|
case swipe
|
|
13
14
|
case findText
|
|
@@ -39,6 +40,7 @@ struct Command: Codable {
|
|
|
39
40
|
let x: Double?
|
|
40
41
|
let y: Double?
|
|
41
42
|
let button: String?
|
|
43
|
+
let remoteButton: String?
|
|
42
44
|
let count: Double?
|
|
43
45
|
let intervalMs: Double?
|
|
44
46
|
let doubleTap: Bool?
|
|
@@ -162,6 +164,7 @@ struct SnapshotNode: Codable {
|
|
|
162
164
|
let value: String?
|
|
163
165
|
let rect: SnapshotRect
|
|
164
166
|
let enabled: Bool
|
|
167
|
+
let focused: Bool?
|
|
165
168
|
let hittable: Bool
|
|
166
169
|
let depth: Int
|
|
167
170
|
let parentIndex: Int?
|
|
@@ -28,6 +28,7 @@ extension RunnerTests {
|
|
|
28
28
|
let identifier: String
|
|
29
29
|
let valueText: String?
|
|
30
30
|
let hittable: Bool
|
|
31
|
+
let focused: Bool
|
|
31
32
|
let visible: Bool
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -341,6 +342,7 @@ extension RunnerTests {
|
|
|
341
342
|
identifier: identifier,
|
|
342
343
|
valueText: valueText,
|
|
343
344
|
hittable: computedSnapshotHittable(snapshot, viewport: context.viewport, laterNodes: laterNodes),
|
|
345
|
+
focused: snapshotHasFocus(snapshot),
|
|
344
346
|
visible: isVisibleInViewport(snapshot.frame, context.viewport)
|
|
345
347
|
)
|
|
346
348
|
}
|
|
@@ -360,6 +362,7 @@ extension RunnerTests {
|
|
|
360
362
|
value: evaluation.valueText,
|
|
361
363
|
rect: snapshotRect(from: snapshot.frame),
|
|
362
364
|
enabled: snapshot.isEnabled,
|
|
365
|
+
focused: evaluation.focused ? true : nil,
|
|
363
366
|
hittable: evaluation.hittable,
|
|
364
367
|
depth: depth,
|
|
365
368
|
parentIndex: parentIndex,
|
|
@@ -525,6 +528,7 @@ extension RunnerTests {
|
|
|
525
528
|
value: node.value,
|
|
526
529
|
rect: node.rect,
|
|
527
530
|
enabled: node.enabled,
|
|
531
|
+
focused: node.focused,
|
|
528
532
|
hittable: node.hittable,
|
|
529
533
|
depth: depth,
|
|
530
534
|
parentIndex: parentIndex,
|
|
@@ -575,6 +579,7 @@ extension RunnerTests {
|
|
|
575
579
|
value: valueText,
|
|
576
580
|
rect: snapshotRect(from: frame),
|
|
577
581
|
enabled: element.isEnabled,
|
|
582
|
+
focused: elementHasFocus(element) ? true : nil,
|
|
578
583
|
hittable: element.isHittable,
|
|
579
584
|
depth: 0,
|
|
580
585
|
parentIndex: nil,
|
|
@@ -592,6 +597,16 @@ extension RunnerTests {
|
|
|
592
597
|
return node
|
|
593
598
|
}
|
|
594
599
|
|
|
600
|
+
private func snapshotHasFocus(_ snapshot: XCUIElementSnapshot) -> Bool {
|
|
601
|
+
var focused = false
|
|
602
|
+
_ = RunnerObjCExceptionCatcher.catchException({
|
|
603
|
+
if let value = (snapshot as! NSObject).value(forKey: "hasFocus") as? Bool {
|
|
604
|
+
focused = value
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
return focused
|
|
608
|
+
}
|
|
609
|
+
|
|
595
610
|
private func shouldExpandCollapsedTabContainer(_ snapshot: XCUIElementSnapshot) -> Bool {
|
|
596
611
|
let frame = snapshot.frame
|
|
597
612
|
if frame.isNull || frame.isEmpty { return false }
|
|
@@ -186,6 +186,7 @@ extension RunnerTests {
|
|
|
186
186
|
value: nil,
|
|
187
187
|
rect: snapshotRect(from: element.frame),
|
|
188
188
|
enabled: element.isEnabled,
|
|
189
|
+
focused: elementHasFocus(element) ? true : nil,
|
|
189
190
|
hittable: hittableOverride ?? element.isHittable,
|
|
190
191
|
depth: depth,
|
|
191
192
|
parentIndex: nil,
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
enum RunnerInteractionOutcome {
|
|
4
|
+
case performed
|
|
5
|
+
case unsupported(String)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
enum TvRemoteButton {
|
|
9
|
+
case select
|
|
10
|
+
case menu
|
|
11
|
+
case home
|
|
12
|
+
case up
|
|
13
|
+
case down
|
|
14
|
+
case left
|
|
15
|
+
case right
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
extension RunnerTests {
|
|
19
|
+
func resolveTvRemoteDoublePressDelay() -> TimeInterval {
|
|
20
|
+
guard
|
|
21
|
+
let raw = ProcessInfo.processInfo.environment["AGENT_DEVICE_TV_REMOTE_DOUBLE_PRESS_DELAY_MS"],
|
|
22
|
+
!raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
23
|
+
else {
|
|
24
|
+
return tvRemoteDoublePressDelayDefault
|
|
25
|
+
}
|
|
26
|
+
guard let parsedMs = Double(raw), parsedMs >= 0 else {
|
|
27
|
+
return tvRemoteDoublePressDelayDefault
|
|
28
|
+
}
|
|
29
|
+
return min(parsedMs, 1000) / 1000.0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@discardableResult
|
|
33
|
+
func pressTvRemote(_ button: TvRemoteButton, duration: TimeInterval? = nil) -> Bool {
|
|
34
|
+
#if os(tvOS)
|
|
35
|
+
let remoteButton = xcuiRemoteButton(button)
|
|
36
|
+
if let duration, duration > 0 {
|
|
37
|
+
XCUIRemote.shared.press(remoteButton, forDuration: duration)
|
|
38
|
+
} else {
|
|
39
|
+
XCUIRemote.shared.press(remoteButton)
|
|
40
|
+
}
|
|
41
|
+
return true
|
|
42
|
+
#else
|
|
43
|
+
return false
|
|
44
|
+
#endif
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func tvRemoteButton(from raw: String?) -> TvRemoteButton? {
|
|
48
|
+
switch raw?.lowercased() {
|
|
49
|
+
case "select":
|
|
50
|
+
return .select
|
|
51
|
+
case "menu":
|
|
52
|
+
return .menu
|
|
53
|
+
case "home":
|
|
54
|
+
return .home
|
|
55
|
+
case "up":
|
|
56
|
+
return .up
|
|
57
|
+
case "down":
|
|
58
|
+
return .down
|
|
59
|
+
case "left":
|
|
60
|
+
return .left
|
|
61
|
+
case "right":
|
|
62
|
+
return .right
|
|
63
|
+
default:
|
|
64
|
+
return nil
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func elementHasFocus(_ element: XCUIElement) -> Bool {
|
|
69
|
+
var focused = false
|
|
70
|
+
_ = RunnerObjCExceptionCatcher.catchException({
|
|
71
|
+
if let value = (element as NSObject).value(forKey: "hasFocus") as? Bool {
|
|
72
|
+
focused = value
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
return focused
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func activateElement(app: XCUIApplication, element: XCUIElement, action: String) -> RunnerInteractionOutcome {
|
|
79
|
+
if let outcome = selectFocusedTvElement(app: app, element: element, action: action) {
|
|
80
|
+
return outcome
|
|
81
|
+
}
|
|
82
|
+
return performElementTap(element)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func selectFocusedTvElement(app: XCUIApplication, point: CGPoint, action: String) -> RunnerInteractionOutcome? {
|
|
86
|
+
#if os(tvOS)
|
|
87
|
+
guard let focused = focusedTvElement(app: app), !focused.frame.isEmpty, focused.frame.contains(point) else {
|
|
88
|
+
return .unsupported("\(action) is supported on tvOS only when the requested point is inside the focused element")
|
|
89
|
+
}
|
|
90
|
+
_ = pressTvRemote(.select)
|
|
91
|
+
return .performed
|
|
92
|
+
#else
|
|
93
|
+
return nil
|
|
94
|
+
#endif
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func longSelectFocusedTvElement(app: XCUIApplication, point: CGPoint, duration: TimeInterval) -> RunnerInteractionOutcome? {
|
|
98
|
+
#if os(tvOS)
|
|
99
|
+
guard let focused = focusedTvElement(app: app), !focused.frame.isEmpty, focused.frame.contains(point) else {
|
|
100
|
+
return .unsupported("long press is supported on tvOS only when the requested point is inside the focused element")
|
|
101
|
+
}
|
|
102
|
+
_ = pressTvRemote(.select, duration: duration)
|
|
103
|
+
return .performed
|
|
104
|
+
#else
|
|
105
|
+
return nil
|
|
106
|
+
#endif
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private func performElementTap(_ element: XCUIElement) -> RunnerInteractionOutcome {
|
|
110
|
+
#if os(tvOS)
|
|
111
|
+
return .unsupported("element tap is not supported on tvOS; move focus with swipe or scroll, then select the focused element")
|
|
112
|
+
#else
|
|
113
|
+
element.tap()
|
|
114
|
+
return .performed
|
|
115
|
+
#endif
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private func selectFocusedTvElement(app: XCUIApplication, element: XCUIElement, action: String) -> RunnerInteractionOutcome? {
|
|
119
|
+
#if os(tvOS)
|
|
120
|
+
guard tvFocusedElementMatches(app: app, target: element) else {
|
|
121
|
+
return .unsupported("\(action) is supported on tvOS only when the requested element is focused")
|
|
122
|
+
}
|
|
123
|
+
_ = pressTvRemote(.select)
|
|
124
|
+
return .performed
|
|
125
|
+
#else
|
|
126
|
+
return nil
|
|
127
|
+
#endif
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private func tvFocusedElementMatches(app: XCUIApplication, target: XCUIElement) -> Bool {
|
|
131
|
+
#if os(tvOS)
|
|
132
|
+
if target.hasFocus {
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
guard let focused = focusedTvElement(app: app) else {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
let targetFrame = target.frame
|
|
139
|
+
let focusedFrame = focused.frame
|
|
140
|
+
guard !targetFrame.isEmpty && !focusedFrame.isEmpty else {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
let focusedCenter = CGPoint(x: focusedFrame.midX, y: focusedFrame.midY)
|
|
144
|
+
let targetCenter = CGPoint(x: targetFrame.midX, y: targetFrame.midY)
|
|
145
|
+
return targetFrame.contains(focusedCenter)
|
|
146
|
+
|| focusedFrame.contains(targetCenter)
|
|
147
|
+
|| targetFrame.intersects(focusedFrame)
|
|
148
|
+
#else
|
|
149
|
+
return false
|
|
150
|
+
#endif
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func focusedTvElement(app: XCUIApplication) -> XCUIElement? {
|
|
154
|
+
#if os(tvOS)
|
|
155
|
+
let focused = app
|
|
156
|
+
.descendants(matching: .any)
|
|
157
|
+
.matching(NSPredicate(format: "hasFocus == true"))
|
|
158
|
+
.firstMatch
|
|
159
|
+
return focused.exists ? focused : nil
|
|
160
|
+
#else
|
|
161
|
+
return nil
|
|
162
|
+
#endif
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#if os(tvOS)
|
|
166
|
+
private func xcuiRemoteButton(_ button: TvRemoteButton) -> XCUIRemote.Button {
|
|
167
|
+
switch button {
|
|
168
|
+
case .select:
|
|
169
|
+
return .select
|
|
170
|
+
case .menu:
|
|
171
|
+
return .menu
|
|
172
|
+
case .home:
|
|
173
|
+
return .home
|
|
174
|
+
case .up:
|
|
175
|
+
return .up
|
|
176
|
+
case .down:
|
|
177
|
+
return .down
|
|
178
|
+
case .left:
|
|
179
|
+
return .left
|
|
180
|
+
case .right:
|
|
181
|
+
return .right
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
#endif
|
|
185
|
+
}
|