jumpy-lion 0.0.34 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +510 -0
- package/dist/fingerprinting/anti-webgpu/background.js +4 -6
- package/dist/fingerprinting/anti-webgpu/data/content_script/inject.js +42 -42
- package/dist/fingerprinting/anti-webgpu/data/content_script/page_context/inject.js +172 -179
- package/dist/fingerprinting/anti-webgpu/data/icons/128.png +0 -0
- package/dist/fingerprinting/anti-webgpu/data/icons/16.png +0 -0
- package/dist/fingerprinting/anti-webgpu/data/icons/32.png +0 -0
- package/dist/fingerprinting/anti-webgpu/data/icons/48.png +0 -0
- package/dist/fingerprinting/anti-webgpu/data/icons/64.png +0 -0
- package/dist/fingerprinting/anti-webgpu/data/popup/popup.css +88 -0
- package/dist/fingerprinting/anti-webgpu/data/popup/popup.html +58 -0
- package/dist/fingerprinting/anti-webgpu/data/popup/popup.js +96 -95
- package/dist/fingerprinting/anti-webgpu/lib/chrome.js +249 -255
- package/dist/fingerprinting/anti-webgpu/lib/common.js +71 -72
- package/dist/fingerprinting/anti-webgpu/lib/config.js +14 -13
- package/dist/fingerprinting/anti-webgpu/lib/runtime.js +107 -109
- package/dist/fingerprinting/anti-webgpu/manifest.json +58 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/page.d.ts +72 -1
- package/dist/page.d.ts.map +1 -1
- package/dist/page.js +574 -11
- package/dist/page.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
package/dist/page.js
CHANGED
|
@@ -3,6 +3,22 @@ import { load as cheerioLoad } from 'cheerio';
|
|
|
3
3
|
import CDP from 'chrome-remote-interface';
|
|
4
4
|
import { log } from 'crawlee';
|
|
5
5
|
import { PAGE_CLOSED, PAGE_CREATED } from './events.js';
|
|
6
|
+
// Constants for timing and delays
|
|
7
|
+
const PRESS_RELEASE_DELAY_MS = 50; // ms between mouse press and release
|
|
8
|
+
const SCROLL_ANIMATION_DELAY_MS = 200; // ms to wait for scroll animations
|
|
9
|
+
const OPTION_LOAD_DELAY_MS = 300; // ms to wait for new options to load in dropdowns
|
|
10
|
+
const DROPDOWN_OPEN_DELAY_MS = 300; // ms to wait for dropdown to open
|
|
11
|
+
const HUMAN_DELAY_MS = 200; // ms delay for human-like interactions
|
|
12
|
+
const SCROLL_COMPLETE_DELAY_MS = 300; // ms to wait for scroll to complete
|
|
13
|
+
// Constants for stability and positioning
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 30000; // Default timeout for operations
|
|
15
|
+
const POSITION_STABILITY_TIMEOUT_MS = 2000; // Timeout for position stabilization
|
|
16
|
+
const POSITION_CHECK_INTERVAL_MS = 100; // Interval for position checks
|
|
17
|
+
const POSITION_STABILITY_THRESHOLD = 3; // Number of consecutive stable checks required
|
|
18
|
+
const POSITION_TOLERANCE_PX = 1; // Maximum pixel difference to consider "stable"
|
|
19
|
+
const VIEWPORT_TOLERANCE_PX = 50; // Tolerance for viewport positioning
|
|
20
|
+
// Constants for polling and retries
|
|
21
|
+
const POLLING_INTERVAL_MS = 100; // Default polling interval
|
|
6
22
|
/**
|
|
7
23
|
* Creates a proxy that wraps CdpPage methods with reconnection logic
|
|
8
24
|
|
|
@@ -192,7 +208,7 @@ export default class CdpPage extends EventEmitter {
|
|
|
192
208
|
// Navigation method
|
|
193
209
|
async goto(url, options) {
|
|
194
210
|
const { Page, Runtime } = this.client;
|
|
195
|
-
const timeout = options?.timeout ??
|
|
211
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
196
212
|
// Clear browser cache before navigation
|
|
197
213
|
// await Network.clearBrowserCache().catch(e => log.error('Failed to clear cache:', e));
|
|
198
214
|
await Page.navigate({ url });
|
|
@@ -221,7 +237,7 @@ export default class CdpPage extends EventEmitter {
|
|
|
221
237
|
*/
|
|
222
238
|
async reload(options) {
|
|
223
239
|
const { Page, Runtime } = this.client;
|
|
224
|
-
const timeout = options?.timeout ??
|
|
240
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
225
241
|
const waitUntil = options?.waitUntil ?? 'load';
|
|
226
242
|
await Page.reload({});
|
|
227
243
|
let waitEvent;
|
|
@@ -270,23 +286,346 @@ export default class CdpPage extends EventEmitter {
|
|
|
270
286
|
return result.value;
|
|
271
287
|
}
|
|
272
288
|
// Click method
|
|
273
|
-
async click(selector) {
|
|
289
|
+
async click(selector, options) {
|
|
274
290
|
await Promise.all([
|
|
275
291
|
this._enableDomain('DOM'),
|
|
276
292
|
this._enableDomain('Input'),
|
|
277
293
|
]);
|
|
278
294
|
const { DOM, Input } = this.client;
|
|
279
|
-
|
|
295
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
296
|
+
const force = options?.force ?? false;
|
|
297
|
+
const delay = options?.delay ?? 0;
|
|
298
|
+
const button = options?.button ?? 'left';
|
|
299
|
+
const clickCount = options?.clickCount ?? 1;
|
|
300
|
+
// Wait for element to exist
|
|
301
|
+
await this.waitForSelector(selector, { timeout });
|
|
302
|
+
// Get element details and check visibility
|
|
303
|
+
const elementInfo = await this._getElementInfo(selector);
|
|
304
|
+
if (!elementInfo) {
|
|
305
|
+
throw new Error(`Element not found: ${selector}`);
|
|
306
|
+
}
|
|
307
|
+
// Check if element is visible (unless force is true)
|
|
308
|
+
if (!force) {
|
|
309
|
+
const isVisible = await this._isElementVisible(selector);
|
|
310
|
+
if (!isVisible) {
|
|
311
|
+
throw new Error(`Element is not visible: ${selector}. Use force: true to bypass the visibility check and attempt interaction anyway. Warning: Interacting with non-visible elements may lead to unexpected behavior or errors.`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Check if element is clickable (unless force is true)
|
|
315
|
+
if (!force) {
|
|
316
|
+
const isClickable = await this._isElementClickable(selector);
|
|
317
|
+
if (!isClickable) {
|
|
318
|
+
throw new Error(`Element is not clickable: ${selector}. Use force: true to bypass this check.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Scroll element into view if needed
|
|
322
|
+
await this._scrollElementIntoView(selector);
|
|
323
|
+
// Wait for scroll animations to complete by polling for position stabilization
|
|
324
|
+
// Wait for the element's position to stabilize after scrolling.
|
|
325
|
+
// This is necessary because scroll animations or layout changes may cause the element's position
|
|
326
|
+
// to shift for a short period. If we attempt to click before the position is stable, the click
|
|
327
|
+
// may be sent to the wrong location, potentially missing the target element or causing unreliable
|
|
328
|
+
// automation. Polling for position stabilization ensures the click is performed accurately.
|
|
329
|
+
await this._waitForElementPositionToStabilize(selector, POSITION_STABILITY_TIMEOUT_MS, POSITION_CHECK_INTERVAL_MS);
|
|
330
|
+
// Get updated element position after scrolling
|
|
280
331
|
const { nodeId } = await this.getNodeId(selector);
|
|
281
|
-
|
|
332
|
+
// For hidden elements, we need to handle the case where box model can't be computed
|
|
333
|
+
let model;
|
|
334
|
+
try {
|
|
335
|
+
const boxModelResult = await DOM.getBoxModel({ nodeId });
|
|
336
|
+
model = boxModelResult.model;
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (force) {
|
|
340
|
+
// If force is true, try to get a default position
|
|
341
|
+
const fallbackElementInfo = await this._getElementInfo(selector);
|
|
342
|
+
if (fallbackElementInfo) {
|
|
343
|
+
const x = fallbackElementInfo.boundingBox.x + fallbackElementInfo.boundingBox.width / 2;
|
|
344
|
+
const y = fallbackElementInfo.boundingBox.y + fallbackElementInfo.boundingBox.height / 2;
|
|
345
|
+
await this._performClick(Input, x, y, button, clickCount, delay);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
throw new Error(`Could not get box model for node: ${nodeId}. Element might be hidden or not rendered.`);
|
|
350
|
+
}
|
|
282
351
|
if (!model) {
|
|
283
352
|
throw new Error(`Could not get box model for node: ${nodeId}`);
|
|
284
353
|
}
|
|
354
|
+
// Calculate click coordinates (center of the element)
|
|
285
355
|
const x = (model.content[0] + model.content[4]) / 2;
|
|
286
356
|
const y = (model.content[1] + model.content[5]) / 2;
|
|
357
|
+
// Verify element is still in viewport after scrolling (with some tolerance)
|
|
358
|
+
if (!force) {
|
|
359
|
+
const isInViewport = await this._isElementInViewport(selector);
|
|
360
|
+
if (!isInViewport) {
|
|
361
|
+
// Give a bit more time for scroll animations to complete
|
|
362
|
+
await new Promise(resolve => setTimeout(resolve, HUMAN_DELAY_MS));
|
|
363
|
+
const isInViewportAfterDelay = await this._isElementInViewport(selector);
|
|
364
|
+
if (!isInViewportAfterDelay) {
|
|
365
|
+
throw new Error(`Element is not in viewport after scrolling: ${selector}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Perform the click with human-like interaction
|
|
370
|
+
await this._performClick(Input, x, y, button, clickCount, delay);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Performs a human-like click with proper mouse events
|
|
374
|
+
*/
|
|
375
|
+
async _performClick(Input, x, y, button, clickCount, delay) {
|
|
376
|
+
// Move mouse cursor to target coordinates (x, y)
|
|
287
377
|
await Input.dispatchMouseEvent({ type: 'mouseMoved', x, y });
|
|
288
|
-
|
|
289
|
-
|
|
378
|
+
// Small delay to simulate human behavior
|
|
379
|
+
if (delay > 0) {
|
|
380
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
381
|
+
}
|
|
382
|
+
// Press mouse button
|
|
383
|
+
await Input.dispatchMouseEvent({
|
|
384
|
+
type: 'mousePressed',
|
|
385
|
+
x,
|
|
386
|
+
y,
|
|
387
|
+
button,
|
|
388
|
+
clickCount,
|
|
389
|
+
});
|
|
390
|
+
// Small delay between press and release
|
|
391
|
+
await new Promise(resolve => setTimeout(resolve, PRESS_RELEASE_DELAY_MS));
|
|
392
|
+
// Release mouse button
|
|
393
|
+
await Input.dispatchMouseEvent({
|
|
394
|
+
type: 'mouseReleased',
|
|
395
|
+
x,
|
|
396
|
+
y,
|
|
397
|
+
button,
|
|
398
|
+
clickCount,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Gets detailed information about an element
|
|
403
|
+
*/
|
|
404
|
+
async _getElementInfo(selector) {
|
|
405
|
+
return await this.evaluate((sel) => {
|
|
406
|
+
const element = document.querySelector(sel);
|
|
407
|
+
if (!element)
|
|
408
|
+
return null;
|
|
409
|
+
const rect = element.getBoundingClientRect();
|
|
410
|
+
const style = window.getComputedStyle(element);
|
|
411
|
+
// Check if element is visible
|
|
412
|
+
const isVisible = style.display !== 'none' &&
|
|
413
|
+
style.visibility !== 'hidden' &&
|
|
414
|
+
rect.width > 0 &&
|
|
415
|
+
rect.height > 0;
|
|
416
|
+
// Check if element is clickable
|
|
417
|
+
const isClickable = !element.hasAttribute('disabled') &&
|
|
418
|
+
!element.classList.contains('disabled') &&
|
|
419
|
+
style.pointerEvents !== 'none' &&
|
|
420
|
+
rect.width > 0 &&
|
|
421
|
+
rect.height > 0;
|
|
422
|
+
return {
|
|
423
|
+
nodeId: 0, // Will be set by CDP
|
|
424
|
+
boundingBox: {
|
|
425
|
+
x: rect.left,
|
|
426
|
+
y: rect.top,
|
|
427
|
+
width: rect.width,
|
|
428
|
+
height: rect.height,
|
|
429
|
+
},
|
|
430
|
+
isVisible,
|
|
431
|
+
isClickable,
|
|
432
|
+
};
|
|
433
|
+
}, selector);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Enhanced visibility check that considers more factors
|
|
437
|
+
*/
|
|
438
|
+
async _isElementVisible(selector) {
|
|
439
|
+
return await this.evaluate((sel) => {
|
|
440
|
+
const element = document.querySelector(sel);
|
|
441
|
+
if (!element)
|
|
442
|
+
return false;
|
|
443
|
+
const rect = element.getBoundingClientRect();
|
|
444
|
+
const style = window.getComputedStyle(element);
|
|
445
|
+
// Basic visibility checks
|
|
446
|
+
if (style.display === 'none' || style.visibility === 'hidden') {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
// Check if element has dimensions
|
|
450
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
// Check if element is not transparent
|
|
454
|
+
if (parseFloat(style.opacity) === 0) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
// Check if element is not clipped by overflow
|
|
458
|
+
const parent = element.parentElement;
|
|
459
|
+
if (parent) {
|
|
460
|
+
const parentStyle = window.getComputedStyle(parent);
|
|
461
|
+
if (parentStyle.overflow === 'hidden') {
|
|
462
|
+
const parentRect = parent.getBoundingClientRect();
|
|
463
|
+
if (rect.left >= parentRect.right ||
|
|
464
|
+
rect.right <= parentRect.left ||
|
|
465
|
+
rect.top >= parentRect.bottom ||
|
|
466
|
+
rect.bottom <= parentRect.top) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return true;
|
|
472
|
+
}, selector);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Enhanced clickable check that considers more factors
|
|
476
|
+
*/
|
|
477
|
+
async _isElementClickable(selector) {
|
|
478
|
+
return await this.evaluate((sel) => {
|
|
479
|
+
const element = document.querySelector(sel);
|
|
480
|
+
if (!element)
|
|
481
|
+
return false;
|
|
482
|
+
const rect = element.getBoundingClientRect();
|
|
483
|
+
const style = window.getComputedStyle(element);
|
|
484
|
+
// Check if element is disabled
|
|
485
|
+
if (element.hasAttribute('disabled') || element.classList.contains('disabled')) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
// Check if pointer events are disabled
|
|
489
|
+
if (style.pointerEvents === 'none') {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
// Check if element has dimensions
|
|
493
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
// Check if element is not transparent
|
|
497
|
+
if (parseFloat(style.opacity) === 0) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
// Check if element is not read-only (for form elements)
|
|
501
|
+
if (element.hasAttribute('readonly')) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
return true;
|
|
505
|
+
}, selector);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Enhanced scroll into view with better positioning
|
|
509
|
+
*/
|
|
510
|
+
async _scrollElementIntoView(selector) {
|
|
511
|
+
await this.evaluate((sel, tolerance) => {
|
|
512
|
+
const element = document.querySelector(sel);
|
|
513
|
+
if (!element)
|
|
514
|
+
return;
|
|
515
|
+
const rect = element.getBoundingClientRect();
|
|
516
|
+
const viewportHeight = window.innerHeight;
|
|
517
|
+
const viewportWidth = window.innerWidth;
|
|
518
|
+
// Check if element is already in viewport (with tolerance)
|
|
519
|
+
const isInViewport = rect.top >= -tolerance &&
|
|
520
|
+
rect.left >= -tolerance &&
|
|
521
|
+
rect.bottom <= viewportHeight + tolerance &&
|
|
522
|
+
rect.right <= viewportWidth + tolerance;
|
|
523
|
+
if (!isInViewport) {
|
|
524
|
+
// Scroll element into view with better positioning
|
|
525
|
+
element.scrollIntoView({
|
|
526
|
+
behavior: 'smooth',
|
|
527
|
+
block: 'center',
|
|
528
|
+
inline: 'center',
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}, selector, VIEWPORT_TOLERANCE_PX);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Waits for an element's position to stabilize by polling its position over time.
|
|
535
|
+
* This is useful for waiting for scroll animations or dynamic layout changes to complete.
|
|
536
|
+
*
|
|
537
|
+
* @param selector - The CSS selector for the element to monitor
|
|
538
|
+
* @param timeout - Maximum time to wait in milliseconds (default: 2000)
|
|
539
|
+
* @param checkInterval - How often to check position in milliseconds (default: 100)
|
|
540
|
+
* @param stabilityThreshold - Number of consecutive stable checks required (default: 3)
|
|
541
|
+
* @param tolerance - Maximum pixel difference to consider "stable" (default: 1)
|
|
542
|
+
*/
|
|
543
|
+
async _waitForElementPositionToStabilize(selector, timeout = POSITION_STABILITY_TIMEOUT_MS, checkInterval = POSITION_CHECK_INTERVAL_MS, stabilityThreshold = POSITION_STABILITY_THRESHOLD, tolerance = POSITION_TOLERANCE_PX) {
|
|
544
|
+
const startTime = Date.now();
|
|
545
|
+
let stableCount = 0;
|
|
546
|
+
let lastPosition = null;
|
|
547
|
+
while (Date.now() - startTime < timeout) {
|
|
548
|
+
try {
|
|
549
|
+
const currentPosition = await this.evaluate((sel) => {
|
|
550
|
+
const element = document.querySelector(sel);
|
|
551
|
+
if (!element)
|
|
552
|
+
return null;
|
|
553
|
+
const rect = element.getBoundingClientRect();
|
|
554
|
+
return {
|
|
555
|
+
x: Math.round(rect.left),
|
|
556
|
+
y: Math.round(rect.top),
|
|
557
|
+
width: Math.round(rect.width),
|
|
558
|
+
height: Math.round(rect.height),
|
|
559
|
+
};
|
|
560
|
+
}, selector);
|
|
561
|
+
if (!currentPosition) {
|
|
562
|
+
// Element not found, reset stability counter
|
|
563
|
+
stableCount = 0;
|
|
564
|
+
lastPosition = null;
|
|
565
|
+
}
|
|
566
|
+
else if (!lastPosition) {
|
|
567
|
+
// First position reading
|
|
568
|
+
lastPosition = currentPosition;
|
|
569
|
+
stableCount = 1;
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// Check if position is stable (within tolerance)
|
|
573
|
+
const deltaX = Math.abs(currentPosition.x - lastPosition.x);
|
|
574
|
+
const deltaY = Math.abs(currentPosition.y - lastPosition.y);
|
|
575
|
+
const deltaWidth = Math.abs(currentPosition.width - lastPosition.width);
|
|
576
|
+
const deltaHeight = Math.abs(currentPosition.height - lastPosition.height);
|
|
577
|
+
const isStable = deltaX <= tolerance &&
|
|
578
|
+
deltaY <= tolerance &&
|
|
579
|
+
deltaWidth <= tolerance &&
|
|
580
|
+
deltaHeight <= tolerance;
|
|
581
|
+
if (isStable) {
|
|
582
|
+
stableCount++;
|
|
583
|
+
if (stableCount >= stabilityThreshold) {
|
|
584
|
+
// Position has been stable for required number of checks
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
// Position changed, reset counter
|
|
590
|
+
stableCount = 1;
|
|
591
|
+
}
|
|
592
|
+
lastPosition = currentPosition;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
// Error reading position, reset stability counter
|
|
597
|
+
stableCount = 0;
|
|
598
|
+
lastPosition = null;
|
|
599
|
+
}
|
|
600
|
+
// Wait before next check
|
|
601
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
602
|
+
}
|
|
603
|
+
// If we reach here, we timed out - this is acceptable as some elements may never stabilize
|
|
604
|
+
// If we reach here, we timed out. This means the element's position did not stabilize within the allotted time.
|
|
605
|
+
// Proceeding may cause interaction failures if the element is still moving or changing position.
|
|
606
|
+
// Callers should be aware that stabilization was not achieved and handle this case appropriately.
|
|
607
|
+
}
|
|
608
|
+
// Public wrapper for tests and external use
|
|
609
|
+
async waitForElementPositionToStabilize(selector, timeout = POSITION_STABILITY_TIMEOUT_MS, checkInterval = POSITION_CHECK_INTERVAL_MS, stabilityThreshold = POSITION_STABILITY_THRESHOLD, tolerance = POSITION_TOLERANCE_PX) {
|
|
610
|
+
return this._waitForElementPositionToStabilize(selector, timeout, checkInterval, stabilityThreshold, tolerance);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Check if element is within the viewport (with some tolerance)
|
|
614
|
+
*/
|
|
615
|
+
async _isElementInViewport(selector) {
|
|
616
|
+
return await this.evaluate((sel, tolerance) => {
|
|
617
|
+
const element = document.querySelector(sel);
|
|
618
|
+
if (!element)
|
|
619
|
+
return false;
|
|
620
|
+
const rect = element.getBoundingClientRect();
|
|
621
|
+
const viewportHeight = window.innerHeight;
|
|
622
|
+
const viewportWidth = window.innerWidth;
|
|
623
|
+
// Add some tolerance for scroll positioning
|
|
624
|
+
return rect.top >= -tolerance &&
|
|
625
|
+
rect.left >= -tolerance &&
|
|
626
|
+
rect.bottom <= viewportHeight + tolerance &&
|
|
627
|
+
rect.right <= viewportWidth + tolerance;
|
|
628
|
+
}, selector, VIEWPORT_TOLERANCE_PX);
|
|
290
629
|
}
|
|
291
630
|
// Type method
|
|
292
631
|
async type(selector, text, options) {
|
|
@@ -334,16 +673,15 @@ export default class CdpPage extends EventEmitter {
|
|
|
334
673
|
}
|
|
335
674
|
// Wait for selector
|
|
336
675
|
async waitForSelector(selector, options) {
|
|
337
|
-
const timeout = options?.timeout ??
|
|
338
|
-
const
|
|
339
|
-
const maxAttempts = Math.ceil(timeout / pollingInterval);
|
|
676
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
677
|
+
const maxAttempts = Math.ceil(timeout / POLLING_INTERVAL_MS);
|
|
340
678
|
let attempts = 0;
|
|
341
679
|
while (attempts < maxAttempts) {
|
|
342
680
|
const exists = await this.elementExists(selector);
|
|
343
681
|
if (exists) {
|
|
344
682
|
return;
|
|
345
683
|
}
|
|
346
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
684
|
+
await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL_MS));
|
|
347
685
|
attempts++;
|
|
348
686
|
}
|
|
349
687
|
throw new Error(`Timeout waiting for selector: ${selector}`);
|
|
@@ -652,6 +990,231 @@ export default class CdpPage extends EventEmitter {
|
|
|
652
990
|
this.isReconnecting = false;
|
|
653
991
|
}
|
|
654
992
|
}
|
|
993
|
+
/**
|
|
994
|
+
* Selects one or more options from a select element or dropdown.
|
|
995
|
+
* Supports both regular select elements and virtualized dropdowns with hidden options.
|
|
996
|
+
*
|
|
997
|
+
* @param selector - The CSS selector for the select element or dropdown trigger
|
|
998
|
+
* @param targetSelector - CSS selector(s) for the option(s) to select. Can be a single selector or array of selectors
|
|
999
|
+
* @param options - Additional options for the selection
|
|
1000
|
+
*/
|
|
1001
|
+
async selectOption(selector, targetSelector, options) {
|
|
1002
|
+
await Promise.all([
|
|
1003
|
+
this._enableDomain('DOM'),
|
|
1004
|
+
this._enableDomain('Input'),
|
|
1005
|
+
]);
|
|
1006
|
+
// DOM and Input domains are enabled and actively used in this method
|
|
1007
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
1008
|
+
const force = options?.force ?? false;
|
|
1009
|
+
const waitForOptions = options?.waitForOptions ?? true;
|
|
1010
|
+
const maxScrollAttempts = options?.maxScrollAttempts ?? 10;
|
|
1011
|
+
const optionSelector = options?.optionSelector;
|
|
1012
|
+
const dropdownSelector = options?.dropdownSelector;
|
|
1013
|
+
// Normalize target selectors to array
|
|
1014
|
+
const targetSelectors = Array.isArray(targetSelector) ? targetSelector : [targetSelector];
|
|
1015
|
+
// Wait for element to exist
|
|
1016
|
+
await this.waitForSelector(selector, { timeout });
|
|
1017
|
+
// Check if element is visible (unless force is true)
|
|
1018
|
+
if (!force) {
|
|
1019
|
+
const isVisible = await this._isElementVisible(selector);
|
|
1020
|
+
if (!isVisible) {
|
|
1021
|
+
throw new Error(`Element is not visible: ${selector}. Use force: true to bypass this check.`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// Check if element is disabled (unless force is true)
|
|
1025
|
+
if (!force) {
|
|
1026
|
+
const isDisabled = await this.evaluate((sel) => {
|
|
1027
|
+
const element = document.querySelector(sel);
|
|
1028
|
+
return element?.hasAttribute('disabled') || element?.getAttribute('disabled') === 'true';
|
|
1029
|
+
}, selector);
|
|
1030
|
+
if (isDisabled) {
|
|
1031
|
+
throw new Error(`Element is disabled: ${selector}. Use force: true to bypass this check.`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// Scroll element into view if needed
|
|
1035
|
+
await this._scrollElementIntoView(selector);
|
|
1036
|
+
// Wait for scroll animations to complete
|
|
1037
|
+
await new Promise(resolve => setTimeout(resolve, SCROLL_ANIMATION_DELAY_MS));
|
|
1038
|
+
// Check if this is a regular select element
|
|
1039
|
+
const isSelectElement = await this.evaluate((sel) => {
|
|
1040
|
+
const element = document.querySelector(sel);
|
|
1041
|
+
return element?.tagName === 'SELECT';
|
|
1042
|
+
}, selector);
|
|
1043
|
+
if (!isSelectElement) {
|
|
1044
|
+
// For non-select elements, both dropdownSelector and optionSelector are required
|
|
1045
|
+
if (!dropdownSelector) {
|
|
1046
|
+
throw new Error(`Element at ${selector} is not a select element. For custom dropdowns, you must provide a dropdownSelector option.`);
|
|
1047
|
+
}
|
|
1048
|
+
if (!optionSelector) {
|
|
1049
|
+
throw new Error(`Element at ${selector} is not a select element. For custom dropdowns, you must provide an optionSelector option.`);
|
|
1050
|
+
}
|
|
1051
|
+
// Handle custom dropdown
|
|
1052
|
+
await this._handleCustomDropdown(selector, targetSelectors, {
|
|
1053
|
+
timeout,
|
|
1054
|
+
force,
|
|
1055
|
+
waitForOptions,
|
|
1056
|
+
maxScrollAttempts,
|
|
1057
|
+
optionSelector,
|
|
1058
|
+
dropdownSelector,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
// Handle regular select element - use 'option' as default
|
|
1063
|
+
await this._handleRegularSelect(selector, targetSelectors, {
|
|
1064
|
+
timeout,
|
|
1065
|
+
force,
|
|
1066
|
+
optionSelector: optionSelector ?? 'option',
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Handles selection for regular HTML select elements
|
|
1072
|
+
*/
|
|
1073
|
+
async _handleRegularSelect(selector, targetSelectors, _options) {
|
|
1074
|
+
// Get option selector from options for HTML select element handling
|
|
1075
|
+
const result = await this.evaluate((sel, targetSels) => {
|
|
1076
|
+
const select = document.querySelector(sel);
|
|
1077
|
+
if (!select) {
|
|
1078
|
+
throw new Error(`Select element not found: ${sel}`);
|
|
1079
|
+
}
|
|
1080
|
+
// Check if multiple selection is allowed
|
|
1081
|
+
if (!select.multiple && targetSels.length > 1) {
|
|
1082
|
+
throw new Error(`Select element does not support multiple selection. Found ${targetSels.length} selectors but multiple is false.`);
|
|
1083
|
+
}
|
|
1084
|
+
// Clear previous selections
|
|
1085
|
+
if (select.multiple) {
|
|
1086
|
+
Array.from(select.options).forEach(option => {
|
|
1087
|
+
option.selected = false;
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
let foundCount = 0;
|
|
1091
|
+
const foundSelectors = [];
|
|
1092
|
+
// Select the options by CSS selector
|
|
1093
|
+
for (const targetSelector of targetSels) {
|
|
1094
|
+
let optionFound = false;
|
|
1095
|
+
// Find option by CSS selector
|
|
1096
|
+
const option = select.querySelector(targetSelector);
|
|
1097
|
+
if (option) {
|
|
1098
|
+
option.selected = true;
|
|
1099
|
+
optionFound = true;
|
|
1100
|
+
foundSelectors.push(targetSelector);
|
|
1101
|
+
}
|
|
1102
|
+
if (optionFound) {
|
|
1103
|
+
foundCount++;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
// Trigger change event
|
|
1107
|
+
const event = new Event('change', { bubbles: true });
|
|
1108
|
+
select.dispatchEvent(event);
|
|
1109
|
+
return { foundCount, foundSelectors, totalOptions: select.options.length };
|
|
1110
|
+
}, selector, targetSelectors);
|
|
1111
|
+
if (result.foundCount === 0) {
|
|
1112
|
+
throw new Error(`No options found for selectors: ${targetSelectors.join(', ')}. Available options: ${result.totalOptions}`);
|
|
1113
|
+
}
|
|
1114
|
+
if (result.foundCount < targetSelectors.length) {
|
|
1115
|
+
const missingSelectors = targetSelectors.filter(s => !result.foundSelectors.includes(s));
|
|
1116
|
+
throw new Error(`Some selectors not found: ${missingSelectors.join(', ')}. Found: ${result.foundSelectors.join(', ')}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Handles selection for custom dropdown elements
|
|
1121
|
+
*/
|
|
1122
|
+
async _handleCustomDropdown(selector, targetSelectors, options) {
|
|
1123
|
+
const { timeout, force, waitForOptions, maxScrollAttempts, optionSelector, dropdownSelector: dropdownSel, dropdownOpenDelay = DROPDOWN_OPEN_DELAY_MS } = options;
|
|
1124
|
+
// Click to open the dropdown
|
|
1125
|
+
await this.click(selector, { force });
|
|
1126
|
+
// Wait for dropdown to open
|
|
1127
|
+
await new Promise(resolve => setTimeout(resolve, dropdownOpenDelay));
|
|
1128
|
+
// Use the provided dropdown selector (required for custom dropdowns)
|
|
1129
|
+
const dropdownContainer = dropdownSel;
|
|
1130
|
+
// Wait for dropdown to be visible
|
|
1131
|
+
if (waitForOptions) {
|
|
1132
|
+
await this.waitForSelector(dropdownContainer, { timeout });
|
|
1133
|
+
}
|
|
1134
|
+
// Select each target selector
|
|
1135
|
+
for (const targetSelector of targetSelectors) {
|
|
1136
|
+
await this._selectOptionFromDropdown(dropdownContainer, targetSelector, {
|
|
1137
|
+
timeout,
|
|
1138
|
+
force,
|
|
1139
|
+
maxScrollAttempts,
|
|
1140
|
+
optionSelector,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Selects a single option from a dropdown container, handling virtualized lists
|
|
1146
|
+
*/
|
|
1147
|
+
async _selectOptionFromDropdown(dropdownSelector, targetSelector, dropdownOptions) {
|
|
1148
|
+
const { timeout, maxScrollAttempts, optionSelector } = dropdownOptions;
|
|
1149
|
+
const startTime = Date.now();
|
|
1150
|
+
let scrollAttempts = 0;
|
|
1151
|
+
let optionFound = false;
|
|
1152
|
+
while (scrollAttempts < maxScrollAttempts && !optionFound && (Date.now() - startTime) < timeout) {
|
|
1153
|
+
// Try to find and click the option
|
|
1154
|
+
const result = await this.evaluate((dropdownSel, targetSel, optSel) => {
|
|
1155
|
+
const dropdown = document.querySelector(dropdownSel);
|
|
1156
|
+
if (!dropdown)
|
|
1157
|
+
return { found: false, needsScroll: false };
|
|
1158
|
+
// Look for option elements using the provided selector
|
|
1159
|
+
const options = Array.from(dropdown.querySelectorAll(optSel));
|
|
1160
|
+
let targetOption = null;
|
|
1161
|
+
for (const option of options) {
|
|
1162
|
+
// Check if the option matches the target selector
|
|
1163
|
+
if (option.matches(targetSel)) {
|
|
1164
|
+
targetOption = option;
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (!targetOption) {
|
|
1169
|
+
// Check if we need to scroll to find more options
|
|
1170
|
+
const isScrollable = dropdown.scrollHeight > dropdown.clientHeight;
|
|
1171
|
+
const isAtBottom = dropdown.scrollTop + dropdown.clientHeight >= dropdown.scrollHeight - 1;
|
|
1172
|
+
return { found: false, needsScroll: isScrollable && !isAtBottom };
|
|
1173
|
+
}
|
|
1174
|
+
// Check if option is visible
|
|
1175
|
+
const rect = targetOption.getBoundingClientRect();
|
|
1176
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
1177
|
+
const isVisible = rect.top >= dropdownRect.top &&
|
|
1178
|
+
rect.bottom <= dropdownRect.bottom &&
|
|
1179
|
+
rect.left >= dropdownRect.left &&
|
|
1180
|
+
rect.right <= dropdownRect.right;
|
|
1181
|
+
if (!isVisible) {
|
|
1182
|
+
// Scroll to make the option visible
|
|
1183
|
+
targetOption.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1184
|
+
return { found: false, needsScroll: false, scrolled: true };
|
|
1185
|
+
}
|
|
1186
|
+
// Click the option
|
|
1187
|
+
targetOption.click();
|
|
1188
|
+
return { found: true, needsScroll: false };
|
|
1189
|
+
}, dropdownSelector, targetSelector, optionSelector);
|
|
1190
|
+
if (result.found) {
|
|
1191
|
+
optionFound = true;
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
if (result.needsScroll) {
|
|
1195
|
+
// Scroll down to load more options
|
|
1196
|
+
await this.evaluate((dropdownSel) => {
|
|
1197
|
+
const dropdown = document.querySelector(dropdownSel);
|
|
1198
|
+
if (dropdown) {
|
|
1199
|
+
dropdown.scrollTop += dropdown.clientHeight * 0.8;
|
|
1200
|
+
}
|
|
1201
|
+
}, dropdownSelector);
|
|
1202
|
+
scrollAttempts++;
|
|
1203
|
+
await new Promise(resolve => setTimeout(resolve, OPTION_LOAD_DELAY_MS)); // Wait for new options to load
|
|
1204
|
+
}
|
|
1205
|
+
else if (result.scrolled) {
|
|
1206
|
+
// Option was scrolled into view, try clicking it now
|
|
1207
|
+
await new Promise(resolve => setTimeout(resolve, SCROLL_COMPLETE_DELAY_MS)); // Wait for scroll to complete
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
// No more scrolling needed, option not found
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (!optionFound) {
|
|
1215
|
+
throw new Error(`Option "${targetSelector}" not found`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
655
1218
|
/**
|
|
656
1219
|
* Checks if the element specified by selector is visible (not display:none and not visibility:hidden).
|
|
657
1220
|
* The selector should be the root item which can be hidden, otherwise this function could return a false positive.
|