Skip to content

Commit 65a9dbf

Browse files
committed
fix(ios): dispatch the caret edge-tap app-relative instead of through the handle (#3329 review)
A successful `snapshot()` proves the handle answered its query when the frame was read, not that it still answers when the tap dispatches — and an element-anchored `coordinate(withNormalizedOffset:).tap()` re-runs the resolving query to place its anchor when the action executes, the same #3060 recorder one call later. Both branches now leave the handle: the edge-tap point is computed from the snapshot frame and dispatched through `tapAt`, the app-relative coordinate route that resolves no element at all — the shape `waitForTextEntryReadinessAfterTap` settled on in #3237 — and the empty-frame `element.tap()` (how a departed handle reads) is folded into the same fail-closed guard, degrading to the point-free delete burst rather than tapping a handle that may not resolve. `clearTextInput` threads `app` to reach `tapAt`; its three call sites already hold it. No CLI/daemon/API change.
1 parent 2c95883 commit 65a9dbf

2 files changed

Lines changed: 22 additions & 21 deletions

File tree

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift‎

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ extension RunnerTests {
243243
}
244244
}
245245

246-
func clearTextInput(_ element: XCUIElement) {
246+
func clearTextInput(app: XCUIApplication, _ element: XCUIElement) {
247247
// Skip the clear (delete burst + moveCaretToEnd edge-tap) ONLY when we can confirm the
248248
// field is empty. Why skip: the edge-tap computes a point from the element frame, which can
249249
// be stale after the field repositions on focus (e.g. the Settings search bar jumps
@@ -257,7 +257,7 @@ extension RunnerTests {
257257
return
258258
}
259259
#if !os(tvOS)
260-
moveCaretToEnd(element: element)
260+
moveCaretToEnd(app: app, element: element)
261261
#endif
262262
let count = estimatedDeleteCount(for: element)
263263
let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: count)
@@ -410,28 +410,29 @@ extension RunnerTests {
410410
return nil
411411
}
412412

413-
private func moveCaretToEnd(element: XCUIElement) {
413+
private func moveCaretToEnd(app: XCUIApplication, element: XCUIElement) {
414414
#if os(tvOS)
415415
return
416416
#else
417-
// The edge-tap point must come from a frame read that cannot record: this runs AFTER the
418-
// command's own focus tap, and an input that stopped answering its resolving query on focus
419-
// (#3060) turns `element.frame` into a recorded failure. With no trustworthy frame there is no
420-
// point to dispatch — an edge-tap from a stale frame records a failure or navigates away, the
421-
// shape `clearTextInput`'s comment refuses — so degrade to the caller's point-free delete
422-
// burst rather than tapping a handle that can no longer resolve.
423-
guard let frame = textEntrySnapshotFrame(element) else {
417+
// Both the point AND its dispatch must stay off the handle: this runs AFTER the command's own
418+
// focus tap, and an input that stopped answering its resolving query on focus (#3060) makes
419+
// every post-focus handle touch a recorder — `element.frame` records directly, and an
420+
// element-anchored `coordinate(...).tap()` re-runs that same query to resolve its anchor when
421+
// the action executes. A fresh `snapshot()` proves the handle answered moments earlier, not
422+
// that it still answers at tap-dispatch time. So the point comes from the snapshot frame and
423+
// goes out app-relative through `tapAt`, which resolves no element at all — the shape
424+
// `waitForTextEntryReadinessAfterTap` settled on in #3237. With no trustworthy frame, or a
425+
// zero-size one (how a departed handle reads), there is no point to dispatch — an edge-tap
426+
// from a frame the field moved after records a failure or navigates away, the shape
427+
// `clearTextInput`'s comment refuses — so degrade to the caller's point-free delete burst.
428+
guard let frame = textEntrySnapshotFrame(element), !frame.isEmpty else {
424429
return
425430
}
426-
guard !frame.isEmpty else {
427-
element.tap()
428-
return
429-
}
430-
let origin = element.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
431-
let target = origin.withOffset(
432-
CGVector(dx: max(2, frame.width - 4), dy: max(2, frame.height / 2))
431+
_ = tapAt(
432+
app: app,
433+
x: frame.minX + max(2, frame.width - 4),
434+
y: frame.minY + max(2, frame.height / 2)
433435
)
434-
target.tap()
435436
#endif
436437
}
437438

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextTyping.swift‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ extension RunnerTests {
3434
failure: .notFocused
3535
)
3636
}
37-
clearTextInput(clearTarget)
37+
clearTextInput(app: app, clearTarget)
3838
// nil means the value is unreadable (secure fields), not "not empty": leave `verified`
3939
// nil there rather than reporting a mismatch the runner cannot actually observe.
4040
let observed = editableTextValue(for: clearTarget, treatingPlaceholderAsEmpty: true)
@@ -125,7 +125,7 @@ extension RunnerTests {
125125
}
126126
if currentText == nil || currentText?.isEmpty == false {
127127
let clearStartedAt = Date()
128-
clearTextInput(replacementTarget)
128+
clearTextInput(app: app, replacementTarget)
129129
activeTarget = activeTarget.withElement(replacementTarget)
130130
logTextEntryPhase(
131131
commandId: commandId,
@@ -465,7 +465,7 @@ extension RunnerTests {
465465
expectedText.count,
466466
observedText.count
467467
)
468-
clearTextInput(repairTarget)
468+
clearTextInput(app: app, repairTarget)
469469
repairTarget.typeText(expectedText)
470470
let repairedTarget = target.withElement(repairTarget)
471471
let repairedResult = verifyTextEntry(

0 commit comments

Comments
 (0)