enterText method
Focuses the field at target and sets text as one editing value, the
way a widget reports an edit the user made.
Both halves have to be told, which is why this is not
TextInput.updateEditingValue. That call is control-side: it pushes a
value into the framework, and the platform's own editing state — the
UITextField/InputConnection shadow the IME edits against — never
hears about it. Measured on both an iOS simulator and an Android
emulator (2026-08-11): after the agent wrote a sentence, the human's
next keystroke on the soft keyboard replaced it, because as far as the
platform knew the field was still empty. On a desktop with no soft
keyboard this goes unnoticed; on a phone it breaks co-driving, which is
the workflow this surface exists for.
EditableTextState.userUpdateTextEditingValue is the framework's own
name for "a user edit that did not come from the platform": it runs the
input formatters, fires onChanged, and — through endBatchEdit —
calls setEditingState on the live input connection, so the IME's next
edit is a delta against what is actually on screen. It needs the
connection to exist, which is what EditableTextState.requestKeyboard
below is for.
Implementation
Future<DriveStep> enterText(dynamic target, String text, {Duration? settle}) {
return _act('enterText', target, settle, (finder) async {
var elements = editableWithin(finder).evaluate().toList();
if (elements.length != 1) {
throw TargetError(
TargetFailure.notFound,
'${describeTarget(target)} contains ${elements.length} text fields, '
'and `enterText` needs one.',
);
}
var state =
(elements.single as StatefulElement).state as EditableTextState;
state.requestKeyboard();
// Focus and the input connection apply over a frame; give them one.
await lane.settle(const Duration(milliseconds: 100));
state.userUpdateTextEditingValue(
TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
),
SelectionChangedCause.keyboard,
);
});
}