diff --git a/cloakbrowser/human/__init__.py b/cloakbrowser/human/__init__.py index 1079b22..4861f2e 100644 --- a/cloakbrowser/human/__init__.py +++ b/cloakbrowser/human/__init__.py @@ -1257,13 +1257,16 @@ def _patch_single_element_handle_sync( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - ensure_actionable_handle(page, el, CHECKS_CLICK, timeout=timeout, force=force) + ensure_actionable_handle(page, el, CHECKS_CLICK, timeout=_remaining_ms(), force=force) info = _move_to_element(call_cfg) if info is None: return _orig_click(**kwargs) if not force: - check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) human_click(raw_mouse, info['is_inp'], call_cfg) # --- el.dblclick() --- @@ -1271,13 +1274,16 @@ def _patch_single_element_handle_sync( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - ensure_actionable_handle(page, el, CHECKS_CLICK, timeout=timeout, force=force) + ensure_actionable_handle(page, el, CHECKS_CLICK, timeout=_remaining_ms(), force=force) info = _move_to_element(call_cfg) if info is None: return _orig_dblclick(**kwargs) if not force: - check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) raw_mouse.down(click_count=2) sleep_ms(rand(30, 60)) raw_mouse.up(click_count=2) @@ -1287,8 +1293,11 @@ def _patch_single_element_handle_sync( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - ensure_actionable_handle(page, el, CHECKS_HOVER, timeout=timeout, force=force) + ensure_actionable_handle(page, el, CHECKS_HOVER, timeout=_remaining_ms(), force=force) info = _move_to_element(call_cfg) if info is None: return _orig_hover(**kwargs) @@ -1298,13 +1307,16 @@ def _patch_single_element_handle_sync( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - ensure_actionable_handle(page, el, CHECKS_INPUT, timeout=timeout, force=force) + ensure_actionable_handle(page, el, CHECKS_INPUT, timeout=_remaining_ms(), force=force) info = _move_to_element(call_cfg) if info is None: return _orig_type(text, **kwargs) if not force: - check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) human_click(raw_mouse, info['is_inp'], call_cfg) sleep_ms(rand(100, 250)) human_type(page, raw_keyboard, text, call_cfg, cdp_session=cdp_session) @@ -1314,13 +1326,16 @@ def _patch_single_element_handle_sync( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - ensure_actionable_handle(page, el, CHECKS_INPUT, timeout=timeout, force=force) + ensure_actionable_handle(page, el, CHECKS_INPUT, timeout=_remaining_ms(), force=force) info = _move_to_element(call_cfg) if info is None: return _orig_fill(value, **kwargs) if not force: - check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) human_click(raw_mouse, info['is_inp'], call_cfg) sleep_ms(rand(100, 250)) originals.keyboard_press(_SELECT_ALL) @@ -1367,8 +1382,11 @@ def _patch_single_element_handle_sync( def _human_el_select_option(value: Any = None, **kwargs: Any) -> Any: force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - ensure_actionable_handle(page, el, CHECKS_FOCUS, timeout=timeout, force=force) + ensure_actionable_handle(page, el, CHECKS_FOCUS, timeout=_remaining_ms(), force=force) info = _move_to_element() if info is None: return _orig_select_option(value, **kwargs) @@ -1380,8 +1398,11 @@ def _patch_single_element_handle_sync( def _human_el_check(**kwargs: Any) -> None: force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=timeout, force=force) + ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=_remaining_ms(), force=force) try: if el.is_checked(): return @@ -1391,15 +1412,18 @@ def _patch_single_element_handle_sync( if info is None: return _orig_check(**kwargs) if not force: - check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) human_click(raw_mouse, info['is_inp'], cfg) # --- el.uncheck() --- def _human_el_uncheck(**kwargs: Any) -> None: force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=timeout, force=force) + ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=_remaining_ms(), force=force) try: if not el.is_checked(): return @@ -1409,15 +1433,18 @@ def _patch_single_element_handle_sync( if info is None: return _orig_uncheck(**kwargs) if not force: - check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) human_click(raw_mouse, info['is_inp'], cfg) # --- el.set_checked() --- def _human_el_set_checked(checked: bool, **kwargs: Any) -> None: force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=timeout, force=force) + ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=_remaining_ms(), force=force) try: current = el.is_checked() if current == checked: @@ -1429,7 +1456,7 @@ def _patch_single_element_handle_sync( return _orig_set_checked(checked, **kwargs) if info: if not force: - check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) human_click(raw_mouse, info['is_inp'], cfg) # --- el.tap() --- @@ -2158,13 +2185,16 @@ def _patch_single_element_handle_async( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - await async_ensure_actionable_handle(page, el, CHECKS_CLICK, timeout=timeout, force=force) + await async_ensure_actionable_handle(page, el, CHECKS_CLICK, timeout=_remaining_ms(), force=force) info = await _move_to_element(call_cfg) if info is None: return await _orig_click(**kwargs) if not force: - await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) await async_human_click(raw_mouse, info['is_inp'], call_cfg) # --- el.dblclick() --- @@ -2172,13 +2202,16 @@ def _patch_single_element_handle_async( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - await async_ensure_actionable_handle(page, el, CHECKS_CLICK, timeout=timeout, force=force) + await async_ensure_actionable_handle(page, el, CHECKS_CLICK, timeout=_remaining_ms(), force=force) info = await _move_to_element(call_cfg) if info is None: return await _orig_dblclick(**kwargs) if not force: - await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) await raw_mouse.down(click_count=2) await async_sleep_ms(rand(30, 60)) await raw_mouse.up(click_count=2) @@ -2188,8 +2221,11 @@ def _patch_single_element_handle_async( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - await async_ensure_actionable_handle(page, el, CHECKS_HOVER, timeout=timeout, force=force) + await async_ensure_actionable_handle(page, el, CHECKS_HOVER, timeout=_remaining_ms(), force=force) info = await _move_to_element(call_cfg) if info is None: return await _orig_hover(**kwargs) @@ -2199,13 +2235,16 @@ def _patch_single_element_handle_async( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - await async_ensure_actionable_handle(page, el, CHECKS_INPUT, timeout=timeout, force=force) + await async_ensure_actionable_handle(page, el, CHECKS_INPUT, timeout=_remaining_ms(), force=force) info = await _move_to_element(call_cfg) if info is None: return await _orig_type(text, **kwargs) if not force: - await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) await async_human_click(raw_mouse, info['is_inp'], call_cfg) await async_sleep_ms(rand(100, 250)) cdp = await _get_cdp() @@ -2216,13 +2255,16 @@ def _patch_single_element_handle_async( call_cfg = merge_config(cfg, kwargs.get("human_config")) force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - await async_ensure_actionable_handle(page, el, CHECKS_INPUT, timeout=timeout, force=force) + await async_ensure_actionable_handle(page, el, CHECKS_INPUT, timeout=_remaining_ms(), force=force) info = await _move_to_element(call_cfg) if info is None: return await _orig_fill(value, **kwargs) if not force: - await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) await async_human_click(raw_mouse, info['is_inp'], call_cfg) await async_sleep_ms(rand(100, 250)) await originals.keyboard_press(_SELECT_ALL) @@ -2269,8 +2311,11 @@ def _patch_single_element_handle_async( async def _human_el_select_option(value: Any = None, **kwargs: Any) -> Any: force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - await async_ensure_actionable_handle(page, el, CHECKS_FOCUS, timeout=timeout, force=force) + await async_ensure_actionable_handle(page, el, CHECKS_FOCUS, timeout=_remaining_ms(), force=force) info = await _move_to_element() if info is None: return await _orig_select_option(value, **kwargs) @@ -2282,8 +2327,11 @@ def _patch_single_element_handle_async( async def _human_el_check(**kwargs: Any) -> None: force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - await async_ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=timeout, force=force) + await async_ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=_remaining_ms(), force=force) try: if await el.is_checked(): return @@ -2293,15 +2341,18 @@ def _patch_single_element_handle_async( if info is None: return await _orig_check(**kwargs) if not force: - await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) await async_human_click(raw_mouse, info['is_inp'], cfg) # --- el.uncheck() --- async def _human_el_uncheck(**kwargs: Any) -> None: force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - await async_ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=timeout, force=force) + await async_ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=_remaining_ms(), force=force) try: if not await el.is_checked(): return @@ -2311,15 +2362,18 @@ def _patch_single_element_handle_async( if info is None: return await _orig_uncheck(**kwargs) if not force: - await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) await async_human_click(raw_mouse, info['is_inp'], cfg) # --- el.set_checked() --- async def _human_el_set_checked(checked: bool, **kwargs: Any) -> None: force = kwargs.get("force", False) timeout = kwargs.get("timeout", 30000) + deadline = time.monotonic() + timeout / 1000.0 + def _remaining_ms(): + return max(0, (deadline - time.monotonic()) * 1000) if not force: - await async_ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=timeout, force=force) + await async_ensure_actionable_handle(page, el, CHECKS_CHECK, timeout=_remaining_ms(), force=force) try: current = await el.is_checked() if current == checked: @@ -2331,7 +2385,7 @@ def _patch_single_element_handle_async( return await _orig_set_checked(checked, **kwargs) if info: if not force: - await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(timeout, 5000)) + await async_check_pointer_events_handle(page, el, cursor.x, cursor.y, timeout=min(_remaining_ms(), 5000)) await async_human_click(raw_mouse, info['is_inp'], cfg) # --- el.tap() --- diff --git a/js/src/human/elementhandle.ts b/js/src/human/elementhandle.ts index 5a29d91..d87ce00 100644 --- a/js/src/human/elementhandle.ts +++ b/js/src/human/elementhandle.ts @@ -196,10 +196,12 @@ export function patchSingleElementHandle( const callCfg = mergeConfig(cfg, options?.human_config ?? options); const force = options?.force ?? false; const timeout = options?.timeout ?? 30000; - if (!force) await ensureActionableHandle(el, CHECKS_CLICK, timeout, force); + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + if (!force) await ensureActionableHandle(el, CHECKS_CLICK, remainingMs(), force); const info = await moveToElement(callCfg); if (!info) return origElClick(options); - if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(timeout, 5000)); + if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(remainingMs(), 5000)); await humanClick(raw, info.isInp, callCfg); }; @@ -216,10 +218,12 @@ export function patchSingleElementHandle( const callCfg = mergeConfig(cfg, options?.human_config ?? options); const force = options?.force ?? false; const timeout = options?.timeout ?? 30000; - if (!force) await ensureActionableHandle(el, CHECKS_CLICK, timeout, force); + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + if (!force) await ensureActionableHandle(el, CHECKS_CLICK, remainingMs(), force); const info = await moveToElement(callCfg); if (!info) return origElDblclick(options); - if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(timeout, 5000)); + if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(remainingMs(), 5000)); await raw.down({ clickCount: 2 }); await sleep(rand(30, 60)); await raw.up({ clickCount: 2 }); @@ -235,7 +239,9 @@ export function patchSingleElementHandle( const callCfg = mergeConfig(cfg, options?.human_config ?? options); const force = options?.force ?? false; const timeout = options?.timeout ?? 30000; - if (!force) await ensureActionableHandle(el, CHECKS_HOVER, timeout, force); + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + if (!force) await ensureActionableHandle(el, CHECKS_HOVER, remainingMs(), force); const info = await moveToElement(callCfg); if (!info) return origElHover(options); }; @@ -248,10 +254,12 @@ export function patchSingleElementHandle( const callCfg = mergeConfig(cfg, options?.human_config ?? options); const force = (options as any)?.force ?? false; const timeout = options?.timeout ?? 30000; - if (!force) await ensureActionableHandle(el, CHECKS_INPUT, timeout, force); + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + if (!force) await ensureActionableHandle(el, CHECKS_INPUT, remainingMs(), force); const info = await moveToElement(callCfg); if (!info) return origElType(text, options); - if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(timeout, 5000)); + if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(remainingMs(), 5000)); await humanClick(raw, info.isInp, callCfg); await sleep(rand(100, 250)); let cdpSession: CDPSession | null = null; @@ -267,10 +275,12 @@ export function patchSingleElementHandle( const callCfg = mergeConfig(cfg, options?.human_config ?? options); const force = options?.force ?? false; const timeout = options?.timeout ?? 30000; - if (!force) await ensureActionableHandle(el, CHECKS_INPUT, timeout, force); + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + if (!force) await ensureActionableHandle(el, CHECKS_INPUT, remainingMs(), force); const info = await moveToElement(callCfg); if (!info) return origElFill(value, options); - if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(timeout, 5000)); + if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(remainingMs(), 5000)); await humanClick(raw, info.isInp, callCfg); await sleep(rand(100, 250)); await originals.keyboardPress(SELECT_ALL); @@ -298,7 +308,9 @@ export function patchSingleElementHandle( }) => { const force = options?.force ?? false; const timeout = options?.timeout ?? 30000; - if (!force) await ensureActionableHandle(el, CHECKS_FOCUS, timeout, force); + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + if (!force) await ensureActionableHandle(el, CHECKS_FOCUS, remainingMs(), force); const info = await moveToElement(); if (!info) return origElSelectOption(values, options); await humanClick(raw, false, cfg); @@ -316,14 +328,16 @@ export function patchSingleElementHandle( }) => { const force = options?.force ?? false; const timeout = options?.timeout ?? 30000; - if (!force) await ensureActionableHandle(el, CHECKS_CHECK, timeout, force); + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + if (!force) await ensureActionableHandle(el, CHECKS_CHECK, remainingMs(), force); try { const checked = await el.isChecked(); if (checked) return; } catch {} const info = await moveToElement(); if (!info) return origElCheck(options); - if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(timeout, 5000)); + if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(remainingMs(), 5000)); await humanClick(raw, info.isInp, cfg); }; @@ -337,14 +351,16 @@ export function patchSingleElementHandle( }) => { const force = options?.force ?? false; const timeout = options?.timeout ?? 30000; - if (!force) await ensureActionableHandle(el, CHECKS_CHECK, timeout, force); + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + if (!force) await ensureActionableHandle(el, CHECKS_CHECK, remainingMs(), force); try { const checked = await el.isChecked(); if (!checked) return; } catch {} const info = await moveToElement(); if (!info) return origElUncheck(options); - if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(timeout, 5000)); + if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(remainingMs(), 5000)); await humanClick(raw, info.isInp, cfg); }; @@ -359,14 +375,16 @@ export function patchSingleElementHandle( }) => { const force = options?.force ?? false; const timeout = options?.timeout ?? 30000; - if (!force) await ensureActionableHandle(el, CHECKS_CHECK, timeout, force); + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + if (!force) await ensureActionableHandle(el, CHECKS_CHECK, remainingMs(), force); try { const current = await el.isChecked(); if (current === checked) return; } catch {} const info = await moveToElement(); if (!info) return origElSetChecked(checked, options); - if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(timeout, 5000)); + if (!force) await checkPointerEventsHandle(el, cursor.x, cursor.y, Math.min(remainingMs(), 5000)); await humanClick(raw, info.isInp, cfg); }; } diff --git a/js/src/human/index.ts b/js/src/human/index.ts index 1fb2985..220970e 100644 --- a/js/src/human/index.ts +++ b/js/src/human/index.ts @@ -691,7 +691,12 @@ function patchSingleFrame( const origFrameTap = (frame as any).tap?.bind(frame); const origFrameDragAndDrop = frame.dragAndDrop.bind(frame); - const moveToFrameSelector = async (selector: string, options?: HumanActionOptions, inputBias = false) => { + const moveToFrameSelector = async ( + selector: string, + options: HumanActionOptions | undefined, + inputBias: boolean, + remainingMs: () => number, + ) => { const callCfg = mergeConfig(cfg, options?.human_config ?? options); if (callCfg.idle_between_actions) { await humanIdle(raw, cursor.x, cursor.y, callCfg); @@ -699,9 +704,9 @@ function patchSingleFrame( const locator = firstFrameLocator(frame, selector); if (typeof locator.scrollIntoViewIfNeeded === 'function') { - await locator.scrollIntoViewIfNeeded({ timeout: options?.timeout }).catch(() => undefined); + await locator.scrollIntoViewIfNeeded({ timeout: Math.max(1, remainingMs()) }).catch(() => undefined); } - const box = await locator.boundingBox({ timeout: options?.timeout ?? 30000 }).catch(() => null); + const box = await locator.boundingBox({ timeout: Math.max(1, remainingMs()) }).catch(() => null); if (!box) return null; const isInput = inputBias || await isFrameInputElement(frame, selector); @@ -713,23 +718,32 @@ function patchSingleFrame( }; const frameClick = async (selector: string, options?: HumanActionOptions) => { - const moved = await moveToFrameSelector(selector, options); - if (!moved) return origFrameClick(selector, options); + const timeout = options?.timeout ?? 30000; + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + const moved = await moveToFrameSelector(selector, options, false, remainingMs); + if (!moved) return origFrameClick(selector, { ...options, timeout: Math.max(1, remainingMs()) }); await humanClick(raw, moved.isInput, moved.callCfg); }; const getFrameCdp = async () => stealth.getCdpSession().catch(() => null); const frameHover = async (selector: string, options?: HumanActionOptions) => { - const moved = await moveToFrameSelector(selector, options, false); - if (!moved) return origFrameHover(selector, options); + const timeout = options?.timeout ?? 30000; + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + const moved = await moveToFrameSelector(selector, options, false, remainingMs); + if (!moved) return origFrameHover(selector, { ...options, timeout: Math.max(1, remainingMs()) }); }; (frame as any).click = frameClick; (frame as any).dblclick = async (selector: string, options?: HumanActionOptions) => { - const moved = await moveToFrameSelector(selector, options); - if (!moved) return origFrameDblclick(selector, options); + const timeout = options?.timeout ?? 30000; + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(0, deadline - Date.now()); + const moved = await moveToFrameSelector(selector, options, false, remainingMs); + if (!moved) return origFrameDblclick(selector, { ...options, timeout: Math.max(1, remainingMs()) }); await raw.down({ clickCount: 2 }); await sleep(rand(30, 60)); await raw.up({ clickCount: 2 }); @@ -820,8 +834,11 @@ function patchSingleFrame( timeout?: number; trial?: boolean; }) => { - const srcBox = await firstFrameLocator(frame, source).boundingBox({ timeout: options?.timeout ?? 30000 }).catch(() => null); - const tgtBox = await firstFrameLocator(frame, target).boundingBox({ timeout: options?.timeout ?? 30000 }).catch(() => null); + const timeout = options?.timeout ?? 30000; + const deadline = Date.now() + timeout; + const remainingMs = () => Math.max(1, deadline - Date.now()); + const srcBox = await firstFrameLocator(frame, source).boundingBox({ timeout: remainingMs() }).catch(() => null); + const tgtBox = await firstFrameLocator(frame, target).boundingBox({ timeout: remainingMs() }).catch(() => null); if (srcBox && tgtBox) { const sx = srcBox.x + srcBox.width / 2; @@ -837,7 +854,7 @@ function patchSingleFrame( await sleep(rand(80, 150)); await originals.mouseUp(); } else { - return origFrameDragAndDrop(source, target, options); + return origFrameDragAndDrop(source, target, { ...options, timeout: Math.max(1, remainingMs()) }); } }; } diff --git a/js/tests/humanize.test.ts b/js/tests/humanize.test.ts index 0890aa3..44735c7 100644 --- a/js/tests/humanize.test.ts +++ b/js/tests/humanize.test.ts @@ -1479,3 +1479,83 @@ describe("el.scrollIntoViewIfNeeded humanization", () => { spy.mockRestore(); }); }); + + +// ========================================================================= +// Issue #307: frame.click timeout should not multiply +// ========================================================================= +describe("frame.click timeout budget (#307)", () => { + it("total wait time should not exceed the specified timeout", async () => { + const { patchPage } = await import("../src/human/index.js"); + + const TIMEOUT_MS = 500; + const delay = (ms: number) => new Promise(r => setTimeout(r, ms)); + + // Build a frame where the element does NOT exist: + // scrollIntoViewIfNeeded and boundingBox each wait until their + // individual timeout before failing, and origFrameClick does the same. + const frameLoc: any = { + boundingBox: vi.fn(async (opts?: { timeout?: number }) => { + await delay(opts?.timeout ?? 30000); + return null; + }), + scrollIntoViewIfNeeded: vi.fn(async (opts?: { timeout?: number }) => { + await delay(opts?.timeout ?? 30000); + throw new Error("timeout"); + }), + evaluate: vi.fn(async () => ({ hit: true })), + isChecked: vi.fn(async () => false), + }; + frameLoc.first = vi.fn(() => frameLoc); + + const origClickFn = vi.fn(async (_sel: string, opts?: any) => { + await delay(opts?.timeout ?? 30000); + throw new Error("timeout"); + }); + + const childFrame: any = { + click: origClickFn, + dblclick: vi.fn(async () => {}), + hover: vi.fn(async () => {}), + type: vi.fn(async () => {}), + fill: vi.fn(async () => {}), + check: vi.fn(async () => {}), + uncheck: vi.fn(async () => {}), + selectOption: vi.fn(async () => {}), + press: vi.fn(async () => {}), + pressSequentially: vi.fn(async () => {}), + tap: vi.fn(async () => {}), + clear: vi.fn(async () => {}), + dragAndDrop: vi.fn(async () => {}), + locator: vi.fn(() => frameLoc), + childFrames: vi.fn(() => []), + }; + + const mainFrame = { + ...buildMockFrame(), + childFrames: vi.fn(() => [childFrame]), + }; + + const page = buildMockPage({ mainFrameReturn: mainFrame }); + const cfg = resolveConfig("default", { + mouse_min_steps: 1, + mouse_max_steps: 1, + idle_between_actions: false, + }); + const cursor = { x: 0, y: 0, initialized: true }; + patchPage(page as any, cfg, cursor as any); + + const start = Date.now(); + try { + await (childFrame as any).click("#does-not-exist", { timeout: TIMEOUT_MS }); + } catch { + // expected — element doesn't exist + } + const elapsed = Date.now() - start; + + // With the bug, elapsed ≈ 3 * TIMEOUT_MS (scrollIntoView + boundingBox + origClick). + // Fixed: elapsed should be ≈ 1 * TIMEOUT_MS (shared deadline). + // Allow 1.8x as upper bound to account for test overhead but catch the 3x bug. + expect(elapsed).toBeLessThan(TIMEOUT_MS * 1.8); + }); +}); diff --git a/tests/test_humanize_unit.py b/tests/test_humanize_unit.py index 4a721c4..1fde82c 100644 --- a/tests/test_humanize_unit.py +++ b/tests/test_humanize_unit.py @@ -708,7 +708,7 @@ class TestBrowserBotDetection: time.sleep(0.3) page.locator('#password').fill('SecurePass!123') time.sleep(0.5) - page.locator('button[type="submit"]').click() + page.locator('#loginForm button[type="submit"]').click() time.sleep(5) body = page.locator('body').text_content() assert '"superHumanSpeed": true' not in body @@ -725,7 +725,7 @@ class TestBrowserBotDetection: t0 = time.time() page.locator('#email').fill('test@example.com') page.locator('#password').fill('MyPassword!99') - page.locator('button[type="submit"]').click() + page.locator('#loginForm button[type="submit"]').click() elapsed_ms = int((time.time() - t0) * 1000) time.sleep(3) assert elapsed_ms > 3000 @@ -1828,6 +1828,66 @@ class TestScrollIntoViewIfNeeded: assert cursor.x == 200 and cursor.y == 200 +# ========================================================================= +# Issue #307: frame/page click timeout should not multiply +# ========================================================================= + +class TestTimeoutBudget307: + """Verify timeout budget is shared across sequential operations.""" + + def test_page_click_total_time_within_budget(self): + """page.click on a missing element should not exceed ~1x the timeout.""" + import cloakbrowser.human as h + from cloakbrowser.human import _CursorState + from cloakbrowser.human.config import resolve_config + from unittest.mock import MagicMock, patch + + TIMEOUT_MS = 500 + cfg = resolve_config("default", {"idle_between_actions": False}) + cursor = _CursorState() + cursor.initialized = True + cursor.x = 100 + cursor.y = 100 + + page = MagicMock() + page.click = MagicMock() + page.dblclick = MagicMock() + page.hover = MagicMock() + page.type = MagicMock() + page.fill = MagicMock() + page.goto = MagicMock() + page.is_checked = MagicMock(return_value=False) + page.viewport_size = {"width": 1280, "height": 720} + page.evaluate = MagicMock(return_value={"hit": True}) + page.context.new_cdp_session = MagicMock(side_effect=Exception("no cdp")) + page.mouse = MagicMock() + page.keyboard = MagicMock() + page.query_selector = MagicMock(return_value=None) + page.query_selector_all = MagicMock(return_value=[]) + page.wait_for_selector = MagicMock(return_value=None) + page.main_frame = MagicMock() + page.main_frame.child_frames = [] + + loc = MagicMock() + loc.wait_for = MagicMock(side_effect=lambda **kw: time.sleep(kw.get("timeout", 30000) / 1000.0)) + loc.is_visible = MagicMock(return_value=False) + loc.first = loc + page.locator = MagicMock(return_value=loc) + + h.patch_page(page, cfg, cursor) + + start = time.monotonic() + try: + page.click("#does-not-exist", timeout=TIMEOUT_MS) + except Exception: + pass + elapsed_ms = (time.monotonic() - start) * 1000 + + assert elapsed_ms < TIMEOUT_MS * 1.8, ( + f"expected <{TIMEOUT_MS * 1.8}ms, got {elapsed_ms:.0f}ms" + ) + + # ========================================================================= # Direct runner (backwards compat) # =========================================================================