Skip to content

feat: real key-event typing in send_keys/type - #30

Open
4EYES44 wants to merge 4 commits into
omkarcloud:masterfrom
4EYES44:feat/real-key-events
Open

feat: real key-event typing in send_keys/type#30
4EYES44 wants to merge 4 commits into
omkarcloud:masterfrom
4EYES44:feat/real-key-events

Conversation

@4EYES44

@4EYES44 4EYES44 commented Aug 30, 2026

Copy link
Copy Markdown

feat: real key-event typing in send_keys/type

Summary

Element.send_keys (and therefore Driver.type) now dispatches real CDP key events — keyDown + keyUp per character with correct code, key, windowsVirtualKeyCode, and Shift-wrapping for shifted characters — instead of a single synthetic char event per character. Characters without a physical key (emoji, non-Latin scripts, IME input) fall back to Input.insertText. The public API is unchanged.

Problem

The previous send_keys implemented typing as one Input.dispatchKeyEvent("char", text=char) per character. That synthetic event:

  • Carries no code / windowsVirtualKeyCode, so pages that read event.code or event.keyCode see empty/zero values.
  • Does not produce a keydown/keyup pair, so listeners on those events (very common — captchas, masked inputs, JS frameworks, "press Enter to submit") never fire.
  • Breaks bypass_insecure_connection_warning(), which relies on typing thisisunsafe into Chrome's SSL interstitial — the interstitial listens for real key events and ignores char-only dispatch.

Solution

New botasaurus_driver/core/keys.py centralizes key-event dispatch:

  • KEYBOARD_LAYOUT / SHIFT_CHARS / VK_CODES maps resolve each character to its physical-key code, Windows VK code, and whether Shift is required.
  • resolve_key(char) → key info, or None for unmapped characters.
  • dispatch_key(tab, char) emits the rawKeyDown-Shift (if needed) → keyDown-with-text → keyUp → Shift-keyUp sequence with faithful unmodified_text/text.
  • type_text(tab, text) walks the string, routing unmapped chars to Input.insertText, and collapsing \r\n to a single Enter.

Element.send_keys now calls keys.type_text(self._tab, text).

Behavior

  • Mapped printable chars (ASCII + common Latin) → real keydown/keyup with correct code/keyCode, Shift held for capitals and symbols.
  • \n, \r, and \r\n → one Enter (NumpadEnter/Enter code, VK 13); CRLF counts as a single Enter.
  • \t → Tab.
  • Unmapped chars (e.g. 你好, 👋, IME output) → Input.insertText(char), the standard CDP primitive for non-keypress text insertion — no fabricated VK codes.
  • bypass_insecure_connection_warning() works again because thisisunsafe now produces real key events the interstitial accepts.

Testing

demo_typing.py is a self-contained gate script (no external deps beyond the driver) that launches a browser against a local HTML harness page which captures every keyboard event, then asserts:

  1. keydown/keypress/keyup ordering and count for "ab".
  2. code is never empty for any mapped printable character.
  3. keyCode fidelity for a sample of letters/digits/symbols (matches VK_CODES).
  4. Shift state: capitals and shifted symbols arrive with Shift modifier, lowercase without; Shift is pressed before and released after the key.
  5. Newline contract: \n, \r, and \r\n each produce exactly one Enter.
  6. Unicode round-trip: 你好 👋 reaches the input via insertText and the final .value matches.
  7. Final input value equals the typed text for a mixed string.
  8. bypass_insecure_connection_warning() succeeds against a bad-SSL page.

Run with: python demo_typing.py (exits non-zero on any failure).

Compatibility

  • No public API change: driver.type(selector, text) and element.send_keys(text) signatures and behavior from the caller's perspective are identical.
  • No new dependencies; uses the existing cdp.input_ primitives already imported by the driver.
  • Pure additive change to event fidelity; existing scripts that worked before continue to work, and scripts that previously failed on keydown-listening pages now work.

Out of scope

  • Human-like typing cadence (Gaussian timing, word-boundary pauses, fatigue) — intentionally not included here; this PR is about event fidelity, not timing. A separate follow-up can add cadence behind an opt-in.
  • Mouse / scroll humanization — unrelated.
  • Public type_human API — not introduced; send_keys is fixed in place per the "fix the existing API" approach.

Files

  • botasaurus_driver/core/keys.py (new, 124 lines) — key maps + dispatch.
  • botasaurus_driver/core/element.py (+2/-3) — send_keys calls keys.type_text.
  • demo_typing.py (new, 386 lines) — gate script.
  • README.md (+7) — short note next to the type example.

Replace Element.send_keys' single bare char-event-per-character
dispatch with a faithful keyDown+keyUp sequence (Shift wrapping for
shifted chars), using Input.insertText as the fallback for characters
outside the US layout map. \n/\r/\r\n each produce one Enter; \t
produces Tab.

- New botasaurus_driver/core/keys.py: US-layout KEYBOARD_LAYOUT,
  SHIFT_CHARS, VK_CODES maps, resolve_key(), dispatch_key(), and
  type_text() (the per-character dispatch loop with \r\n collapse and
  insertText fallback, no cadence).
- Element.send_keys now calls keys.type_text(self._tab, text),
  preserving raise_if_disconnected / self.update / the docstring.
- bypass_insecure_connection_warning (tab.py) untouched; still calls
  body.send_keys("thisisunsafe") through the new path.

No cadence, no field_selector, no _resolve_tab -- those are split out
to a separate follow-up per Plan-human-type.md section 6.
Self-contained script (no pytest) that launches a headless Driver,
opens a local HTML harness page capturing keydown/keypress/keyup/
input/beforeinput, and asserts the 8 properties from
Plan-human-type.md section 8:

1. event ordering and count (ab -> 6 events, no missing keyup,
   no doubled keypress)
2. event.code never empty for mapped printable ASCII
3. keyCode fidelity (keydown 65, keypress 97 for "a")
4. Shift state and pairing for "@"
5. \n/\r/\r\n -> one Enter + one submit; \t -> Tab; " " -> click
6. Unicode round-trip (é中😀)
7. final value equals input string
8. bypass_insecure_connection_warning (SKIP if badssl unreachable)

Exits non-zero on FAIL; SKIP is not a failure. Runnable as
`python3 demo_typing.py` without pip install via sys.path insert.
Document that type/send_keys now dispatches real keydown/keyup (with
insertText fallback for unmapped chars and CRLF collapsed to one Enter),
placed next to the existing type example. Public API unchanged.
Typing "thisisunsafe" into Chrome's SSL interstitial makes the page advance
past the warning, which navigates the document and invalidates the body
node. The rewritten Element.send_keys ends with self.update(), which calls
cdp.dom.resolve_node on that now-stale body node and raises
"No node with given id found".

Route the bypass through the keyboard helper directly on the tab instead,
skipping Element.send_keys' trailing self.update(). The typing dispatch is
unchanged; only the stale-node cleanup at this one call site is avoided.

Verified: demo_typing.py now passes all 8 tests against real Chrome,
including test_bypass_insecure_connection_warning (title advances from
"Privacy error" to "self-signed.badssl.com").
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