@@ -219,20 +219,27 @@ final class RunnerTests: XCTestCase {
219219 let normalizedBundleId = command. appBundleId?
220220 . trimmingCharacters ( in: . whitespacesAndNewlines)
221221 let requestedBundleId = ( normalizedBundleId? . isEmpty == true ) ? nil : normalizedBundleId
222+ let switchedApp : Bool
222223 if let bundleId = requestedBundleId, currentBundleId != bundleId {
223224 let target = XCUIApplication ( bundleIdentifier: bundleId)
224225 NSLog ( " AGENT_DEVICE_RUNNER_ACTIVATE bundle=%@ state=%d " , bundleId, target. state. rawValue)
225226 // activate avoids terminating and relaunching the target app
226227 target. activate ( )
227228 currentApp = target
228229 currentBundleId = bundleId
230+ switchedApp = true
229231 } else if requestedBundleId == nil {
230232 // Do not reuse stale bundle targets when the caller does not explicitly request one.
231233 currentApp = nil
232234 currentBundleId = nil
235+ switchedApp = false
236+ } else {
237+ switchedApp = false
233238 }
234239 let activeApp = currentApp ?? app
235- _ = activeApp. waitForExistence ( timeout: 5 )
240+ if switchedApp {
241+ _ = activeApp. waitForExistence ( timeout: 5 )
242+ }
236243
237244 switch command. command {
238245 case . shutdown:
@@ -250,6 +257,23 @@ final class RunnerTests: XCTestCase {
250257 return Response ( ok: true , data: DataPayload ( message: " tapped " ) )
251258 }
252259 return Response ( ok: false , error: ErrorPayload ( message: " tap requires text or x/y " ) )
260+ case . tapSeries:
261+ guard let x = command. x, let y = command. y else {
262+ return Response ( ok: false , error: ErrorPayload ( message: " tapSeries requires x and y " ) )
263+ }
264+ let count = max ( Int ( command. count ?? 1 ) , 1 )
265+ let intervalMs = max ( command. intervalMs ?? 0 , 0 )
266+ let doubleTap = command. doubleTap ?? false
267+ if doubleTap {
268+ runSeries ( count: count, pauseMs: intervalMs) { _ in
269+ doubleTapAt ( app: activeApp, x: x, y: y)
270+ }
271+ return Response ( ok: true , data: DataPayload ( message: " tap series " ) )
272+ }
273+ runSeries ( count: count, pauseMs: intervalMs) { _ in
274+ tapAt ( app: activeApp, x: x, y: y)
275+ }
276+ return Response ( ok: true , data: DataPayload ( message: " tap series " ) )
253277 case . longPress:
254278 guard let x = command. x, let y = command. y else {
255279 return Response ( ok: false , error: ErrorPayload ( message: " longPress requires x and y " ) )
@@ -264,6 +288,26 @@ final class RunnerTests: XCTestCase {
264288 let holdDuration = min ( max ( ( command. durationMs ?? 60 ) / 1000.0 , 0.016 ) , 10.0 )
265289 dragAt ( app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
266290 return Response ( ok: true , data: DataPayload ( message: " dragged " ) )
291+ case . dragSeries:
292+ guard let x = command. x, let y = command. y, let x2 = command. x2, let y2 = command. y2 else {
293+ return Response ( ok: false , error: ErrorPayload ( message: " dragSeries requires x, y, x2, and y2 " ) )
294+ }
295+ let count = max ( Int ( command. count ?? 1 ) , 1 )
296+ let pauseMs = max ( command. pauseMs ?? 0 , 0 )
297+ let pattern = command. pattern ?? " one-way "
298+ if pattern != " one-way " && pattern != " ping-pong " {
299+ return Response ( ok: false , error: ErrorPayload ( message: " dragSeries pattern must be one-way or ping-pong " ) )
300+ }
301+ let holdDuration = min ( max ( ( command. durationMs ?? 60 ) / 1000.0 , 0.016 ) , 10.0 )
302+ runSeries ( count: count, pauseMs: pauseMs) { idx in
303+ let reverse = pattern == " ping-pong " && ( idx % 2 == 1 )
304+ if reverse {
305+ dragAt ( app: activeApp, x: x2, y: y2, x2: x, y2: y, holdDuration: holdDuration)
306+ } else {
307+ dragAt ( app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
308+ }
309+ }
310+ return Response ( ok: true , data: DataPayload ( message: " drag series " ) )
267311 case . type:
268312 guard let text = command. text else {
269313 return Response ( ok: false , error: ErrorPayload ( message: " type requires text " ) )
@@ -443,6 +487,12 @@ final class RunnerTests: XCTestCase {
443487 coordinate. tap ( )
444488 }
445489
490+ private func doubleTapAt( app: XCUIApplication , x: Double , y: Double ) {
491+ let origin = app. coordinate ( withNormalizedOffset: CGVector ( dx: 0 , dy: 0 ) )
492+ let coordinate = origin. withOffset ( CGVector ( dx: x, dy: y) )
493+ coordinate. doubleTap ( )
494+ }
495+
446496 private func longPressAt( app: XCUIApplication , x: Double , y: Double , duration: TimeInterval ) {
447497 let origin = app. coordinate ( withNormalizedOffset: CGVector ( dx: 0 , dy: 0 ) )
448498 let coordinate = origin. withOffset ( CGVector ( dx: x, dy: y) )
@@ -463,6 +513,17 @@ final class RunnerTests: XCTestCase {
463513 start. press ( forDuration: holdDuration, thenDragTo: end)
464514 }
465515
516+ private func runSeries( count: Int , pauseMs: Double , operation: ( Int ) -> Void ) {
517+ let total = max ( count, 1 )
518+ let pause = max ( pauseMs, 0 )
519+ for idx in 0 ..< total {
520+ operation ( idx)
521+ if idx < total - 1 && pause > 0 {
522+ Thread . sleep ( forTimeInterval: pause / 1000.0 )
523+ }
524+ }
525+ }
526+
466527 private func swipe( app: XCUIApplication , direction: SwipeDirection ) {
467528 let target = app. windows. firstMatch. exists ? app. windows. firstMatch : app
468529 let start = target. coordinate ( withNormalizedOffset: CGVector ( dx: 0.5 , dy: 0.2 ) )
@@ -982,8 +1043,10 @@ private func resolveRunnerPort() -> UInt16 {
9821043
9831044enum CommandType : String , Codable {
9841045 case tap
1046+ case tapSeries
9851047 case longPress
9861048 case drag
1049+ case dragSeries
9871050 case type
9881051 case swipe
9891052 case findText
@@ -1012,6 +1075,11 @@ struct Command: Codable {
10121075 let action : String ?
10131076 let x : Double ?
10141077 let y : Double ?
1078+ let count : Double ?
1079+ let intervalMs : Double ?
1080+ let doubleTap : Bool ?
1081+ let pauseMs : Double ?
1082+ let pattern : String ?
10151083 let x2 : Double ?
10161084 let y2 : Double ?
10171085 let durationMs : Double ?
0 commit comments