Skip to content

Commit 40ba711

Browse files
committed
refactor: split RunnerTests into focused files
1 parent 97f5925 commit 40ba711

11 files changed

Lines changed: 1989 additions & 1901 deletions

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

Lines changed: 381 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
3+
// MARK: - Environment
4+
5+
enum RunnerEnv {
6+
static func resolvePort() -> UInt16 {
7+
if let env = ProcessInfo.processInfo.environment["AGENT_DEVICE_RUNNER_PORT"], let port = UInt16(env) {
8+
return port
9+
}
10+
for arg in CommandLine.arguments {
11+
if arg.hasPrefix("AGENT_DEVICE_RUNNER_PORT=") {
12+
let value = arg.replacingOccurrences(of: "AGENT_DEVICE_RUNNER_PORT=", with: "")
13+
if let port = UInt16(value) { return port }
14+
}
15+
}
16+
return 0
17+
}
18+
19+
static func isTruthy(_ name: String) -> Bool {
20+
guard let raw = ProcessInfo.processInfo.environment[name] else {
21+
return false
22+
}
23+
switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
24+
case "1", "true", "yes", "on":
25+
return true
26+
default:
27+
return false
28+
}
29+
}
30+
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import XCTest
2+
3+
extension RunnerTests {
4+
// MARK: - Navigation Gestures
5+
6+
func tapNavigationBack(app: XCUIApplication) -> Bool {
7+
let buttons = app.navigationBars.buttons.allElementsBoundByIndex
8+
if let back = buttons.first(where: { $0.isHittable }) {
9+
back.tap()
10+
return true
11+
}
12+
return pressTvRemoteMenuIfAvailable()
13+
}
14+
15+
func performBackGesture(app: XCUIApplication) {
16+
if pressTvRemoteMenuIfAvailable() {
17+
return
18+
}
19+
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
20+
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.05, dy: 0.5))
21+
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
22+
start.press(forDuration: 0.05, thenDragTo: end)
23+
}
24+
25+
func performAppSwitcherGesture(app: XCUIApplication) {
26+
if performTvRemoteAppSwitcherIfAvailable() {
27+
return
28+
}
29+
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
30+
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.99))
31+
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.7))
32+
start.press(forDuration: 0.6, thenDragTo: end)
33+
}
34+
35+
func pressHomeButton() {
36+
if pressTvRemoteHomeIfAvailable() {
37+
return
38+
}
39+
XCUIDevice.shared.press(.home)
40+
}
41+
42+
private func pressTvRemoteMenuIfAvailable() -> Bool {
43+
#if os(tvOS)
44+
XCUIRemote.shared.press(.menu)
45+
return true
46+
#else
47+
return false
48+
#endif
49+
}
50+
51+
private func pressTvRemoteHomeIfAvailable() -> Bool {
52+
#if os(tvOS)
53+
XCUIRemote.shared.press(.home)
54+
return true
55+
#else
56+
return false
57+
#endif
58+
}
59+
60+
private func performTvRemoteAppSwitcherIfAvailable() -> Bool {
61+
#if os(tvOS)
62+
XCUIRemote.shared.press(.home)
63+
sleepFor(resolveTvRemoteDoublePressDelay())
64+
XCUIRemote.shared.press(.home)
65+
return true
66+
#else
67+
return false
68+
#endif
69+
}
70+
71+
private func resolveTvRemoteDoublePressDelay() -> TimeInterval {
72+
guard
73+
let raw = ProcessInfo.processInfo.environment["AGENT_DEVICE_TV_REMOTE_DOUBLE_PRESS_DELAY_MS"],
74+
!raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
75+
else {
76+
return tvRemoteDoublePressDelayDefault
77+
}
78+
guard let parsedMs = Double(raw), parsedMs >= 0 else {
79+
return tvRemoteDoublePressDelayDefault
80+
}
81+
return min(parsedMs, 1000) / 1000.0
82+
}
83+
84+
func findElement(app: XCUIApplication, text: String) -> XCUIElement? {
85+
let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR identifier CONTAINS[c] %@ OR value CONTAINS[c] %@", text, text, text)
86+
let element = app.descendants(matching: .any).matching(predicate).firstMatch
87+
return element.exists ? element : nil
88+
}
89+
90+
func clearTextInput(_ element: XCUIElement) {
91+
moveCaretToEnd(element: element)
92+
let count = estimatedDeleteCount(for: element)
93+
let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: count)
94+
element.typeText(deletes)
95+
}
96+
97+
func focusedTextInput(app: XCUIApplication) -> XCUIElement? {
98+
let focused = app
99+
.descendants(matching: .any)
100+
.matching(NSPredicate(format: "hasKeyboardFocus == 1"))
101+
.firstMatch
102+
guard focused.exists else { return nil }
103+
104+
switch focused.elementType {
105+
case .textField, .secureTextField, .searchField, .textView:
106+
return focused
107+
default:
108+
return nil
109+
}
110+
}
111+
112+
private func moveCaretToEnd(element: XCUIElement) {
113+
let frame = element.frame
114+
guard !frame.isEmpty else {
115+
element.tap()
116+
return
117+
}
118+
let origin = element.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
119+
let target = origin.withOffset(
120+
CGVector(dx: max(2, frame.width - 4), dy: max(2, frame.height / 2))
121+
)
122+
target.tap()
123+
}
124+
125+
private func estimatedDeleteCount(for element: XCUIElement) -> Int {
126+
let valueText = String(describing: element.value ?? "")
127+
.trimmingCharacters(in: .whitespacesAndNewlines)
128+
let base = valueText.isEmpty ? 24 : (valueText.count + 8)
129+
return max(24, min(120, base))
130+
}
131+
132+
func findScopeElement(app: XCUIApplication, scope: String) -> XCUIElement? {
133+
let predicate = NSPredicate(
134+
format: "label CONTAINS[c] %@ OR identifier CONTAINS[c] %@",
135+
scope,
136+
scope
137+
)
138+
let element = app.descendants(matching: .any).matching(predicate).firstMatch
139+
return element.exists ? element : nil
140+
}
141+
142+
func tapAt(app: XCUIApplication, x: Double, y: Double) {
143+
let origin = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
144+
let coordinate = origin.withOffset(CGVector(dx: x, dy: y))
145+
coordinate.tap()
146+
}
147+
148+
func doubleTapAt(app: XCUIApplication, x: Double, y: Double) {
149+
let origin = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
150+
let coordinate = origin.withOffset(CGVector(dx: x, dy: y))
151+
coordinate.doubleTap()
152+
}
153+
154+
func longPressAt(app: XCUIApplication, x: Double, y: Double, duration: TimeInterval) {
155+
let origin = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
156+
let coordinate = origin.withOffset(CGVector(dx: x, dy: y))
157+
coordinate.press(forDuration: duration)
158+
}
159+
160+
func dragAt(
161+
app: XCUIApplication,
162+
x: Double,
163+
y: Double,
164+
x2: Double,
165+
y2: Double,
166+
holdDuration: TimeInterval
167+
) {
168+
let origin = app.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
169+
let start = origin.withOffset(CGVector(dx: x, dy: y))
170+
let end = origin.withOffset(CGVector(dx: x2, dy: y2))
171+
start.press(forDuration: holdDuration, thenDragTo: end)
172+
}
173+
174+
func runSeries(count: Int, pauseMs: Double, operation: (Int) -> Void) {
175+
let total = max(count, 1)
176+
let pause = max(pauseMs, 0)
177+
for idx in 0..<total {
178+
operation(idx)
179+
if idx < total - 1 && pause > 0 {
180+
Thread.sleep(forTimeInterval: pause / 1000.0)
181+
}
182+
}
183+
}
184+
185+
func swipe(app: XCUIApplication, direction: SwipeDirection) {
186+
if performTvRemoteSwipeIfAvailable(direction: direction) {
187+
return
188+
}
189+
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
190+
let start = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2))
191+
let end = target.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8))
192+
let left = target.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
193+
let right = target.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
194+
195+
switch direction {
196+
case .up:
197+
end.press(forDuration: 0.1, thenDragTo: start)
198+
case .down:
199+
start.press(forDuration: 0.1, thenDragTo: end)
200+
case .left:
201+
right.press(forDuration: 0.1, thenDragTo: left)
202+
case .right:
203+
left.press(forDuration: 0.1, thenDragTo: right)
204+
}
205+
}
206+
207+
private func performTvRemoteSwipeIfAvailable(direction: SwipeDirection) -> Bool {
208+
#if os(tvOS)
209+
switch direction {
210+
case .up:
211+
XCUIRemote.shared.press(.up)
212+
case .down:
213+
XCUIRemote.shared.press(.down)
214+
case .left:
215+
XCUIRemote.shared.press(.left)
216+
case .right:
217+
XCUIRemote.shared.press(.right)
218+
}
219+
return true
220+
#else
221+
return false
222+
#endif
223+
}
224+
225+
func pinch(app: XCUIApplication, scale: Double, x: Double?, y: Double?) {
226+
let target = app.windows.firstMatch.exists ? app.windows.firstMatch : app
227+
228+
// Use double-tap + drag gesture for reliable map zoom
229+
// Zoom in (scale > 1): tap then drag UP
230+
// Zoom out (scale < 1): tap then drag DOWN
231+
232+
// Determine center point (use provided x/y or screen center)
233+
let centerX = x.map { $0 / target.frame.width } ?? 0.5
234+
let centerY = y.map { $0 / target.frame.height } ?? 0.5
235+
let center = target.coordinate(withNormalizedOffset: CGVector(dx: centerX, dy: centerY))
236+
237+
// Calculate drag distance based on scale (clamped to reasonable range)
238+
// Larger scale = more drag distance
239+
let dragAmount: CGFloat
240+
if scale > 1.0 {
241+
// Zoom in: drag up (negative Y direction in normalized coords)
242+
dragAmount = min(0.4, CGFloat(scale - 1.0) * 0.2)
243+
} else {
244+
// Zoom out: drag down (positive Y direction)
245+
dragAmount = min(0.4, CGFloat(1.0 - scale) * 0.4)
246+
}
247+
248+
let endY = scale > 1.0 ? (centerY - Double(dragAmount)) : (centerY + Double(dragAmount))
249+
let endPoint = target.coordinate(withNormalizedOffset: CGVector(dx: centerX, dy: max(0.1, min(0.9, endY))))
250+
251+
// Tap first (first tap of double-tap)
252+
center.tap()
253+
254+
// Immediately press and drag (second tap + drag)
255+
center.press(forDuration: 0.05, thenDragTo: endPoint)
256+
}
257+
258+
func aggregatedLabel(for element: XCUIElement, depth: Int = 0) -> String? {
259+
if depth > 2 { return nil }
260+
let text = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
261+
if !text.isEmpty { return text }
262+
if let value = element.value {
263+
let valueText = String(describing: value).trimmingCharacters(in: .whitespacesAndNewlines)
264+
if !valueText.isEmpty { return valueText }
265+
}
266+
let children = element.children(matching: .any).allElementsBoundByIndex
267+
for child in children {
268+
if let childLabel = aggregatedLabel(for: child, depth: depth + 1) {
269+
return childLabel
270+
}
271+
}
272+
return nil
273+
}
274+
}

0 commit comments

Comments
 (0)