mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
fix: support non-ASCII characters (Cyrillic, CJK, emoji) in humanized typing
This commit is contained in:
@@ -41,7 +41,15 @@ def _get_nearby_key(ch: str) -> str:
|
||||
|
||||
def human_type(page: Any, raw: RawKeyboard, text: str, cfg: HumanConfig) -> None:
|
||||
for i, ch in enumerate(text):
|
||||
# Mistype chance — press wrong key, notice, backspace, then correct
|
||||
# Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
|
||||
if not ch.isascii():
|
||||
sleep_ms(rand_range(cfg.key_hold))
|
||||
raw.insert_text(ch)
|
||||
if i < len(text) - 1:
|
||||
_inter_char_delay(cfg)
|
||||
continue
|
||||
|
||||
# Mistype chance — only for ASCII alphanumeric
|
||||
if random.random() < cfg.mistype_chance and ch.isalnum():
|
||||
wrong = _get_nearby_key(ch)
|
||||
_type_normal_char(raw, wrong, cfg)
|
||||
|
||||
@@ -22,7 +22,15 @@ class AsyncRawKeyboard(Protocol):
|
||||
|
||||
async def async_human_type(page: Any, raw: AsyncRawKeyboard, text: str, cfg: HumanConfig) -> None:
|
||||
for i, ch in enumerate(text):
|
||||
# Mistype chance — press wrong key, notice, backspace, then correct
|
||||
# Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
|
||||
if not ch.isascii():
|
||||
await async_sleep_ms(rand_range(cfg.key_hold))
|
||||
await raw.insert_text(ch)
|
||||
if i < len(text) - 1:
|
||||
await _inter_char_delay(cfg)
|
||||
continue
|
||||
|
||||
# Mistype chance — only for ASCII alphanumeric
|
||||
if random.random() < cfg.mistype_chance and ch.isalnum():
|
||||
wrong = _get_nearby_key(ch)
|
||||
await _type_normal_char(raw, wrong, cfg)
|
||||
|
||||
@@ -22,6 +22,11 @@ const NEARBY_KEYS: Record<string, string> = {
|
||||
'6': '57ty', '7': '68yu', '8': '79ui', '9': '80io', '0': '9p',
|
||||
};
|
||||
|
||||
function isAscii(ch: string): boolean {
|
||||
const code = ch.codePointAt(0);
|
||||
return code !== undefined && code < 128;
|
||||
}
|
||||
|
||||
function getNearbyKey(ch: string): string {
|
||||
const lower = ch.toLowerCase();
|
||||
if (lower in NEARBY_KEYS) {
|
||||
@@ -38,11 +43,23 @@ export async function humanType(
|
||||
text: string,
|
||||
cfg: HumanConfig,
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
const chars = [...text]; // Handle emoji surrogate pairs correctly
|
||||
|
||||
// Mistype chance — press wrong key, notice, backspace, then correct
|
||||
if (Math.random() < cfg.mistype_chance && /[a-zA-Z0-9]/.test(ch)) {
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const ch = chars[i];
|
||||
|
||||
// Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
|
||||
if (!isAscii(ch)) {
|
||||
await sleep(randRange(cfg.key_hold));
|
||||
await raw.insertText(ch);
|
||||
if (i < chars.length - 1) {
|
||||
await interCharDelay(cfg);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mistype chance — only for ASCII alphanumeric
|
||||
if (Math.random() < cfg.mistype_chance && /^[a-zA-Z0-9]$/.test(ch)) {
|
||||
const wrong = getNearbyKey(ch);
|
||||
await typeNormalChar(raw, wrong, cfg);
|
||||
await sleep(randRange(cfg.mistype_delay_notice));
|
||||
@@ -60,7 +77,7 @@ export async function humanType(
|
||||
await typeNormalChar(raw, ch, cfg);
|
||||
}
|
||||
|
||||
if (i < text.length - 1) {
|
||||
if (i < chars.length - 1) {
|
||||
await interCharDelay(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +530,81 @@ function buildMockPage(overrides: Record<string, any> = {}): any {
|
||||
return page;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// humanType non-ASCII
|
||||
// =========================================================================
|
||||
describe("humanType non-ASCII", () => {
|
||||
function makeRawKeyboardMock() {
|
||||
const downKeys: string[] = [];
|
||||
const insertedChars: string[] = [];
|
||||
const raw = {
|
||||
down: vi.fn(async (k: string) => { downKeys.push(k); }),
|
||||
up: vi.fn(async () => {}),
|
||||
type: vi.fn(async () => {}),
|
||||
insertText: vi.fn(async (t: string) => { insertedChars.push(t); }),
|
||||
};
|
||||
return { raw, downKeys, insertedChars };
|
||||
}
|
||||
|
||||
it("types Cyrillic via insertText, not down", async () => {
|
||||
const { humanType } = await import("../src/human/keyboard.js");
|
||||
const cfg = resolveConfig("default", { mistype_chance: 0 });
|
||||
const { raw, downKeys, insertedChars } = makeRawKeyboardMock();
|
||||
|
||||
await humanType({} as any, raw, "Привет", cfg);
|
||||
|
||||
expect(insertedChars.join("")).toBe("Привет");
|
||||
for (const k of downKeys) {
|
||||
expect(k.charCodeAt(0)).toBeLessThan(128);
|
||||
}
|
||||
});
|
||||
|
||||
it("types mixed ASCII + Cyrillic correctly", async () => {
|
||||
const { humanType } = await import("../src/human/keyboard.js");
|
||||
const cfg = resolveConfig("default", { mistype_chance: 0 });
|
||||
const { raw, downKeys, insertedChars } = makeRawKeyboardMock();
|
||||
|
||||
await humanType({} as any, raw, "Hi Мир", cfg);
|
||||
|
||||
expect(downKeys).toContain("H");
|
||||
expect(downKeys).toContain("i");
|
||||
expect(insertedChars.join("")).toContain("М");
|
||||
expect(insertedChars.join("")).toContain("и");
|
||||
expect(insertedChars.join("")).toContain("р");
|
||||
});
|
||||
|
||||
it("types CJK via insertText", async () => {
|
||||
const { humanType } = await import("../src/human/keyboard.js");
|
||||
const cfg = resolveConfig("default", { mistype_chance: 0 });
|
||||
const { raw, insertedChars } = makeRawKeyboardMock();
|
||||
|
||||
await humanType({} as any, raw, "你好", cfg);
|
||||
|
||||
expect(insertedChars.join("")).toBe("你好");
|
||||
});
|
||||
|
||||
it("types emoji via insertText", async () => {
|
||||
const { humanType } = await import("../src/human/keyboard.js");
|
||||
const cfg = resolveConfig("default", { mistype_chance: 0 });
|
||||
const { raw, insertedChars } = makeRawKeyboardMock();
|
||||
|
||||
await humanType({} as any, raw, "Hi 👋", cfg);
|
||||
|
||||
expect(insertedChars.join("")).toContain("👋");
|
||||
});
|
||||
|
||||
it("mistype only triggers for ASCII, not Cyrillic", async () => {
|
||||
const { humanType } = await import("../src/human/keyboard.js");
|
||||
const cfg = resolveConfig("default", { mistype_chance: 1.0 });
|
||||
const { raw, downKeys } = makeRawKeyboardMock();
|
||||
|
||||
await humanType({} as any, raw, "AБ", cfg);
|
||||
|
||||
expect(downKeys).toContain("Backspace");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
function buildMockFrame(): any {
|
||||
return {
|
||||
|
||||
@@ -406,6 +406,127 @@ class TestSelectAllPlatform:
|
||||
assert _SELECT_ALL == "Control+a"
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# 11. Non-ASCII keyboard input
|
||||
# =========================================================================
|
||||
|
||||
class TestNonAsciiKeyboard:
|
||||
def test_cyrillic_uses_insert_text(self):
|
||||
from cloakbrowser.human.keyboard import human_type
|
||||
from cloakbrowser.human.config import resolve_config
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
cfg = resolve_config("default", {"mistype_chance": 0})
|
||||
page = MagicMock()
|
||||
raw = MagicMock()
|
||||
|
||||
down_keys = []
|
||||
inserted = []
|
||||
raw.down = MagicMock(side_effect=lambda k: down_keys.append(k))
|
||||
raw.up = MagicMock()
|
||||
raw.insert_text = MagicMock(side_effect=lambda t: inserted.append(t))
|
||||
|
||||
human_type(page, raw, "Привет", cfg)
|
||||
|
||||
assert "".join(inserted) == "Привет"
|
||||
for k in down_keys:
|
||||
assert ord(k[0]) < 128 or k in ("Shift", "Backspace")
|
||||
|
||||
def test_mixed_ascii_cyrillic(self):
|
||||
from cloakbrowser.human.keyboard import human_type
|
||||
from cloakbrowser.human.config import resolve_config
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
cfg = resolve_config("default", {"mistype_chance": 0})
|
||||
page = MagicMock()
|
||||
raw = MagicMock()
|
||||
|
||||
down_keys = []
|
||||
inserted = []
|
||||
raw.down = MagicMock(side_effect=lambda k: down_keys.append(k))
|
||||
raw.up = MagicMock()
|
||||
raw.insert_text = MagicMock(side_effect=lambda t: inserted.append(t))
|
||||
|
||||
human_type(page, raw, "Hi Мир", cfg)
|
||||
|
||||
assert "H" in down_keys
|
||||
assert "i" in down_keys
|
||||
assert "М" in "".join(inserted)
|
||||
|
||||
def test_cjk_uses_insert_text(self):
|
||||
from cloakbrowser.human.keyboard import human_type
|
||||
from cloakbrowser.human.config import resolve_config
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
cfg = resolve_config("default", {"mistype_chance": 0})
|
||||
page = MagicMock()
|
||||
raw = MagicMock()
|
||||
|
||||
inserted = []
|
||||
raw.down = MagicMock()
|
||||
raw.up = MagicMock()
|
||||
raw.insert_text = MagicMock(side_effect=lambda t: inserted.append(t))
|
||||
|
||||
human_type(page, raw, "你好", cfg)
|
||||
|
||||
assert "".join(inserted) == "你好"
|
||||
|
||||
def test_mistype_only_ascii(self):
|
||||
from cloakbrowser.human.keyboard import human_type
|
||||
from cloakbrowser.human.config import resolve_config
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
cfg = resolve_config("default", {"mistype_chance": 1.0})
|
||||
page = MagicMock()
|
||||
raw = MagicMock()
|
||||
|
||||
down_keys = []
|
||||
raw.down = MagicMock(side_effect=lambda k: down_keys.append(k))
|
||||
raw.up = MagicMock()
|
||||
raw.insert_text = MagicMock()
|
||||
|
||||
human_type(page, raw, "AБ", cfg)
|
||||
|
||||
assert "Backspace" in down_keys
|
||||
|
||||
def test_no_error_on_cyrillic(self):
|
||||
from cloakbrowser.human.keyboard import human_type
|
||||
from cloakbrowser.human.config import resolve_config
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
cfg = resolve_config("default", {"mistype_chance": 0})
|
||||
page = MagicMock()
|
||||
raw = MagicMock()
|
||||
raw.down = MagicMock()
|
||||
raw.up = MagicMock()
|
||||
raw.insert_text = MagicMock()
|
||||
|
||||
# Should not raise
|
||||
human_type(page, raw, "Тест кириллицы", cfg)
|
||||
|
||||
|
||||
class TestNonAsciiKeyboardAsync:
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_cyrillic_uses_insert_text(self):
|
||||
from cloakbrowser.human.keyboard_async import async_human_type
|
||||
from cloakbrowser.human.config import resolve_config
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
|
||||
cfg = resolve_config("default", {"mistype_chance": 0})
|
||||
page = MagicMock()
|
||||
raw = MagicMock()
|
||||
|
||||
inserted = []
|
||||
raw.down = AsyncMock()
|
||||
raw.up = AsyncMock()
|
||||
raw.insert_text = AsyncMock(side_effect=lambda t: inserted.append(t))
|
||||
|
||||
await async_human_type(page, raw, "Привет", cfg)
|
||||
|
||||
assert "".join(inserted) == "Привет"
|
||||
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# SLOW TESTS — require browser (skipped in CI unless pytest -m slow)
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user