diff --git a/README.md b/README.md index 687b45f..6430ffe 100644 --- a/README.md +++ b/README.md @@ -115,11 +115,10 @@ Toggles random mouse movements on or off. Simulates a mouse click at the specified selector or element. - **selector (optional):** CSS selector or ElementHandle to identify the target element. -- **options (optional):** Additional options for clicking. +- **options (optional):** Additional options for clicking. **Extends the `options` of the `move` function (below)** - `hesitate (number):` Delay before initiating the click action in milliseconds. Default is `0`. - `waitForClick (number):` Delay between mousedown and mouseup in milliseconds. Default is `0`. - `moveDelay (number):` Delay after moving the mouse in milliseconds. Default is `2000`. If `randomizeMoveDelay=true`, delay is randomized from 0 to `moveDelay`. - - `randomizeMoveDelay (boolean):` Randomize delay between actions from `0` to `moveDelay`. Default is `true`. #### `move(selector: string | ElementHandle, options?: MoveOptions): Promise` @@ -134,6 +133,8 @@ Moves the mouse to the specified selector or element. - `maxTries (number):` Maximum number of attempts to mouse-over the element. Default is `10`. - `moveSpeed (number):` Speed of mouse movement. Default is random. - `overshootThreshold (number):` Distance from current location to destination that triggers overshoot to occur. (Below this distance, no overshoot will occur). Default is `500`. + - `scrollBehavior (ScrollBehavior):` Scroll behavior when target element is outside the visible window ([docs/available values](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#behavior)). **If this is specified, will use JS scrolling instead of CDP scrolling, which is detectable**. Default is `undefined` (CDP `scrollIntoView`). + - `scrollWait (number):` Time to wait after scrolling (when scrolling occurs due to target element being outside the visible window). Default is `200`. #### `moveTo(destination: Vector, options?: MoveToOptions): Promise` diff --git a/src/__test__/spoof.spec.ts b/src/__test__/spoof.spec.ts index 11b8517..2535276 100644 --- a/src/__test__/spoof.spec.ts +++ b/src/__test__/spoof.spec.ts @@ -12,7 +12,8 @@ const cursorDefaultOptions = { moveDelay: 0, moveSpeed: 99, hesitate: 0, - waitForClick: 0 + waitForClick: 0, + scrollWait: 0 } as const satisfies ClickOptions describe('Mouse movements', () => { diff --git a/src/spoof.ts b/src/spoof.ts index 9cc2b3d..1b0cc18 100644 --- a/src/spoof.ts +++ b/src/spoof.ts @@ -52,6 +52,21 @@ export interface MoveOptions extends BoxOptions, Pick * @default 500 */ readonly overshootThreshold?: number + /** + * Scroll behavior when target element is outside the visible window. + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#behavior} + * + * NOTE: If this is specified, will use JS scrolling instead of CDP scrolling, which is detectable! + * + * @default undefined (use CDP scrolling) + */ + readonly scrollBehavior?: ScrollBehavior + /** + * Time to wait after scrolling (when scrolling occurs due to target element being outside the visible window) + * @default 200 + */ + readonly scrollWait?: number } export interface ClickOptions extends MoveOptions { @@ -472,6 +487,7 @@ export const createCursor = ( maxTries: 10, overshootThreshold: 500, randomizeMoveDelay: true, + scrollWait: 200, ...defaultOptions?.move, ...options } satisfies MoveOptions @@ -514,19 +530,32 @@ export const createCursor = ( } // Make sure the object is in view - const objectId = elem.remoteObject().objectId - if (objectId !== undefined) { - try { - await getCDPClient(page).send('DOM.scrollIntoViewIfNeeded', { - objectId - }) - } catch (e) { + if (!(await elem.isIntersectingViewport())) { + const scrollElemIntoView = async (): Promise => + await elem.evaluate((e, scrollBehavior) => e.scrollIntoView({ + block: 'center', + behavior: scrollBehavior + }), optionsResolved.scrollBehavior) + + if (optionsResolved.scrollBehavior !== undefined) { + // DOM.scrollIntoViewIfNeeded is instant scroll, so do the JS scroll if scrollBehavior passed + await scrollElemIntoView() + } else { + try { + const { objectId } = elem.remoteObject() + if (objectId === undefined) throw new Error() + await getCDPClient(page).send('DOM.scrollIntoViewIfNeeded', { + objectId + }) + } catch (e) { // use regular JS scroll method as a fallback - log('Falling back to JS scroll method', e) - await elem.evaluate((e) => e.scrollIntoView({ block: 'center' })) - await new Promise((resolve) => setTimeout(resolve, 2000)) // Wait a bit until the scroll has finished + log('Falling back to JS scroll method', e) + await scrollElemIntoView() + } } + await delay(optionsResolved.scrollWait) } + const box = await boundingBoxWithFallback(page, elem) const { height, width } = box const destination = getRandomBoxPoint(box, optionsResolved)