Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ extension RunnerTests {
ok: false,
error: ErrorPayload(
code: "UNSUPPORTED_OPERATION",
message: "Unable to dismiss the iOS keyboard without a native dismiss gesture or control"
message: "Unable to dismiss the iOS keyboard without a safe native dismiss control"
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,27 +355,12 @@ extension RunnerTests {
let visible = isKeyboardVisible(app: app)
return (wasVisible: true, dismissed: !visible, visible: visible)
#else
let keyboard = app.keyboards.firstMatch
keyboard.swipeDown()
sleepFor(0.2)
if !isKeyboardVisible(app: app) {
return (wasVisible: true, dismissed: true, visible: false)
}

if tapKeyboardDismissControl(app: app) {
sleepFor(0.2)
let visible = isKeyboardVisible(app: app)
return (wasVisible: true, dismissed: !visible, visible: visible)
}

if tapKeyboardReturnControl(app: app, allowCoordinateFallback: true) {
sleepFor(0.2)
let visible = isKeyboardVisible(app: app)
if !visible {
return (wasVisible: true, dismissed: true, visible: false)
}
}

return (wasVisible: true, dismissed: false, visible: isKeyboardVisible(app: app))
#endif
}
Expand Down Expand Up @@ -478,7 +463,7 @@ extension RunnerTests {
label,
label
)
let toolbarButtons = app.toolbars.buttons
let toolbarButtons = app.descendants(matching: .button)
.matching(toolbarButtonPredicate)
.allElementsBoundByIndex
if let hittable = toolbarButtons.first(where: {
Expand All @@ -492,10 +477,7 @@ extension RunnerTests {
#endif
}

private func tapKeyboardReturnControl(
app: XCUIApplication,
allowCoordinateFallback: Bool = false
) -> Bool {
private func tapKeyboardReturnControl(app: XCUIApplication) -> Bool {
#if os(iOS)
for label in ["return", "Return", "Enter", "Go", "Search", "Next", "Done", "Send", "Join"] {
let candidates = [
Expand All @@ -506,21 +488,6 @@ extension RunnerTests {
hittable.tap()
return true
}
if allowCoordinateFallback,
let keyboardFrame = visibleKeyboardFrame(app: app),
let framed = candidates.first(where: {
guard $0.exists else { return false }
let frame = $0.frame
return !frame.isEmpty && keyboardFrame.contains(CGPoint(x: frame.midX, y: frame.midY))
}) {
let frame = framed.frame
switch tapAt(app: app, x: frame.midX, y: frame.midY) {
case .performed:
return true
case .unsupported:
return false
}
}
}
#endif
return false
Expand Down
2 changes: 1 addition & 1 deletion ios-runner/RUNNER_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Successful and failed responses use the same top-level envelope:
"ok": false,
"error": {
"code": "UNSUPPORTED_OPERATION",
"message": "Unable to dismiss the iOS keyboard without a native dismiss gesture or control"
"message": "Unable to dismiss the iOS keyboard without a safe native dismiss control"
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion src/cli/parser/cli-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Text entry:
agent-device type "Handle with care" --delay-ms 80
Empty replacement is not a supported clear-field command: do not plan fill <target> "" or fill <target> ''. Prefer a visible clear/reset control; if the app exposes none, report the tool gap instead of inventing a clear command.
Debounced field with no result selector: agent-device wait 1000. Keyboard read-only: keyboard status/get. Blocked control: try keyboard dismiss when supported.
On iOS, prefer keyboard dismiss before manually pressing visible Done; the runner can use safe native keyboard controls and still reports unsupported layouts explicitly. If it returns UNSUPPORTED_OPERATION, prefer a visible app dismiss control, or use back --system only when system navigation is an acceptable side effect.
To hide the keyboard, use keyboard dismiss. It taps safe controls like Done when available and verifies the keyboard closed. If it reports UNSUPPORTED_OPERATION, press a visible app control such as Done only when that is the intended fallback.
Use plain fill/type first for ordinary login and form fields. If an iOS debounced or search-as-you-type field actually drops characters, or must receive incremental updates, retry with --delay-ms before trying clipboard paste; --delay-ms intentionally paces character entry.
iOS Allow Paste prompt cannot be exercised under XCUITest. To test paste-driven app behavior, prefill first with agent-device clipboard write "some text"; test the system prompt manually.
Android Gboard handwriting/stylus UI can capture text in an IME-owned input instead of the app field. If fill reports that input was captured by the keyboard/IME, use the diagnostic targetInput/actualInput details, inspect keyboard status/get if needed, and switch or disable handwriting outside the command plan before retrying. Do not keep retrying fill/type against the same field while the IME owns focus.
Expand Down
2 changes: 1 addition & 1 deletion src/commands/system/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const rotateCliSchema = {
const keyboardCliSchema = {
usageOverride: 'keyboard [status|get|dismiss|enter|return]',
helpDescription:
'Inspect Android keyboard visibility/type or press/dismiss the device keyboard. For iOS toolbar Done dismissal, focus the field by id/ref first, then run keyboard dismiss; do not manually press Done unless keyboard dismiss reports unsupported.',
'Inspect Android keyboard visibility/type or press/dismiss the device keyboard. To hide the keyboard, use keyboard dismiss. It taps safe controls like Done when available, verifies the keyboard closed, and reports UNSUPPORTED_OPERATION when no safe control is available.',
summary: 'Inspect, press, or dismiss the device keyboard',
positionalArgs: ['action?'],
} as const satisfies CommandSchemaOverride;
Expand Down
2 changes: 1 addition & 1 deletion src/platforms/ios/__tests__/runner-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ test('parseRunnerResponse preserves runner unsupported-operation codes', async (
ok: false,
error: {
code: 'UNSUPPORTED_OPERATION',
message: 'Unable to dismiss the iOS keyboard without a native dismiss gesture or control',
message: 'Unable to dismiss the iOS keyboard without a safe native dismiss control',
},
}),
);
Expand Down
11 changes: 8 additions & 3 deletions src/utils/__tests__/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1110,8 +1110,9 @@ test('usageForCommand supports legacy long-press alias', () => {
test('usageForCommand documents keyboard dismissal flow', () => {
const help = usageForCommand('keyboard');
assert.equal(help === null, false);
assert.match(help ?? '', /focus the field by id\/ref first/);
assert.match(help ?? '', /keyboard dismiss reports unsupported/);
assert.match(help ?? '', /To hide the keyboard, use keyboard dismiss/);
assert.match(help ?? '', /taps safe controls like Done/);
assert.match(help ?? '', /UNSUPPORTED_OPERATION/);
});

test('usageForCommand supports metrics alias', () => {
Expand Down Expand Up @@ -1434,7 +1435,11 @@ test('usageForCommand resolves workflow help topic', () => {
assert.match(help, /iOS Allow Paste prompt cannot be exercised under XCUITest/);
assert.match(help, /Empty replacement is not a supported clear-field command/);
assert.match(help, /do not plan fill <target> ""/);
assert.match(help, /prefer keyboard dismiss before manually pressing visible Done/);
assert.match(help, /To hide the keyboard, use keyboard dismiss/);
assert.match(
help,
/press a visible app control such as Done only when that is the intended fallback/,
);
assert.match(help, /UNSUPPORTED_OPERATION/);
assert.match(help, /Stateful commands within one session must run serially/);
assert.match(
Expand Down
5 changes: 2 additions & 3 deletions website/docs/docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,8 @@ agent-device keyboard dismiss
```

- `keyboard status` (or `keyboard get`) returns keyboard visibility and best-effort input type classification on Android.
- `keyboard dismiss` attempts a non-navigation keyboard dismissal on Android and a native dismiss gesture/control on iOS, including common safe controls such as a keyboard toolbar `Done` button, then confirms the keyboard is hidden.
- If the keyboard remains visible after the platform-native dismiss path, the command returns an explicit `UNSUPPORTED_OPERATION` error instead of falling back to back navigation.
- On iOS, `keyboard dismiss` is best-effort and can fail when the active app exposes no native dismiss gesture/control. Prefer a visible app dismiss control, or use `back --system` only when system navigation is an acceptable side effect.
- To hide the keyboard, use `keyboard dismiss`. It taps safe controls like `Done` when available and verifies the keyboard closed.
- If it reports `UNSUPPORTED_OPERATION`, press a visible app control such as `Done` only when that is the intended fallback.
- Works with active sessions and explicit selectors (`--platform`, `--device`, `--udid`, `--serial`).
- `keyboard status|get` is supported on Android emulator/device.
- `keyboard dismiss` is supported on Android emulator/device and best-effort on iOS simulator/device.
Expand Down
Loading