@@ -1604,13 +1604,51 @@ extension RunnerTests {
16041604 degrees: 0 ,
16051605 durationMs: 300
16061606 )
1607+ #elseif os(tvOS)
1608+ return . unsupported(
1609+ message: " pinch is not supported on tvOS " ,
1610+ hint: " tvOS has no touch input; pinch requires a touchscreen (run on iOS). "
1611+ )
16071612#else
1608- return performCoordinatePinch ( app: app, scale: scale, x: x, y: y)
1613+ return . unsupported(
1614+ message: " pinch is not supported on macOS " ,
1615+ hint: " macOS automation has no multi-touch input; pinch requires a touchscreen (run on iOS). "
1616+ )
16091617#endif
16101618 }
16111619
16121620 func rotateGesture( app: XCUIApplication , degrees: Double , x: Double ? , y: Double ? , velocity: Double ) -> RunnerInteractionOutcome {
1613- return performCoordinateRotateGesture ( app: app, degrees: degrees, x: x, y: y, velocity: velocity)
1621+ #if os(iOS)
1622+ // Drive the two-finger XCTest synthesis path (the same one pinch/transformGesture use, #634)
1623+ // with zero translation/scale so React Native's rotation recognizer actually fires. The native
1624+ // XCUIElement.rotate(withVelocity:) injects a single synthetic rotation that RN's gesture
1625+ // handler does not read reliably — the same class of problem #629/#634 fixed for pinch.
1626+ // velocity is unused on iOS (synthesis speed is governed by durationMs); the wire contract
1627+ // keeps it for compatibility and direction is carried entirely by the sign of `degrees`.
1628+ let frame = interactionRoot ( app: app) . frame
1629+ let centerX = x ?? Double ( frame. midX)
1630+ let centerY = y ?? Double ( frame. midY)
1631+ return transformGesture (
1632+ app: app,
1633+ x: centerX,
1634+ y: centerY,
1635+ dx: 0 ,
1636+ dy: 0 ,
1637+ scale: 1 ,
1638+ degrees: degrees,
1639+ durationMs: 300
1640+ )
1641+ #elseif os(tvOS)
1642+ return . unsupported(
1643+ message: " rotate-gesture is not supported on tvOS " ,
1644+ hint: " tvOS has no touch input; rotation gestures require a touchscreen (run on iOS). "
1645+ )
1646+ #else
1647+ return . unsupported(
1648+ message: " rotate-gesture is not supported on macOS " ,
1649+ hint: " macOS automation has no multi-touch input; rotation gestures require a touchscreen (run on iOS). "
1650+ )
1651+ #endif
16141652 }
16151653
16161654 func transformGesture(
@@ -1636,13 +1674,22 @@ extension RunnerTests {
16361674 radius: transformGestureRadius ( frame: target. frame, scale: scale) ,
16371675 durationMs: durationMs
16381676 ) {
1639- return . unsupported( message)
1677+ return . unsupported(
1678+ message: message,
1679+ hint: " This gesture uses private XCTest event-synthesis APIs; rebuild the runner with a supported Xcode (these APIs can change across Xcode versions). "
1680+ )
16401681 }
16411682 return . performed
16421683#elseif os(tvOS)
1643- return . unsupported( " transformGesture is not supported on tvOS " )
1684+ return . unsupported(
1685+ message: " transformGesture is not supported on tvOS " ,
1686+ hint: " tvOS has no touch input; transform gestures require a touchscreen (run on iOS). "
1687+ )
16441688#else
1645- return . unsupported( " transformGesture is not supported on macOS " )
1689+ return . unsupported(
1690+ message: " transformGesture is not supported on macOS " ,
1691+ hint: " macOS automation has no multi-touch input; transform gestures require a touchscreen (run on iOS). "
1692+ )
16461693#endif
16471694 }
16481695
@@ -1654,57 +1701,6 @@ extension RunnerTests {
16541701 return min ( max ( scaleAdjustedRadius, 48.0 ) , shorterSide * 0.35 )
16551702 }
16561703
1657- private func performCoordinatePinch( app: XCUIApplication , scale: Double , x: Double ? , y: Double ? ) -> RunnerInteractionOutcome {
1658- #if os(tvOS)
1659- return . unsupported( " pinch is not supported on tvOS " )
1660- #else
1661- let target = app. windows. firstMatch. exists ? app. windows. firstMatch : app
1662-
1663- // Use double-tap + drag gesture for reliable map zoom
1664- // Zoom in (scale > 1): tap then drag UP
1665- // Zoom out (scale < 1): tap then drag DOWN
1666-
1667- // Determine center point (use provided x/y or screen center)
1668- let centerX = x. map { $0 / target. frame. width } ?? 0.5
1669- let centerY = y. map { $0 / target. frame. height } ?? 0.5
1670- let center = target. coordinate ( withNormalizedOffset: CGVector ( dx: centerX, dy: centerY) )
1671-
1672- // Calculate drag distance based on scale (clamped to reasonable range)
1673- // Larger scale = more drag distance
1674- let dragAmount : CGFloat
1675- if scale > 1.0 {
1676- // Zoom in: drag up (negative Y direction in normalized coords)
1677- dragAmount = min ( 0.4 , CGFloat ( scale - 1.0 ) * 0.2 )
1678- } else {
1679- // Zoom out: drag down (positive Y direction)
1680- dragAmount = min ( 0.4 , CGFloat ( 1.0 - scale) * 0.4 )
1681- }
1682-
1683- let endY = scale > 1.0 ? ( centerY - Double( dragAmount) ) : ( centerY + Double( dragAmount) )
1684- let endPoint = target. coordinate ( withNormalizedOffset: CGVector ( dx: centerX, dy: max ( 0.1 , min ( 0.9 , endY) ) ) )
1685-
1686- // Tap first (first tap of double-tap)
1687- center. tap ( )
1688-
1689- // Immediately press and drag (second tap + drag)
1690- center. press ( forDuration: 0.05 , thenDragTo: endPoint)
1691- return . performed
1692- #endif
1693- }
1694-
1695- private func performCoordinateRotateGesture( app: XCUIApplication , degrees: Double , x: Double ? , y: Double ? , velocity: Double ) -> RunnerInteractionOutcome {
1696- #if os(iOS)
1697- let target = app. windows. firstMatch. exists ? app. windows. firstMatch : app
1698- let radians = CGFloat ( degrees * . pi / 180.0 )
1699- target. rotate ( radians, withVelocity: CGFloat ( velocity) )
1700- return . performed
1701- #elseif os(tvOS)
1702- return . unsupported( " rotate-gesture is not supported on tvOS " )
1703- #else
1704- return . unsupported( " rotate-gesture is not supported on macOS " )
1705- #endif
1706- }
1707-
17081704 private func interactionRoot( app: XCUIApplication ) -> XCUIElement {
17091705 let windows = app. windows. allElementsBoundByIndex
17101706 if let window = windows. first ( where: { $0. exists && !$0. frame. isEmpty } ) {
@@ -1715,7 +1711,10 @@ extension RunnerTests {
17151711
17161712 private func performCoordinateTap( app: XCUIApplication , x: Double , y: Double ) -> RunnerInteractionOutcome {
17171713#if os(tvOS)
1718- return . unsupported( " coordinate tap is not supported on tvOS; move focus with swipe or scroll, then select the focused element " )
1714+ return . unsupported(
1715+ message: " coordinate tap is not supported on tvOS; move focus with swipe or scroll, then select the focused element " ,
1716+ hint: " tvOS has no coordinate input; move focus with swipe/scroll to the target, then select it. "
1717+ )
17191718#else
17201719 interactionCoordinate ( app: app, x: x, y: y) . tap ( )
17211720 return . performed
@@ -1724,7 +1723,10 @@ extension RunnerTests {
17241723
17251724 private func performCoordinateDoubleTap( app: XCUIApplication , x: Double , y: Double ) -> RunnerInteractionOutcome {
17261725#if os(tvOS)
1727- return . unsupported( " coordinate double tap is not supported on tvOS; move focus with swipe or scroll, then select the focused element " )
1726+ return . unsupported(
1727+ message: " coordinate double tap is not supported on tvOS; move focus with swipe or scroll, then select the focused element " ,
1728+ hint: " tvOS has no coordinate input; move focus with swipe/scroll to the target, then select it. "
1729+ )
17281730#else
17291731 interactionCoordinate ( app: app, x: x, y: y) . doubleTap ( )
17301732 return . performed
@@ -1733,7 +1735,10 @@ extension RunnerTests {
17331735
17341736 private func performCoordinateLongPress( app: XCUIApplication , x: Double , y: Double , duration: TimeInterval ) -> RunnerInteractionOutcome {
17351737#if os(tvOS)
1736- return . unsupported( " coordinate long press is not supported on tvOS; move focus with swipe or scroll, then long-select the focused element " )
1738+ return . unsupported(
1739+ message: " coordinate long press is not supported on tvOS; move focus with swipe or scroll, then long-select the focused element " ,
1740+ hint: " tvOS has no coordinate input; move focus with swipe/scroll to the target, then long-select it. "
1741+ )
17371742#else
17381743 interactionCoordinate ( app: app, x: x, y: y) . press ( forDuration: duration)
17391744 return . performed
@@ -1749,7 +1754,10 @@ extension RunnerTests {
17491754 holdDuration: TimeInterval
17501755 ) -> RunnerInteractionOutcome {
17511756#if os(tvOS)
1752- return . unsupported( " coordinate drag is not supported on tvOS " )
1757+ return . unsupported(
1758+ message: " coordinate drag is not supported on tvOS " ,
1759+ hint: " tvOS has no coordinate input; use remote-driven swipe/scroll to move focus instead. "
1760+ )
17531761#else
17541762 let start = interactionCoordinate ( app: app, x: x, y: y)
17551763 let end = interactionCoordinate ( app: app, x: x2, y: y2)
0 commit comments