@@ -64,27 +64,37 @@ extension RunnerTests {
6464 keyboardVisibleBeforeTap : Bool ? = nil
6565 ) -> TextEntryStabilization {
6666#if os( tvOS )
67- return TextEntryStabilization ( element : target , focusConfirmed : true )
67+ return TextEntryStabilization ( element : target , evidence : . focusEngine )
6868#else
6969 let keyboardVisibleAtEntry = keyboardVisibleBeforeTap ?? isKeyboardVisible ( app : app )
7070 let deadline = Date ( ) . addingTimeInterval ( TextEntryTiming . focusTimeout )
7171 while Date ( ) < deadline {
7272 if let focused = focusedTextInput ( app : app ) {
73- return TextEntryStabilization ( element : focused , focusConfirmed : true )
73+ return TextEntryStabilization ( element : focused , evidence : . focusedInput )
7474 }
75- // focusedTextInput is intentionally nil on iOS; treat the keyboard transitioning to
76- // visible after our tap as the focus-moved signal. Don't fast-path when it was already up.
75+ #if os( iOS )
76+ // The tapped input's own read comes before the keyboard. While a software keyboard is
77+ // presented, the keyboard query blocks and then finds none: after a fill's focus tap it took
78+ // 0.68 s on an iPhone 17 Pro simulator (iOS 27.0) and 0.57–3.77 s on CI. An input that
79+ // reports keyboard focus is stronger evidence than the keyboard's arrival, which shows only
80+ // that some input took focus.
81+ if let target , let frame = keyboardFocusedInputFrame ( target ) {
82+ return TextEntryStabilization ( element : target , evidence : . inputReportsKeyboardFocus , inputFrame : frame )
83+ }
84+ #endif
85+ // focusedTextInput is intentionally nil on iOS; the keyboard transitioning to visible after
86+ // our tap is also a focus-moved signal. Don't fast-path when it was already up.
7787 if keyboardBecameVisible ( app : app , wasVisibleAtEntry : keyboardVisibleAtEntry ) {
78- return TextEntryStabilization ( element : target , focusConfirmed : true )
88+ return TextEntryStabilization ( element : target , evidence : . keyboardAppeared )
7989 }
8090#if os( iOS )
8191 if let target , keyboardFocusConfirmed ( app : app , element : target ) {
82- return TextEntryStabilization ( element : target , focusConfirmed : true )
92+ return TextEntryStabilization ( element : target , evidence : . focusQueryMatchesInput )
8393 }
8494#endif
8595 sleepFor ( TextEntryTiming . pollInterval )
8696 }
87- return TextEntryStabilization ( element : target , focusConfirmed : false )
97+ return TextEntryStabilization ( element : target , evidence : nil )
8898#endif
8999 }
90100
@@ -95,10 +105,10 @@ extension RunnerTests {
95105 if ! softwareKeyboardVisible , let rememberedTarget = rememberedTextEntryTarget ( ) {
96106 return rememberedTarget
97107 }
98- // Bare `type` targets the current first responder. On iOS we intentionally do not trust
99- // `hasKeyboardFocus`, but an already-visible software keyboard is sufficient evidence that
100- // app.typeText has a receiver; waiting the full readiness timeout cannot prove a stronger
101- // target because there is no selector/coordinate focus move to validate.
108+ // Bare `type` targets the current first responder. On iOS we intentionally do not let the
109+ // app-wide `hasKeyboardFocus` query pick it , but an already-visible software keyboard is
110+ // sufficient evidence that app.typeText has a receiver; waiting the full readiness timeout
111+ // cannot prove a stronger target because there is no selector/coordinate focus move to validate.
102112 if softwareKeyboardVisible {
103113 return TextEntryTarget (
104114 element : focusedTextInput ( app : app ) ,
@@ -145,6 +155,15 @@ extension RunnerTests {
145155 target : target ,
146156 keyboardVisibleBeforeTap : keyboardVisibleBeforeTap
147157 )
158+ NSLog ( " AGENT_DEVICE_RUNNER_TEXT_ENTRY_FOCUS evidence=%@ " , stabilized . evidence ? . rawValue ?? " none " )
159+ if let frame = stabilized . inputFrame {
160+ // The read that confirmed focus came after the tap, so its frame serves the refresh point.
161+ return TextEntryTarget (
162+ element : stabilized . element ,
163+ refreshPoint : textEntryRefreshPoint ( frame : frame , fallbackTo : requestedPoint ) ,
164+ prefersFocusedElement : false
165+ )
166+ }
148167 let readyTarget = TextEntryTarget (
149168 element : stabilized . element ?? target ,
150169 refreshPoint : requestedPoint ,
@@ -176,7 +195,12 @@ extension RunnerTests {
176195 guard let element else {
177196 return requestedPoint
178197 }
179- guard let frame = textEntrySnapshotFrame ( element ) else {
198+ return textEntryRefreshPoint ( frame : textEntrySnapshotFrame ( element ) , fallbackTo : requestedPoint )
199+ }
200+
201+ /// `textEntryRefreshPoint(for:)` from a frame already read after the focus tap.
202+ private func textEntryRefreshPoint ( frame : CGRect ? , fallbackTo requestedPoint : CGPoint ? ) -> CGPoint ? {
203+ guard let frame else {
180204 return nil
181205 }
182206 guard ! frame . isEmpty else {
0 commit comments