agent-device 0.14.8 → 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 +1 -2
- package/android-snapshot-helper/README.md +4 -2
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.14.8.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.8.manifest.json → agent-device-android-snapshot-helper-0.14.9.manifest.json} +6 -6
- package/dist/src/6108.js +17 -17
- package/dist/src/7462.js +1 -1
- package/dist/src/9542.js +1 -1
- package/dist/src/9639.js +2 -2
- 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 +46 -46
- package/dist/src/command-schema.js +1 -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/internal/daemon.js +20 -20
- package/dist/src/selectors.d.ts +1 -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 +1 -1
- package/server.json +3 -3
- package/skills/agent-device/SKILL.md +11 -1
- package/skills/dogfood/SKILL.md +3 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.14.8.apk.sha256 +0 -1
- package/skills/react-devtools/SKILL.md +0 -48
package/dist/src/selectors.d.ts
CHANGED
package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift
CHANGED
|
@@ -13,6 +13,18 @@ extension RunnerTests {
|
|
|
13
13
|
return (gestureStartUptimeMs, currentUptimeMs())
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
private func unsupportedResponse(for outcome: RunnerInteractionOutcome) -> Response? {
|
|
17
|
+
switch outcome {
|
|
18
|
+
case .performed:
|
|
19
|
+
return nil
|
|
20
|
+
case .unsupported(let message):
|
|
21
|
+
return Response(
|
|
22
|
+
ok: false,
|
|
23
|
+
error: ErrorPayload(code: "UNSUPPORTED_OPERATION", message: message)
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
func execute(command: Command) throws -> Response {
|
|
17
29
|
if Thread.isMainThread {
|
|
18
30
|
return try executeOnMainSafely(command: command)
|
|
@@ -231,11 +243,15 @@ extension RunnerTests {
|
|
|
231
243
|
case .tap:
|
|
232
244
|
if let text = command.text {
|
|
233
245
|
if let element = findElement(app: activeApp, text: text) {
|
|
246
|
+
var outcome = RunnerInteractionOutcome.performed
|
|
234
247
|
let timing = measureGesture {
|
|
235
248
|
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
|
|
236
|
-
element
|
|
249
|
+
outcome = activateElement(app: activeApp, element: element, action: "tap by text")
|
|
237
250
|
}
|
|
238
251
|
}
|
|
252
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
253
|
+
return response
|
|
254
|
+
}
|
|
239
255
|
return Response(
|
|
240
256
|
ok: true,
|
|
241
257
|
data: DataPayload(
|
|
@@ -249,11 +265,15 @@ extension RunnerTests {
|
|
|
249
265
|
}
|
|
250
266
|
if let x = command.x, let y = command.y {
|
|
251
267
|
let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
|
|
268
|
+
var outcome = RunnerInteractionOutcome.performed
|
|
252
269
|
let timing = measureGesture {
|
|
253
270
|
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
|
|
254
|
-
tapAt(app: activeApp, x: x, y: y)
|
|
271
|
+
outcome = tapAt(app: activeApp, x: x, y: y)
|
|
255
272
|
}
|
|
256
273
|
}
|
|
274
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
275
|
+
return response
|
|
276
|
+
}
|
|
257
277
|
return Response(
|
|
258
278
|
ok: true,
|
|
259
279
|
data: DataPayload(
|
|
@@ -309,13 +329,19 @@ extension RunnerTests {
|
|
|
309
329
|
let doubleTap = command.doubleTap ?? false
|
|
310
330
|
let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
|
|
311
331
|
if doubleTap {
|
|
332
|
+
var outcome = RunnerInteractionOutcome.performed
|
|
312
333
|
let timing = measureGesture {
|
|
313
334
|
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
|
|
314
335
|
runSeries(count: count, pauseMs: intervalMs) { _ in
|
|
315
|
-
|
|
336
|
+
if case .performed = outcome {
|
|
337
|
+
outcome = doubleTapAt(app: activeApp, x: x, y: y)
|
|
338
|
+
}
|
|
316
339
|
}
|
|
317
340
|
}
|
|
318
341
|
}
|
|
342
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
343
|
+
return response
|
|
344
|
+
}
|
|
319
345
|
return Response(
|
|
320
346
|
ok: true,
|
|
321
347
|
data: DataPayload(
|
|
@@ -329,13 +355,19 @@ extension RunnerTests {
|
|
|
329
355
|
)
|
|
330
356
|
)
|
|
331
357
|
}
|
|
358
|
+
var outcome = RunnerInteractionOutcome.performed
|
|
332
359
|
let timing = measureGesture {
|
|
333
360
|
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
|
|
334
361
|
runSeries(count: count, pauseMs: intervalMs) { _ in
|
|
335
|
-
|
|
362
|
+
if case .performed = outcome {
|
|
363
|
+
outcome = tapAt(app: activeApp, x: x, y: y)
|
|
364
|
+
}
|
|
336
365
|
}
|
|
337
366
|
}
|
|
338
367
|
}
|
|
368
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
369
|
+
return response
|
|
370
|
+
}
|
|
339
371
|
return Response(
|
|
340
372
|
ok: true,
|
|
341
373
|
data: DataPayload(
|
|
@@ -354,11 +386,15 @@ extension RunnerTests {
|
|
|
354
386
|
}
|
|
355
387
|
let duration = (command.durationMs ?? 800) / 1000.0
|
|
356
388
|
let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
|
|
389
|
+
var outcome = RunnerInteractionOutcome.performed
|
|
357
390
|
let timing = measureGesture {
|
|
358
391
|
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
|
|
359
|
-
longPressAt(app: activeApp, x: x, y: y, duration: duration)
|
|
392
|
+
outcome = longPressAt(app: activeApp, x: x, y: y, duration: duration)
|
|
360
393
|
}
|
|
361
394
|
}
|
|
395
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
396
|
+
return response
|
|
397
|
+
}
|
|
362
398
|
return Response(
|
|
363
399
|
ok: true,
|
|
364
400
|
data: DataPayload(
|
|
@@ -377,11 +413,15 @@ extension RunnerTests {
|
|
|
377
413
|
}
|
|
378
414
|
let holdDuration = min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0)
|
|
379
415
|
let dragFrame = resolvedDragVisualizationFrame(app: activeApp, x: x, y: y, x2: x2, y2: y2)
|
|
416
|
+
var outcome = RunnerInteractionOutcome.performed
|
|
380
417
|
let timing = measureGesture {
|
|
381
418
|
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
|
|
382
|
-
dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
|
|
419
|
+
outcome = dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
|
|
383
420
|
}
|
|
384
421
|
}
|
|
422
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
423
|
+
return response
|
|
424
|
+
}
|
|
385
425
|
return Response(
|
|
386
426
|
ok: true,
|
|
387
427
|
data: DataPayload(
|
|
@@ -407,18 +447,25 @@ extension RunnerTests {
|
|
|
407
447
|
return Response(ok: false, error: ErrorPayload(message: "dragSeries pattern must be one-way or ping-pong"))
|
|
408
448
|
}
|
|
409
449
|
let holdDuration = min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0)
|
|
450
|
+
var outcome = RunnerInteractionOutcome.performed
|
|
410
451
|
let timing = measureGesture {
|
|
411
452
|
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
|
|
412
453
|
runSeries(count: count, pauseMs: pauseMs) { idx in
|
|
454
|
+
guard case .performed = outcome else {
|
|
455
|
+
return
|
|
456
|
+
}
|
|
413
457
|
let reverse = pattern == "ping-pong" && (idx % 2 == 1)
|
|
414
458
|
if reverse {
|
|
415
|
-
dragAt(app: activeApp, x: x2, y: y2, x2: x, y2: y, holdDuration: holdDuration)
|
|
459
|
+
outcome = dragAt(app: activeApp, x: x2, y: y2, x2: x, y2: y, holdDuration: holdDuration)
|
|
416
460
|
} else {
|
|
417
|
-
dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
|
|
461
|
+
outcome = dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
|
|
418
462
|
}
|
|
419
463
|
}
|
|
420
464
|
}
|
|
421
465
|
}
|
|
466
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
467
|
+
return response
|
|
468
|
+
}
|
|
422
469
|
return Response(
|
|
423
470
|
ok: true,
|
|
424
471
|
data: DataPayload(
|
|
@@ -427,6 +474,18 @@ extension RunnerTests {
|
|
|
427
474
|
gestureEndUptimeMs: timing.gestureEndUptimeMs
|
|
428
475
|
)
|
|
429
476
|
)
|
|
477
|
+
case .remotePress:
|
|
478
|
+
guard let button = tvRemoteButton(from: command.remoteButton) else {
|
|
479
|
+
return Response(ok: false, error: ErrorPayload(message: "remotePress requires remoteButton"))
|
|
480
|
+
}
|
|
481
|
+
let duration = (command.durationMs ?? 0) / 1000.0
|
|
482
|
+
guard pressTvRemote(button, duration: duration) else {
|
|
483
|
+
return Response(
|
|
484
|
+
ok: false,
|
|
485
|
+
error: ErrorPayload(code: "UNSUPPORTED_OPERATION", message: "remotePress is only supported on tvOS")
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
return Response(ok: true, data: DataPayload(message: "remote pressed"))
|
|
430
489
|
case .type:
|
|
431
490
|
guard let text = command.text else {
|
|
432
491
|
return Response(ok: false, error: ErrorPayload(message: "type requires text"))
|
|
@@ -633,13 +692,23 @@ extension RunnerTests {
|
|
|
633
692
|
return Response(ok: false, error: ErrorPayload(message: "alert not found"))
|
|
634
693
|
}
|
|
635
694
|
if action == "accept" {
|
|
636
|
-
let button = alert.buttons.allElementsBoundByIndex.first
|
|
637
|
-
|
|
695
|
+
guard let button = alert.buttons.allElementsBoundByIndex.first else {
|
|
696
|
+
return Response(ok: false, error: ErrorPayload(message: "alert accept button not found"))
|
|
697
|
+
}
|
|
698
|
+
let outcome = activateElement(app: activeApp, element: button, action: "alert accept")
|
|
699
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
700
|
+
return response
|
|
701
|
+
}
|
|
638
702
|
return Response(ok: true, data: DataPayload(message: "accepted"))
|
|
639
703
|
}
|
|
640
704
|
if action == "dismiss" {
|
|
641
|
-
let button = alert.buttons.allElementsBoundByIndex.last
|
|
642
|
-
|
|
705
|
+
guard let button = alert.buttons.allElementsBoundByIndex.last else {
|
|
706
|
+
return Response(ok: false, error: ErrorPayload(message: "alert dismiss button not found"))
|
|
707
|
+
}
|
|
708
|
+
let outcome = activateElement(app: activeApp, element: button, action: "alert dismiss")
|
|
709
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
710
|
+
return response
|
|
711
|
+
}
|
|
643
712
|
return Response(ok: true, data: DataPayload(message: "dismissed"))
|
|
644
713
|
}
|
|
645
714
|
let buttonLabels = alert.buttons.allElementsBoundByIndex.map { $0.label }
|
|
@@ -648,8 +717,12 @@ extension RunnerTests {
|
|
|
648
717
|
guard let scale = command.scale, scale > 0 else {
|
|
649
718
|
return Response(ok: false, error: ErrorPayload(message: "pinch requires scale > 0"))
|
|
650
719
|
}
|
|
720
|
+
var outcome = RunnerInteractionOutcome.performed
|
|
651
721
|
let timing = measureGesture {
|
|
652
|
-
pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
|
|
722
|
+
outcome = pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
|
|
723
|
+
}
|
|
724
|
+
if let response = unsupportedResponse(for: outcome) {
|
|
725
|
+
return response
|
|
653
726
|
}
|
|
654
727
|
return Response(
|
|
655
728
|
ok: true,
|
|
@@ -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?
|