@@ -335,7 +335,7 @@ extension RunnerTests {
335335 switch element. elementType {
336336 case . textField, . secureTextField, . searchField, . textView:
337337 let frame = element. frame
338- return !frame. isEmpty && frame . contains ( point)
338+ return !frame. isEmpty && frameContainsPoint ( frame , point, tolerance : 2 )
339339 default :
340340 return false
341341 }
@@ -366,20 +366,31 @@ extension RunnerTests {
366366 return matched
367367 }
368368
369+ private func frameContainsPoint( _ frame: CGRect , _ point: CGPoint , tolerance: CGFloat ) -> Bool {
370+ point. x >= frame. minX - tolerance
371+ && point. x <= frame. maxX + tolerance
372+ && point. y >= frame. minY - tolerance
373+ && point. y <= frame. maxY + tolerance
374+ }
375+
369376 func focusedTextInput( app: XCUIApplication ) -> XCUIElement ? {
377+ #if os(iOS)
378+ return nil
379+ #else
370380 var focused : XCUIElement ?
371381 let exceptionMessage = RunnerObjCExceptionCatcher . catchException ( {
372- let candidate = app
382+ let candidates = app
373383 . descendants ( matching: . any)
374384 . matching ( NSPredicate ( format: " hasKeyboardFocus == 1 " ) )
375- . firstMatch
376- guard candidate. exists else { return }
377-
378- switch candidate. elementType {
379- case . textField, . secureTextField, . searchField, . textView:
380- focused = candidate
381- default :
382- return
385+ . allElementsBoundByIndex
386+ for candidate in candidates where candidate. exists {
387+ switch candidate. elementType {
388+ case . textField, . secureTextField, . searchField, . textView:
389+ focused = candidate
390+ return
391+ default :
392+ continue
393+ }
383394 }
384395 } )
385396 if let exceptionMessage {
@@ -390,6 +401,7 @@ extension RunnerTests {
390401 return nil
391402 }
392403 return focused
404+ #endif
393405 }
394406
395407 func stabilizeTextInputBeforeTyping( app: XCUIApplication , target: XCUIElement ? ) -> XCUIElement ? {
@@ -449,6 +461,36 @@ extension RunnerTests {
449461 )
450462 }
451463
464+ func focusTextInputForTextEntry( app: XCUIApplication , element: XCUIElement ) -> TextEntryTarget {
465+ let point = textEntryRefreshPoint ( for: element)
466+ if let point {
467+ _ = tapAt ( app: app, x: point. x, y: point. y)
468+ }
469+ let stabilized = stabilizeTextInputBeforeTyping ( app: app, target: element)
470+ let resolved = waitForTextEntryReadiness (
471+ app: app,
472+ target: TextEntryTarget (
473+ element: stabilized ?? element,
474+ refreshPoint: point,
475+ prefersFocusedElement: false
476+ )
477+ ) ?? stabilized ?? element
478+ return TextEntryTarget (
479+ element: resolved,
480+ refreshPoint: textEntryRefreshPoint ( for: resolved) ?? point,
481+ prefersFocusedElement: false
482+ )
483+ }
484+
485+ func isTextEntryElement( _ element: XCUIElement ) -> Bool {
486+ switch element. elementType {
487+ case . textField, . secureTextField, . searchField, . textView:
488+ return true
489+ default :
490+ return false
491+ }
492+ }
493+
452494 func resolveTextEntryMode( _ command: Command ) -> TextTypingRepairMode {
453495 switch command. textEntryMode {
454496 case " append " :
@@ -629,7 +671,7 @@ extension RunnerTests {
629671 guard let observedText = editableTextValue ( for: targetElement) else {
630672 return TextEntryResult ( verified: nil , repaired: repaired, expectedText: expectedText, observedText: nil )
631673 }
632- guard observedText == expectedText else {
674+ guard textEntryValueMatchesExpected ( targetElement , observedText: observedText , expectedText: expectedText ) else {
633675 return TextEntryResult (
634676 verified: false ,
635677 repaired: repaired,
@@ -645,7 +687,11 @@ extension RunnerTests {
645687 return TextEntryResult ( verified: nil , repaired: repaired, expectedText: expectedText, observedText: nil )
646688 }
647689 latestObservedText = nextObservedText
648- guard nextObservedText == expectedText else {
690+ guard textEntryValueMatchesExpected (
691+ resolveTextEntryElement ( app: app, target: target) ,
692+ observedText: nextObservedText,
693+ expectedText: expectedText
694+ ) else {
649695 return TextEntryResult (
650696 verified: false ,
651697 repaired: repaired,
@@ -662,6 +708,28 @@ extension RunnerTests {
662708 )
663709 }
664710
711+ private func textEntryValueMatchesExpected(
712+ _ element: XCUIElement ? ,
713+ observedText: String ,
714+ expectedText: String
715+ ) -> Bool {
716+ if observedText == expectedText {
717+ return true
718+ }
719+ guard hasTextEntrySubmitSuffix ( expectedText) , element? . elementType != . textView else {
720+ return false
721+ }
722+ var submittedText = expectedText
723+ while hasTextEntrySubmitSuffix ( submittedText) {
724+ submittedText. removeLast ( )
725+ }
726+ return observedText == submittedText
727+ }
728+
729+ private func hasTextEntrySubmitSuffix( _ text: String ) -> Bool {
730+ text. hasSuffix ( " \n " ) || text. hasSuffix ( " \r " )
731+ }
732+
665733 private func expectedTextEntryValue(
666734 typedText: String ,
667735 mode: TextTypingRepairMode ,
@@ -693,7 +761,11 @@ extension RunnerTests {
693761 guard let observedText = editableTextValue ( for: resolveTextEntryElement ( app: app, target: target) ) else {
694762 return false
695763 }
696- if observedText == expectedText {
764+ if textEntryValueMatchesExpected (
765+ resolveTextEntryElement ( app: app, target: target) ,
766+ observedText: observedText,
767+ expectedText: expectedText
768+ ) {
697769 return false
698770 }
699771 latestObservedText = observedText
@@ -710,7 +782,11 @@ extension RunnerTests {
710782 guard let latestObservedText else {
711783 return false
712784 }
713- guard latestObservedText != expectedText else {
785+ guard !textEntryValueMatchesExpected(
786+ resolveTextEntryElement ( app: app, target: target) ,
787+ observedText: latestObservedText,
788+ expectedText: expectedText
789+ ) else {
714790 return false
715791 }
716792 return isRepairableTextEntryMismatch (
@@ -904,6 +980,85 @@ extension RunnerTests {
904980#endif
905981 }
906982
983+ func pressKeyboardReturn( app: XCUIApplication ) -> ( wasVisible: Bool , pressed: Bool , visible: Bool ) {
984+ #if os(tvOS)
985+ return ( wasVisible: false , pressed: pressTvRemote ( . select) , visible: false )
986+ #elseif os(iOS)
987+ let wasVisible = isKeyboardVisible ( app: app)
988+ if tapKeyboardReturnControl ( app: app) {
989+ sleepFor ( 0.2 )
990+ return ( wasVisible: wasVisible, pressed: true , visible: isKeyboardVisible ( app: app) )
991+ }
992+
993+ var typed = false
994+ let exceptionMessage = RunnerObjCExceptionCatcher . catchException ( {
995+ app. typeText ( XCUIKeyboardKey . return. rawValue)
996+ typed = true
997+ } )
998+ if let exceptionMessage {
999+ NSLog (
1000+ " AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_IGNORED_EXCEPTION=%@ " ,
1001+ exceptionMessage
1002+ )
1003+ if let singleTarget = singleTextEntryElement ( app: app) {
1004+ return pressKeyboardReturn ( on: singleTarget, app: app, wasVisible: wasVisible)
1005+ }
1006+ return ( wasVisible: wasVisible, pressed: false , visible: isKeyboardVisible ( app: app) )
1007+ }
1008+ sleepFor ( 0.2 )
1009+ return ( wasVisible: wasVisible, pressed: typed, visible: isKeyboardVisible ( app: app) )
1010+ #else
1011+ return ( wasVisible: false , pressed: false , visible: false )
1012+ #endif
1013+ }
1014+
1015+ private func pressKeyboardReturn(
1016+ on element: XCUIElement ,
1017+ app: XCUIApplication ,
1018+ wasVisible: Bool
1019+ ) -> ( wasVisible: Bool , pressed: Bool , visible: Bool ) {
1020+ let exceptionMessage = RunnerObjCExceptionCatcher . catchException ( {
1021+ element. tap ( )
1022+ element. typeText ( XCUIKeyboardKey . return. rawValue)
1023+ } )
1024+ if let exceptionMessage {
1025+ NSLog (
1026+ " AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TARGET_IGNORED_EXCEPTION=%@ " ,
1027+ exceptionMessage
1028+ )
1029+ return ( wasVisible: wasVisible, pressed: false , visible: isKeyboardVisible ( app: app) )
1030+ }
1031+ sleepFor ( 0.2 )
1032+ return ( wasVisible: wasVisible, pressed: true , visible: isKeyboardVisible ( app: app) )
1033+ }
1034+
1035+ private func singleTextEntryElement( app: XCUIApplication ) -> XCUIElement ? {
1036+ #if os(iOS)
1037+ var matches : [ XCUIElement ] = [ ]
1038+ let exceptionMessage = RunnerObjCExceptionCatcher . catchException ( {
1039+ matches = app. descendants ( matching: . any) . allElementsBoundByIndex. filter { element in
1040+ guard element. exists else { return false }
1041+ switch element. elementType {
1042+ case . textField, . secureTextField, . searchField, . textView:
1043+ return true
1044+ default :
1045+ return false
1046+ }
1047+ }
1048+ } )
1049+ if let exceptionMessage {
1050+ NSLog (
1051+ " AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TEXT_ENTRY_QUERY_IGNORED_EXCEPTION=%@ " ,
1052+ exceptionMessage
1053+ )
1054+ return nil
1055+ }
1056+ return matches. count == 1 ? matches [ 0 ] : nil
1057+ #else
1058+ return nil
1059+ #endif
1060+ }
1061+
9071062 private func tapKeyboardDismissControl( app: XCUIApplication ) -> Bool {
9081063#if os(tvOS)
9091064 return false
@@ -941,6 +1096,22 @@ extension RunnerTests {
9411096#endif
9421097 }
9431098
1099+ private func tapKeyboardReturnControl( app: XCUIApplication ) -> Bool {
1100+ #if os(iOS)
1101+ for label in [ " return " , " Return " , " Enter " , " Go " , " Search " , " Next " , " Done " , " Send " , " Join " ] {
1102+ let candidates = [
1103+ app. keyboards. buttons [ label] ,
1104+ app. keyboards. keys [ label] ,
1105+ ]
1106+ if let hittable = candidates. first ( where: { $0. exists && $0. isHittable } ) {
1107+ hittable. tap ( )
1108+ return true
1109+ }
1110+ }
1111+ #endif
1112+ return false
1113+ }
1114+
9441115 private func isKeyboardAccessoryControl( _ element: XCUIElement , keyboardFrame: CGRect ) -> Bool {
9451116 let frame = element. frame
9461117 guard !frame. isEmpty && !keyboardFrame. isEmpty else {
@@ -1003,11 +1174,24 @@ extension RunnerTests {
10031174 guard !normalizedValue. isEmpty else {
10041175 return false
10051176 }
1006- guard let placeholder = element. placeholderValue? . trimmingCharacters ( in: . whitespacesAndNewlines) ,
1007- !placeholder. isEmpty else {
1177+ let placeholder = element. placeholderValue? . trimmingCharacters ( in: . whitespacesAndNewlines) ?? " "
1178+ if !placeholder. isEmpty && normalizedValue == placeholder {
1179+ return true
1180+ }
1181+ if isGenericTextInputLabel ( normalizedValue) {
1182+ return true
1183+ }
1184+ let normalizedLabel = element. label. trimmingCharacters ( in: . whitespacesAndNewlines)
1185+ return normalizedLabel == normalizedValue && isGenericTextInputLabel ( normalizedLabel)
1186+ }
1187+
1188+ private func isGenericTextInputLabel( _ value: String ) -> Bool {
1189+ switch value {
1190+ case " Text input field " :
1191+ return true
1192+ default :
10081193 return false
10091194 }
1010- return normalizedValue == placeholder
10111195 }
10121196
10131197 private func readableText( for element: XCUIElement ) -> String ? {
0 commit comments