fix(humanize): shiftKey modifier on Shift-punctuation IME events

Shift-punctuation (?!:) dispatched its CDP key events without the Shift
modifier, so the DOM reported shiftKey=false while Shift was held — differing
from a real capture. Pass modifiers=8 on every event dispatched while Shift is
down (Shift keydown, the Process keydown, and the keyups that precede the Shift
keyup), and 0 at/after the Shift keyup, for both release orders.

Python (sync+async) + JS (Playwright+Puppeteer), tests added, verified live.
This commit is contained in:
CloakHQ committed 2026-08-08 19:38:03 +02:00
1 parent 825f84227f
commit 057fb797fd
6 files changed
+83 -35

No files matched your search

+19 -11
View File
@@ -156,8 +156,11 @@ def _compose_display(typed: list[tuple[int, str]]) -> str:
return "'".join(parts[si] for si in order)
def _key_event(type_: str, vk: int, key: str, code: str) -> dict:
return {"type": type_, "windowsVirtualKeyCode": vk, "key": key, "code": code}
def _key_event(type_: str, vk: int, key: str, code: str, modifiers: int = 0) -> dict:
ev = {"type": type_, "windowsVirtualKeyCode": vk, "key": key, "code": code}
if modifiers:
ev["modifiers"] = modifiers # 8 = Shift, so DOM events report shiftKey=true
return ev
def _type_cjk_phrase(
@@ -225,30 +228,35 @@ def _type_cjk_punct(ch: str, cfg: HumanConfig, cdp_session: Any) -> None:
Microsoft Pinyin capture: keydown Process/229 on its physical key → composition →
commit (compositionend, untrusted) → dual keyup (229 + physical keyCode). Shift
marks wrap it in Shift; the Shift-release order is randomized (real users vary it),
and the keyup `key` char follows the Shift state at release."""
and the keyup `key` char follows the Shift state at release. Every event dispatched
while Shift is held carries modifiers=8 so the DOM reports shiftKey=true, matching a
real capture; events at/after the Shift keyup carry 0."""
shift, code, kc, base_key, shifted_key = _CJK_PUNCT[ch]
held = 8 if shift else 0 # Shift modifier flag while Shift is down
if shift:
cdp_session.send("Input.dispatchKeyEvent",
_key_event("keyDown", 16, "Shift", "ShiftLeft"))
_key_event("keyDown", 16, "Shift", "ShiftLeft", 8))
sleep_ms(rand_range(cfg.shift_down_delay))
cdp_session.send("Input.dispatchKeyEvent", _key_event("keyDown", 229, "Process", code))
cdp_session.send("Input.dispatchKeyEvent", _key_event("keyDown", 229, "Process", code, held))
cdp_session.send("Input.imeSetComposition",
{"text": ch, "selectionStart": len(ch), "selectionEnd": len(ch)})
sleep_ms(rand_range(cfg.key_hold))
cdp_session.send("Input.insertText", {"text": ch}) # commit → compositionend
release_shift_first = shift and random.random() < 0.5
if release_shift_first:
# Shift released first → subsequent keyups are already shiftKey=false.
cdp_session.send("Input.dispatchKeyEvent",
_key_event("keyUp", 16, "Shift", "ShiftLeft"))
cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code))
cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", kc, base_key, code))
_key_event("keyUp", 16, "Shift", "ShiftLeft", 0))
cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code, 0))
cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", kc, base_key, code, 0))
else:
cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code))
# Shift still held → these keyups are shiftKey=true; Shift keyup last is false.
cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code, held))
cdp_session.send("Input.dispatchKeyEvent",
_key_event("keyUp", kc, shifted_key if shift else base_key, code))
_key_event("keyUp", kc, shifted_key if shift else base_key, code, held))
if shift:
cdp_session.send("Input.dispatchKeyEvent",
_key_event("keyUp", 16, "Shift", "ShiftLeft"))
_key_event("keyUp", 16, "Shift", "ShiftLeft", 0))
def human_type(
+9 -8
View File
@@ -64,11 +64,12 @@ async def _type_cjk_phrase(
async def _type_cjk_punct(ch: str, cfg: HumanConfig, cdp_session: Any) -> None:
"""Async mirror of keyboard._type_cjk_punct — full-width punctuation via the IME."""
shift, code, kc, base_key, shifted_key = _CJK_PUNCT[ch]
held = 8 if shift else 0 # Shift modifier flag while Shift is down
if shift:
await cdp_session.send("Input.dispatchKeyEvent",
_key_event("keyDown", 16, "Shift", "ShiftLeft"))
_key_event("keyDown", 16, "Shift", "ShiftLeft", 8))
await async_sleep_ms(rand_range(cfg.shift_down_delay))
await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyDown", 229, "Process", code))
await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyDown", 229, "Process", code, held))
await cdp_session.send("Input.imeSetComposition",
{"text": ch, "selectionStart": len(ch), "selectionEnd": len(ch)})
await async_sleep_ms(rand_range(cfg.key_hold))
@@ -76,16 +77,16 @@ async def _type_cjk_punct(ch: str, cfg: HumanConfig, cdp_session: Any) -> None:
release_shift_first = shift and random.random() < 0.5
if release_shift_first:
await cdp_session.send("Input.dispatchKeyEvent",
_key_event("keyUp", 16, "Shift", "ShiftLeft"))
await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code))
await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", kc, base_key, code))
_key_event("keyUp", 16, "Shift", "ShiftLeft", 0))
await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code, 0))
await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", kc, base_key, code, 0))
else:
await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code))
await cdp_session.send("Input.dispatchKeyEvent", _key_event("keyUp", 229, "Process", code, held))
await cdp_session.send("Input.dispatchKeyEvent",
_key_event("keyUp", kc, shifted_key if shift else base_key, code))
_key_event("keyUp", kc, shifted_key if shift else base_key, code, held))
if shift:
await cdp_session.send("Input.dispatchKeyEvent",
_key_event("keyUp", 16, "Shift", "ShiftLeft"))
_key_event("keyUp", 16, "Shift", "ShiftLeft", 0))
async def async_human_type(
+9 -8
View File
@@ -20,23 +20,24 @@ import { LETTER_CODES, buildPhrases, composeDisplay, letterKeycode, CJK_PUNCT }
* Playwright keyboard.ts typeCjkPunct for the model. */
async function typeCjkPunct(ch: string, cfg: HumanConfig, cdpSession: CDPSession): Promise<void> {
const [shift, code, kc, baseKey, shiftedKey] = CJK_PUNCT[ch];
const held = shift ? 8 : 0; // Shift modifier while Shift is held → shiftKey=true
if (shift) {
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', modifiers: 8, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await sleep(randRange(cfg.shift_down_delay));
}
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', modifiers: held, windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.imeSetComposition', { text: ch, selectionStart: ch.length, selectionEnd: ch.length });
await sleep(randRange(cfg.key_hold));
await cdpSession.send('Input.insertText', { text: ch });
const releaseShiftFirst = shift && Math.random() < 0.5;
if (releaseShiftFirst) {
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: kc, key: baseKey, code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: kc, key: baseKey, code });
} else {
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: kc, key: shift ? shiftedKey : baseKey, code });
if (shift) await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: held, windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: held, windowsVirtualKeyCode: kc, key: shift ? shiftedKey : baseKey, code });
if (shift) await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
}
}
+11 -8
View File
@@ -241,23 +241,26 @@ export async function typeCjkPunct(
cdpSession: CDPSession,
): Promise<void> {
const [shift, code, kc, baseKey, shiftedKey] = CJK_PUNCT[ch];
// modifiers=8 (Shift) on every event dispatched while Shift is held, so the DOM
// reports shiftKey=true, matching a real capture; 0 at/after the Shift keyup.
const held = shift ? 8 : 0;
if (shift) {
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', modifiers: 8, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await sleep(randRange(cfg.shift_down_delay));
}
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyDown', modifiers: held, windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.imeSetComposition', { text: ch, selectionStart: ch.length, selectionEnd: ch.length });
await sleep(randRange(cfg.key_hold));
await cdpSession.send('Input.insertText', { text: ch });
const releaseShiftFirst = shift && Math.random() < 0.5;
if (releaseShiftFirst) {
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: kc, key: baseKey, code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: kc, key: baseKey, code });
} else {
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: kc, key: shift ? shiftedKey : baseKey, code });
if (shift) await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: held, windowsVirtualKeyCode: 229, key: 'Process', code });
await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: held, windowsVirtualKeyCode: kc, key: shift ? shiftedKey : baseKey, code });
if (shift) await cdpSession.send('Input.dispatchKeyEvent', { type: 'keyUp', modifiers: 0, windowsVirtualKeyCode: 16, key: 'Shift', code: 'ShiftLeft' });
}
}
+16
View File
@@ -1864,6 +1864,22 @@ describe("CJK pinyin-IME humanize (ime_language='zh')", () => {
expect(kd).toContainEqual([229, "Slash"]);
expect(calls).toContainEqual(["Input.insertText", { text: "?" }]);
expect(raw.insertText).not.toHaveBeenCalled();
// Shift modifier: events while Shift is held carry modifiers=8, then 0 after keyup.
const evs = calls.filter(([m]) => m === "Input.dispatchKeyEvent").map(([, p]) => p);
const shiftUp = evs.findIndex(p => p.type === "keyUp" && p.code === "ShiftLeft");
evs.forEach((p, i) => {
expect(p.modifiers).toBe(i < shiftUp ? 8 : 0);
});
});
it("non-shift punctuation (,) carries no Shift modifier", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { ime_language: "zh", mistype_chance: 0 });
const raw = mockRaw();
const { cdp, calls } = mockCdp();
await humanType({} as any, raw as any, ",", cfg, cdp);
const evs = calls.filter(([m]) => m === "Input.dispatchKeyEvent").map(([, p]) => p);
evs.forEach(p => expect(p.modifiers ?? 0).toBe(0));
});
it("punctuation falls back to insertText when disabled", async () => {
+19
View File
@@ -1706,6 +1706,25 @@ class TestCJKPunctuation:
seen_last = True
assert seen_first and seen_last # both orderings occur
def test_shift_modifier_tracks_shift_state(self):
# Every event while Shift is held carries modifiers=8 (shiftKey=true); the
# Shift keyup and everything after carry 0. Holds for both release orders.
for _ in range(30):
_, calls = self._run("!")
evs = [p for m, p in calls if m == "Input.dispatchKeyEvent"]
shift_up = next(i for i, p in enumerate(evs)
if p["type"] == "keyUp" and p["code"] == "ShiftLeft")
for i, p in enumerate(evs):
if i < shift_up:
assert p.get("modifiers") == 8, f"{p} should be shiftKey=true"
else:
assert p.get("modifiers", 0) == 0, f"{p} should be shiftKey=false"
def test_non_shift_punct_has_no_modifier(self):
_, calls = self._run(",")
evs = [p for m, p in calls if m == "Input.dispatchKeyEvent"]
assert all("modifiers" not in p for p in evs)
def test_disabled_and_no_cdp_fall_back(self):
raw, calls = self._run(",", ime=None)
raw.insert_text.assert_called_once_with(",")