@@ -182,7 +182,7 @@ extension RunnerTests {
182182 XCTAssertNil ( xctestRecordedFailureResponse ( command : tapCommand , response : runnerFatalResponse ) )
183183 }
184184
185- func testSkipAppActivationPreflightIncludesCoordinateOnlySynthesizedGestures ( ) throws {
185+ func testSkipAppActivationPreflightOnlyIncludesCoordinateOnlySynthesizedTaps ( ) throws {
186186 currentApp = app
187187 currentBundleId = nil
188188 defer {
@@ -192,25 +192,8 @@ extension RunnerTests {
192192 let tap = try runnerCommandFixture (
193193 #"{"command":"tap","commandId":"tap-1","x":10,"y":20,"synthesized":true}"#
194194 )
195- let drag = try runnerCommandFixture (
196- #"{"command":"drag","commandId":"drag-1","x":10,"y":20,"x2":30,"y2":40,"synthesized":true}"#
197- )
198- let scroll = try runnerCommandFixture (
199- #"{"command":"scroll","commandId":"scroll-1","direction":"down","pixels":400}"#
200- )
201- let sequence = try runnerCommandFixture (
202- """
203- { " command " : " sequence " , " commandId " : " seq-1 " , " steps " :[
204- { " kind " : " tap " , " x " :10, " y " :20, " synthesized " :true},
205- { " kind " : " drag " , " x " :10, " y " :200, " x2 " :10, " y2 " :100, " synthesized " :true}
206- ]}
207- """
208- )
209195
210196 XCTAssertTrue ( shouldSkipAppActivationPreflight ( tap ) )
211- XCTAssertTrue ( shouldSkipAppActivationPreflight ( drag ) )
212- XCTAssertTrue ( shouldSkipAppActivationPreflight ( scroll ) )
213- XCTAssertTrue ( shouldSkipAppActivationPreflight ( sequence ) )
214197 }
215198
216199 func testSkipAppActivationPreflightRejectsSelectorAndMixedSequenceGestures ( ) throws {
@@ -250,6 +233,33 @@ extension RunnerTests {
250233 XCTAssertFalse ( shouldSkipAppActivationPreflight ( scroll ) )
251234 }
252235
236+ func testSkipAppActivationPreflightKeepsDragScrollAndSequenceOnForegroundGuard ( ) throws {
237+ currentApp = app
238+ currentBundleId = nil
239+ defer {
240+ currentApp = nil
241+ currentBundleId = nil
242+ }
243+ let drag = try runnerCommandFixture (
244+ #"{"command":"drag","commandId":"drag-1","x":10,"y":20,"x2":30,"y2":40,"synthesized":true}"#
245+ )
246+ let scroll = try runnerCommandFixture (
247+ #"{"command":"scroll","commandId":"scroll-1","direction":"down","pixels":400}"#
248+ )
249+ let sequence = try runnerCommandFixture (
250+ """
251+ { " command " : " sequence " , " commandId " : " seq-1 " , " steps " :[
252+ { " kind " : " tap " , " x " :10, " y " :20, " synthesized " :true},
253+ { " kind " : " drag " , " x " :10, " y " :200, " x2 " :10, " y2 " :100, " synthesized " :true}
254+ ]}
255+ """
256+ )
257+
258+ XCTAssertFalse ( shouldSkipAppActivationPreflight ( drag ) )
259+ XCTAssertFalse ( shouldSkipAppActivationPreflight ( scroll ) )
260+ XCTAssertFalse ( shouldSkipAppActivationPreflight ( sequence ) )
261+ }
262+
253263 func testExecuteDispatchedReturnsBusyBeforeMainThreadFastPath ( ) throws {
254264 let command = try runnerCommandFixture ( #"{"command":"snapshot","commandId":"snapshot-busy"}"# )
255265 abandonedMainThreadWorkCount = 1
@@ -855,7 +865,9 @@ extension RunnerTests {
855865 y2 : frame . minY + plan . y2 ,
856866 durationMs : command . durationMs ,
857867 synthesized : shouldUseSynthesizedScrollPath ( ) ,
858- message : " scrolled "
868+ message : " scrolled " ,
869+ synthesizedReferenceFrame : frame ,
870+ allowSynthesizedCoordinateFallback : false
859871 )
860872 case . desktopScroll :
861873 guard let direction = command . direction ,
@@ -1218,7 +1230,9 @@ extension RunnerTests {
12181230 y2 : Double ,
12191231 durationMs : Double ? ,
12201232 synthesized : Bool ,
1221- message : String
1233+ message : String ,
1234+ synthesizedReferenceFrame : CGRect ? = nil ,
1235+ allowSynthesizedCoordinateFallback : Bool = true
12221236 ) -> Response {
12231237 let commandName = dragCommandName ( message : message )
12241238 guard x . isFinite , y . isFinite , x2 . isFinite , y2 . isFinite else {
@@ -1234,7 +1248,9 @@ extension RunnerTests {
12341248 x2 : x2 ,
12351249 y2 : y2 ,
12361250 durationMs : durationMs ,
1237- message : message
1251+ message : message ,
1252+ referenceFrame : synthesizedReferenceFrame ,
1253+ allowCoordinateFallback : allowSynthesizedCoordinateFallback
12381254 ) {
12391255 return synthesizedResponse
12401256 }
@@ -1295,11 +1311,33 @@ extension RunnerTests {
12951311 x2 : Double ,
12961312 y2 : Double ,
12971313 durationMs : Double ? ,
1298- message : String
1314+ message : String ,
1315+ referenceFrame : CGRect ? ,
1316+ allowCoordinateFallback : Bool
12991317 ) -> Response ? {
13001318#if os( iOS )
1301- guard let plan = axFreeSynthesizedDragPlan ( app : activeApp , x : x , y : y , x2 : x2 , y2 : y2 )
1319+ guard let plan = axFreeSynthesizedDragPlan (
1320+ app : activeApp ,
1321+ x : x ,
1322+ y : y ,
1323+ x2 : x2 ,
1324+ y2 : y2 ,
1325+ referenceFrame : referenceFrame ,
1326+ avoidKeyboardWhenSafe : true
1327+ )
13021328 else {
1329+ if allowCoordinateFallback {
1330+ return executeCoordinateDragFallback (
1331+ activeApp : activeApp ,
1332+ x : x ,
1333+ y : y ,
1334+ x2 : x2 ,
1335+ y2 : y2 ,
1336+ durationMs : durationMs ,
1337+ message : message ,
1338+ fallback : nil
1339+ )
1340+ }
13031341 return Response (
13041342 ok : false ,
13051343 error : ErrorPayload (
@@ -1323,21 +1361,77 @@ extension RunnerTests {
13231361 y : plan . points . y ,
13241362 x2 : plan . points . x2 ,
13251363 y2 : plan . points . y2 ,
1326- durationMs : durationMs
1364+ durationMs : durationMs ,
1365+ referenceFrame : plan . referenceFrame
13271366 )
13281367 }
13291368 if case . performed = outcome {
13301369 return gestureResponse ( message : message , timing : timing , frame : . drag ( dragFrame ) )
13311370 }
1371+ if allowCoordinateFallback {
1372+ return executeCoordinateDragFallback (
1373+ activeApp : activeApp ,
1374+ x : plan . points . x ,
1375+ y : plan . points . y ,
1376+ x2 : plan . points . x2 ,
1377+ y2 : plan . points . y2 ,
1378+ durationMs : durationMs ,
1379+ message : message ,
1380+ fallback : gestureFallback ( strategy : " xctest-coordinate-drag " , from : outcome )
1381+ )
1382+ }
13321383 return unsupportedResponse ( for : outcome )
13331384#else
13341385 return nil
13351386#endif
13361387 }
13371388
1389+ private func executeCoordinateDragFallback (
1390+ activeApp : XCUIApplication ,
1391+ x : Double ,
1392+ y : Double ,
1393+ x2 : Double ,
1394+ y2 : Double ,
1395+ durationMs : Double ? ,
1396+ message : String ,
1397+ fallback : GestureFallback ?
1398+ ) -> Response {
1399+ let dragPoints = keyboardAvoidingDragPoints ( app : activeApp , x : x , y : y , x2 : x2 , y2 : y2 )
1400+ let dragFrame = resolvedDragVisualizationFrame (
1401+ app : activeApp ,
1402+ x : dragPoints . x ,
1403+ y : dragPoints . y ,
1404+ x2 : dragPoints . x2 ,
1405+ y2 : dragPoints . y2
1406+ )
1407+ let holdDuration = synthesizedSwipeFallbackHoldDuration ( durationMs : durationMs ?? 250 )
1408+ let ( timing , outcome ) = performGesture ( activeApp ) {
1409+ dragAt (
1410+ app : activeApp ,
1411+ x : dragPoints . x ,
1412+ y : dragPoints . y ,
1413+ x2 : dragPoints . x2 ,
1414+ y2 : dragPoints . y2 ,
1415+ holdDuration : holdDuration
1416+ )
1417+ }
1418+ if let response = unsupportedResponse ( for : outcome ) {
1419+ return response
1420+ }
1421+ return gestureResponse (
1422+ message : message ,
1423+ timing : timing ,
1424+ frame : . drag ( dragFrame ) ,
1425+ fallback : fallback
1426+ )
1427+ }
1428+
13381429 private func scrollReferenceFrame ( app : XCUIApplication ) -> CGRect {
13391430#if os( iOS )
1340- return synthesizedGestureReferenceFrame ( app : app ) ?? CGRect ( x : 0 , y : 0 , width : 0 , height : 0 )
1431+ guard let frame = synthesizedGestureReferenceFrame ( app : app ) else {
1432+ return CGRect ( x : 0 , y : 0 , width : 0 , height : 0 )
1433+ }
1434+ return synthesizedFrameAvoidingKeyboardWhenSafe ( app : app , frame : frame )
13411435#else
13421436 return resolvedTouchReferenceFrame ( app : app , appFrame : app . frame )
13431437#endif
@@ -1387,42 +1481,16 @@ extension RunnerTests {
13871481
13881482 private func shouldSkipAppActivationPreflight ( _ command : Command ) -> Bool {
13891483#if os( iOS )
1390- // Coordinate-only synthesized gestures can run after an AX-fatal screen because they do not
1484+ // Coordinate-only synthesized taps can run after an AX-fatal screen because they do not
13911485 // need app activation, window lookup, keyboard lookup, or element resolution. Selector/text
13921486 // interactions intentionally stay on the normal AX path because they need an element query.
1487+ // Scroll/drag/sequence keep the normal foreground guard and stabilization path.
13931488 guard command . text == nil , command . selectorKey == nil else { return false }
13941489 guard hasCachedTargetForActivationSkip ( command : command ) else { return false }
1395- switch command . command {
1396- case . tap :
1397- return command . synthesized == true
1398- && command . x != nil
1399- && command . y != nil
1400- case . drag :
1401- return command . synthesized == true
1402- && command . x != nil
1403- && command . y != nil
1404- && command . x2 != nil
1405- && command . y2 != nil
1406- case . scroll :
1407- return true
1408- case . sequence :
1409- guard let steps = command . steps , ! steps . isEmpty else { return false }
1410- return steps . allSatisfy { step in
1411- if step . kind == " tap " {
1412- return step . synthesized == true && step . x != nil && step . y != nil
1413- }
1414- if step . kind == " drag " {
1415- return step . synthesized == true
1416- && step . x != nil
1417- && step . y != nil
1418- && step . x2 != nil
1419- && step . y2 != nil
1420- }
1421- return false
1422- }
1423- default :
1424- return false
1425- }
1490+ return command . command == . tap
1491+ && command . synthesized == true
1492+ && command . x != nil
1493+ && command . y != nil
14261494#else
14271495 return false
14281496#endif
0 commit comments