Skip to content

Commit 92eac19

Browse files
authored
Merge branch 'master' into path
2 parents 5c4b08c + 429518d commit 92eac19

2 files changed

Lines changed: 68 additions & 58 deletions

File tree

src/math.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export const mult = (a: Vector, b: number): Vector => ({ x: a.x * b, y: a.y * b
2222
export const add = (a: Vector, b: Vector): Vector => ({ x: a.x + b.x, y: a.y + b.y })
2323

2424
export const extrapolate = (a: Vector, b: Vector): Vector => add(b, sub(b, a))
25-
2625
export const scale = (value: number, range1: [number, number], range2: [number, number]): number =>
2726
(value - range1[0]) * (range2[1] - range2[0]) / (range1[1] - range1[0]) + range2[0]
2827

src/spoof.ts

Lines changed: 68 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ export interface ScrollOptions {
5151
*/
5252
readonly scrollSpeed?: number
5353
/**
54-
* Time to wait after scrolling.
55-
* @default 200
56-
*/
54+
* Time to wait after scrolling.
55+
* @default 200
56+
*/
5757
readonly scrollDelay?: number
5858
}
5959

@@ -142,36 +142,44 @@ export interface MoveToOptions extends PathOptions, Pick<MoveOptions, 'moveDelay
142142
export type ScrollToDestination = Partial<Vector> | 'top' | 'bottom' | 'left' | 'right'
143143

144144
export interface GhostCursor {
145+
/** Toggles random mouse movements on or off. */
145146
toggleRandomMove: (random: boolean) => void
147+
/** Simulates a mouse click at the specified selector or element. */
146148
click: (
147149
selector?: string | ElementHandle,
148150
options?: ClickOptions
149151
) => Promise<void>
152+
/** Moves the mouse to the specified selector or element. */
150153
move: (
151154
selector: string | ElementHandle,
152155
options?: MoveOptions
153156
) => Promise<void>
157+
/** Moves the mouse to the specified destination point. */
154158
moveTo: (
155159
destination: Vector,
156160
options?: MoveToOptions) => Promise<void>
161+
/** Scrolls the element into view. If already in view, no scroll occurs. */
157162
scrollIntoView: (
158163
selector: ElementHandle,
159164
options?: ScrollIntoViewOptions) => Promise<void>
165+
/** Scrolls to the specified destination point. */
160166
scrollTo: (
161167
destination: ScrollToDestination,
162168
options?: ScrollOptions) => Promise<void>
169+
/** Scrolls the page the distance set by `delta`. */
163170
scroll: (
164171
delta: Partial<Vector>,
165172
options?: ScrollOptions) => Promise<void>
173+
/** Gets the element via a selector. Can use an XPath. */
166174
getElement: (
167175
selector: string | ElementHandle,
168176
options?: GetElementOptions) => Promise<ElementHandle<Element>>
177+
/** Get current location of the cursor. */
169178
getLocation: () => Vector
170179
/**
171-
* Defined only if `visible=true` is passed.
172-
*
173-
* NOTE: Must be a promise, since we can't `await` in the constructor.
174-
*/
180+
* Make the cursor no longer visible.
181+
* Defined only if `visible=true` was passed.
182+
*/
175183
removeMouseHelper?: Promise<() => Promise<void>>
176184
}
177185

@@ -222,50 +230,55 @@ export const getRandomPagePoint = async (page: Page): Promise<Vector> => {
222230
})
223231
}
224232

