From 33cf31c8eabf049e4619bef7699e4d35f8493fb6 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Sun, 10 May 2026 11:03:40 +0400 Subject: [PATCH] fix: make press_key handle keyboard shortcuts --- src/browser_harness/helpers.py | 18 +++++++++--- tests/unit/test_helpers.py | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/browser_harness/helpers.py b/src/browser_harness/helpers.py index 3efb609c..deb0c12f 100644 --- a/src/browser_harness/helpers.py +++ b/src/browser_harness/helpers.py @@ -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): diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py index 4a45ee07..f6db5f91 100644 --- a/tests/unit/test_helpers.py +++ b/tests/unit/test_helpers.py @@ -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():