@@ -140,11 +140,16 @@ extension RunnerTests {
140140 if let bundleId = requestedBundleId, targetNeedsActivation ( activeApp) {
141141 activeApp = activateTarget ( bundleId: bundleId, reason: " stale_target " )
142142 } else if requestedBundleId == nil , targetNeedsActivation ( activeApp) {
143- app . activate ( )
143+ ensureRunnerHostAppActive ( reason : " missing_app_bundle " )
144144 activeApp = app
145145 }
146146
147- if !activeApp. waitForExistence ( timeout: appExistenceTimeout) {
147+ let skipExistenceWait = canUseFastForegroundAppGuard (
148+ activeApp: activeApp,
149+ requestedBundleId: requestedBundleId,
150+ command: command. command
151+ )
152+ if !skipExistenceWait && !activeApp. waitForExistence ( timeout: appExistenceTimeout) {
148153 if let bundleId = requestedBundleId {
149154 activeApp = activateTarget ( bundleId: bundleId, reason: " missing_after_wait " )
150155 guard activeApp. waitForExistence ( timeout: appExistenceTimeout) else {
@@ -159,10 +164,15 @@ extension RunnerTests {
159164 if let bundleId = requestedBundleId, activeApp. state != . runningForeground {
160165 activeApp = activateTarget ( bundleId: bundleId, reason: " interaction_foreground_guard " )
161166 } else if requestedBundleId == nil , activeApp. state != . runningForeground {
162- app . activate ( )
167+ ensureRunnerHostAppActive ( reason : " interaction_missing_app_bundle " )
163168 activeApp = app
164169 }
165- if !activeApp. waitForExistence ( timeout: 2 ) {
170+ let skipInteractionExistenceWait = canUseFastForegroundAppGuard (
171+ activeApp: activeApp,
172+ requestedBundleId: requestedBundleId,
173+ command: command. command
174+ )
175+ if !skipInteractionExistenceWait && !activeApp. waitForExistence ( timeout: 2 ) {
166176 if let bundleId = requestedBundleId {
167177 return Response ( ok: false , error: ErrorPayload ( message: " app ' \( bundleId) ' is not available " ) )
168178 }
@@ -241,6 +251,40 @@ extension RunnerTests {
241251 data: DataPayload ( currentUptimeMs: currentUptimeMs ( ) )
242252 )
243253 case . tap:
254+ if let selectorKey = command. selectorKey, let selectorValue = command. selectorValue {
255+ let match = findElement ( app: activeApp, selectorKey: selectorKey, selectorValue: selectorValue)
256+ if match. isAmbiguous {
257+ return Response ( ok: false , error: ErrorPayload ( code: " AMBIGUOUS_MATCH " , message: " selector matched multiple elements " ) )
258+ }
259+ if let element = match. element {
260+ let frame = element. frame
261+ let touchFrame = frame. isEmpty
262+ ? nil
263+ : resolvedTouchVisualizationFrame ( app: activeApp, x: frame. midX, y: frame. midY)
264+ var outcome = RunnerInteractionOutcome . performed
265+ let timing = measureGesture {
266+ withTemporaryScrollIdleTimeoutIfSupported ( activeApp) {
267+ outcome = activateElement ( app: activeApp, element: element, action: " tap by selector " )
268+ }
269+ }
270+ if let response = unsupportedResponse ( for: outcome) {
271+ return response
272+ }
273+ return Response (
274+ ok: true ,
275+ data: DataPayload (
276+ message: " tapped " ,
277+ gestureStartUptimeMs: timing. gestureStartUptimeMs,
278+ gestureEndUptimeMs: timing. gestureEndUptimeMs,
279+ x: touchFrame? . x,
280+ y: touchFrame? . y,
281+ referenceWidth: touchFrame? . referenceWidth,
282+ referenceHeight: touchFrame? . referenceHeight
283+ )
284+ )
285+ }
286+ return Response ( ok: false , error: ErrorPayload ( code: " ELEMENT_NOT_FOUND " , message: " element not found " ) )
287+ }
244288 if let text = command. text {
245289 if let element = findElement ( app: activeApp, text: text) {
246290 var outcome = RunnerInteractionOutcome . performed
@@ -412,11 +456,25 @@ extension RunnerTests {
412456 return Response ( ok: false , error: ErrorPayload ( message: " drag requires x, y, x2, and y2 " ) )
413457 }
414458 let holdDuration = min ( max ( ( command. durationMs ?? 60 ) / 1000.0 , 0.016 ) , 10.0 )
415- let dragFrame = resolvedDragVisualizationFrame ( app: activeApp, x: x, y: y, x2: x2, y2: y2)
459+ let dragPoints = keyboardAvoidingDragPoints ( app: activeApp, x: x, y: y, x2: x2, y2: y2)
460+ let dragFrame = resolvedDragVisualizationFrame (
461+ app: activeApp,
462+ x: dragPoints. x,
463+ y: dragPoints. y,
464+ x2: dragPoints. x2,
465+ y2: dragPoints. y2
466+ )
416467 var outcome = RunnerInteractionOutcome . performed
417468 let timing = measureGesture {
418469 withTemporaryScrollIdleTimeoutIfSupported ( activeApp) {
419- outcome = dragAt ( app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
470+ outcome = dragAt (
471+ app: activeApp,
472+ x: dragPoints. x,
473+ y: dragPoints. y,
474+ x2: dragPoints. x2,
475+ y2: dragPoints. y2,
476+ holdDuration: holdDuration
477+ )
420478 }
421479 }
422480 if let response = unsupportedResponse ( for: outcome) {
@@ -447,6 +505,7 @@ extension RunnerTests {
447505 return Response ( ok: false , error: ErrorPayload ( message: " dragSeries pattern must be one-way or ping-pong " ) )
448506 }
449507 let holdDuration = min ( max ( ( command. durationMs ?? 60 ) / 1000.0 , 0.016 ) , 10.0 )
508+ let dragPoints = keyboardAvoidingDragPoints ( app: activeApp, x: x, y: y, x2: x2, y2: y2)
450509 var outcome = RunnerInteractionOutcome . performed
451510 let timing = measureGesture {
452511 withTemporaryScrollIdleTimeoutIfSupported ( activeApp) {
@@ -456,9 +515,23 @@ extension RunnerTests {
456515 }
457516 let reverse = pattern == " ping-pong " && ( idx % 2 == 1 )
458517 if reverse {
459- outcome = dragAt ( app: activeApp, x: x2, y: y2, x2: x, y2: y, holdDuration: holdDuration)
518+ outcome = dragAt (
519+ app: activeApp,
520+ x: dragPoints. x2,
521+ y: dragPoints. y2,
522+ x2: dragPoints. x,
523+ y2: dragPoints. y,
524+ holdDuration: holdDuration
525+ )
460526 } else {
461- outcome = dragAt ( app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
527+ outcome = dragAt (
528+ app: activeApp,
529+ x: dragPoints. x,
530+ y: dragPoints. y,
531+ x2: dragPoints. x2,
532+ y2: dragPoints. y2,
533+ holdDuration: holdDuration
534+ )
462535 }
463536 }
464537 }
@@ -487,45 +560,11 @@ extension RunnerTests {
487560 }
488561 return Response ( ok: true , data: DataPayload ( message: " remote pressed " ) )
489562 case . type:
490- guard let text = command. text else {
491- return Response ( ok: false , error: ErrorPayload ( message: " type requires text " ) )
492- }
493- let delaySeconds = Double ( max ( command. delayMs ?? 0 , 0 ) ) / 1000.0
494- let target : XCUIElement ?
495- if let x = command. x, let y = command. y {
496- target = textInputAt ( app: activeApp, x: x, y: y) ?? focusedTextInput ( app: activeApp)
497- } else {
498- target = focusedTextInput ( app: activeApp)
499- }
500- func typeIntoTarget( _ value: String ) {
501- if let focused = target {
502- focused. typeText ( value)
503- } else {
504- activeApp. typeText ( value)
505- }
506- }
507- if command. clearFirst == true {
508- guard let focused = target else {
509- let message =
510- ( command. x != nil && command. y != nil )
511- ? " no text input found at the provided coordinates to clear "
512- : " no focused text input to clear "
513- return Response ( ok: false , error: ErrorPayload ( message: message) )
514- }
515- clearTextInput ( focused)
516- }
517- if delaySeconds > 0 && text. count > 1 {
518- let chunks = Array ( text)
519- for (index, character) in chunks. enumerated ( ) {
520- typeIntoTarget ( String ( character) )
521- if index + 1 < chunks. count {
522- Thread . sleep ( forTimeInterval: delaySeconds)
523- }
524- }
525- } else {
526- typeIntoTarget ( text)
563+ var response : Response ?
564+ withTemporaryScrollIdleTimeoutIfSupported ( activeApp) {
565+ response = executeTypeCommand ( activeApp: activeApp, command: command)
527566 }
528- return Response ( ok: true , data : DataPayload ( message: " typed " ) )
567+ return response ?? Response ( ok: false , error : ErrorPayload ( message: " type produced no response " ) )
529568 case . interactionFrame:
530569 let frame = resolvedTouchReferenceFrame ( app: activeApp, appFrame: activeApp. frame)
531570 return Response (
@@ -573,6 +612,11 @@ extension RunnerTests {
573612 }
574613 let found = findElement ( app: activeApp, text: text) != nil
575614 return Response ( ok: true , data: DataPayload ( found: found) )
615+ case . querySelector:
616+ guard let selectorKey = command. selectorKey, let selectorValue = command. selectorValue else {
617+ return Response ( ok: false , error: ErrorPayload ( message: " querySelector requires selectorKey and selectorValue " ) )
618+ }
619+ return queryElement ( app: activeApp, selectorKey: selectorKey, selectorValue: selectorValue)
576620 case . readText:
577621 guard let x = command. x, let y = command. y else {
578622 return Response ( ok: false , error: ErrorPayload ( message: " readText requires x and y " ) )
@@ -604,7 +648,7 @@ extension RunnerTests {
604648 targetApp. activate ( )
605649 activeApp = targetApp
606650 // Brief wait for the app transition animation to complete
607- Thread . sleep ( forTimeInterval : 0.5 )
651+ sleepFor ( 0.5 )
608652 }
609653 if command. fullscreen == true {
610654 screenshot = XCUIScreen . main. screenshot ( )
@@ -734,4 +778,41 @@ extension RunnerTests {
734778 )
735779 }
736780 }
781+
782+ private func executeTypeCommand( activeApp: XCUIApplication , command: Command ) -> Response {
783+ guard let text = command. text else {
784+ return Response ( ok: false , error: ErrorPayload ( message: " type requires text " ) )
785+ }
786+ let delaySeconds = Double ( max ( command. delayMs ?? 0 , 0 ) ) / 1000.0
787+ let textEntryMode = resolveTextEntryMode ( command)
788+ let target = focusTextInputForTextEntry ( app: activeApp, x: command. x, y: command. y)
789+ if textEntryMode == . replacement {
790+ guard target. element != nil else {
791+ let message =
792+ ( command. x != nil && command. y != nil )
793+ ? " no text input found at the provided coordinates to clear "
794+ : " no focused text input to clear "
795+ return Response ( ok: false , error: ErrorPayload ( message: message) )
796+ }
797+ }
798+ let textResult = typeTextReliably (
799+ app: activeApp,
800+ target: target,
801+ text: text,
802+ delaySeconds: delaySeconds,
803+ repairMode: textEntryMode
804+ )
805+ if textResult. verified == false {
806+ let expected = textResult. expectedText ?? " "
807+ let observed = textResult. observedText ?? " "
808+ return Response (
809+ ok: false ,
810+ error: ErrorPayload (
811+ code: " TEXT_ENTRY_MISMATCH " ,
812+ message: " text entry verification failed: expected \" \( expected) \" , observed \" \( observed) \" "
813+ )
814+ )
815+ }
816+ return Response ( ok: true , data: DataPayload ( message: textResult. repaired ? " typed after repair " : " typed " ) )
817+ }
737818}
0 commit comments