Skip to content

press_key double-types printable characters via duplicate keyDown+char events #382

Description

@Madhatt4

press_key double-types printable characters via duplicate keyDown+char events

Problem

Calling press_key (or fill_input / type_text which use it) with a single printable character inserts that character twice into framework-controlled inputs (React, Vue, Ember).

Example:

from browser_harness.helpers import fill_input
fill_input("#email", "test1@propxchain.com")
# Read back: document.querySelector("#email").value
# Expected: "test1@propxchain.com"
# Actual:   "tteesstt11@@pprrooppxxcchhaaiinn..ccoomm"

Discovered while building an e2e test suite against a React/Supabase consumer app (PropXchain) — the password-reset email field couldn't be filled correctly, the form submitted with an invalid address and was silently accepted by the backend.

Minimal reproduction

# Inside `browser-harness` against any page with a text <input>
press_key("a")
# Then check the focused input: it now contains "aa", not "a"

Also reproduces via fill_input("#anyInput", "abc") → value becomes "aabbcc".

Root cause

In src/browser_harness/helpers.py, press_key() fires two events that both insert the character:

def press_key(key, modifiers=0):
    vk, code, text = _KEYS.get(key, (ord(key[0]) if len(key) == 1 else 0, key, key if len(key) == 1 else ""))
    base = {"key": key, "code": code, "modifiers": modifiers,
            "windowsVirtualKeyCode": vk, "nativeVirtualKeyCode": vk}
    cdp("Input.dispatchKeyEvent", type="keyDown", **base,
        **({"text": text} if text else {}))               # ← inserts char
    if text and len(text) == 1:
        cdp("Input.dispatchKeyEvent", type="char", text=text,
            **{k: v for k, v in base.items() if k != "text"})  # ← inserts char AGAIN
    cdp("Input.dispatchKeyEvent", type="keyUp", **base)

When text is included on the keyDown event, Chrome's Input.dispatchKeyEvent inserts the character into the focused input. The subsequent char event then inserts it a second time.

Per the CDP Input.dispatchKeyEvent docs and how Puppeteer/Playwright implement the same operation, the standard pattern for typing a printable character is:

  1. keyDown without text — fires the keydown event for listeners
  2. char with text — this is the event that actually inserts the character
  3. keyUp — fires the keyup event

Setting text on both keyDown and char produces double-insertion.

Proposed fix

Drop text from the keyDown event when a printable char event will follow:

def press_key(key, modifiers=0):
    vk, code, text = _KEYS.get(key, (ord(key[0]) if len(key) == 1 else 0, key, key if len(key) == 1 else ""))
    base = {"key": key, "code": code, "modifiers": modifiers,
            "windowsVirtualKeyCode": vk, "nativeVirtualKeyCode": vk}
    is_printable = text and len(text) == 1

    # keyDown — never carry text when a char event will fire; only attach text
    # for special keys (Enter/Tab) where there is no char event.
    keydown_extras = {} if is_printable else ({"text": text} if text else {})
    cdp("Input.dispatchKeyEvent", type="keyDown", **base, **keydown_extras)

    if is_printable:
        cdp("Input.dispatchKeyEvent", type="char", text=text, **base)

    cdp("Input.dispatchKeyEvent", type="keyUp", **base)

Listeners watching keydown events still see e.key, e.code, e.keyCode (those come from key/code/windowsVirtualKeyCode, not text). The text field is what causes Chrome to insert the character — and only the char event needs it.

Workaround in user code

For React/Vue/Angular inputs, set the value via the native setter then fire input+change:

js(f"""
const el = document.querySelector({selector!r});
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
setter.call(el, {value!r});
el.dispatchEvent(new Event('input', {{bubbles: true}}));
el.dispatchEvent(new Event('change', {{bubbles: true}}));
""")

This bypasses press_key entirely. Works reliably on React/Supabase auth forms.

Why this matters

fill_input is documented (and shipped as a fix for #257) as the way to fill framework-managed inputs. With this bug, every flow that types into an input gets corrupted data. Login forms, search boxes, signup flows — all affected.

We discovered this while building an e2e test suite against a real React/Supabase app. The first password-reset flow failed silently — the backend's "Check your email" response is identical whether the submitted email is real or garbage, so the broken value was invisible until we read back input.value.

Environment

  • browser-harness v0.1.0 (editable install from main, commit at the time of report)
  • Chrome 147.0.7727.116
  • WSL2 Ubuntu, Linux 5.15.167.4-microsoft-standard

Searched existing issues

Checked open + closed issues for press_key, type_text, fill_input, double, doubled, character, duplicate keyDown — found #257 (the closed feature-request that introduced fill_input) but no existing report of the doubling bug.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions