Skip to content

feat: add press_key and type_text tools - #153

Open
freema wants to merge 3 commits into
mainfrom
feat/keyboard-input-tools
Open

feat: add press_key and type_text tools#153
freema wants to merge 3 commits into
mainfrom
feat/keyboard-input-tools

Conversation

@freema

@freema freema commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Why

The input module has no keyboard capability at all. The only sendKeys calls in src/ are bound to a specific element inside fill_by_uid and upload_file_by_uid, plus one on a prompt dialog in pages.ts. There is no way for an agent to press a key on its own, so it cannot submit a form with Enter, close a modal with Escape, walk an autocomplete list with the arrow keys, or move focus with Tab.

I did not find an existing bug for this on the meta, so let me know if you would rather I file one on Bugzilla and re-land this under a bug number.

What I added

Two tools in the input module. That module is part of slim, so they are available in every preset.

  • press_key presses a key or combination on the focused element: "Enter", "Escape", "Control+Shift+R".
  • type_text types into the focused element, with an optional submitKey. This is for elements that only react to real typing, such as autocomplete widgets and rich text editors. fill_by_uid stays the right tool for replacing the value of a known input, and I said so in both descriptions so the model does not treat them as interchangeable.

Notes on the implementation

Key naming. Names follow the DOM KeyboardEvent.key vocabulary ("Enter", "ArrowDown", "a"), so a model can name keys the same way it would in page code. src/utils/keyboard.ts maps them onto the Selenium Key constants. Unknown names fail with an error listing what is accepted rather than silently doing nothing. A trailing + is handled, so "Control++" and "+" both mean the plus key.

Modifier release. Modifiers are held only for the duration of the key press. If the action sequence throws part way through, the recovery path issues a release-actions command. Without it a failed Control+… would leave Control logically held down for the rest of the session and corrupt every later key press, which is an unpleasant failure mode because it looks like the page misbehaving.

type_text validates submitKey before typing, so an invalid key name fails without leaving the page half-filled.

How I verified it

32 unit tests for the parser and the handlers, and 6 integration tests against a real Firefox (152.0.5, headless) in tests/integration/keyboard.integration.test.ts:

  • typing lands in the focused element
  • type_text with submitKey: "Enter" submits the form fixture
  • Tab moves focus from #name to #email
  • Control+Shift+b arrives at the page with ctrlKey and shiftKey set
  • a plain x pressed afterwards has both flags clear, so the modifiers really were released
  • an unknown key name rejects

Full suite is green locally: 50 files, 672 tests, including the integration tests.

Follow-ups I did not include here

While reading dom.ts I noticed the clear-input fallback in fillBySelector/fillByUid uses Key.chord(Key.CONTROL, 'a'). On macOS select-all is Cmd+A, and Ctrl+A is the "move to start of line" binding, so that path moves the caret and deletes a single character instead of clearing. It only runs when el.clear() throws, so it does not fire often, but when it does it silently writes a mixed value. I left it out to keep this PR additive. Happy to send it separately.

Thanks for the reviews.

The input module had no keyboard capability at all. The only sendKeys
calls in src/ were bound to a specific element inside fill_by_uid and
upload_file_by_uid, plus one on a prompt dialog, so an agent could not
submit a form with Enter, close a modal with Escape, walk an autocomplete
list with the arrow keys, or move focus with Tab.

I added two tools to the input module, which is part of the slim preset,
so they are available in every configuration:

- press_key presses a key or combination on the focused element, e.g.
  "Enter", "Escape" or "Control+Shift+R".
- type_text types into the focused element with an optional submitKey,
  for elements that only react to real typing such as autocomplete
  widgets and rich text editors. fill_by_uid stays the right tool for
  replacing the value of a known input.

Key names follow the DOM KeyboardEvent.key vocabulary ("Enter",
"ArrowDown", "a") so a model can name keys the same way it would in page
code, and src/utils/keyboard.ts maps them onto the Selenium Key
constants. Unknown names fail with an error that lists what is accepted
rather than silently doing nothing.

Modifiers are held only for the duration of the key press. If the action
sequence throws part way through, the recovery path issues a release
actions command, otherwise the modifiers would stay logically held down
for the rest of the session and corrupt every later key press.

Tested with 32 unit tests covering the parser and the handlers, plus 6
integration tests against a real Firefox that assert typing, Enter
submitting a form, Tab moving focus, modifiers reaching the page as
ctrlKey/shiftKey, and modifiers being released afterwards.
@freema
freema requested a review from juliandescottes August 16, 2026 09:01
freema added 2 commits August 16, 2026 11:41
…ests

Review feedback on this PR.

- typeText built two separate action sequences, one for the text and one
  for the submit key. MCP tool handlers are not serialised, so a
  concurrent call could move focus between the two and the submit key
  would land on a different element. Both now go into a single Actions
  object performed once.
- An empty submitKey passed the handler but was dropped by a truthy
  check, so the tool reported success without pressing anything. It is
  now rejected, and the schema declares minLength.
- The handler tests mocked pressKey/typeText wholesale, so nothing
  asserted the keyDown/keyUp ordering, the release-on-failure path or how
  many times perform() runs. Added tests/firefox/dom-keyboard.test.ts,
  which drives DomInteractions against a mocked actions API. These run in
  PR Check, unlike the integration tests.

I also checked the review's concern that the requestAnimationFrame ping in
waitForEventsAfterAction races a navigation triggered by Enter. I could not
reproduce it: with a form that really navigates (no preventDefault),
press_key and type_text both completed and landed on the target page in 24
out of 24 runs, and clickByUid on the same form behaved identically. The
wait is shared by every existing input tool rather than introduced here, so
I left it alone. Making it navigation-aware is worth doing on its own.
Review feedback on this PR: form.html cancels its submit with
preventDefault, so nothing here exercised a key press that tears the
document away while the tools are still finishing up.

Added nav-form.html and nav-target.html, a form with no preventDefault,
and two integration tests asserting that press_key("Enter") and
type_text(text, "Enter") both complete and land on the target page. The
second also checks the query string, so it fails if the typed text and
the submit key were separated by the navigation.

This replaces the manual check described in the previous commit with
something CI can verify. Ran the file three times, green each time.
@juliandescottes

Copy link
Copy Markdown
Collaborator

@freema thanks for the PR, I will take a look, but did you check #82 by any chance. The timing is unfortunate but both PRs seem to overlap - and #82 was just updated as well.

I will compare them locally, but you might have an easier time since you authored this one.

@juliandescottes

Copy link
Copy Markdown
Collaborator

@freema I would try to finalize and merge #82 first and then rebase your work on top of it.

@freema

freema commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Hi @juliandescottes, thanks for taking a look! And sorry — that's a fail on my side, I should have checked the open PRs before starting this one and I completely missed #82.

I fully agree with your suggestion: let's finalize and merge #82 first, and once it lands I'll rebase my branch on top of it, so my PR will effectively shrink to type_text plus whatever is still useful on top.

For a bit of context: I opened this PR because I need keyboard input for e2e testing of the login flows on my magazine websites, so I'm happy to help with reviewing/testing #82 to move things along.

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.

2 participants