11import XCTest
22
3+ private enum RunnerInterfaceOrientation {
4+ static let unknown = 0
5+ static let portrait = 1
6+ static let portraitUpsideDown = 2
7+ static let landscapeRight = 3
8+ static let landscapeLeft = 4
9+ }
10+
311extension RunnerTests {
412 struct TouchVisualizationFrame {
513 let x : Double
@@ -634,6 +642,50 @@ extension RunnerTests {
634642 return performCoordinateDrag ( app: app, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
635643 }
636644
645+ /// Rotates an interface-oriented point into the device-native (portrait) space the
646+ /// synthesized event path consumes — synthesized events skip XCTest's orientation
647+ /// handling, so without this a landscape tap lands in the wrong place.
648+ func nativeSynthesizedPoint(
649+ orientedX x: Double ,
650+ orientedY y: Double ,
651+ in frame: CGRect ,
652+ interfaceOrientation: Int
653+ ) -> CGPoint {
654+ let localX = x - Double( frame. minX)
655+ let localY = y - Double( frame. minY)
656+ let width = Double ( frame. width)
657+ let height = Double ( frame. height)
658+ switch interfaceOrientation {
659+ case RunnerInterfaceOrientation . landscapeRight:
660+ return CGPoint ( x: height - localY, y: localX)
661+ case RunnerInterfaceOrientation . landscapeLeft:
662+ return CGPoint ( x: localY, y: width - localX)
663+ case RunnerInterfaceOrientation . portraitUpsideDown:
664+ return CGPoint ( x: width - localX, y: height - localY)
665+ default : // portrait or unknown
666+ return CGPoint ( x: localX, y: localY)
667+ }
668+ }
669+
670+ /// Rotates an interface-oriented translation vector into the same native
671+ /// coordinate space as `nativeSynthesizedPoint`.
672+ func nativeSynthesizedVector(
673+ orientedDx dx: Double ,
674+ orientedDy dy: Double ,
675+ interfaceOrientation: Int
676+ ) -> CGVector {
677+ switch interfaceOrientation {
678+ case RunnerInterfaceOrientation . landscapeRight:
679+ return CGVector ( dx: - dy, dy: dx)
680+ case RunnerInterfaceOrientation . landscapeLeft:
681+ return CGVector ( dx: dy, dy: - dx)
682+ case RunnerInterfaceOrientation . portraitUpsideDown:
683+ return CGVector ( dx: - dx, dy: - dy)
684+ default : // portrait or unknown
685+ return CGVector ( dx: dx, dy: dy)
686+ }
687+ }
688+
637689 func synthesizedDragAt(
638690 app: XCUIApplication ,
639691 x: Double ,
@@ -643,12 +695,16 @@ extension RunnerTests {
643695 durationMs: Double
644696 ) -> RunnerInteractionOutcome {
645697#if os(iOS)
698+ let orientation = Int ( RunnerSynthesizedGesture . interfaceOrientation ( forApplication: app) )
699+ let frame = app. frame
700+ let start = nativeSynthesizedPoint ( orientedX: x, orientedY: y, in: frame, interfaceOrientation: orientation)
701+ let end = nativeSynthesizedPoint ( orientedX: x2, orientedY: y2, in: frame, interfaceOrientation: orientation)
646702 if let message = RunnerSynthesizedGesture . synthesizeSwipe (
647703 withApplication: app,
648- x: x ,
649- y: y ,
650- x2: x2 ,
651- y2: y2 ,
704+ x: Double ( start . x ) ,
705+ y: Double ( start . y ) ,
706+ x2: Double ( end . x ) ,
707+ y2: Double ( end . y ) ,
652708 durationMs: durationMs
653709 ) {
654710 return . unsupported(
@@ -672,10 +728,12 @@ extension RunnerTests {
672728
673729 func synthesizedTapAt( app: XCUIApplication , x: Double , y: Double ) -> RunnerInteractionOutcome {
674730#if os(iOS)
731+ let orientation = Int ( RunnerSynthesizedGesture . interfaceOrientation ( forApplication: app) )
732+ let point = nativeSynthesizedPoint ( orientedX: x, orientedY: y, in: app. frame, interfaceOrientation: orientation)
675733 if let message = RunnerSynthesizedGesture . synthesizeTap (
676734 withApplication: app,
677- x: x ,
678- y: y
735+ x: Double ( point . x ) ,
736+ y: Double ( point . y )
679737 ) {
680738 return . unsupported(
681739 message: message,
@@ -952,12 +1010,15 @@ extension RunnerTests {
9521010 ) -> RunnerInteractionOutcome {
9531011#if os(iOS)
9541012 let target = interactionRoot ( app: app)
1013+ let orientation = Int ( RunnerSynthesizedGesture . interfaceOrientation ( forApplication: app) )
1014+ let point = nativeSynthesizedPoint ( orientedX: x, orientedY: y, in: app. frame, interfaceOrientation: orientation)
1015+ let vector = nativeSynthesizedVector ( orientedDx: dx, orientedDy: dy, interfaceOrientation: orientation)
9551016 if let message = RunnerSynthesizedGesture . synthesizeTransform (
9561017 withApplication: app,
957- x: x ,
958- y: y ,
959- dx: dx ,
960- dy: dy ,
1018+ x: Double ( point . x ) ,
1019+ y: Double ( point . y ) ,
1020+ dx: Double ( vector . dx ) ,
1021+ dy: Double ( vector . dy ) ,
9611022 scale: scale,
9621023 degrees: degrees,
9631024 radius: transformGestureRadius ( frame: target. frame, scale: scale) ,
@@ -1091,4 +1152,51 @@ extension RunnerTests {
10911152 let element = app. descendants ( matching: . any) . matching ( predicate) . firstMatch
10921153 return element. exists ? element : nil
10931154 }
1155+
1156+ // Identity in portrait/unknown, 90° per landscape, 180° upside-down.
1157+ func testNativeSynthesizedPointRotatesByInterfaceOrientation( ) {
1158+ let portrait = CGRect ( x: 0 , y: 0 , width: 834 , height: 1210 )
1159+ let landscape = CGRect ( x: 0 , y: 0 , width: 1210 , height: 834 )
1160+ let offsetLandscape = CGRect ( x: 10 , y: 20 , width: 1210 , height: 834 )
1161+ // (frame, UIInterfaceOrientation, expected native point) for a tap at (170, 268).
1162+ let cases : [ ( CGRect , Int , CGPoint ) ] = [
1163+ ( portrait, RunnerInterfaceOrientation . portrait, CGPoint ( x: 170 , y: 268 ) ) ,
1164+ ( landscape, RunnerInterfaceOrientation . landscapeRight, CGPoint ( x: 566 , y: 170 ) ) ,
1165+ ( landscape, RunnerInterfaceOrientation . landscapeLeft, CGPoint ( x: 268 , y: 1040 ) ) ,
1166+ ( portrait, RunnerInterfaceOrientation . portraitUpsideDown, CGPoint ( x: 664 , y: 942 ) ) ,
1167+ ( portrait, RunnerInterfaceOrientation . unknown, CGPoint ( x: 170 , y: 268 ) ) ,
1168+ ]
1169+ for (frame, orientation, expected) in cases {
1170+ XCTAssertEqual (
1171+ nativeSynthesizedPoint ( orientedX: 170 , orientedY: 268 , in: frame, interfaceOrientation: orientation) ,
1172+ expected,
1173+ " interfaceOrientation \( orientation) "
1174+ )
1175+ }
1176+ XCTAssertEqual (
1177+ nativeSynthesizedPoint (
1178+ orientedX: 180 ,
1179+ orientedY: 288 ,
1180+ in: offsetLandscape,
1181+ interfaceOrientation: RunnerInterfaceOrientation . landscapeLeft
1182+ ) ,
1183+ CGPoint ( x: 268 , y: 1040 ) ,
1184+ " non-zero frame origin is localized before rotation "
1185+ )
1186+ }
1187+
1188+ func testNativeSynthesizedVectorRotatesByInterfaceOrientation( ) {
1189+ let cases : [ ( Int , CGVector ) ] = [
1190+ ( RunnerInterfaceOrientation . portrait, CGVector ( dx: 40 , dy: - 20 ) ) ,
1191+ ( RunnerInterfaceOrientation . landscapeRight, CGVector ( dx: 20 , dy: 40 ) ) ,
1192+ ( RunnerInterfaceOrientation . landscapeLeft, CGVector ( dx: - 20 , dy: - 40 ) ) ,
1193+ ( RunnerInterfaceOrientation . portraitUpsideDown, CGVector ( dx: - 40 , dy: 20 ) ) ,
1194+ ( RunnerInterfaceOrientation . unknown, CGVector ( dx: 40 , dy: - 20 ) ) ,
1195+ ]
1196+ for (orientation, expected) in cases {
1197+ let vector = nativeSynthesizedVector ( orientedDx: 40 , orientedDy: - 20 , interfaceOrientation: orientation)
1198+ XCTAssertEqual ( vector. dx, expected. dx, " dx interfaceOrientation \( orientation) " )
1199+ XCTAssertEqual ( vector. dy, expected. dy, " dy interfaceOrientation \( orientation) " )
1200+ }
1201+ }
10941202}
0 commit comments