mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-10-22 08:32:17 -05:00
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:
6 files changed
+83
-35
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(",")
|
||||
|
||||
Reference in new issue
Block a user