agent-device 0.16.8 → 0.16.10

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 (27) hide show
  1. package/README.md +1 -0
  2. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.8.apk → agent-device-android-multitouch-helper-0.16.10.apk} +0 -0
  3. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.10.apk.sha256 +1 -0
  4. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.8.manifest.json → agent-device-android-multitouch-helper-0.16.10.manifest.json} +4 -4
  5. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.8.apk → agent-device-android-snapshot-helper-0.16.10.apk} +0 -0
  6. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.10.apk.sha256 +1 -0
  7. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.8.manifest.json → agent-device-android-snapshot-helper-0.16.10.manifest.json} +6 -6
  8. package/dist/src/2415.js +19 -19
  9. package/dist/src/8114.js +3 -3
  10. package/dist/src/apps.js +2 -2
  11. package/dist/src/generic.js +4 -3
  12. package/dist/src/input-actions.js +1 -1
  13. package/dist/src/session.js +2 -2
  14. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +197 -232
  15. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +282 -0
  16. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +29 -0
  17. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +8 -771
  18. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +30 -0
  19. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +2 -20
  20. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +10 -50
  21. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +723 -0
  22. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +64 -22
  23. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +7 -4
  24. package/package.json +1 -1
  25. package/server.json +2 -2
  26. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.8.apk.sha256 +0 -1
  27. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.8.apk.sha256 +0 -1
@@ -25,7 +25,116 @@ extension RunnerTests {
25
25
  }
26
26
  }
27
27
 
28
+ /// Optional visualization frame returned with a gesture response.
29
+ enum GestureFrame {
30
+ case none
31
+ case touch(TouchVisualizationFrame?)
32
+ case drag(DragVisualizationFrame)
33
+ }
34
+
35
+ /// Runs a gesture action with uniform timing capture. Touch gestures pass `idleTimeout: true`
36
+ /// (the default) to run inside the scroll idle-timeout + quiescence-skip wrapper; synthesis
37
+ /// gestures (pinch/rotate/transform) pass `false` because RunnerSynthesizedGesture governs its
38
+ /// own timing. Returns the captured timing and the action's outcome.
39
+ ///
40
+ /// NOTE: a new SYNTHESIS gesture must pass `idleTimeout: false` — the default `true` would wrap
41
+ /// it in the scroll idle-timeout/quiescence-skip path and change its runtime behavior.
42
+ private func performGesture(
43
+ _ app: XCUIApplication,
44
+ idleTimeout: Bool = true,
45
+ _ action: () -> RunnerInteractionOutcome
46
+ ) -> (timing: (gestureStartUptimeMs: Double, gestureEndUptimeMs: Double), outcome: RunnerInteractionOutcome) {
47
+ var outcome = RunnerInteractionOutcome.performed
48
+ let timing = measureGesture {
49
+ if idleTimeout {
50
+ withTemporaryScrollIdleTimeoutIfSupported(app) { outcome = action() }
51
+ } else {
52
+ outcome = action()
53
+ }
54
+ }
55
+ return (timing, outcome)
56
+ }
57
+
58
+ /// Single factory for the success payload every gesture returns (message + gesture timing +
59
+ /// an optional touch/drag visualization frame), so the field shape lives in one place.
60
+ private func gestureResponse(
61
+ message: String,
62
+ timing: (gestureStartUptimeMs: Double, gestureEndUptimeMs: Double),
63
+ frame: GestureFrame = .none
64
+ ) -> Response {
65
+ let data: DataPayload
66
+ switch frame {
67
+ case .none:
68
+ data = DataPayload(
69
+ message: message,
70
+ gestureStartUptimeMs: timing.gestureStartUptimeMs,
71
+ gestureEndUptimeMs: timing.gestureEndUptimeMs
72
+ )
73
+ case .touch(let f):
74
+ data = DataPayload(
75
+ message: message,
76
+ gestureStartUptimeMs: timing.gestureStartUptimeMs,
77
+ gestureEndUptimeMs: timing.gestureEndUptimeMs,
78
+ x: f?.x,
79
+ y: f?.y,
80
+ referenceWidth: f?.referenceWidth,
81
+ referenceHeight: f?.referenceHeight
82
+ )
83
+ case .drag(let f):
84
+ data = DataPayload(
85
+ message: message,
86
+ gestureStartUptimeMs: timing.gestureStartUptimeMs,
87
+ gestureEndUptimeMs: timing.gestureEndUptimeMs,
88
+ x: f.x,
89
+ y: f.y,
90
+ x2: f.x2,
91
+ y2: f.y2,
92
+ referenceWidth: f.referenceWidth,
93
+ referenceHeight: f.referenceHeight
94
+ )
95
+ }
96
+ return Response(ok: true, data: data)
97
+ }
98
+
28
99
  func execute(command: Command) throws -> Response {
100
+ if command.command == .status {
101
+ return executeStatus(command: command)
102
+ }
103
+ commandJournal.accept(command: command)
104
+ return try executeAccepted(command: command)
105
+ }
106
+
107
+ func executeAccepted(command: Command) throws -> Response {
108
+ commandJournal.start(command: command)
109
+ do {
110
+ let response = try executeDispatched(command: command)
111
+ commandJournal.finish(command: command, response: response)
112
+ return response
113
+ } catch {
114
+ commandJournal.fail(command: command, error: error)
115
+ throw error
116
+ }
117
+ }
118
+
119
+ func executeStatus(command: Command) -> Response {
120
+ guard
121
+ let statusCommandId = command.statusCommandId?
122
+ .trimmingCharacters(in: .whitespacesAndNewlines),
123
+ !statusCommandId.isEmpty
124
+ else {
125
+ return Response(
126
+ ok: false,
127
+ error: ErrorPayload(
128
+ code: "INVALID_ARGS",
129
+ message: "status requires statusCommandId",
130
+ hint: "Set statusCommandId to the commandId of the runner command to inspect."
131
+ )
132
+ )
133
+ }
134
+ return Response(ok: true, data: commandJournal.status(commandId: statusCommandId))
135
+ }
136
+
137
+ private func executeDispatched(command: Command) throws -> Response {
29
138
  if Thread.isMainThread {
30
139
  return try executeOnMainSafely(command: command)
31
140
  }
@@ -183,6 +292,8 @@ extension RunnerTests {
183
292
  }
184
293
 
185
294
  switch command.command {
295
+ case .status:
296
+ return executeStatus(command: command)
186
297
  case .shutdown:
187
298
  stopRecordingIfNeeded()
188
299
  return Response(ok: true, data: DataPayload(message: "shutdown"))
@@ -266,83 +377,46 @@ extension RunnerTests {
266
377
  let touchFrame = frame.isEmpty
267
378
  ? nil
268
379
  : resolvedTouchVisualizationFrame(app: activeApp, x: frame.midX, y: frame.midY)
269
- var outcome = RunnerInteractionOutcome.performed
270
- let timing = measureGesture {
271
- withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
272
- if match.usedNonHittableFallback {
273
- // Maestro compatibility: RN E2E backdoor controls can be 1x1 and
274
- // reported non-hittable by XCTest, while Maestro still taps their
275
- // resolved bounds. Keep this behind the explicit replay-only flag.
276
- outcome = tapAt(app: activeApp, x: frame.midX, y: frame.midY)
277
- } else {
278
- outcome = activateElement(app: activeApp, element: element, action: "tap by selector")
279
- }
380
+ let (timing, outcome) = performGesture(activeApp) {
381
+ if match.usedNonHittableFallback {
382
+ // Maestro compatibility: RN E2E backdoor controls can be 1x1 and
383
+ // reported non-hittable by XCTest, while Maestro still taps their
384
+ // resolved bounds. Keep this behind the explicit replay-only flag.
385
+ return tapAt(app: activeApp, x: frame.midX, y: frame.midY)
280
386
  }
387
+ return activateElement(app: activeApp, element: element, action: "tap by selector")
281
388
  }
282
389
  if let response = unsupportedResponse(for: outcome) {
283
390
  return response
284
391
  }
285
392
  waitForTextEntryReadinessAfterTap(app: activeApp, element: element)
286
- return Response(
287
- ok: true,
288
- data: DataPayload(
289
- message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
290
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
291
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
292
- x: touchFrame?.x,
293
- y: touchFrame?.y,
294
- referenceWidth: touchFrame?.referenceWidth,
295
- referenceHeight: touchFrame?.referenceHeight
296
- )
393
+ return gestureResponse(
394
+ message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
395
+ timing: timing,
396
+ frame: .touch(touchFrame)
297
397
  )
298
398
  }
299
399
  return Response(ok: false, error: ErrorPayload(code: "ELEMENT_NOT_FOUND", message: "element not found"))
300
400
  }
301
401
  if let text = command.text {
302
402
  if let element = findElement(app: activeApp, text: text) {
303
- var outcome = RunnerInteractionOutcome.performed
304
- let timing = measureGesture {
305
- withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
306
- outcome = activateElement(app: activeApp, element: element, action: "tap by text")
307
- }
403
+ let (timing, outcome) = performGesture(activeApp) {
404
+ activateElement(app: activeApp, element: element, action: "tap by text")
308
405
  }
309
406
  if let response = unsupportedResponse(for: outcome) {
310
407
  return response
311
408
  }
312
- return Response(
313
- ok: true,
314
- data: DataPayload(
315
- message: "tapped",
316
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
317
- gestureEndUptimeMs: timing.gestureEndUptimeMs
318
- )
319
- )
409
+ return gestureResponse(message: "tapped", timing: timing)
320
410
  }
321
411
  return Response(ok: false, error: ErrorPayload(message: "element not found"))
322
412
  }
323
413
  if let x = command.x, let y = command.y {
324
414
  let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
325
- var outcome = RunnerInteractionOutcome.performed
326
- let timing = measureGesture {
327
- withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
328
- outcome = tapAt(app: activeApp, x: x, y: y)
329
- }
330
- }
415
+ let (timing, outcome) = performGesture(activeApp) { tapAt(app: activeApp, x: x, y: y) }
331
416
  if let response = unsupportedResponse(for: outcome) {
332
417
  return response
333
418
  }
334
- return Response(
335
- ok: true,
336
- data: DataPayload(
337
- message: "tapped",
338
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
339
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
340
- x: touchFrame.x,
341
- y: touchFrame.y,
342
- referenceWidth: touchFrame.referenceWidth,
343
- referenceHeight: touchFrame.referenceHeight
344
- )
345
- )
419
+ return gestureResponse(message: "tapped", timing: timing, frame: .touch(touchFrame))
346
420
  }
347
421
  return Response(ok: false, error: ErrorPayload(message: "tap requires text or x/y"))
348
422
  case .mouseClick:
@@ -351,6 +425,8 @@ extension RunnerTests {
351
425
  }
352
426
  let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
353
427
  do {
428
+ // mouseClick throws (it has no RunnerInteractionOutcome), so it keeps raw measureGesture
429
+ // and only routes the success payload through gestureResponse.
354
430
  var clickError: Error?
355
431
  let timing = measureGesture {
356
432
  do {
@@ -362,18 +438,7 @@ extension RunnerTests {
362
438
  if let clickError {
363
439
  throw clickError
364
440
  }
365
- return Response(
366
- ok: true,
367
- data: DataPayload(
368
- message: "clicked",
369
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
370
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
371
- x: touchFrame.x,
372
- y: touchFrame.y,
373
- referenceWidth: touchFrame.referenceWidth,
374
- referenceHeight: touchFrame.referenceHeight
375
- )
376
- )
441
+ return gestureResponse(message: "clicked", timing: timing, frame: .touch(touchFrame))
377
442
  } catch {
378
443
  return Response(ok: false, error: ErrorPayload(message: error.localizedDescription))
379
444
  }
@@ -386,84 +451,46 @@ extension RunnerTests {
386
451
  let doubleTap = command.doubleTap ?? false
387
452
  let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
388
453
  if doubleTap {
389
- var outcome = RunnerInteractionOutcome.performed
390
- let timing = measureGesture {
391
- withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
392
- runSeries(count: count, pauseMs: intervalMs) { _ in
393
- if case .performed = outcome {
394
- outcome = doubleTapAt(app: activeApp, x: x, y: y)
395
- }
454
+ let (timing, outcome) = performGesture(activeApp) {
455
+ var outcome = RunnerInteractionOutcome.performed
456
+ runSeries(count: count, pauseMs: intervalMs) { _ in
457
+ if case .performed = outcome {
458
+ outcome = doubleTapAt(app: activeApp, x: x, y: y)
396
459
  }
397
460
  }
461
+ return outcome
398
462
  }
399
463
  if let response = unsupportedResponse(for: outcome) {
400
464
  return response
401
465
  }
402
- return Response(
403
- ok: true,
404
- data: DataPayload(
405
- message: "tap series",
406
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
407
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
408
- x: touchFrame.x,
409
- y: touchFrame.y,
410
- referenceWidth: touchFrame.referenceWidth,
411
- referenceHeight: touchFrame.referenceHeight
412
- )
413
- )
466
+ return gestureResponse(message: "tap series", timing: timing, frame: .touch(touchFrame))
414
467
  }
415
- var outcome = RunnerInteractionOutcome.performed
416
- let timing = measureGesture {
417
- withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
418
- runSeries(count: count, pauseMs: intervalMs) { _ in
419
- if case .performed = outcome {
420
- outcome = tapAt(app: activeApp, x: x, y: y)
421
- }
468
+ let (timing, outcome) = performGesture(activeApp) {
469
+ var outcome = RunnerInteractionOutcome.performed
470
+ runSeries(count: count, pauseMs: intervalMs) { _ in
471
+ if case .performed = outcome {
472
+ outcome = tapAt(app: activeApp, x: x, y: y)
422
473
  }
423
474
  }
475
+ return outcome
424
476
  }
425
477
  if let response = unsupportedResponse(for: outcome) {
426
478
  return response
427
479
  }
428
- return Response(
429
- ok: true,
430
- data: DataPayload(
431
- message: "tap series",
432
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
433
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
434
- x: touchFrame.x,
435
- y: touchFrame.y,
436
- referenceWidth: touchFrame.referenceWidth,
437
- referenceHeight: touchFrame.referenceHeight
438
- )
439
- )
480
+ return gestureResponse(message: "tap series", timing: timing, frame: .touch(touchFrame))
440
481
  case .longPress:
441
482
  guard let x = command.x, let y = command.y else {
442
483
  return Response(ok: false, error: ErrorPayload(message: "longPress requires x and y"))
443
484
  }
444
485
  let duration = (command.durationMs ?? 800) / 1000.0
445
486
  let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
446
- var outcome = RunnerInteractionOutcome.performed
447
- let timing = measureGesture {
448
- withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
449
- outcome = longPressAt(app: activeApp, x: x, y: y, duration: duration)
450
- }
487
+ let (timing, outcome) = performGesture(activeApp) {
488
+ longPressAt(app: activeApp, x: x, y: y, duration: duration)
451
489
  }
452
490
  if let response = unsupportedResponse(for: outcome) {
453
491
  return response
454
492
  }
455
- return Response(
456
- ok: true,
457
- data: DataPayload(
458
- message: "long pressed",
459
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
460
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
461
- x: touchFrame.x,
462
- y: touchFrame.y,
463
- referenceWidth: touchFrame.referenceWidth,
464
- referenceHeight: touchFrame.referenceHeight
465
- )
466
- )
493
+ return gestureResponse(message: "long pressed", timing: timing, frame: .touch(touchFrame))
467
494
  case .drag:
468
495
  guard let x = command.x, let y = command.y, let x2 = command.x2, let y2 = command.y2 else {
469
496
  return Response(ok: false, error: ErrorPayload(message: "drag requires x, y, x2, and y2"))
@@ -477,36 +504,20 @@ extension RunnerTests {
477
504
  x2: dragPoints.x2,
478
505
  y2: dragPoints.y2
479
506
  )
480
- var outcome = RunnerInteractionOutcome.performed
481
- let timing = measureGesture {
482
- withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
483
- outcome = dragAt(
484
- app: activeApp,
485
- x: dragPoints.x,
486
- y: dragPoints.y,
487
- x2: dragPoints.x2,
488
- y2: dragPoints.y2,
489
- holdDuration: holdDuration
490
- )
491
- }
507
+ let (timing, outcome) = performGesture(activeApp) {
508
+ dragAt(
509
+ app: activeApp,
510
+ x: dragPoints.x,
511
+ y: dragPoints.y,
512
+ x2: dragPoints.x2,
513
+ y2: dragPoints.y2,
514
+ holdDuration: holdDuration
515
+ )
492
516
  }
493
517
  if let response = unsupportedResponse(for: outcome) {
494
518
  return response
495
519
  }
496
- return Response(
497
- ok: true,
498
- data: DataPayload(
499
- message: "dragged",
500
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
501
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
502
- x: dragFrame.x,
503
- y: dragFrame.y,
504
- x2: dragFrame.x2,
505
- y2: dragFrame.y2,
506
- referenceWidth: dragFrame.referenceWidth,
507
- referenceHeight: dragFrame.referenceHeight
508
- )
509
- )
520
+ return gestureResponse(message: "dragged", timing: timing, frame: .drag(dragFrame))
510
521
  case .dragSeries:
511
522
  guard let x = command.x, let y = command.y, let x2 = command.x2, let y2 = command.y2 else {
512
523
  return Response(ok: false, error: ErrorPayload(message: "dragSeries requires x, y, x2, and y2"))
@@ -519,47 +530,39 @@ extension RunnerTests {
519
530
  }
520
531
  let holdDuration = min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0)
521
532
  let dragPoints = keyboardAvoidingDragPoints(app: activeApp, x: x, y: y, x2: x2, y2: y2)
522
- var outcome = RunnerInteractionOutcome.performed
523
- let timing = measureGesture {
524
- withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
525
- runSeries(count: count, pauseMs: pauseMs) { idx in
526
- guard case .performed = outcome else {
527
- return
528
- }
529
- let reverse = pattern == "ping-pong" && (idx % 2 == 1)
530
- if reverse {
531
- outcome = dragAt(
532
- app: activeApp,
533
- x: dragPoints.x2,
534
- y: dragPoints.y2,
535
- x2: dragPoints.x,
536
- y2: dragPoints.y,
537
- holdDuration: holdDuration
538
- )
539
- } else {
540
- outcome = dragAt(
541
- app: activeApp,
542
- x: dragPoints.x,
543
- y: dragPoints.y,
544
- x2: dragPoints.x2,
545
- y2: dragPoints.y2,
546
- holdDuration: holdDuration
547
- )
548
- }
533
+ let (timing, outcome) = performGesture(activeApp) {
534
+ var outcome = RunnerInteractionOutcome.performed
535
+ runSeries(count: count, pauseMs: pauseMs) { idx in
536
+ guard case .performed = outcome else {
537
+ return
538
+ }
539
+ let reverse = pattern == "ping-pong" && (idx % 2 == 1)
540
+ if reverse {
541
+ outcome = dragAt(
542
+ app: activeApp,
543
+ x: dragPoints.x2,
544
+ y: dragPoints.y2,
545
+ x2: dragPoints.x,
546
+ y2: dragPoints.y,
547
+ holdDuration: holdDuration
548
+ )
549
+ } else {
550
+ outcome = dragAt(
551
+ app: activeApp,
552
+ x: dragPoints.x,
553
+ y: dragPoints.y,
554
+ x2: dragPoints.x2,
555
+ y2: dragPoints.y2,
556
+ holdDuration: holdDuration
557
+ )
549
558
  }
550
559
  }
560
+ return outcome
551
561
  }
552
562
  if let response = unsupportedResponse(for: outcome) {
553
563
  return response
554
564
  }
555
- return Response(
556
- ok: true,
557
- data: DataPayload(
558
- message: "drag series",
559
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
560
- gestureEndUptimeMs: timing.gestureEndUptimeMs
561
- )
562
- )
565
+ return gestureResponse(message: "drag series", timing: timing)
563
566
  case .remotePress:
564
567
  guard let button = tvRemoteButton(from: command.remoteButton) else {
565
568
  return Response(ok: false, error: ErrorPayload(message: "remotePress requires remoteButton"))
@@ -593,32 +596,18 @@ extension RunnerTests {
593
596
  guard let direction = command.direction else {
594
597
  return Response(ok: false, error: ErrorPayload(message: "swipe requires direction"))
595
598
  }
599
+ // swipe returns an optional frame (tvOS-only) rather than a RunnerInteractionOutcome, so it
600
+ // keeps raw measureGesture and only routes the success payload through gestureResponse.
596
601
  var executedFrame: DragVisualizationFrame?
597
602
  let timing = measureGesture {
598
603
  withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
599
- executedFrame = swipe(
600
- app: activeApp,
601
- direction: direction
602
- )
604
+ executedFrame = swipe(app: activeApp, direction: direction)
603
605
  }
604
606
  }
605
607
  guard let dragFrame = executedFrame else {
606
608
  return Response(ok: false, error: ErrorPayload(message: "swipe is only supported on tvOS"))
607
609
  }
608
- return Response(
609
- ok: true,
610
- data: DataPayload(
611
- message: "swiped",
612
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
613
- gestureEndUptimeMs: timing.gestureEndUptimeMs,
614
- x: dragFrame.x,
615
- y: dragFrame.y,
616
- x2: dragFrame.x2,
617
- y2: dragFrame.y2,
618
- referenceWidth: dragFrame.referenceWidth,
619
- referenceHeight: dragFrame.referenceHeight
620
- )
621
- )
610
+ return gestureResponse(message: "swiped", timing: timing, frame: .drag(dragFrame))
622
611
  case .findText:
623
612
  guard let text = command.text else {
624
613
  return Response(ok: false, error: ErrorPayload(message: "findText requires text"))
@@ -785,21 +774,13 @@ extension RunnerTests {
785
774
  guard let scale = command.scale, scale > 0 else {
786
775
  return Response(ok: false, error: ErrorPayload(message: "pinch requires scale > 0"))
787
776
  }
788
- var outcome = RunnerInteractionOutcome.performed
789
- let timing = measureGesture {
790
- outcome = pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
777
+ let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
778
+ pinch(app: activeApp, scale: scale, x: command.x, y: command.y)
791
779
  }
792
780
  if let response = unsupportedResponse(for: outcome) {
793
781
  return response
794
782
  }
795
- return Response(
796
- ok: true,
797
- data: DataPayload(
798
- message: "pinched",
799
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
800
- gestureEndUptimeMs: timing.gestureEndUptimeMs
801
- )
802
- )
783
+ return gestureResponse(message: "pinched", timing: timing)
803
784
  case .rotateGesture:
804
785
  guard let degrees = command.degrees, degrees.isFinite else {
805
786
  return Response(ok: false, error: ErrorPayload(message: "rotateGesture requires degrees"))
@@ -808,9 +789,8 @@ extension RunnerTests {
808
789
  guard velocity.isFinite && velocity != 0 else {
809
790
  return Response(ok: false, error: ErrorPayload(message: "rotateGesture velocity must be non-zero"))
810
791
  }
811
- var outcome = RunnerInteractionOutcome.performed
812
- let timing = measureGesture {
813
- outcome = rotateGesture(
792
+ let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
793
+ rotateGesture(
814
794
  app: activeApp,
815
795
  degrees: degrees,
816
796
  x: command.x,
@@ -821,14 +801,7 @@ extension RunnerTests {
821
801
  if let response = unsupportedResponse(for: outcome) {
822
802
  return response
823
803
  }
824
- return Response(
825
- ok: true,
826
- data: DataPayload(
827
- message: "rotatedGesture",
828
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
829
- gestureEndUptimeMs: timing.gestureEndUptimeMs
830
- )
831
- )
804
+ return gestureResponse(message: "rotatedGesture", timing: timing)
832
805
  case .transformGesture:
833
806
  guard
834
807
  let x = command.x,
@@ -852,9 +825,8 @@ extension RunnerTests {
852
825
  guard durationMs.isFinite && durationMs >= 16 else {
853
826
  return Response(ok: false, error: ErrorPayload(message: "transformGesture durationMs must be >= 16"))
854
827
  }
855
- var outcome = RunnerInteractionOutcome.performed
856
- let timing = measureGesture {
857
- outcome = transformGesture(
828
+ let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
829
+ transformGesture(
858
830
  app: activeApp,
859
831
  x: x,
860
832
  y: y,
@@ -868,14 +840,7 @@ extension RunnerTests {
868
840
  if let response = unsupportedResponse(for: outcome) {
869
841
  return response
870
842
  }
871
- return Response(
872
- ok: true,
873
- data: DataPayload(
874
- message: "transformedGesture",
875
- gestureStartUptimeMs: timing.gestureStartUptimeMs,
876
- gestureEndUptimeMs: timing.gestureEndUptimeMs
877
- )
878
- )
843
+ return gestureResponse(message: "transformedGesture", timing: timing)
879
844
  }
880
845
  }
881
846