Skip to content

Commit b78fa71

Browse files
committed
Merge branch 'master' into scrollTo
2 parents 80ebab0 + 6d3227f commit b78fa71

File tree

4 files changed

+121
-71
lines changed

4 files changed

+121
-71
lines changed

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Toggles random mouse movements on or off.
115115
Simulates a mouse click at the specified selector or element.
116116

117117
- **selector (optional):** CSS selector or ElementHandle to identify the target element.
118-
- **options (optional):** Additional options for clicking. **Extends the `options` of the `move` and `scrollIntoView` functions (below)**
118+
- **options (optional):** Additional options for clicking. **Extends the `options` of the `move`, `scrollIntoView`, and `getElement` functions (below)**
119119
- `hesitate (number):` Delay before initiating the click action in milliseconds. Default is `0`.
120120
- `waitForClick (number):` Delay between mousedown and mouseup in milliseconds. Default is `0`.
121121
- `moveDelay (number):` Delay after moving the mouse in milliseconds. Default is `2000`. If `randomizeMoveDelay=true`, delay is randomized from 0 to `moveDelay`.
@@ -125,10 +125,9 @@ Simulates a mouse click at the specified selector or element.
125125
Moves the mouse to the specified selector or element.
126126

127127
- **selector:** CSS selector or ElementHandle to identify the target element.
128-
- **options (optional):** Additional options for moving. **Extends the `options` of the `scrollIntoView` function (below)**
128+
- **options (optional):** Additional options for moving. **Extends the `options` of the `scrollIntoView` and `getElement` functions (below)**
129129
- `paddingPercentage (number):` Percentage of padding to be added inside the element when determining the target point. Default is `0` (may move to anywhere within the element). `100` will always move to center of element.
130130
- `destination (Vector):` Destination to move the cursor to, relative to the top-left corner of the element. If specified, `paddingPercentage` is not used. If not specified (default), destination is random point within the `paddingPercentage`.
131-
- `waitForSelector (number):` Time to wait for the selector to appear in milliseconds. Default is to not wait for selector.
132131
- `moveDelay (number):` Delay after moving the mouse in milliseconds. Default is `0`. If `randomizeMoveDelay=true`, delay is randomized from 0 to `moveDelay`.
133132
- `randomizeMoveDelay (boolean):` Randomize delay between actions from `0` to `moveDelay`. Default is `true`.
134133
- `maxTries (number):` Maximum number of attempts to mouse-over the element. Default is `10`.
@@ -145,12 +144,12 @@ Moves the mouse to the specified destination point.
145144
- `moveDelay (number):` Delay after moving the mouse in milliseconds. Default is `0`. If `randomizeMoveDelay=true`, delay is randomized from 0 to `moveDelay`.
146145
- `randomizeMoveDelay (boolean):` Randomize delay between actions from `0` to `moveDelay`. Default is `true`.
147146

148-
#### `scrollIntoView(element: ElementHandle, options?: ScrollOptions) => Promise<void>`
147+
#### `scrollIntoView(selector: string | ElementHandle, options?: ScrollOptions) => Promise<void>`
149148

150149
Scrolls the element into view. If already in view, no scroll occurs.
151150

152-
- **element:** ElementHandle to identify the target element.
153-
- **options (optional):** Additional options for scrolling.
151+
- **selector:** CSS selector or ElementHandle to identify the target element.
152+
- **options (optional):** Additional options for scrolling. **Extends the `options` of the `getElement` function (below)**
154153
- `scrollSpeed (number):` Scroll speed (when scrolling occurs). 0 to 100. 100 is instant. Default is `100`.
155154
- `scrollDelay (number):` Time to wait after scrolling (when scrolling occurs). Default is `200`.
156155
- `inViewportMargin (number):` Margin (in px) to add around the element when ensuring it is in the viewport. Default is `0`.
@@ -163,6 +162,13 @@ Scrolls to the specified destination point.
163162
- **options (optional):** Additional options for scrolling.
164163
- `behavior (ScrollBehavior):` Directly passed to `window.scrollTo`.
165164
- `scrollDelay (number):` Time to wait after scrolling. Default is `200`.
165+
#### `getElement(selector: string | ElementHandle, options?: GetElementOptions) => Promise<void>`
166+
167+
Gets the element via a selector. Can use an XPath.
168+
169+
- **selector:** CSS selector or ElementHandle to identify the target element.
170+
- **options (optional):** Additional options.
171+
- `waitForSelector (number):` Time to wait for the selector to appear in milliseconds. Default is to not wait for selector.
166172

167173
#### `getLocation(): Vector`
168174

src/__test__/custom-page.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
setTimeout(() => {
3939
box.style.top = Math.random() * 500
4040
}, 1000)
41+
window.boxWasClicked = false
42+
box.addEventListener('click', () => {
43+
window.boxWasClicked = true
44+
})
4145
</script>
4246

4347
</html>

src/__test__/spoof.spec.ts

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { Page } from 'puppeteer'
1+
import type { ElementHandle, Page } from 'puppeteer'
22
import { type ClickOptions, createCursor, GhostCursor } from '../spoof'
33
import { join } from 'path'
4-
import { promises as fs } from 'fs'
4+
import { readFileSync } from 'fs'
55
import installMouseHelper from '../mouse-helper'
66

77
declare const page: Page
@@ -18,63 +18,76 @@ const cursorDefaultOptions = {
1818
inViewportMargin: 50
1919
} as const satisfies ClickOptions
2020

21+
declare global {
22+
// eslint-disable-next-line no-var
23+
var boxWasClicked: boolean
24+
}
25+
2126
describe('Mouse movements', () => {
27+
const html = readFileSync(join(__dirname, 'custom-page.html'), 'utf8')
28+
2229
beforeAll(async () => {
2330
await installMouseHelper(page)
24-
const html = await fs.readFile(join(__dirname, 'custom-page.html'), 'utf8')
31+
})
32+
33+
beforeEach(async () => {
2534
await page.goto('data:text/html,' + encodeURIComponent(html), {
2635
waitUntil: 'networkidle2'
2736
})
28-
})
2937

30-
beforeEach(() => {
3138
cursor = createCursor(page, undefined, undefined, {
3239
move: cursorDefaultOptions,
3340
click: cursorDefaultOptions,
3441
moveTo: cursorDefaultOptions
3542
})
3643
})
3744

45+
const testClick = async (clickSelector: string): Promise<void> => {
46+
expect(await page.evaluate(() => window.boxWasClicked)).toEqual(false)
47+
await cursor.click(clickSelector)
48+
expect(await page.evaluate(() => window.boxWasClicked)).toEqual(true)
49+
}
50+
3851
it('Should click on the element without throwing an error (CSS selector)', async () => {
39-
await cursor.click('#box1')
52+
await testClick('#box1')
4053
})
4154

4255
it('Should click on the element without throwing an error (XPath selector)', async () => {
43-
await cursor.click('//*[@id="box1"]')
56+
await testClick('//*[@id="box1"]')
4457
})
4558

4659
it('Should scroll to elements correctly', async () => {
4760
const getScrollPosition = async (): Promise<{ top: number, left: number }> => await page.evaluate(() => (
4861
{ top: window.scrollY, left: window.scrollX }
4962
))
5063

51-
const box1 = await page.waitForSelector('#box1')
52-
if (box1 == null) throw new Error('box not found')
53-
const box2 = await page.waitForSelector('#box2')
54-
if (box2 == null) throw new Error('box not found')
55-
const box3 = await page.waitForSelector('#box3')
56-
if (box3 == null) throw new Error('box not found')
64+
const boxes = await Promise.all([1, 2, 3].map(async (number: number): Promise<ElementHandle<HTMLElement>> => {
65+
const selector = `#box${number}`
66+
const box = await page.waitForSelector(selector) as ElementHandle<HTMLElement> | null
67+
if (box == null) throw new Error(`${selector} not found`)
68+
return box
69+
}))
5770

5871
expect(await getScrollPosition()).toEqual({ top: 0, left: 0 })
5972

60-
expect(await box1.isIntersectingViewport()).toBeTruthy()
61-
await cursor.click(box1)
73+
expect(await boxes[0].isIntersectingViewport()).toBeTruthy()
74+
await cursor.click(boxes[0])
6275
expect(await getScrollPosition()).toEqual({ top: 0, left: 0 })
63-
expect(await box1.isIntersectingViewport()).toBeTruthy()
76+
expect(await boxes[0].isIntersectingViewport()).toBeTruthy()
6477

65-
expect(await box2.isIntersectingViewport()).toBeFalsy()
66-
await cursor.move(box2)
78+
expect(await boxes[1].isIntersectingViewport()).toBeFalsy()
79+
await cursor.move(boxes[1])
6780
expect(await getScrollPosition()).toEqual({ top: 2500, left: 0 })
68-
expect(await box2.isIntersectingViewport()).toBeTruthy()
81+
expect(await boxes[1].isIntersectingViewport()).toBeTruthy()
6982

70-
expect(await box3.isIntersectingViewport()).toBeFalsy()
71-
await cursor.move(box3)
83+
expect(await boxes[2].isIntersectingViewport()).toBeFalsy()
84+
await cursor.move(boxes[2])
7285
expect(await getScrollPosition()).toEqual({ top: 4450, left: 2250 })
73-
expect(await box3.isIntersectingViewport()).toBeTruthy()
86+
expect(await boxes[2].isIntersectingViewport()).toBeTruthy()
7487

75-
expect(await box1.isIntersectingViewport()).toBeFalsy()
76-
await cursor.click(box1)
77-
expect(await box1.isIntersectingViewport()).toBeTruthy()
88+
expect(await boxes[0].isIntersectingViewport()).toBeFalsy()
89+
await cursor.click(boxes[0])
90+
expect(await boxes[0].isIntersectingViewport()).toBeTruthy()
7891
})
7992
})
8093

src/spoof.ts

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,15 @@ export interface BoxOptions {
3535
readonly destination?: Vector
3636
}
3737

38-
export interface ScrollOptions {
38+
export interface GetElementOptions {
39+
/**
40+
* Time to wait for the selector to appear in milliseconds.
41+
* Default is to not wait for selector.
42+
*/
43+
readonly waitForSelector?: number
44+
}
45+
46+
export interface ScrollOptions extends GetElementOptions {
3947
/**
4048
* Scroll speed (when scrolling occurs). 0 to 100. 100 is instant.
4149
* @default 100
@@ -55,11 +63,6 @@ export interface ScrollOptions {
5563
}
5664

5765
export interface MoveOptions extends BoxOptions, ScrollOptions, Pick<PathOptions, 'moveSpeed'> {
58-
/**
59-
* Time to wait for the selector to appear in milliseconds.
60-
* Default is to not wait for selector.
61-
*/
62-
readonly waitForSelector?: number
6366
/**
6467
* Delay after moving the mouse in milliseconds. If `randomizeMoveDelay=true`, delay is randomized from 0 to `moveDelay`.
6568
* @default 0
@@ -153,6 +156,9 @@ export interface GhostCursor {
153156
scrollTo: (
154157
destination: Partial<Vector> | 'top' | 'bottom',
155158
options?: ScrollToOptions) => Promise<void>
159+
getElement: (
160+
selector: string | ElementHandle,
161+
options?: GetElementOptions) => Promise<ElementHandle<Element>>
156162
getLocation: () => Vector
157163
}
158164

@@ -389,10 +395,15 @@ export const createCursor = (
389395
*/
390396
click?: ClickOptions
391397
/**
392-
* Default options for the `scrollIntoView` function
393-
* @default ScrollOptions
394-
*/
398+
* Default options for the `scrollIntoView` function
399+
* @default ScrollOptions
400+
*/
395401
scrollIntoView?: ScrollOptions
402+
/**
403+
* Default options for the `getElement` function
404+
* @default GetElementOptions
405+
*/
406+
getElement?: GetElementOptions
396407
} = {}
397408
): GhostCursor => {
398409
// this is kind of arbitrary, not a big fan but it seems to work
@@ -496,9 +507,16 @@ export const createCursor = (
496507

497508
try {
498509
await delay(optionsResolved.hesitate)
499-
await page.mouse.down()
510+
511+
const cdpClient = getCDPClient(page)
512+
const dispatchParams: Omit<Protocol.Input.DispatchMouseEventRequest, 'type'> = {
513+
...previous,
514+
button: 'left',
515+
clickCount: 1
516+
}
517+
await cdpClient.send('Input.dispatchMouseEvent', { ...dispatchParams, type: 'mousePressed' })
500518
await delay(optionsResolved.waitForClick)
501-
await page.mouse.up()
519+
await cdpClient.send('Input.dispatchMouseEvent', { ...dispatchParams, type: 'mouseReleased' })
502520
} catch (error) {
503521
log('Warning: could not click mouse, error message:', error)
504522
}
@@ -529,34 +547,8 @@ export const createCursor = (
529547
}
530548

531549
actions.toggleRandomMove(false)
532-
let elem: ElementHandle<Element> | null = null
533-
if (typeof selector === 'string') {
534-
if (selector.startsWith('//') || selector.startsWith('(//')) {
535-
selector = `xpath/.${selector}`
536-
if (optionsResolved.waitForSelector !== undefined) {
537-
await page.waitForSelector(selector, {
538-
timeout: optionsResolved.waitForSelector
539-
})
540-
}
541-
const [handle] = await page.$$(selector)
542-
elem = handle.asElement() as ElementHandle<Element>
543-
} else {
544-
if (optionsResolved.waitForSelector !== undefined) {
545-
await page.waitForSelector(selector, {
546-
timeout: optionsResolved.waitForSelector
547-
})
548-
}
549-
elem = await page.$(selector)
550-
}
551-
if (elem === null) {
552-
throw new Error(
553-
`Could not find element with selector "${selector}", make sure you're waiting for the elements by specifying "waitForSelector"`
554-
)
555-
}
556-
} else {
557-
// ElementHandle
558-
elem = selector
559-
}
550+
551+
const elem = await this.getElement(selector, optionsResolved)
560552

561553
// Make sure the object is in view
562554
await this.scrollIntoView(elem, optionsResolved)
@@ -623,7 +615,7 @@ export const createCursor = (
623615
await delay(optionsResolved.moveDelay * (optionsResolved.randomizeMoveDelay ? Math.random() : 1))
624616
},
625617

626-
async scrollIntoView (elem: ElementHandle, options?: ScrollOptions): Promise<void> {
618+
async scrollIntoView (selector: string | ElementHandle, options?: ScrollOptions): Promise<void> {
627619
const optionsResolved = {
628620
scrollSpeed: 100,
629621
scrollDelay: 200,
@@ -632,6 +624,8 @@ export const createCursor = (
632624
...options
633625
} satisfies ScrollOptions
634626

627+
const elem = await this.getElement(selector, optionsResolved)
628+
635629
const {
636630
viewportWidth,
637631
viewportHeight,
@@ -807,6 +801,39 @@ export const createCursor = (
807801
destination
808802
)
809803
await delay(optionsResolved.scrollDelay)
804+
},
805+
806+
async getElement (selector: string | ElementHandle, options?: GetElementOptions): Promise<ElementHandle<Element>> {
807+
const optionsResolved = {
808+
...defaultOptions?.getElement,
809+
...options
810+
} satisfies GetElementOptions
811+
812+
let elem: ElementHandle<Element> | null = null
813+
if (typeof selector === 'string') {
814+
if (selector.startsWith('//') || selector.startsWith('(//')) {
815+
selector = `xpath/.${selector}`
816+
if (optionsResolved.waitForSelector !== undefined) {
817+
await page.waitForSelector(selector, { timeout: optionsResolved.waitForSelector })
818+
}
819+
const [handle] = await page.$$(selector)
820+
elem = handle.asElement() as ElementHandle<Element> | null
821+
} else {
822+
if (optionsResolved.waitForSelector !== undefined) {
823+
await page.waitForSelector(selector, { timeout: optionsResolved.waitForSelector })
824+
}
825+
elem = await page.$(selector)
826+
}
827+
if (elem === null) {
828+
throw new Error(
829+
`Could not find element with selector "${selector}", make sure you're waiting for the elements by specifying "waitForSelector"`
830+
)
831+
}
832+
} else {
833+
// ElementHandle
834+
elem = selector
835+
}
836+
return elem
810837
}
811838
}
812839

0 commit comments

Comments
 (0)