@@ -36,6 +36,21 @@ extension RunnerTests {
3636 case drag( DragVisualizationFrame )
3737 }
3838
39+ struct GestureFallback {
40+ let strategy : String
41+ let message : String
42+ let hint : String ?
43+ }
44+
45+ private func gestureFallback( strategy: String , from outcome: RunnerInteractionOutcome ) -> GestureFallback ? {
46+ switch outcome {
47+ case . performed:
48+ return nil
49+ case . unsupported( let message, let hint) :
50+ return GestureFallback ( strategy: strategy, message: message, hint: hint)
51+ }
52+ }
53+
3954 /// Runs a gesture action with uniform timing capture. Touch gestures pass `idleTimeout: true`
4055 /// (the default) to run inside the scroll idle-timeout + quiescence-skip wrapper; synthesis
4156 /// gestures (pinch/rotate/transform) pass `false` because RunnerSynthesizedGesture governs its
@@ -64,15 +79,19 @@ extension RunnerTests {
6479 private func gestureResponse(
6580 message: String ,
6681 timing: ( gestureStartUptimeMs: Double , gestureEndUptimeMs: Double ) ,
67- frame: GestureFrame = . none
82+ frame: GestureFrame = . none,
83+ fallback: GestureFallback ? = nil
6884 ) -> Response {
6985 let data : DataPayload
7086 switch frame {
7187 case . none:
7288 data = DataPayload (
7389 message: message,
7490 gestureStartUptimeMs: timing. gestureStartUptimeMs,
75- gestureEndUptimeMs: timing. gestureEndUptimeMs
91+ gestureEndUptimeMs: timing. gestureEndUptimeMs,
92+ gestureFallback: fallback? . strategy,
93+ gestureFallbackMessage: fallback? . message,
94+ gestureFallbackHint: fallback? . hint
7695 )
7796 case . touch( let f) :
7897 data = DataPayload (
@@ -82,7 +101,10 @@ extension RunnerTests {
82101 x: f? . x,
83102 y: f? . y,
84103 referenceWidth: f? . referenceWidth,
85- referenceHeight: f? . referenceHeight
104+ referenceHeight: f? . referenceHeight,
105+ gestureFallback: fallback? . strategy,
106+ gestureFallbackMessage: fallback? . message,
107+ gestureFallbackHint: fallback? . hint
86108 )
87109 case . drag( let f) :
88110 data = DataPayload (
@@ -94,7 +116,10 @@ extension RunnerTests {
94116 x2: f. x2,
95117 y2: f. y2,
96118 referenceWidth: f. referenceWidth,
97- referenceHeight: f. referenceHeight
119+ referenceHeight: f. referenceHeight,
120+ gestureFallback: fallback? . strategy,
121+ gestureFallbackMessage: fallback? . message,
122+ gestureFallbackHint: fallback? . hint
98123 )
99124 }
100125 return Response ( ok: true , data: data)
@@ -507,6 +532,7 @@ extension RunnerTests {
507532 x2: dragPoints. x2,
508533 y2: dragPoints. y2
509534 )
535+ var fallback : GestureFallback ?
510536 if command. synthesized == true {
511537 let durationMs = min ( max ( command. durationMs ?? 250 , 16 ) , 10000 )
512538 let ( timing, outcome) = performGesture ( activeApp, idleTimeout: false ) {
@@ -522,6 +548,7 @@ extension RunnerTests {
522548 if case . performed = outcome {
523549 return gestureResponse ( message: " dragged " , timing: timing, frame: . drag( dragFrame) )
524550 }
551+ fallback = gestureFallback ( strategy: " xctest-coordinate-drag " , from: outcome)
525552 }
526553 let holdDuration = command. synthesized == true
527554 ? synthesizedSwipeFallbackHoldDuration ( durationMs: command. durationMs ?? 250 )
@@ -539,7 +566,12 @@ extension RunnerTests {
539566 if let response = unsupportedResponse ( for: outcome) {
540567 return response
541568 }
542- return gestureResponse ( message: " dragged " , timing: timing, frame: . drag( dragFrame) )
569+ return gestureResponse (
570+ message: " dragged " ,
571+ timing: timing,
572+ frame: . drag( dragFrame) ,
573+ fallback: fallback
574+ )
543575 case . dragSeries:
544576 guard let x = command. x, let y = command. y, let x2 = command. x2, let y2 = command. y2 else {
545577 return Response ( ok: false , error: ErrorPayload ( message: " dragSeries requires x, y, x2, and y2 " ) )
@@ -550,8 +582,47 @@ extension RunnerTests {
550582 if pattern != " one-way " && pattern != " ping-pong " {
551583 return Response ( ok: false , error: ErrorPayload ( message: " dragSeries pattern must be one-way or ping-pong " ) )
552584 }
553- let holdDuration = min ( max ( ( command. durationMs ?? 60 ) / 1000.0 , 0.016 ) , 10.0 )
554585 let dragPoints = keyboardAvoidingDragPoints ( app: activeApp, x: x, y: y, x2: x2, y2: y2)
586+ var fallback : GestureFallback ?
587+ if command. synthesized == true {
588+ let durationMs = min ( max ( command. durationMs ?? 250 , 16 ) , 10000 )
589+ let ( timing, outcome) = performGesture ( activeApp, idleTimeout: false ) {
590+ var outcome = RunnerInteractionOutcome . performed
591+ runSeries ( count: count, pauseMs: pauseMs) { idx in
592+ guard case . performed = outcome else {
593+ return
594+ }
595+ let reverse = pattern == " ping-pong " && ( idx % 2 == 1 )
596+ if reverse {
597+ outcome = synthesizedDragAt (
598+ app: activeApp,
599+ x: dragPoints. x2,
600+ y: dragPoints. y2,
601+ x2: dragPoints. x,
602+ y2: dragPoints. y,
603+ durationMs: durationMs
604+ )
605+ } else {
606+ outcome = synthesizedDragAt (
607+ app: activeApp,
608+ x: dragPoints. x,
609+ y: dragPoints. y,
610+ x2: dragPoints. x2,
611+ y2: dragPoints. y2,
612+ durationMs: durationMs
613+ )
614+ }
615+ }
616+ return outcome
617+ }
618+ if case . performed = outcome {
619+ return gestureResponse ( message: " drag series " , timing: timing)
620+ }
621+ fallback = gestureFallback ( strategy: " xctest-coordinate-drag-series " , from: outcome)
622+ }
623+ let holdDuration = command. synthesized == true
624+ ? synthesizedSwipeFallbackHoldDuration ( durationMs: command. durationMs ?? 250 )
625+ : min ( max ( ( command. durationMs ?? 60 ) / 1000.0 , 0.016 ) , 10.0 )
555626 let ( timing, outcome) = performGesture ( activeApp) {
556627 var outcome = RunnerInteractionOutcome . performed
557628 runSeries ( count: count, pauseMs: pauseMs) { idx in
@@ -584,7 +655,7 @@ extension RunnerTests {
584655 if let response = unsupportedResponse ( for: outcome) {
585656 return response
586657 }
587- return gestureResponse ( message: " drag series " , timing: timing)
658+ return gestureResponse ( message: " drag series " , timing: timing, fallback : fallback )
588659 case . remotePress:
589660 guard let button = tvRemoteButton ( from: command. remoteButton) else {
590661 return Response ( ok: false , error: ErrorPayload ( message: " remotePress requires remoteButton " ) )
0 commit comments