Skip to content

fill_input: clear via element.select() — synthetic Cmd/Ctrl+A never fires select-all - #570

Open
aiba wants to merge 2 commits into
browser-use:mainfrom
aiba:bh-fill-input
Open

fill_input: clear via element.select() — synthetic Cmd/Ctrl+A never fires select-all#570
aiba wants to merge 2 commits into
browser-use:mainfrom
aiba:bh-fill-input

Conversation

@aiba

@aiba aiba commented Jul 30, 2026

Copy link
Copy Markdown

Problem

fill_input(selector, text) does not replace the field's contents. It clears by dispatching a synthetic Cmd/Ctrl+A, but that shortcut never fires select-all over CDP, so Backspace deletes at the caret instead of deleting a selection, and the new text lands beside the old value.

The corruption is silent — no exception, no warning — and caret-dependent, so it isn't even consistent. Two runs of the same call on the same field:

# <input id="a" value="preexisting">
fill_input("#a", "REPLACED")
# run 1 -> 'REPLACEDpreexisting'
# run 2 -> 'preexistinREPLACED'
# expected -> 'REPLACED'

Root cause

Measured directly, on an 11-character field:

js("document.getElementById('a').value='preexisting'; document.getElementById('a').focus()")
sel = {"key":"a","code":"KeyA","modifiers":4,          # Meta on macOS
       "windowsVirtualKeyCode":65,"nativeVirtualKeyCode":65}
cdp("Input.dispatchKeyEvent", type="rawKeyDown", **sel)
cdp("Input.dispatchKeyEvent", type="keyUp", **sel)
js("(()=>{const e=document.getElementById('a');return e.selectionEnd-e.selectionStart})()")
# -> 0        (expected 11)

selectionEnd - selectionStart == 0: nothing is selected. press_key("Backspace") then removes one character at the caret, or none if the caret is at position 0, and the subsequent characters insert wherever the caret happens to be.

This is not environment-specific. I measured the same 0 / 11 in all three tab states:

Tab state visibilityState Cmd+A selects
Activated (foreground) visible 0 / 11
Background hidden 0 / 11
Background + Emulation.setFocusEmulationEnabled visible 0 / 11

Tested on macOS 15, Brave 150 (Chromium 150), dedicated debugging profile on a non-default port.

Fix

Select via element.select() (selectAllChildren for contenteditable) instead of the key dance:

  • Only select when the field has content. select() on an empty field leaves it in a state where subsequent characters don't insert at all — so the Backspace is skipped when there is nothing to clear.
  • clear_first=False parks the caret at the end. focus() puts it at position 0, so appended text was previously prepended. This was a second, separate bug in the same function.

Verified against a live page:

Case Before After
fill_input("#a", "REPLACED") over "preexisting" 'preexistinREPLACED' 'REPLACED'
fill_input("#b", "FRESH") on empty field 'FRESH' 'FRESH'
fill_input("#c", "-more", clear_first=False) over "keep" '-morekeep' 'keep-more'

Tests

uv run --with pytest pytest -q98 passed.

The test asserting the old Cmd/Ctrl+A behaviour is replaced by two tests: one that clearing selects via JS and still sends Backspace, and one that an empty field does not receive a Backspace.

Relationship to other PRs

#498 contains an equivalent fill_input change, bundled into a larger background-tabs-by-default redesign, and attributes the bug to focus-emulated background tabs. The measurements above show it reproduces on ordinary activated tabs too, so it is worth fixing independently of that work — this PR is just the bug fix, and does not touch tab activation, press_key, or click_at_xy.


Summary by cubic

Fixes fill_input so it replaces content by selecting via element.select() (or selectAllChildren) instead of synthetic Cmd/Ctrl+A. Also ensures append mode puts the caret at the end for both inputs and contenteditable.

  • Bug Fixes
    • Selects content via JS, then sends Backspace only if the field had content.
    • For clear_first=False, moves the caret to the end for inputs and contenteditable (via setSelectionRange or a collapsed Range).
    • Updates tests to assert JS-based selection, skipping Backspace on empty fields, and caret behavior for contenteditable append.

Written for commit babf1f6. Summary will update on new commits.

Review in cubic

The select-all shortcut never fires over CDP. Measured on Brave 150:
selectionEnd - selectionStart == 0 on an 11-char field, in every tab state
(activated, background, background with focus emulation). Backspace then
deletes at the caret rather than the selection, and the new text lands beside
the old value instead of replacing it.

The corruption is silent and caret-dependent, so it is not even consistent —
two runs of the same call produced 'REPLACEDpreexisting' and
'preexistinREPLACED' where both should have been 'REPLACED'.

Select with element.select() (selectAllChildren for contenteditable) instead.
Skip the Backspace when the field is already empty: select() on an empty field
leaves it in a state where subsequent characters don't insert at all. For
clear_first=False, park the caret at the end — focus() puts it at 0, so
appended text would otherwise be prepended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G35BbXw4eNsefYAT5Yd45N

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread src/browser_harness/helpers.py Outdated
The clear path handles contenteditable via selectAllChildren, but the
clear_first=False path only called setSelectionRange, which contenteditable
elements do not have. The caret stayed at the browser default (position 0), so
"append" prepended instead:

    <div id="ce" contenteditable>keep</div>
    fill_input("#ce", "-more", clear_first=False)
    # was '-morekeep', now 'keep-more'

Fall back to a Range collapsed to the end of the element's contents. Caught by
cubic-dev-ai review on browser-use#570 and confirmed against a live page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G35BbXw4eNsefYAT5Yd45N
@aiba

aiba commented Jul 30, 2026

Copy link
Copy Markdown
Author

Valid, and fixed in babf1f6 — thanks.

Confirmed against a live page before and after:

# <div id="ce" contenteditable>keep</div>
fill_input("#ce", "-more", clear_first=False)
# before: '-morekeep'
# after:  'keep-more'

Exactly as described: the clear path handled contenteditable via selectAllChildren, but the append path guarded on 'value' in e && e.setSelectionRange, which contenteditable fails, so the caret stayed at position 0 and "append" prepended. It now falls back to a Range collapsed to the end of the element's contents. Added a regression test asserting the fallback is used; suite is 99 passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant