mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
fix: align humanize timeout default with Playwright's 30s auto-retry (#172)
The humanize layer hardcoded timeout=2000ms for element lookups, causing locator.click() and page.click() to fail instantly instead of retrying for 30s like standard Playwright. Aligned all defaults to 30000ms across Python sync/async, JS Playwright, and JS Puppeteer paths. Bumped the outer retry sleep from 200ms to 500ms for DOM mutation settle time.
This commit is contained in:
@@ -421,7 +421,7 @@ def _patch_locator_class_sync():
|
|||||||
native_kwargs = {k: v for k, v in kwargs.items() if k != "human_config"}
|
native_kwargs = {k: v for k, v in kwargs.items() if k != "human_config"}
|
||||||
return _orig_scroll_into_view(self, **native_kwargs)
|
return _orig_scroll_into_view(self, **native_kwargs)
|
||||||
return
|
return
|
||||||
timeout = kwargs.get("timeout", 2000)
|
timeout = kwargs.get("timeout", 30000)
|
||||||
try:
|
try:
|
||||||
_, nx, ny = human_scroll_into_view(
|
_, nx, ny = human_scroll_into_view(
|
||||||
page, raw,
|
page, raw,
|
||||||
@@ -648,7 +648,7 @@ def _patch_locator_class_async():
|
|||||||
native_kwargs = {k: v for k, v in kwargs.items() if k != "human_config"}
|
native_kwargs = {k: v for k, v in kwargs.items() if k != "human_config"}
|
||||||
await _orig_scroll_into_view(self, **native_kwargs)
|
await _orig_scroll_into_view(self, **native_kwargs)
|
||||||
return
|
return
|
||||||
timeout = kwargs.get("timeout", 2000)
|
timeout = kwargs.get("timeout", 30000)
|
||||||
|
|
||||||
async def _get_box():
|
async def _get_box():
|
||||||
return await self.bounding_box(timeout=timeout)
|
return await self.bounding_box(timeout=timeout)
|
||||||
@@ -868,7 +868,7 @@ def patch_page(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
|
|||||||
def _human_click(selector: str, **kwargs: Any) -> None:
|
def _human_click(selector: str, **kwargs: Any) -> None:
|
||||||
_ensure_cursor_init()
|
_ensure_cursor_init()
|
||||||
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
||||||
timeout = kwargs.get("timeout", 2000)
|
timeout = kwargs.get("timeout", 30000)
|
||||||
if call_cfg.idle_between_actions:
|
if call_cfg.idle_between_actions:
|
||||||
human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
||||||
box, cx, cy = scroll_to_element(
|
box, cx, cy = scroll_to_element(
|
||||||
@@ -886,7 +886,7 @@ def patch_page(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
|
|||||||
def _human_dblclick(selector: str, **kwargs: Any) -> None:
|
def _human_dblclick(selector: str, **kwargs: Any) -> None:
|
||||||
_ensure_cursor_init()
|
_ensure_cursor_init()
|
||||||
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
||||||
timeout = kwargs.get("timeout", 2000)
|
timeout = kwargs.get("timeout", 30000)
|
||||||
if call_cfg.idle_between_actions:
|
if call_cfg.idle_between_actions:
|
||||||
human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
||||||
box, cx, cy = scroll_to_element(
|
box, cx, cy = scroll_to_element(
|
||||||
@@ -906,7 +906,7 @@ def patch_page(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
|
|||||||
def _human_hover(selector: str, **kwargs: Any) -> None:
|
def _human_hover(selector: str, **kwargs: Any) -> None:
|
||||||
_ensure_cursor_init()
|
_ensure_cursor_init()
|
||||||
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
||||||
timeout = kwargs.get("timeout", 2000)
|
timeout = kwargs.get("timeout", 30000)
|
||||||
if call_cfg.idle_between_actions:
|
if call_cfg.idle_between_actions:
|
||||||
human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
||||||
box, cx, cy = scroll_to_element(
|
box, cx, cy = scroll_to_element(
|
||||||
@@ -1621,7 +1621,7 @@ def patch_page_async(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
|
|||||||
async def _human_click(selector: str, **kwargs: Any) -> None:
|
async def _human_click(selector: str, **kwargs: Any) -> None:
|
||||||
await _ensure_cursor_init()
|
await _ensure_cursor_init()
|
||||||
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
||||||
timeout = kwargs.get("timeout", 2000)
|
timeout = kwargs.get("timeout", 30000)
|
||||||
if call_cfg.idle_between_actions:
|
if call_cfg.idle_between_actions:
|
||||||
await async_human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
await async_human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
||||||
box, cx, cy = await async_scroll_to_element(
|
box, cx, cy = await async_scroll_to_element(
|
||||||
@@ -1639,7 +1639,7 @@ def patch_page_async(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
|
|||||||
async def _human_dblclick(selector: str, **kwargs: Any) -> None:
|
async def _human_dblclick(selector: str, **kwargs: Any) -> None:
|
||||||
await _ensure_cursor_init()
|
await _ensure_cursor_init()
|
||||||
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
||||||
timeout = kwargs.get("timeout", 2000)
|
timeout = kwargs.get("timeout", 30000)
|
||||||
if call_cfg.idle_between_actions:
|
if call_cfg.idle_between_actions:
|
||||||
await async_human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
await async_human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
||||||
box, cx, cy = await async_scroll_to_element(
|
box, cx, cy = await async_scroll_to_element(
|
||||||
@@ -1659,7 +1659,7 @@ def patch_page_async(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
|
|||||||
async def _human_hover(selector: str, **kwargs: Any) -> None:
|
async def _human_hover(selector: str, **kwargs: Any) -> None:
|
||||||
await _ensure_cursor_init()
|
await _ensure_cursor_init()
|
||||||
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
call_cfg = merge_config(cfg, kwargs.get("human_config"))
|
||||||
timeout = kwargs.get("timeout", 2000)
|
timeout = kwargs.get("timeout", 30000)
|
||||||
if call_cfg.idle_between_actions:
|
if call_cfg.idle_between_actions:
|
||||||
await async_human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
await async_human_idle(raw_mouse, rand(call_cfg.idle_between_duration[0], call_cfg.idle_between_duration[1]), cursor.x, cursor.y, call_cfg)
|
||||||
box, cx, cy = await async_scroll_to_element(
|
box, cx, cy = await async_scroll_to_element(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def _is_in_viewport(bounds: dict, viewport_height: int, cfg: HumanConfig) -> boo
|
|||||||
return top_edge >= zone_top and bottom_edge <= zone_bottom
|
return top_edge >= zone_top and bottom_edge <= zone_bottom
|
||||||
|
|
||||||
|
|
||||||
def _get_element_box(page: Any, selector: str, timeout: float = 2000) -> Optional[dict]:
|
def _get_element_box(page: Any, selector: str, timeout: float = 30000) -> Optional[dict]:
|
||||||
"""Locate ``selector`` and return its bounding box.
|
"""Locate ``selector`` and return its bounding box.
|
||||||
|
|
||||||
The ``timeout`` is forwarded to Playwright's ``boundingBox(timeout=...)``
|
The ``timeout`` is forwarded to Playwright's ``boundingBox(timeout=...)``
|
||||||
@@ -66,9 +66,6 @@ def human_scroll_into_view(
|
|||||||
viewport_height = viewport["height"]
|
viewport_height = viewport["height"]
|
||||||
viewport_width = viewport["width"]
|
viewport_width = viewport["width"]
|
||||||
|
|
||||||
box = get_box()
|
|
||||||
if box is None:
|
|
||||||
sleep_ms(200)
|
|
||||||
box = get_box()
|
box = get_box()
|
||||||
if box is None:
|
if box is None:
|
||||||
raise RuntimeError("Element not found while scrolling into view")
|
raise RuntimeError("Element not found while scrolling into view")
|
||||||
@@ -151,13 +148,13 @@ def scroll_to_element(
|
|||||||
selector: str,
|
selector: str,
|
||||||
cursor_x: float, cursor_y: float,
|
cursor_x: float, cursor_y: float,
|
||||||
cfg: HumanConfig,
|
cfg: HumanConfig,
|
||||||
timeout: float = 2000,
|
timeout: float = 30000,
|
||||||
) -> Tuple[dict, float, float]:
|
) -> Tuple[dict, float, float]:
|
||||||
"""Selector-based humanized scroll.
|
"""Selector-based humanized scroll.
|
||||||
|
|
||||||
``timeout`` is forwarded to ``locator.bounding_box(timeout=...)`` so callers
|
``timeout`` is forwarded to ``locator.bounding_box(timeout=...)`` so callers
|
||||||
such as ``page.click('#x', timeout=5000)`` can wait longer for slow elements
|
such as ``page.click('#x', timeout=5000)`` can wait longer for slow elements
|
||||||
(#172). Default stays at 2000ms when not specified.
|
(#172). Default matches Playwright's 30000ms when not specified.
|
||||||
"""
|
"""
|
||||||
return human_scroll_into_view(
|
return human_scroll_into_view(
|
||||||
page, raw,
|
page, raw,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from .scroll import _is_in_viewport
|
|||||||
|
|
||||||
|
|
||||||
async def _get_element_box_async(
|
async def _get_element_box_async(
|
||||||
page: Any, selector: str, timeout: float = 2000,
|
page: Any, selector: str, timeout: float = 30000,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
"""Async variant. ``timeout`` is forwarded to Playwright's
|
"""Async variant. ``timeout`` is forwarded to Playwright's
|
||||||
``boundingBox(timeout=...)`` so callers can extend it for slow-loading
|
``boundingBox(timeout=...)`` so callers can extend it for slow-loading
|
||||||
@@ -62,9 +62,6 @@ async def async_human_scroll_into_view(
|
|||||||
viewport_height = viewport["height"]
|
viewport_height = viewport["height"]
|
||||||
viewport_width = viewport["width"]
|
viewport_width = viewport["width"]
|
||||||
|
|
||||||
box = await get_box()
|
|
||||||
if box is None:
|
|
||||||
await async_sleep_ms(200)
|
|
||||||
box = await get_box()
|
box = await get_box()
|
||||||
if box is None:
|
if box is None:
|
||||||
raise RuntimeError("Element not found while scrolling into view")
|
raise RuntimeError("Element not found while scrolling into view")
|
||||||
@@ -147,13 +144,13 @@ async def async_scroll_to_element(
|
|||||||
selector: str,
|
selector: str,
|
||||||
cursor_x: float, cursor_y: float,
|
cursor_x: float, cursor_y: float,
|
||||||
cfg: HumanConfig,
|
cfg: HumanConfig,
|
||||||
timeout: float = 2000,
|
timeout: float = 30000,
|
||||||
) -> Tuple[dict, float, float]:
|
) -> Tuple[dict, float, float]:
|
||||||
"""Selector-based humanized scroll (async).
|
"""Selector-based humanized scroll (async).
|
||||||
|
|
||||||
``timeout`` is forwarded to ``locator.bounding_box(timeout=...)`` so callers
|
``timeout`` is forwarded to ``locator.bounding_box(timeout=...)`` so callers
|
||||||
such as ``page.click('#x', timeout=5000)`` can wait longer for slow elements
|
such as ``page.click('#x', timeout=5000)`` can wait longer for slow elements
|
||||||
(#172). Default stays at 2000ms when not specified.
|
(#172). Default matches Playwright's 30000ms when not specified.
|
||||||
"""
|
"""
|
||||||
async def _get():
|
async def _get():
|
||||||
return await _get_element_box_async(page, selector, timeout)
|
return await _get_element_box_async(page, selector, timeout)
|
||||||
|
|||||||
@@ -57,12 +57,12 @@ export async function smoothWheel(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Poll ``page.$(selector)`` for up to ``timeout`` ms, returning the element's
|
* Poll ``page.$(selector)`` for up to ``timeout`` ms, returning the element's
|
||||||
* bounding box when found. ``timeout`` defaults to 2000ms when not specified.
|
* bounding box when found. ``timeout`` defaults to 30000ms when not specified.
|
||||||
*/
|
*/
|
||||||
async function getElementBox(
|
async function getElementBox(
|
||||||
page: Page,
|
page: Page,
|
||||||
selector: string,
|
selector: string,
|
||||||
timeout: number = 2000,
|
timeout: number = 30000,
|
||||||
): Promise<ElementBounds | null> {
|
): Promise<ElementBounds | null> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const pollInterval = 100;
|
const pollInterval = 100;
|
||||||
@@ -97,11 +97,7 @@ export async function humanScrollIntoView(
|
|||||||
if (!viewport) throw new Error('Viewport size not available');
|
if (!viewport) throw new Error('Viewport size not available');
|
||||||
|
|
||||||
let box = await getBox();
|
let box = await getBox();
|
||||||
if (!box) {
|
|
||||||
await sleep(200);
|
|
||||||
box = await getBox();
|
|
||||||
if (!box) throw new Error('Element not found while scrolling into view');
|
if (!box) throw new Error('Element not found while scrolling into view');
|
||||||
}
|
|
||||||
|
|
||||||
if (isInViewport(box, viewport.height, cfg)) {
|
if (isInViewport(box, viewport.height, cfg)) {
|
||||||
return { box, cursorX, cursorY };
|
return { box, cursorX, cursorY };
|
||||||
@@ -188,7 +184,7 @@ export async function humanScrollIntoView(
|
|||||||
*
|
*
|
||||||
* ``timeout`` controls how long we poll ``page.$(selector)`` before giving up,
|
* ``timeout`` controls how long we poll ``page.$(selector)`` before giving up,
|
||||||
* so callers like ``page.click('#x', { timeout: 5000 })`` can wait longer for
|
* so callers like ``page.click('#x', { timeout: 5000 })`` can wait longer for
|
||||||
* slow-loading elements (#172). Default stays 2000ms when not specified.
|
* slow-loading elements (#172). Default matches Playwright's 30000ms when not specified.
|
||||||
*/
|
*/
|
||||||
export async function scrollToElement(
|
export async function scrollToElement(
|
||||||
page: Page,
|
page: Page,
|
||||||
|
|||||||
@@ -57,11 +57,7 @@ export async function humanScrollIntoView(
|
|||||||
if (!viewport) throw new Error('Viewport size not available');
|
if (!viewport) throw new Error('Viewport size not available');
|
||||||
|
|
||||||
let box = await getBox();
|
let box = await getBox();
|
||||||
if (!box) {
|
|
||||||
await sleep(200);
|
|
||||||
box = await getBox();
|
|
||||||
if (!box) throw new Error('Element not found while scrolling into view');
|
if (!box) throw new Error('Element not found while scrolling into view');
|
||||||
}
|
|
||||||
|
|
||||||
if (isInViewport(box, viewport.height, cfg)) {
|
if (isInViewport(box, viewport.height, cfg)) {
|
||||||
return { box, cursorX, cursorY };
|
return { box, cursorX, cursorY };
|
||||||
@@ -151,7 +147,7 @@ export async function humanScrollIntoView(
|
|||||||
*
|
*
|
||||||
* ``timeout`` is forwarded to Playwright's ``boundingBox({ timeout })`` so
|
* ``timeout`` is forwarded to Playwright's ``boundingBox({ timeout })`` so
|
||||||
* callers like ``page.click('#x', { timeout: 5000 })`` can wait longer for
|
* callers like ``page.click('#x', { timeout: 5000 })`` can wait longer for
|
||||||
* slow-loading elements (#172). Default stays 2000ms when not specified.
|
* slow-loading elements (#172). Default matches Playwright's 30000ms when not specified.
|
||||||
*/
|
*/
|
||||||
export async function scrollToElement(
|
export async function scrollToElement(
|
||||||
page: Page,
|
page: Page,
|
||||||
@@ -172,7 +168,7 @@ export async function scrollToElement(
|
|||||||
async function getElementBox(
|
async function getElementBox(
|
||||||
page: Page,
|
page: Page,
|
||||||
selector: string,
|
selector: string,
|
||||||
timeout: number = 2000,
|
timeout: number = 30000,
|
||||||
): Promise<ElementBounds | null> {
|
): Promise<ElementBounds | null> {
|
||||||
const el = page.locator(selector).first();
|
const el = page.locator(selector).first();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1108,7 +1108,7 @@ describe("page.click(selector, { timeout }) forwards timeout to scroll", () => {
|
|||||||
expect(boundingBox).toHaveBeenCalledWith({ timeout: 5000 });
|
expect(boundingBox).toHaveBeenCalledWith({ timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("default timeout stays 2000ms when not specified", async () => {
|
it("default timeout matches Playwright's 30000ms when not specified", async () => {
|
||||||
const { scrollToElement } = await import("../src/human/scroll.js");
|
const { scrollToElement } = await import("../src/human/scroll.js");
|
||||||
const cfg = resolveConfig("default");
|
const cfg = resolveConfig("default");
|
||||||
|
|
||||||
@@ -1125,7 +1125,7 @@ describe("page.click(selector, { timeout }) forwards timeout to scroll", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await scrollToElement(page, raw, "#x", 0, 0, cfg);
|
await scrollToElement(page, raw, "#x", 0, 0, cfg);
|
||||||
expect(boundingBox).toHaveBeenCalledWith({ timeout: 2000 });
|
expect(boundingBox).toHaveBeenCalledWith({ timeout: 30000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("page.click({ timeout }) reaches scrollToElement", async () => {
|
it("page.click({ timeout }) reaches scrollToElement", async () => {
|
||||||
|
|||||||
@@ -1671,11 +1671,13 @@ describe("Puppeteer: isInputElement stealth integration via patchPage", () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockEl = buildMockElementHandle();
|
||||||
const page = buildMockPage({
|
const page = buildMockPage({
|
||||||
evaluate: vi.fn(async (...args: any[]) => {
|
evaluate: vi.fn(async (...args: any[]) => {
|
||||||
evaluateCalls.push(args);
|
evaluateCalls.push(args);
|
||||||
return false;
|
return false;
|
||||||
}),
|
}),
|
||||||
|
$: vi.fn(async () => mockEl),
|
||||||
});
|
});
|
||||||
page.createCDPSession = vi.fn(async () => mockCdp);
|
page.createCDPSession = vi.fn(async () => mockCdp);
|
||||||
|
|
||||||
|
|||||||
@@ -1305,7 +1305,7 @@ class TestPerCallTimeoutForwarding:
|
|||||||
not silently use the hardcoded 2000ms in scroll."""
|
not silently use the hardcoded 2000ms in scroll."""
|
||||||
|
|
||||||
def test_get_element_box_default_timeout(self):
|
def test_get_element_box_default_timeout(self):
|
||||||
"""Default timeout stays 2000ms for backwards compatibility."""
|
"""Default timeout matches Playwright's 30000ms."""
|
||||||
from cloakbrowser.human.scroll import _get_element_box
|
from cloakbrowser.human.scroll import _get_element_box
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@@ -1315,7 +1315,7 @@ class TestPerCallTimeoutForwarding:
|
|||||||
page.locator = MagicMock(return_value=MagicMock(first=loc))
|
page.locator = MagicMock(return_value=MagicMock(first=loc))
|
||||||
|
|
||||||
_get_element_box(page, "#x")
|
_get_element_box(page, "#x")
|
||||||
loc.bounding_box.assert_called_once_with(timeout=2000)
|
loc.bounding_box.assert_called_once_with(timeout=30000)
|
||||||
|
|
||||||
def test_get_element_box_custom_timeout(self):
|
def test_get_element_box_custom_timeout(self):
|
||||||
"""Caller can pass a custom timeout that overrides the default."""
|
"""Caller can pass a custom timeout that overrides the default."""
|
||||||
@@ -1387,7 +1387,7 @@ class TestPerCallTimeoutForwarding:
|
|||||||
page.main_frame.child_frames = []
|
page.main_frame.child_frames = []
|
||||||
|
|
||||||
captured = {}
|
captured = {}
|
||||||
def fake_scroll(page_arg, raw, selector, cx, cy, cfg_arg, timeout=2000):
|
def fake_scroll(page_arg, raw, selector, cx, cy, cfg_arg, timeout=30000):
|
||||||
captured["timeout"] = timeout
|
captured["timeout"] = timeout
|
||||||
return ({"x": 100, "y": 100, "width": 50, "height": 30}, cx, cy)
|
return ({"x": 100, "y": 100, "width": 50, "height": 30}, cx, cy)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user