Skip to content

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

@Madhatt4

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions