Skip to content

Commit d2a3742

Browse files
si-huynhthymikee
andauthored
fix: enable tvOS compilation for XCUITest runner (#492)
* fix: enable tvOS compilation for XCUITest runner XCUICoordinate type and XCUIElement.tap() are unavailable on tvOS. Gate coordinate-based interactions and touch APIs behind #if !os(tvOS) and provide tvOS alternatives using XCUIRemote (Siri Remote) actions: - tap/doubleTap → XCUIRemote.shared.press(.select) - longPress → XCUIRemote.shared.press(.select, forDuration:) - drag → directional remote press based on primary axis - back gesture → XCUIRemote menu button - app switcher → double home press - pinch/rotate → return unsupported (no-op / false) - keyboard dismiss → remote menu button - alert accept/dismiss → remote select button Verified: builds successfully for both tvOS Simulator and iOS Simulator. * fix: harden tvOS runner interactions * refactor: prune tvOS remote helpers * test: keep fallow focused on tvOS changes --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
1 parent 8a1e195 commit d2a3742

21 files changed

Lines changed: 561 additions & 130 deletions

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ extension RunnerTests {
1313
return (gestureStartUptimeMs, currentUptimeMs())
1414
}
1515

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+
1628
func execute(command: Command) throws -> Response {
1729
if Thread.isMainThread {
1830
return try executeOnMainSafely(command: command)
@@ -231,11 +243,15 @@ extension RunnerTests {
231243
case .tap:
232244
if let text = command.text {
233245
if let element = findElement(app: activeApp, text: text) {
246+
var outcome = RunnerInteractionOutcome.performed
234247
let timing = measureGesture {
235248
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
236-
element.tap()
249+
outcome = activateElement(app: activeApp, element: element, action: "tap by text")
237250
}
238251
}
252+
if let response = unsupportedResponse(for: outcome) {
253+
return response
254+
}
239255
return Response(
240256
ok: true,
241257
data: DataPayload(
@@ -249,11 +265,15 @@ extension RunnerTests {
249265
}
250266
if let x = command.x, let y = command.y {
251267
let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
268+
var outcome = RunnerInteractionOutcome.performed
252269
let timing = measureGesture {
253270
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
254-
tapAt(app: activeApp, x: x, y: y)
271+
outcome = tapAt(app: activeApp, x: x, y: y)
255272
}
256273
}
274+
if let response = unsupportedResponse(for: outcome) {
275+
return response
276+
}
257277
return Response(
258278
ok: true,
259279
data: DataPayload(
@@ -309,13 +329,19 @@ extension RunnerTests {
309329
let doubleTap = command.doubleTap ?? false
310330
let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
311331
if doubleTap {
332+
var outcome = RunnerInteractionOutcome.performed
312333
let timing = measureGesture {
313334
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
314335
runSeries(count: count, pauseMs: intervalMs) { _ in
315-
doubleTapAt(app: activeApp, x: x, y: y)
336+
if case .performed = outcome {
337+
outcome = doubleTapAt(app: activeApp, x: x, y: y)
338+
}
316339
}
317340
}
318341
}
342+
if let response = unsupportedResponse(for: outcome) {
343+
return response
344+
}
319345
return Response(
320346
ok: true,
321347
data: DataPayload(
@@ -329,13 +355,19 @@ extension RunnerTests {
329355
)
330356
)
331357
}
358+
var outcome = RunnerInteractionOutcome.performed
332359
let timing = measureGesture {
333360
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
334361
runSeries(count: count, pauseMs: intervalMs) { _ in
335-
tapAt(app: activeApp, x: x, y: y)
362+
if case .performed = outcome {
363+
outcome = tapAt(app: activeApp, x: x, y: y)
364+
}
336365
}
337366
}
338367
}
368+
if let response = unsupportedResponse(for: outcome) {
369+
return response
370+
}
339371
return Response(
340372
ok: true,
341373
data: DataPayload(
@@ -354,11 +386,15 @@ extension RunnerTests {
354386
}
355387
let duration = (command.durationMs ?? 800) / 1000.0
356388
let touchFrame = resolvedTouchVisualizationFrame(app: activeApp, x: x, y: y)
389+
var outcome = RunnerInteractionOutcome.performed
357390
let timing = measureGesture {
358391
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
359-
longPressAt(app: activeApp, x: x, y: y, duration: duration)
392+
outcome = longPressAt(app: activeApp, x: x, y: y, duration: duration)
360393
}
361394
}
395+
if let response = unsupportedResponse(for: outcome) {
396+
return response
397+
}
362398
return Response(
363399
ok: true,
364400
data: DataPayload(
@@ -377,11 +413,15 @@ extension RunnerTests {
377413
}
378414
let holdDuration = min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0)
379415
let dragFrame = resolvedDragVisualizationFrame(app: activeApp, x: x, y: y, x2: x2, y2: y2)
416+
var outcome = RunnerInteractionOutcome.performed
380417
let timing = measureGesture {
381418
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)
383420
}
384421
}
422+
if let response = unsupportedResponse(for: outcome) {
423+
return response
424+
}
385425
return Response(
386426
ok: true,
387427
data: DataPayload(
@@ -407,18 +447,25 @@ extension RunnerTests {
407447
return Response(ok: false, error: ErrorPayload(message: "dragSeries pattern must be one-way or ping-pong"))
408448
}
409449
let holdDuration = min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0)
450+
var outcome = RunnerInteractionOutcome.performed
410451
let timing = measureGesture {
411452
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
412453
runSeries(count: count, pauseMs: pauseMs) { idx in
454+
guard case .performed = outcome else {
455+
return
456+
}
413457
let reverse = pattern == "ping-pong" && (idx % 2 == 1)
414458
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)
416460
} 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)
418462
}
419463
}
420464
}
421465
}
466+
if let response = unsupportedResponse(for: outcome) {
467+
return response
468+
}
422469
return Response(
423470
ok: true,
424471
data: DataPayload(
@@ -427,6 +474,18 @@ extension RunnerTests {
427474
gestureEndUptimeMs: timing.gestureEndUptimeMs
428475
)
429476
)
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"))
430489
case .type:
431490
guard let text = command.text else {
432491
return Response(ok: false, error: ErrorPayload(message: "type requires text"))
@@ -633,13 +692,23 @@ extension RunnerTests {
633692
return Response(ok: false, error: ErrorPayload(message: "alert not found"))
634693
}
635694
if action == "accept" {
636-
let button = alert.buttons.allElementsBoundByIndex.first
637-
button?.tap()
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+
}
638702
return Response(ok: true, data: DataPayload(message: "accepted"))
639703
}
640704
if action == "dismiss" {
641-
let button = alert.buttons.allElementsBoundByIndex.last
642-
button?.tap()
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+
}
643712
return Response(ok: true, data: DataPayload(message: "dismissed"))
644713
}
645714
let buttonLabels = alert.buttons.allElementsBoundByIndex.map { $0.label }
@@ -648,8 +717,12 @@ extension RunnerTests {
648717
guard let scale = command.scale, scale > 0 else {
649718
return Response(ok: false, error: ErrorPayload(message: "pinch requires scale > 0"))
650719
}
720+
var outcome = RunnerInteractionOutcome.performed
651721
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
653726
}
654727
return Response(
655728
ok: true,

0 commit comments

Comments
 (0)