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:
|
def human_type(page: Any, raw: RawKeyboard, text: str, cfg: HumanConfig) -> None:
|
||||||
for i, ch in enumerate(text):
|
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():
|
if random.random() < cfg.mistype_chance and ch.isalnum():
|
||||||
wrong = _get_nearby_key(ch)
|
wrong = _get_nearby_key(ch)
|
||||||
_type_normal_char(raw, wrong, cfg)
|
_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:
|
async def async_human_type(page: Any, raw: AsyncRawKeyboard, text: str, cfg: HumanConfig) -> None:
|
||||||
for i, ch in enumerate(text):
|
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():
|
if random.random() < cfg.mistype_chance and ch.isalnum():
|
||||||
wrong = _get_nearby_key(ch)
|
wrong = _get_nearby_key(ch)
|
||||||
await _type_normal_char(raw, wrong, cfg)
|
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',
|
'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 {
|
function getNearbyKey(ch: string): string {
|
||||||
const lower = ch.toLowerCase();
|
const lower = ch.toLowerCase();
|
||||||
if (lower in NEARBY_KEYS) {
|
if (lower in NEARBY_KEYS) {
|
||||||
@@ -38,11 +43,23 @@ export async function humanType(
|
|||||||
text: string,
|
text: string,
|
||||||
cfg: HumanConfig,
|
cfg: HumanConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (let i = 0; i < text.length; i++) {
|
const chars = [...text]; // Handle emoji surrogate pairs correctly
|
||||||
const ch = text[i];
|
|
||||||
|
|
||||||
// Mistype chance — press wrong key, notice, backspace, then correct
|
for (let i = 0; i < chars.length; i++) {
|
||||||
if (Math.random() < cfg.mistype_chance && /[a-zA-Z0-9]/.test(ch)) {
|
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);
|
const wrong = getNearbyKey(ch);
|
||||||
await typeNormalChar(raw, wrong, cfg);
|
await typeNormalChar(raw, wrong, cfg);
|
||||||
await sleep(randRange(cfg.mistype_delay_notice));
|
await sleep(randRange(cfg.mistype_delay_notice));
|
||||||
@@ -60,7 +77,7 @@ export async function humanType(
|
|||||||
await typeNormalChar(raw, ch, cfg);
|
await typeNormalChar(raw, ch, cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i < text.length - 1) {
|
if (i < chars.length - 1) {
|
||||||
await interCharDelay(cfg);
|
await interCharDelay(cfg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -530,6 +530,81 @@ function buildMockPage(overrides: Record<string, any> = {}): any {
|
|||||||
return page;
|
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 {
|
function buildMockFrame(): any {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -406,6 +406,127 @@ class TestSelectAllPlatform:
|
|||||||
assert _SELECT_ALL == "Control+a"
|
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)
|
# SLOW TESTS — require browser (skipped in CI unless pytest -m slow)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user