Files
CloakBrowser/cloakbrowser/human/__init__.py
T
lilos 7bf8836683 feat: add human-like behavioral layer (humanize option)
Bezier mouse curves, per-character typing with mistype simulation,
smooth micro-step scrolling, idle micro-movements between actions.

Supports both sync and async Playwright APIs. Patches page, frame,
context, browser, and Locator class methods.

Two presets: 'default' (normal speed) and 'careful' (slower, deliberate).
Configurable via HumanConfig dataclass / interface with full override support.

Bug fixes (from PR review):
- fill()/clear(): platform-aware select-all (Meta+a on macOS, Control+a elsewhere)
- sync Locator check()/uncheck(): wrap mouse_move in RawMouse-compatible object
- resolve_config(): raise error on unknown preset name
- Lazy-load human.config via __getattr__ in __init__.py
- humanPreset typed as 'default' | 'careful' literal union
- browser.newPage() patches implicit context

Tests: Python 36/36, JS Vitest 34/34, visual Python 17/17, JS 13/13
2026-03-08 12:49:42 +03:00

1159 lines
42 KiB
Python

"""Human-like behavioral layer for cloakbrowser.
Activated via humanize=True in launch() / launch_async().
Patches page methods to use Bezier mouse curves, realistic typing, and smooth scrolling.
Supports both sync and async Playwright APIs.
"""
from __future__ import annotations
import logging
import sys
from typing import Any
from .config import HumanConfig, HumanPreset, resolve_config
from .config import rand, rand_range, sleep_ms, async_sleep_ms
from .mouse import RawMouse, human_move, human_click, click_target, human_idle
from .keyboard import RawKeyboard, human_type
from .scroll import scroll_to_element
from .mouse_async import AsyncRawMouse, async_human_move, async_human_click, async_human_idle
from .keyboard_async import AsyncRawKeyboard, async_human_type
from .scroll_async import async_scroll_to_element
_SELECT_ALL = "Meta+a" if sys.platform == "darwin" else "Control+a"
__all__ = [
"patch_browser", "patch_context", "patch_page",
"patch_browser_async", "patch_context_async", "patch_page_async",
"HumanConfig", "resolve_config",
"human_move", "human_click", "click_target", "human_idle",
"human_type", "scroll_to_element",
]
logger = logging.getLogger("cloakbrowser.human")
class _CursorState:
__slots__ = ("x", "y", "initialized")
def __init__(self) -> None:
self.x: float = 0
self.y: float = 0
self.initialized: bool = False
def _is_input_element(page: Any, selector: str) -> bool:
try:
return page.evaluate(
"""(sel) => {
const el = document.querySelector(sel);
if (!el) return false;
const tag = el.tagName.toLowerCase();
return tag === 'input' || tag === 'textarea'
|| el.getAttribute('contenteditable') === 'true';
}""",
selector,
)
except Exception:
return False
async def _async_is_input_element(page: Any, selector: str) -> bool:
try:
return await page.evaluate(
"""(sel) => {
const el = document.querySelector(sel);
if (!el) return false;
const tag = el.tagName.toLowerCase();
return tag === 'input' || tag === 'textarea'
|| el.getAttribute('contenteditable') === 'true';
}""",
selector,
)
except Exception:
return False
def _is_selector_focused(page: Any, selector: str) -> bool:
"""Check if the element matching selector is currently focused."""
try:
return page.evaluate(
"""(sel) => {
const el = document.querySelector(sel);
return el === document.activeElement;
}""",
selector,
)
except Exception:
return False
async def _async_is_selector_focused(page: Any, selector: str) -> bool:
"""Check if the element matching selector is currently focused (async)."""
try:
return await page.evaluate(
"""(sel) => {
const el = document.querySelector(sel);
return el === document.activeElement;
}""",
selector,
)
except Exception:
return False
# ============================================================================
# Locator class-level patching (sync)
# ============================================================================
_locator_sync_patched = False
def _patch_locator_class_sync():
"""Patch all Locator interaction methods to go through humanized page methods."""
global _locator_sync_patched
if _locator_sync_patched:
return
_locator_sync_patched = True
from playwright.sync_api._generated import Locator
_orig_fill = Locator.fill
_orig_click = Locator.click
_orig_type = Locator.type
_orig_dblclick = Locator.dblclick
_orig_hover = Locator.hover
_orig_check = Locator.check
_orig_uncheck = Locator.uncheck
_orig_set_checked = Locator.set_checked
_orig_select_option = Locator.select_option
_orig_press = Locator.press
_orig_press_sequentially = Locator.press_sequentially
_orig_tap = Locator.tap
_orig_drag_to = Locator.drag_to
_orig_clear = Locator.clear
def _get_selector(self):
return self._impl_obj._selector
def _is_humanized(self):
return hasattr(self.page, '_original')
def _get_cfg(self):
return getattr(self.page, '_human_cfg', None)
def _humanized_fill(self, value, **kwargs):
if _is_humanized(self):
self.page.fill(_get_selector(self), value)
else:
_orig_fill(self, value, **kwargs)
def _humanized_click(self, **kwargs):
if _is_humanized(self):
self.page.click(_get_selector(self))
else:
_orig_click(self, **kwargs)
def _humanized_type(self, text, **kwargs):
if _is_humanized(self):
self.page.type(_get_selector(self), text)
else:
_orig_type(self, text, **kwargs)
def _humanized_dblclick(self, **kwargs):
if _is_humanized(self):
self.page.dblclick(_get_selector(self))
else:
_orig_dblclick(self, **kwargs)
def _humanized_hover(self, **kwargs):
if _is_humanized(self):
self.page.hover(_get_selector(self))
else:
_orig_hover(self, **kwargs)
def _humanized_check(self, **kwargs):
if _is_humanized(self):
cfg = _get_cfg(self)
if cfg and cfg.idle_between_actions:
raw = type("_R", (), {"move": self.page._original.mouse_move})()
human_idle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), 0, 0, cfg)
checked = self.is_checked()
if not checked:
self.page.click(_get_selector(self))
else:
_orig_check(self, **kwargs)
def _humanized_uncheck(self, **kwargs):
if _is_humanized(self):
cfg = _get_cfg(self)
if cfg and cfg.idle_between_actions:
raw = type("_R", (), {"move": self.page._original.mouse_move})()
human_idle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), 0, 0, cfg)
checked = self.is_checked()
if checked:
self.page.click(_get_selector(self))
else:
_orig_uncheck(self, **kwargs)
def _humanized_set_checked(self, checked, **kwargs):
if _is_humanized(self):
current = self.is_checked()
if current != checked:
self.page.click(_get_selector(self))
else:
_orig_set_checked(self, checked, **kwargs)
def _humanized_select_option(self, value=None, **kwargs):
if _is_humanized(self):
selector = _get_selector(self)
self.page.hover(selector)
sleep_ms(rand(100, 300))
_orig_select_option(self, value, **kwargs)
else:
_orig_select_option(self, value, **kwargs)
def _humanized_press(self, key, **kwargs):
if _is_humanized(self):
selector = _get_selector(self)
# Only click if not already focused — avoids redundant mouse moves
if not _is_selector_focused(self.page, selector):
self.page.click(selector)
sleep_ms(rand(50, 150))
self.page.keyboard.press(key)
else:
_orig_press(self, key, **kwargs)
def _humanized_press_sequentially(self, text, **kwargs):
if _is_humanized(self):
selector = _get_selector(self)
if not _is_selector_focused(self.page, selector):
self.page.click(selector)
sleep_ms(rand(50, 150))
self.page.keyboard.type(text)
else:
_orig_press_sequentially(self, text, **kwargs)
def _humanized_tap(self, **kwargs):
if _is_humanized(self):
self.page.click(_get_selector(self))
else:
_orig_tap(self, **kwargs)
def _humanized_drag_to(self, target, **kwargs):
if _is_humanized(self):
page = self.page
originals = getattr(page, '_original', None)
src_box = self.bounding_box()
tgt_box = target.bounding_box()
if src_box and tgt_box and originals:
sx = src_box['x'] + src_box['width'] / 2
sy = src_box['y'] + src_box['height'] / 2
tx = tgt_box['x'] + tgt_box['width'] / 2
ty = tgt_box['y'] + tgt_box['height'] / 2
page.mouse.move(sx, sy)
sleep_ms(rand(100, 200))
originals.mouse_down()
sleep_ms(rand(80, 150))
page.mouse.move(tx, ty)
sleep_ms(rand(80, 150))
originals.mouse_up()
else:
_orig_drag_to(self, target, **kwargs)
else:
_orig_drag_to(self, target, **kwargs)
def _humanized_clear(self, **kwargs):
if _is_humanized(self):
selector = _get_selector(self)
if not _is_selector_focused(self.page, selector):
self.page.click(selector)
sleep_ms(rand(50, 100))
self.page.keyboard.press(_SELECT_ALL)
sleep_ms(rand(30, 80))
self.page.keyboard.press("Backspace")
else:
_orig_clear(self, **kwargs)
Locator.fill = _humanized_fill
Locator.click = _humanized_click
Locator.type = _humanized_type
Locator.dblclick = _humanized_dblclick
Locator.hover = _humanized_hover
Locator.check = _humanized_check
Locator.uncheck = _humanized_uncheck
Locator.set_checked = _humanized_set_checked
Locator.select_option = _humanized_select_option
Locator.press = _humanized_press
Locator.press_sequentially = _humanized_press_sequentially
Locator.tap = _humanized_tap
Locator.drag_to = _humanized_drag_to
Locator.clear = _humanized_clear
# ============================================================================
# Locator class-level patching (async)
# ============================================================================
_locator_async_patched = False
def _patch_locator_class_async():
"""Patch all async Locator interaction methods to go through humanized page methods."""
global _locator_async_patched
if _locator_async_patched:
return
_locator_async_patched = True
from playwright.async_api._generated import Locator as AsyncLocator
_orig_fill = AsyncLocator.fill
_orig_click = AsyncLocator.click
_orig_type = AsyncLocator.type
_orig_dblclick = AsyncLocator.dblclick
_orig_hover = AsyncLocator.hover
_orig_check = AsyncLocator.check
_orig_uncheck = AsyncLocator.uncheck
_orig_set_checked = AsyncLocator.set_checked
_orig_select_option = AsyncLocator.select_option
_orig_press = AsyncLocator.press
_orig_press_sequentially = AsyncLocator.press_sequentially
_orig_tap = AsyncLocator.tap
_orig_drag_to = AsyncLocator.drag_to
_orig_clear = AsyncLocator.clear
def _get_selector(self):
return self._impl_obj._selector
def _is_humanized(self):
return hasattr(self.page, '_original')
def _get_cfg(self):
return getattr(self.page, '_human_cfg', None)
async def _humanized_fill(self, value, **kwargs):
if _is_humanized(self):
await self.page.fill(_get_selector(self), value)
else:
await _orig_fill(self, value, **kwargs)
async def _humanized_click(self, **kwargs):
if _is_humanized(self):
await self.page.click(_get_selector(self))
else:
await _orig_click(self, **kwargs)
async def _humanized_type(self, text, **kwargs):
if _is_humanized(self):
await self.page.type(_get_selector(self), text)
else:
await _orig_type(self, text, **kwargs)
async def _humanized_dblclick(self, **kwargs):
if _is_humanized(self):
await self.page.dblclick(_get_selector(self))
else:
await _orig_dblclick(self, **kwargs)
async def _humanized_hover(self, **kwargs):
if _is_humanized(self):
await self.page.hover(_get_selector(self))
else:
await _orig_hover(self, **kwargs)
async def _humanized_check(self, **kwargs):
if _is_humanized(self):
cfg = _get_cfg(self)
if cfg and cfg.idle_between_actions:
raw = type("_R", (), {"move": self.page._original.mouse_move})()
await async_human_idle(
raw,
rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]),
0, 0, cfg,
)
checked = await self.is_checked()
if not checked:
await self.page.click(_get_selector(self))
else:
await _orig_check(self, **kwargs)
async def _humanized_uncheck(self, **kwargs):
if _is_humanized(self):
cfg = _get_cfg(self)
if cfg and cfg.idle_between_actions:
raw = type("_R", (), {"move": self.page._original.mouse_move})()
await async_human_idle(
raw,
rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]),
0, 0, cfg,
)
checked = await self.is_checked()
if checked:
await self.page.click(_get_selector(self))
else:
await _orig_uncheck(self, **kwargs)
async def _humanized_set_checked(self, checked, **kwargs):
if _is_humanized(self):
current = await self.is_checked()
if current != checked:
await self.page.click(_get_selector(self))
else:
await _orig_set_checked(self, checked, **kwargs)
async def _humanized_select_option(self, value=None, **kwargs):
if _is_humanized(self):
selector = _get_selector(self)
await self.page.hover(selector)
await async_sleep_ms(rand(100, 300))
await _orig_select_option(self, value, **kwargs)
else:
await _orig_select_option(self, value, **kwargs)
async def _humanized_press(self, key, **kwargs):
if _is_humanized(self):
selector = _get_selector(self)
if not await _async_is_selector_focused(self.page, selector):
await self.page.click(selector)
await async_sleep_ms(rand(50, 150))
await self.page.keyboard.press(key)
else:
await _orig_press(self, key, **kwargs)
async def _humanized_press_sequentially(self, text, **kwargs):
if _is_humanized(self):
selector = _get_selector(self)
if not await _async_is_selector_focused(self.page, selector):
await self.page.click(selector)
await async_sleep_ms(rand(50, 150))
await self.page.keyboard.type(text)
else:
await _orig_press_sequentially(self, text, **kwargs)
async def _humanized_tap(self, **kwargs):
if _is_humanized(self):
await self.page.click(_get_selector(self))
else:
await _orig_tap(self, **kwargs)
async def _humanized_drag_to(self, target, **kwargs):
if _is_humanized(self):
page = self.page
originals = getattr(page, '_original', None)
src_box = await self.bounding_box()
tgt_box = await target.bounding_box()
if src_box and tgt_box and originals:
sx = src_box['x'] + src_box['width'] / 2
sy = src_box['y'] + src_box['height'] / 2
tx = tgt_box['x'] + tgt_box['width'] / 2
ty = tgt_box['y'] + tgt_box['height'] / 2
await page.mouse.move(sx, sy)
await async_sleep_ms(rand(100, 200))
await originals.mouse_down()
await async_sleep_ms(rand(80, 150))
await page.mouse.move(tx, ty)
await async_sleep_ms(rand(80, 150))
await originals.mouse_up()
else:
await _orig_drag_to(self, target, **kwargs)
else:
await _orig_drag_to(self, target, **kwargs)
async def _humanized_clear(self, **kwargs):
if _is_humanized(self):
selector = _get_selector(self)
if not await _async_is_selector_focused(self.page, selector):
await self.page.click(selector)
await async_sleep_ms(rand(50, 100))
await self.page.keyboard.press(_SELECT_ALL)
await async_sleep_ms(rand(30, 80))
await self.page.keyboard.press("Backspace")
else:
await _orig_clear(self, **kwargs)
AsyncLocator.fill = _humanized_fill
AsyncLocator.click = _humanized_click
AsyncLocator.type = _humanized_type
AsyncLocator.dblclick = _humanized_dblclick
AsyncLocator.hover = _humanized_hover
AsyncLocator.check = _humanized_check
AsyncLocator.uncheck = _humanized_uncheck
AsyncLocator.set_checked = _humanized_set_checked
AsyncLocator.select_option = _humanized_select_option
AsyncLocator.press = _humanized_press
AsyncLocator.press_sequentially = _humanized_press_sequentially
AsyncLocator.tap = _humanized_tap
AsyncLocator.drag_to = _humanized_drag_to
AsyncLocator.clear = _humanized_clear
# ============================================================================
# SYNC patching
# ============================================================================
def patch_page(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
"""Replace page methods with human-like implementations (sync)."""
originals = type("Originals", (), {
"click": page.click,
"type": page.type,
"fill": page.fill,
"goto": page.goto,
"hover": page.hover,
"dblclick": page.dblclick,
"mouse_move": page.mouse.move,
"mouse_click": page.mouse.click,
"mouse_wheel": page.mouse.wheel,
"mouse_down": page.mouse.down,
"mouse_up": page.mouse.up,
"keyboard_type": page.keyboard.type,
"keyboard_down": page.keyboard.down,
"keyboard_up": page.keyboard.up,
"keyboard_press": page.keyboard.press,
"keyboard_insert_text": page.keyboard.insert_text,
})()
page._original = originals
page._human_cfg = cfg
raw_mouse: RawMouse = type("_RawMouse", (), {
"move": originals.mouse_move,
"down": originals.mouse_down,
"up": originals.mouse_up,
"wheel": originals.mouse_wheel,
})()
raw_keyboard: RawKeyboard = type("_RawKeyboard", (), {
"down": originals.keyboard_down,
"up": originals.keyboard_up,
"type": originals.keyboard_type,
"insert_text": originals.keyboard_insert_text,
})()
def _ensure_cursor_init() -> None:
if not cursor.initialized:
cursor.x = rand(cfg.initial_cursor_x[0], cfg.initial_cursor_x[1])
cursor.y = rand(cfg.initial_cursor_y[0], cfg.initial_cursor_y[1])
originals.mouse_move(cursor.x, cursor.y)
cursor.initialized = True
def _human_goto(url: str, **kwargs: Any) -> Any:
response = originals.goto(url, **kwargs)
return response
def _human_click(selector: str, **kwargs: Any) -> None:
_ensure_cursor_init()
if cfg.idle_between_actions:
human_idle(raw_mouse, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg)
box, cx, cy = scroll_to_element(
page, raw_mouse, selector, cursor.x, cursor.y, cfg
)
cursor.x = cx
cursor.y = cy
is_input = _is_input_element(page, selector)
target = click_target(box, is_input, cfg)
human_move(raw_mouse, cursor.x, cursor.y, target.x, target.y, cfg)
cursor.x = target.x
cursor.y = target.y
human_click(raw_mouse, is_input, cfg)
def _human_dblclick(selector: str, **kwargs: Any) -> None:
_ensure_cursor_init()
if cfg.idle_between_actions:
human_idle(raw_mouse, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg)
box, cx, cy = scroll_to_element(
page, raw_mouse, selector, cursor.x, cursor.y, cfg
)
cursor.x = cx
cursor.y = cy
is_input = _is_input_element(page, selector)
target = click_target(box, is_input, cfg)
human_move(raw_mouse, cursor.x, cursor.y, target.x, target.y, cfg)
cursor.x = target.x
cursor.y = target.y
raw_mouse.down(click_count=2)
sleep_ms(rand(30, 60))
raw_mouse.up(click_count=2)
def _human_hover(selector: str, **kwargs: Any) -> None:
_ensure_cursor_init()
if cfg.idle_between_actions:
human_idle(raw_mouse, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg)
box, cx, cy = scroll_to_element(
page, raw_mouse, selector, cursor.x, cursor.y, cfg
)
cursor.x = cx
cursor.y = cy
target = click_target(box, False, cfg)
human_move(raw_mouse, cursor.x, cursor.y, target.x, target.y, cfg)
cursor.x = target.x
cursor.y = target.y
def _human_type(selector: str, text: str, **kwargs: Any) -> None:
sleep_ms(rand_range(cfg.field_switch_delay))
_human_click(selector)
sleep_ms(rand(100, 250))
human_type(page, raw_keyboard, text, cfg)
def _human_fill(selector: str, value: str, **kwargs: Any) -> None:
sleep_ms(rand_range(cfg.field_switch_delay))
_human_click(selector)
sleep_ms(rand(100, 250))
originals.keyboard_press(_SELECT_ALL)
sleep_ms(rand(30, 80))
originals.keyboard_press("Backspace")
sleep_ms(rand(50, 150))
human_type(page, raw_keyboard, value, cfg)
def _human_check(selector: str, **kwargs: Any) -> None:
try:
checked = page.is_checked(selector)
except Exception:
checked = False
if not checked:
_human_click(selector)
def _human_uncheck(selector: str, **kwargs: Any) -> None:
try:
checked = page.is_checked(selector)
except Exception:
checked = True
if checked:
_human_click(selector)
def _human_select_option(selector: str, value: Any = None, **kwargs: Any) -> Any:
_human_hover(selector)
sleep_ms(rand(100, 300))
return originals.click(selector)
def _human_press(selector: str, key: str, **kwargs: Any) -> None:
if not _is_selector_focused(page, selector):
_human_click(selector)
sleep_ms(rand(50, 150))
originals.keyboard_press(key)
def _human_mouse_move(x: float, y: float, **kwargs: Any) -> None:
_ensure_cursor_init()
human_move(raw_mouse, cursor.x, cursor.y, x, y, cfg)
cursor.x = x
cursor.y = y
def _human_mouse_click(x: float, y: float, **kwargs: Any) -> None:
_ensure_cursor_init()
human_move(raw_mouse, cursor.x, cursor.y, x, y, cfg)
cursor.x = x
cursor.y = y
human_click(raw_mouse, False, cfg)
def _human_keyboard_type(text: str, **kwargs: Any) -> None:
human_type(page, raw_keyboard, text, cfg)
page.goto = _human_goto
page.click = _human_click
page.dblclick = _human_dblclick
page.hover = _human_hover
page.type = _human_type
page.fill = _human_fill
page.check = _human_check
page.uncheck = _human_uncheck
page.press = _human_press
page.mouse.move = _human_mouse_move
page.mouse.click = _human_mouse_click
page.keyboard.type = _human_keyboard_type
# --- Patch Frame-level methods (for sub-frames) ---
_patch_frames_sync(page, cfg, cursor, raw_mouse, raw_keyboard, originals)
# Initialize cursor immediately so it doesn't visibly jump from (0,0)
cursor.x = rand(cfg.initial_cursor_x[0], cfg.initial_cursor_x[1])
cursor.y = rand(cfg.initial_cursor_y[0], cfg.initial_cursor_y[1])
try:
originals.mouse_move(cursor.x, cursor.y)
cursor.initialized = True
except Exception:
pass
# --- Patch Locator class (class-level, runs once) ---
_patch_locator_class_sync()
def _patch_frames_sync(
page: Any, cfg: HumanConfig, cursor: _CursorState,
raw_mouse: RawMouse, raw_keyboard: RawKeyboard, originals: Any,
) -> None:
for frame in _iter_frames(page):
_patch_single_frame_sync(frame, page, cfg, cursor, raw_mouse, raw_keyboard, originals)
orig_main_frame = getattr(page, "_original_main_frame", None)
if orig_main_frame is None:
try:
_orig_goto = originals.goto
def _frame_aware_goto(url: str, **kwargs: Any) -> Any:
response = _orig_goto(url, **kwargs)
for frame in _iter_frames(page):
if not getattr(frame, "_human_patched", False):
_patch_single_frame_sync(frame, page, cfg, cursor, raw_mouse, raw_keyboard, originals)
return response
page.goto = _frame_aware_goto
page._original_main_frame = True
except Exception:
pass
def _patch_single_frame_sync(
frame: Any, page: Any, cfg: HumanConfig, cursor: _CursorState,
raw_mouse: RawMouse, raw_keyboard: RawKeyboard, originals: Any,
) -> None:
if getattr(frame, "_human_patched", False):
return
frame._human_patched = True
# Save originals for methods that need fallback
_orig_frame_select_option = frame.select_option
_orig_frame_drag_and_drop = getattr(frame, 'drag_and_drop', None)
def _frame_click(selector: str, **kwargs: Any) -> None:
page.click(selector)
def _frame_dblclick(selector: str, **kwargs: Any) -> None:
page.dblclick(selector)
def _frame_hover(selector: str, **kwargs: Any) -> None:
page.hover(selector)
def _frame_type(selector: str, text: str, **kwargs: Any) -> None:
page.type(selector, text)
def _frame_fill(selector: str, value: str, **kwargs: Any) -> None:
page.fill(selector, value)
def _frame_check(selector: str, **kwargs: Any) -> None:
page.check(selector)
def _frame_uncheck(selector: str, **kwargs: Any) -> None:
page.uncheck(selector)
def _frame_select_option(selector: str, value: Any = None, **kwargs: Any) -> Any:
page.hover(selector)
sleep_ms(rand(100, 300))
return _orig_frame_select_option(selector, value, **kwargs)
def _frame_press(selector: str, key: str, **kwargs: Any) -> None:
page.press(selector, key)
def _frame_clear(selector: str, **kwargs: Any) -> None:
if not _is_selector_focused(page, selector):
page.click(selector)
sleep_ms(rand(50, 100))
originals.keyboard_press(_SELECT_ALL)
sleep_ms(rand(30, 80))
originals.keyboard_press("Backspace")
def _frame_drag_and_drop(source: str, target: str, **kwargs: Any) -> None:
try:
src_box = frame.locator(source).bounding_box()
tgt_box = frame.locator(target).bounding_box()
except Exception:
src_box = tgt_box = None
if src_box and tgt_box:
sx = src_box['x'] + src_box['width'] / 2
sy = src_box['y'] + src_box['height'] / 2
tx = tgt_box['x'] + tgt_box['width'] / 2
ty = tgt_box['y'] + tgt_box['height'] / 2
page.mouse.move(sx, sy)
sleep_ms(rand(100, 200))
originals.mouse_down()
sleep_ms(rand(80, 150))
page.mouse.move(tx, ty)
sleep_ms(rand(80, 150))
originals.mouse_up()
elif _orig_frame_drag_and_drop:
_orig_frame_drag_and_drop(source, target, **kwargs)
frame.click = _frame_click
frame.dblclick = _frame_dblclick
frame.hover = _frame_hover
frame.type = _frame_type
frame.fill = _frame_fill
frame.check = _frame_check
frame.uncheck = _frame_uncheck
frame.select_option = _frame_select_option
frame.press = _frame_press
frame.clear = _frame_clear
frame.drag_and_drop = _frame_drag_and_drop
def _iter_frames(page: Any):
try:
main = page.main_frame
yield main
for child in main.child_frames:
yield child
except Exception:
pass
def patch_context(context: Any, cfg: HumanConfig) -> None:
cursor = _CursorState()
for page in context.pages:
patch_page(page, cfg, cursor)
context.on("page", lambda p: patch_page(p, cfg, _CursorState()) if not hasattr(p, '_original') else None)
orig_new_page = context.new_page
def _patched_new_page(**kwargs: Any) -> Any:
page = orig_new_page(**kwargs)
if not hasattr(page, '_original'):
patch_page(page, cfg, _CursorState())
return page
context.new_page = _patched_new_page
def patch_browser(browser: Any, cfg: HumanConfig) -> None:
for context in browser.contexts:
patch_context(context, cfg)
orig_new_context = browser.new_context
def _patched_new_context(**kwargs: Any) -> Any:
context = orig_new_context(**kwargs)
patch_context(context, cfg)
return context
browser.new_context = _patched_new_context
orig_new_page = browser.new_page
def _patched_new_page(**kwargs: Any) -> Any:
page = orig_new_page(**kwargs)
if not hasattr(page, '_original'):
patch_page(page, cfg, _CursorState())
return page
browser.new_page = _patched_new_page
# ============================================================================
# ASYNC patching
# ============================================================================
def patch_page_async(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
originals = type("Originals", (), {
"click": page.click,
"type": page.type,
"fill": page.fill,
"goto": page.goto,
"hover": page.hover,
"dblclick": page.dblclick,
"mouse_move": page.mouse.move,
"mouse_click": page.mouse.click,
"mouse_wheel": page.mouse.wheel,
"mouse_down": page.mouse.down,
"mouse_up": page.mouse.up,
"keyboard_type": page.keyboard.type,
"keyboard_down": page.keyboard.down,
"keyboard_up": page.keyboard.up,
"keyboard_press": page.keyboard.press,
"keyboard_insert_text": page.keyboard.insert_text,
})()
page._original = originals
page._human_cfg = cfg
raw_mouse: AsyncRawMouse = type("_AsyncRawMouse", (), {
"move": originals.mouse_move,
"down": originals.mouse_down,
"up": originals.mouse_up,
"wheel": originals.mouse_wheel,
})()
raw_keyboard: AsyncRawKeyboard = type("_AsyncRawKeyboard", (), {
"down": originals.keyboard_down,
"up": originals.keyboard_up,
"type": originals.keyboard_type,
"insert_text": originals.keyboard_insert_text,
})()
async def _ensure_cursor_init() -> None:
if not cursor.initialized:
cursor.x = rand(cfg.initial_cursor_x[0], cfg.initial_cursor_x[1])
cursor.y = rand(cfg.initial_cursor_y[0], cfg.initial_cursor_y[1])
await originals.mouse_move(cursor.x, cursor.y)
cursor.initialized = True
async def _human_goto(url: str, **kwargs: Any) -> Any:
response = await originals.goto(url, **kwargs)
return response
async def _human_click(selector: str, **kwargs: Any) -> None:
await _ensure_cursor_init()
if cfg.idle_between_actions:
await async_human_idle(raw_mouse, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg)
box, cx, cy = await async_scroll_to_element(
page, raw_mouse, selector, cursor.x, cursor.y, cfg
)
cursor.x = cx
cursor.y = cy
is_input = await _async_is_input_element(page, selector)
target = click_target(box, is_input, cfg)
await async_human_move(raw_mouse, cursor.x, cursor.y, target.x, target.y, cfg)
cursor.x = target.x
cursor.y = target.y
await async_human_click(raw_mouse, is_input, cfg)
async def _human_dblclick(selector: str, **kwargs: Any) -> None:
await _ensure_cursor_init()
if cfg.idle_between_actions:
await async_human_idle(raw_mouse, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg)
box, cx, cy = await async_scroll_to_element(
page, raw_mouse, selector, cursor.x, cursor.y, cfg
)
cursor.x = cx
cursor.y = cy
is_input = await _async_is_input_element(page, selector)
target = click_target(box, is_input, cfg)
await async_human_move(raw_mouse, cursor.x, cursor.y, target.x, target.y, cfg)
cursor.x = target.x
cursor.y = target.y
await raw_mouse.down(click_count=2)
await async_sleep_ms(rand(30, 60))
await raw_mouse.up(click_count=2)
async def _human_hover(selector: str, **kwargs: Any) -> None:
await _ensure_cursor_init()
if cfg.idle_between_actions:
await async_human_idle(raw_mouse, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg)
box, cx, cy = await async_scroll_to_element(
page, raw_mouse, selector, cursor.x, cursor.y, cfg
)
cursor.x = cx
cursor.y = cy
target = click_target(box, False, cfg)
await async_human_move(raw_mouse, cursor.x, cursor.y, target.x, target.y, cfg)
cursor.x = target.x
cursor.y = target.y
async def _human_type(selector: str, text: str, **kwargs: Any) -> None:
await async_sleep_ms(rand_range(cfg.field_switch_delay))
await _human_click(selector)
await async_sleep_ms(rand(100, 250))
await async_human_type(page, raw_keyboard, text, cfg)
async def _human_fill(selector: str, value: str, **kwargs: Any) -> None:
await async_sleep_ms(rand_range(cfg.field_switch_delay))
await _human_click(selector)
await async_sleep_ms(rand(100, 250))
await originals.keyboard_press(_SELECT_ALL)
await async_sleep_ms(rand(30, 80))
await originals.keyboard_press("Backspace")
await async_sleep_ms(rand(50, 150))
await async_human_type(page, raw_keyboard, value, cfg)
async def _human_check(selector: str, **kwargs: Any) -> None:
try:
checked = await page.is_checked(selector)
except Exception:
checked = False
if not checked:
await _human_click(selector)
async def _human_uncheck(selector: str, **kwargs: Any) -> None:
try:
checked = await page.is_checked(selector)
except Exception:
checked = True
if checked:
await _human_click(selector)
async def _human_press(selector: str, key: str, **kwargs: Any) -> None:
if not await _async_is_selector_focused(page, selector):
await _human_click(selector)
await async_sleep_ms(rand(50, 150))
await originals.keyboard_press(key)
async def _human_mouse_move(x: float, y: float, **kwargs: Any) -> None:
await _ensure_cursor_init()
await async_human_move(raw_mouse, cursor.x, cursor.y, x, y, cfg)
cursor.x = x
cursor.y = y
async def _human_mouse_click(x: float, y: float, **kwargs: Any) -> None:
await _ensure_cursor_init()
await async_human_move(raw_mouse, cursor.x, cursor.y, x, y, cfg)
cursor.x = x
cursor.y = y
await async_human_click(raw_mouse, False, cfg)
async def _human_keyboard_type(text: str, **kwargs: Any) -> None:
await async_human_type(page, raw_keyboard, text, cfg)
page.goto = _human_goto
page.click = _human_click
page.dblclick = _human_dblclick
page.hover = _human_hover
page.type = _human_type
page.fill = _human_fill
page.check = _human_check
page.uncheck = _human_uncheck
page.press = _human_press
page.mouse.move = _human_mouse_move
page.mouse.click = _human_mouse_click
page.keyboard.type = _human_keyboard_type
# --- Patch Frame-level methods (for sub-frames) ---
_patch_frames_async(page, cfg, cursor, raw_mouse, raw_keyboard, originals)
# --- Patch async Locator class (class-level, runs once) ---
_patch_locator_class_async()
def _patch_frames_async(
page: Any, cfg: HumanConfig, cursor: _CursorState,
raw_mouse: AsyncRawMouse, raw_keyboard: AsyncRawKeyboard, originals: Any,
) -> None:
for frame in _iter_frames(page):
_patch_single_frame_async(frame, page, cfg, cursor, raw_mouse, raw_keyboard, originals)
orig_main_frame = getattr(page, "_original_main_frame", None)
if orig_main_frame is None:
try:
_orig_goto = originals.goto
async def _frame_aware_goto(url: str, **kwargs: Any) -> Any:
response = await _orig_goto(url, **kwargs)
for frame in _iter_frames(page):
if not getattr(frame, "_human_patched", False):
_patch_single_frame_async(frame, page, cfg, cursor, raw_mouse, raw_keyboard, originals)
return response
page.goto = _frame_aware_goto
page._original_main_frame = True
except Exception:
pass
def _patch_single_frame_async(
frame: Any, page: Any, cfg: HumanConfig, cursor: _CursorState,
raw_mouse: AsyncRawMouse, raw_keyboard: AsyncRawKeyboard, originals: Any,
) -> None:
if getattr(frame, "_human_patched", False):
return
frame._human_patched = True
_orig_frame_select_option = frame.select_option
_orig_frame_drag_and_drop = getattr(frame, 'drag_and_drop', None)
async def _frame_click(selector: str, **kwargs: Any) -> None:
await page.click(selector)
async def _frame_dblclick(selector: str, **kwargs: Any) -> None:
await page.dblclick(selector)
async def _frame_hover(selector: str, **kwargs: Any) -> None:
await page.hover(selector)
async def _frame_type(selector: str, text: str, **kwargs: Any) -> None:
await page.type(selector, text)
async def _frame_fill(selector: str, value: str, **kwargs: Any) -> None:
await page.fill(selector, value)
async def _frame_check(selector: str, **kwargs: Any) -> None:
await page.check(selector)
async def _frame_uncheck(selector: str, **kwargs: Any) -> None:
await page.uncheck(selector)
async def _frame_select_option(selector: str, value: Any = None, **kwargs: Any) -> Any:
await page.hover(selector)
await async_sleep_ms(rand(100, 300))
return await _orig_frame_select_option(selector, value, **kwargs)
async def _frame_press(selector: str, key: str, **kwargs: Any) -> None:
await page.press(selector, key)
async def _frame_clear(selector: str, **kwargs: Any) -> None:
if not await _async_is_selector_focused(page, selector):
await page.click(selector)
await async_sleep_ms(rand(50, 100))
await originals.keyboard_press(_SELECT_ALL)
await async_sleep_ms(rand(30, 80))
await originals.keyboard_press("Backspace")
async def _frame_drag_and_drop(source: str, target: str, **kwargs: Any) -> None:
try:
src_box = await frame.locator(source).bounding_box()
tgt_box = await frame.locator(target).bounding_box()
except Exception:
src_box = tgt_box = None
if src_box and tgt_box:
sx = src_box['x'] + src_box['width'] / 2
sy = src_box['y'] + src_box['height'] / 2
tx = tgt_box['x'] + tgt_box['width'] / 2
ty = tgt_box['y'] + tgt_box['height'] / 2
await page.mouse.move(sx, sy)
await async_sleep_ms(rand(100, 200))
await originals.mouse_down()
await async_sleep_ms(rand(80, 150))
await page.mouse.move(tx, ty)
await async_sleep_ms(rand(80, 150))
await originals.mouse_up()
elif _orig_frame_drag_and_drop:
await _orig_frame_drag_and_drop(source, target, **kwargs)
frame.click = _frame_click
frame.dblclick = _frame_dblclick
frame.hover = _frame_hover
frame.type = _frame_type
frame.fill = _frame_fill
frame.check = _frame_check
frame.uncheck = _frame_uncheck
frame.select_option = _frame_select_option
frame.press = _frame_press
frame.clear = _frame_clear
frame.drag_and_drop = _frame_drag_and_drop
def patch_context_async(context: Any, cfg: HumanConfig) -> None:
cursor = _CursorState()
for page in context.pages:
patch_page_async(page, cfg, cursor)
context.on("page", lambda p: patch_page_async(p, cfg, _CursorState()) if not hasattr(p, '_original') else None)
orig_new_page = context.new_page
async def _patched_new_page(**kwargs: Any) -> Any:
page = await orig_new_page(**kwargs)
if not hasattr(page, '_original'):
patch_page_async(page, cfg, _CursorState())
return page
context.new_page = _patched_new_page
def patch_browser_async(browser: Any, cfg: HumanConfig) -> None:
for context in browser.contexts:
patch_context_async(context, cfg)
orig_new_context = browser.new_context
async def _patched_new_context(**kwargs: Any) -> Any:
context = await orig_new_context(**kwargs)
patch_context_async(context, cfg)
return context
browser.new_context = _patched_new_context
orig_new_page = browser.new_page
async def _patched_new_page(**kwargs: Any) -> Any:
page = await orig_new_page(**kwargs)
if not hasattr(page, '_original'):
patch_page_async(page, cfg, _CursorState())
return page
browser.new_page = _patched_new_page