@@ -209,27 +209,30 @@ extension RunnerTests {
209209 /// Nil when the element exists but could not be snapshotted.
210210 let snapshot : XCUIElementSnapshot ?
211211
212- init ( element : XCUIElement , snapshot : XCUIElementSnapshot ? ) {
212+ private init ( element : XCUIElement , snapshot : XCUIElementSnapshot ? ) {
213213 self . element = element
214214 self . snapshot = snapshot
215215 }
216216
217- /// Reads a handle that may name an input the app has removed; nil when it names none. The
218- /// snapshot of a missing element throws only after XCTest waits about two seconds for a match,
219- /// so existence is read first.
217+ /// Reads a handle that may name an input the app has removed; nil when it names none or the read
218+ /// raised. The snapshot of a missing element throws only after XCTest waits about two seconds for
219+ /// a match, so existence is read first.
220220 init ? ( reading element : XCUIElement ) {
221221 var exists = false
222222 var snapshot : XCUIElementSnapshot ?
223- let exception = RunnerObjCExceptionCatcher . catchException {
223+ if let exception = RunnerObjCExceptionCatcher . catchException ( {
224224 exists = element . exists
225225 snapshot = exists ? try? element . snapshot ( ) : nil
226+ } ) {
227+ NSLog ( " AGENT_DEVICE_RUNNER_TEXT_ENTRY_READ_IGNORED_EXCEPTION=%@ " , exception )
228+ return nil
226229 }
227- guard exception == nil , exists else { return nil }
230+ guard exists else { return nil }
228231 self . init ( element : element , snapshot : snapshot )
229232 }
230233
231- /// Reads a handle its query has just listed, so the snapshot is read first and existence only
232- /// when the snapshot fails; nil when it names no element.
234+ /// Reads a handle its query has just listed, inside the caller's exception containment, so the
235+ /// snapshot is read first and existence only when the snapshot fails; nil when it names no element.
233236 init ? ( readingListed element : XCUIElement ) {
234237 let snapshot = try? element . snapshot ( )
235238 guard snapshot != nil || element . exists else { return nil }
@@ -323,8 +326,8 @@ extension RunnerTests {
323326 }
324327 }
325328
329+ /// Clears `input`, which must have been read with nothing dispatched since.
326330 func clearTextInput ( app : XCUIApplication , _ input : ResolvedTextEntryInput ) {
327- let element = input . element
328331 // Skip the clear (delete burst + moveCaretToEnd edge-tap) ONLY when we can confirm the
329332 // field is empty. Why skip: the edge-tap computes a point from the element frame, which can
330333 // be stale after the field repositions on focus (e.g. the Settings search bar jumps
@@ -340,10 +343,10 @@ extension RunnerTests {
340343 // Read before the caret tap, which dispatches to the app.
341344 let count = estimatedDeleteCount ( for : input . attributes )
342345#if !os( tvOS )
343- moveCaretToEnd ( app : app , element : element )
346+ moveCaretToEnd ( app : app , input : input )
344347#endif
345348 let deletes = String ( repeating : XCUIKeyboardKey . delete . rawValue , count : count )
346- element . typeText ( deletes )
349+ input . element . typeText ( deletes )
347350 }
348351
349352 func isTextEntryElement ( _ element : XCUIElement ) -> Bool {
@@ -457,7 +460,7 @@ extension RunnerTests {
457460
458461 /// Resolution for a target not bound to an input yet: the first element that exists.
459462 private func resolveUnboundTextEntryInput ( app : XCUIApplication , target : TextEntryTarget ) -> ResolvedTextEntryInput ? {
460- if target . prefersFocusedElement , let focused = focusedTextInput ( app : app ) . flatMap ( ResolvedTextEntryInput . init ( readingListed : ) ) {
463+ if target . prefersFocusedElement , let focused = focusedTextInput ( app : app ) . flatMap ( ResolvedTextEntryInput . init ( reading : ) ) {
461464 return focused
462465 }
463466 if let element = target . element , let input = ResolvedTextEntryInput ( reading : element ) {
@@ -467,25 +470,26 @@ extension RunnerTests {
467470 let refreshed = textInputAt ( app : app , x : refreshPoint . x , y : refreshPoint . y ) {
468471 return refreshed
469472 }
470- return focusedTextInput ( app : app ) . flatMap ( ResolvedTextEntryInput . init ( readingListed : ) )
473+ return focusedTextInput ( app : app ) . flatMap ( ResolvedTextEntryInput . init ( reading : ) )
471474 }
472475
473- private func moveCaretToEnd ( app : XCUIApplication , element : XCUIElement ) {
476+ private func moveCaretToEnd ( app : XCUIApplication , input : ResolvedTextEntryInput ) {
474477#if os( tvOS )
475478 return
476479#else
477480 // Both the point AND its dispatch must stay off the handle: this runs AFTER the command's own
478481 // focus tap, and an input that stopped answering its resolving query on focus (#3060) makes
479482 // every post-focus handle touch a recorder — `element.frame` records directly, and an
480483 // element-anchored `coordinate(...).tap()` re-runs that same query to resolve its anchor when
481- // the action executes. A fresh `snapshot()` proves the handle answered moments earlier, not
482- // that it still answers at tap-dispatch time. So the point comes from the snapshot frame and
484+ // the action executes. The input's snapshot, read with nothing dispatched since, proves the handle
485+ // answered moments earlier, not that it still answers at tap-dispatch time. So the point comes
486+ // from the snapshot frame and
483487 // goes out app-relative through `tapAt`, which resolves no element at all — the shape
484488 // `waitForTextEntryReadinessAfterTap` settled on in #3237. With no trustworthy frame, or a
485489 // zero-size one (how a departed handle reads), there is no point to dispatch — an edge-tap
486490 // from a frame the field moved after records a failure or navigates away, the shape
487491 // `clearTextInput`'s comment refuses — so degrade to the caller's point-free delete burst.
488- guard let frame = ResolvedTextEntryInput ( reading : element ) ? . snapshot ? . frame , ! frame . isEmpty else {
492+ guard let frame = input . snapshot ? . frame , ! frame . isEmpty else {
489493 return
490494 }
491495 _ = tapAt (
0 commit comments