Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/browser_harness/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,15 +250,25 @@ def fill_input(selector, text, clear_first=True, timeout=0.0):
"Home": (36, "Home", ""), "End": (35, "End", ""),
"PageUp": (33, "PageUp", ""), "PageDown": (34, "PageDown", ""),
}

def _key_metadata(key):
if len(key) == 1 and key.isalpha():
upper = key.upper()
return ord(upper), f"Key{upper}", key
if len(key) == 1 and key.isdigit():
return ord(key), f"Digit{key}", key
return _KEYS.get(key, (ord(key[0]) if len(key) == 1 else 0, key, key if len(key) == 1 else ""))

def press_key(key, modifiers=0):
"""Modifiers bitfield: 1=Alt, 2=Ctrl, 4=Meta(Cmd), 8=Shift.
Special keys (Enter, Tab, Arrow*, Backspace, etc.) carry their virtual key codes
so listeners checking e.keyCode / e.key all fire."""
vk, code, text = _KEYS.get(key, (ord(key[0]) if len(key) == 1 else 0, key, key if len(key) == 1 else ""))
vk, code, text = _key_metadata(key)
printable_text = text if text and not (modifiers & 0b0111) else ""
base = {"key": key, "code": code, "modifiers": modifiers, "windowsVirtualKeyCode": vk, "nativeVirtualKeyCode": vk}
cdp("Input.dispatchKeyEvent", type="keyDown", **base, **({"text": text} if text else {}))
if text and len(text) == 1:
cdp("Input.dispatchKeyEvent", type="char", text=text, **{k: v for k, v in base.items() if k != "text"})
cdp("Input.dispatchKeyEvent", type="keyDown", **base, **({"text": printable_text} if printable_text else {}))
if printable_text and len(printable_text) == 1:
cdp("Input.dispatchKeyEvent", type="char", text=printable_text, **{k: v for k, v in base.items() if k != "text"})
cdp("Input.dispatchKeyEvent", type="keyUp", **base)

def scroll(x, y, dy=-300, dx=0):
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,59 @@ def fake_js(expr, **kwargs):
assert "Backspace" not in keys_seen


# --- press_key ---

def test_press_key_with_ctrl_modifier_does_not_emit_printable_text():
key_events = []

def fake_cdp(method, **kwargs):
if method == "Input.dispatchKeyEvent":
key_events.append(kwargs)
return {}

with patch("browser_harness.helpers.cdp", side_effect=fake_cdp):
helpers.press_key("a", modifiers=2)

assert [e["type"] for e in key_events] == ["keyDown", "keyUp"]
assert all("text" not in e for e in key_events), (
"Ctrl/Cmd/Alt shortcuts must not emit printable text; Chrome can treat "
"that as typing a character instead of invoking the shortcut"
)
assert all(e["code"] == "KeyA" for e in key_events)
assert all(e["windowsVirtualKeyCode"] == 65 for e in key_events)


def test_press_key_letter_uses_canonical_code_and_vk():
key_events = []

def fake_cdp(method, **kwargs):
if method == "Input.dispatchKeyEvent":
key_events.append(kwargs)
return {}

with patch("browser_harness.helpers.cdp", side_effect=fake_cdp):
helpers.press_key("z")

assert key_events[0]["key"] == "z"
assert key_events[0]["code"] == "KeyZ"
assert key_events[0]["windowsVirtualKeyCode"] == 90


def test_press_key_digit_uses_canonical_code_and_vk():
key_events = []

def fake_cdp(method, **kwargs):
if method == "Input.dispatchKeyEvent":
key_events.append(kwargs)
return {}

with patch("browser_harness.helpers.cdp", side_effect=fake_cdp):
helpers.press_key("5")

assert key_events[0]["code"] == "Digit5"
assert key_events[0]["windowsVirtualKeyCode"] == 53


# --- wait_for_element ---

def test_wait_for_element_returns_true_when_found_immediately():
Expand Down