225-
/** Using this method to get correct position of Inline elements (elements like `<a>`) */
226-
const getElementBox = async (
233+
/** Get correct position of Inline elements (elements like `<a>`). Has fallback. */
234+
export const getElementBox = async (
227235
page: Page,
228236
element: ElementHandle,
229-
relativeToMainFrame: boolean = true
230-
): Promise<BoundingBox | null> => {
231-
const objectId = element.remoteObject().objectId
232-
if (objectId === undefined) {
233-
return null
234-
}
235-
237+
relativeToMainFrame: boolean = true): Promise<BoundingBox> => {
236238
try {
239+
const objectId = element.remoteObject().objectId
240+
if (objectId === undefined) throw new Error('Element objectId is undefined, falling back to alternative methods')
241+
237242
const quads = await getCDPClient(page).send('DOM.getContentQuads', { objectId })
238-
const elementBox = {
243+
const elementBox: BoundingBox = {
239244
x: quads.quads[0][0],
240245
y: quads.quads[0][1],
241246
width: quads.quads[0][4] - quads.quads[0][0],
242247
height: quads.quads[0][5] - quads.quads[0][1]
243248
}
244249
if (!relativeToMainFrame) {
245250
const elementFrame = await element.contentFrame()
246-
const iframes =
247-
elementFrame != null
248-
? await elementFrame.parentFrame()?.$$('xpath/.//iframe')
249-
: null
250-
let frame: ElementHandle<Node> | undefined
251-
if (iframes != null) {
251+
const iframes = await elementFrame?.parentFrame()?.$$('xpath/.//iframe')
252+
if (iframes !== undefined && iframes !== null) {
253+
let frame: ElementHandle<Node> | undefined
252254
for (const iframe of iframes) {
253-
if ((await iframe.contentFrame()) === elementFrame) frame = iframe
255+
if ((await iframe.contentFrame()) === elementFrame) {
256+
frame = iframe
257+
}
258+
}
259+
if (frame !== undefined && frame != null) {
260+
const frameBox = await frame.boundingBox()
261+
if (frameBox !== null) {
262+
elementBox.x -= frameBox.x
263+
elementBox.y -= frameBox.y
264+
}
254265
}
255-
}
256-
if (frame != null) {
257-
const boundingBox = await frame.boundingBox()
258-
elementBox.x =
259-
boundingBox !== null ? elementBox.x - boundingBox.x : elementBox.x
260-
elementBox.y =
261-
boundingBox !== null ? elementBox.y - boundingBox.y : elementBox.y
262266
}
263267
}
264268

265269
return elementBox
266-
} catch (_) {
267-
log('Quads not found, trying regular boundingBox')
268-
return await element.boundingBox()
270+
} catch {
271+
try {
272+
log('Quads not found, trying regular boundingBox')
273+
const elementBox = await element.boundingBox()
274+
if (elementBox === null) throw new Error('Element boundingBox is null, falling back to getBoundingClientRect')
275+
return elementBox
276+
} catch {
277+
log('BoundingBox null, using getBoundingClientRect')
278+
return await element.evaluate((el) =>
279+
el.getBoundingClientRect() as BoundingBox
280+
)
281+
}
269282
}
270283
}
271284

@@ -281,20 +294,6 @@ const intersectsElement = (vec: Vector, box: BoundingBox): boolean => {
281294
)
282295
}
283296

284-
const boundingBoxWithFallback = async (
285-
page: Page,
286-
elem: ElementHandle<Element>
287-
): Promise<BoundingBox> => {
288-
let box = await getElementBox(page, elem)
289-
if (box == null) {
290-
box = (await elem.evaluate((el: Element) =>
291-
el.getBoundingClientRect()
292-
)) as BoundingBox
293-
}
294-
295-
return box
296-
}
297-
298297
export const createCursor = (
299298
page: Page,
300299
/**
@@ -350,7 +349,7 @@ export const createCursor = (
350349
// Initial state: mouse is not moving
351350
let moving: boolean = false
352351

353-
// Move the mouse over a number of vectors
352+
/** Move the mouse over a number of vectors */
354353
const tracePath = async (
355354
vectors: Iterable<Vector | TimedVector>,
356355
abortOnMove: boolean = false
@@ -383,7 +382,7 @@ export const createCursor = (
383382
}
384383
}
385384
}
386-
// Start random mouse movements. Function recursively calls itself
385+
/** Start random mouse movements. Function recursively calls itself. */
387386
const randomMove = async (options?: RandomMoveOptions): Promise<void> => {
388387
const optionsResolved = {
389388
moveDelay: 2000,
@@ -409,14 +408,17 @@ export const createCursor = (
409408
}
410409

411410
const actions: GhostCursor = {
411+
/** Toggles random mouse movements on or off. */
412412
toggleRandomMove (random: boolean): void {
413413
moving = !random
414414
},
415415

416+
/** Get current location of the cursor. */
416417
getLocation (): Vector {
417418
return previous
418419
},
419420

421+
/** Simulates a mouse click at the specified selector or element. */
420422
async click (
421423
selector?: string | ElementHandle,
422424
options?: ClickOptions
@@ -465,6 +467,7 @@ export const createCursor = (
465467
actions.toggleRandomMove(wasRandom)
466468
},
467469

470+
/** Moves the mouse to the specified selector or element. */
468471
async move (
469472
selector: string | ElementHandle,
470473
options?: MoveOptions
@@ -492,7 +495,7 @@ export const createCursor = (
492495
// Make sure the object is in view
493496
await this.scrollIntoView(elem, optionsResolved)
494497

495-
const box = await boundingBoxWithFallback(page, elem)
498+
const box = await getElementBox(page, elem)
496499
const { height, width } = box
497500
const destination = (optionsResolved.destination !== undefined)
498501
? add(box, optionsResolved.destination)
@@ -522,7 +525,7 @@ export const createCursor = (
522525

523526
actions.toggleRandomMove(true)
524527

525-
const newBoundingBox = await boundingBoxWithFallback(page, elem)
528+
const newBoundingBox = await getElementBox(page, elem)
526529

527530
// It's possible that the element that is being moved towards
528531
// has moved to a different location by the time
@@ -538,6 +541,7 @@ export const createCursor = (
538541
await delay(optionsResolved.moveDelay * (optionsResolved.randomizeMoveDelay ? Math.random() : 1))
539542
},
540543

544+
/** Moves the mouse to the specified destination point. */
541545
async moveTo (destination: Vector, options?: MoveToOptions): Promise<void> {
542546
const optionsResolved = {
543547
moveDelay: 0,
@@ -554,6 +558,7 @@ export const createCursor = (
554558
await delay(optionsResolved.moveDelay * (optionsResolved.randomizeMoveDelay ? Math.random() : 1))
555559
},
556560

561+
/** Scrolls the element into view. If already in view, no scroll occurs. */
557562
async scrollIntoView (selector: string | ElementHandle, options?: ScrollIntoViewOptions): Promise<void> {
558563
const optionsResolved = {
559564
scrollDelay: 200,
@@ -585,7 +590,7 @@ export const createCursor = (
585590
}
586591
))
587592

588-
const elemBoundingBox = await boundingBoxWithFallback(page, elem) // is relative to viewport
593+
const elemBoundingBox = await getElementBox(page, elem) // is relative to viewport
589594
const elemBox = {
590595
top: elemBoundingBox.y,
591596
left: elemBoundingBox.x,
@@ -672,6 +677,7 @@ export const createCursor = (
672677
}
673678
},
674679

680+
/** Scrolls the page the distance set by `delta`. */
675681
async scroll (delta: Partial<Vector>, options?: ScrollOptions) {
676682
const optionsResolved = {
677683
scrollDelay: 200,
@@ -732,6 +738,7 @@ export const createCursor = (
732738
await delay(optionsResolved.scrollDelay)
733739
},
734740

741+
/** Scrolls to the specified destination point. */
735742
async scrollTo (destination: ScrollToDestination, options?: ScrollOptions) {
736743
const optionsResolved = {
737744
scrollDelay: 200,
@@ -775,6 +782,7 @@ export const createCursor = (
775782
}, optionsResolved)
776783
},
777784

785+
/** Gets the element via a selector. Can use an XPath. */
778786
async getElement (selector: string | ElementHandle, options?: GetElementOptions): Promise<ElementHandle<Element>> {
779787
const optionsResolved = {
780788
...defaultOptions?.getElement,
@@ -809,11 +817,14 @@ export const createCursor = (
809817
}
810818
}
811819

812-
if (visible) {
813-
const removeMouseHelper = installMouseHelper(page).then(
820+
/**
821+
* Make the cursor no longer visible.
822+
* Defined only if `visible=true` was passed.
823+
*/
824+
actions.removeMouseHelper = visible
825+
? installMouseHelper(page).then(
814826
({ removeMouseHelper }) => removeMouseHelper)
815-
actions.removeMouseHelper = removeMouseHelper
816-
}
827+
: undefined
817828

818829
// Start random mouse movements. Do not await the promise but return immediately
819830
if (performRandomMoves) {

0 commit comments

Comments
 (0)