fix(ios): wait for hidden-keyboard synthesized text to commit before responding - #1676
Conversation
…r test
testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden read textField.value
in one shot right after executeTypeCommand returned. The simulator commits
synthesized keystrokes after the command responds, so on a loaded CI machine
the read landed mid-word — observed failures reported ("h") and
("hardware-ke"). It failed on 3 of 5 runs of a branch carrying zero Swift
changes and passed on re-run.
Poll the value until it holds the expected text (10s ceiling) and assert on
the last value read, so a real regression still fails with what the field
actually held. Both reads in the test use the same helper; the assertions are
unchanged.
Size Report
Startup median (7 runs, lower is better):
Top changed chunks: no changes in the largest emitted chunks. |
|
[P1] This is not complementary to #1673; it removes #1673’s strongest production-route oracle. The contract under repair is that the synthesized-first-responder |
…rd runner test" This reverts commit caecdc6.
…responding The synthesized-first-responder bare-type route returned ok as soon as the private XCTest event record was posted, while the target app was still committing characters. On slow CI simulators the trailing characters landed after the response, so agents (and the smoke test) observed a truncated field value through the public type path. After dispatch, poll the tapped element until its value reaches textBefore + typedText, exit immediately when the app transforms the input (formatter, mid-text caret, autocomplete), and if progress stalls as a strict prefix, re-synthesize the missing tail once. Submit-suffixed text keeps the old immediate return so a repair can never double-submit. Validated on a booted iPhone 17 Pro simulator: 5/5 passes of testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden under full-core CPU load, with the commit wait absorbing up to ~390ms of post-dispatch lag that the previous code ignored (uniform ~494ms dispatch-only before); the tail repair never had to fire, consistent with commit lag rather than true drops.
Addresses the P1 on #1673: a 600 ms quiet prefix cannot distinguish delayed app-side commit from a genuinely dropped suffix, so re-synthesizing the tail could post it while the original was still queued and commit the text twice after the command had already reported ok. Drop the repair path entirely — the tail builder, the quiet-window constant, the stall tracking, and the synthesizer dependency the wait only needed in order to repair. What remains is a bounded observation: poll the tapped element until the value commits, the app transforms it, the value becomes unreadable, or the 3s ceiling expires. A dropped suffix still reports ok, exactly as before this change; only the false truncation from commit lag is removed. The submit-key skip stays, for its own reason: the app may clear or rewrite the field on submit, so textBefore + typedText is not the value to wait for. Also wire testSynthesizedTextCommitProgressWalksExpectedPrefixOnly into the iOS smoke lane — that workflow enumerates its tests by hand with -only-testing, so a new test that is not listed never runs.
|
Clean review on head |
|
Consolidates the hidden-keyboard text-entry flake into one PR. It started as a test-side fix; that approach was withdrawn — see below.
The defect
The
synthesized-first-responderbare-type route returned ok as soon as the private XCTest event record was posted, while the target app was still committing characters. Agents callingtypethensnapshotcould observe a truncated field, and the smoke test observed it as a flake:("h")and("hardware-ke")instead of("hardware-keyboard")on 3 of 5 runs of a branch with zero Swift changes (example job).The fix polls the tapped element after dispatch until the value commits, then responds. Originated in #1673 (commit c9575ad, authored there); folded here after that PR was closed.
What changed relative to #1673
The P1 on #1673 is addressed by removing the repair path, not by defending it. A 600 ms quiet prefix cannot distinguish delayed commit from a dropped suffix — re-synthesizing the difference could post it while the original was still queued and commit the text twice, after the command had already reported ok. So the wait is now observation only:
synthesizedTextCommitRepairTail, the quiet-window constant, the stall tracking, and its unit test;synthesizerparameter — with no repair, the wait has no reason to hold aTextEntrySynthesizingat all, so it is a pure observer of the field it was given.What remains is one bounded loop with a single decision point: poll until the value commits, the app transforms it (formatter, mid-caret, autocomplete), the value becomes unreadable (secure field, element stopped resolving), or the 3s ceiling expires. Every stop reason funnels through the pure
synthesizedTextCommitProgresspolicy function, which is unit-tested without a simulator.Known limit, unchanged by this PR: a genuinely dropped suffix still reports ok. Bare
typeis no-verification by design (repairMode == .none); this only removes the false truncation caused by commit lag, at a worst case of 3s.The withdrawn test-side fix
This branch first made the test poll
textField.valueuntil it settled. That was wrong to ship on its own: the flake was a true positive about the product, and a polling assertion would have made the test tolerate a runner that responds before the text commits — deleting the guard that proves this fix works. It is reverted here (commitscaecdc6d5+f91c0e8b1kept for the trail). The one-shot read stays as the assertion, because with the commit wait in place the value is committed by the timetyperesponds.CI gap found while folding
ios.ymlenumerates its runner tests by hand with 25-only-testing:flags, so #1673's new unit tests would never have executed in CI.testSynthesizedTextCommitProgressWalksExpectedPrefixOnlyis now wired into that list.Verification
Built with
AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1, run on a dedicated iPhone 17 Pro Max sim (iOS 26.2) to keep a parallel session's simulator work out of the results:-test-iterations 3→ 12/12 passed, twice (before and after the simplification).testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden: 12.66–12.75s with the commit wait vs 12.5–13.7s without — it exits as soon as the value commits, so it costs nothing in the normal case.Local machines cannot reproduce the CI truncation even under saturation, so this is non-regression evidence plus the mechanism; the 5/5-under-load A/B that showed the wait absorbing ~390ms of post-dispatch lag is in #1673.