jumpy-lion 0.1.1 → 0.1.2
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/dist/browser-controller.d.ts +22 -3
- package/dist/browser-controller.d.ts.map +1 -1
- package/dist/browser-controller.js +316 -71
- package/dist/browser-controller.js.map +1 -1
- package/dist/browser-plugin.d.ts +56 -7
- package/dist/browser-plugin.d.ts.map +1 -1
- package/dist/browser-plugin.js +268 -56
- package/dist/browser-plugin.js.map +1 -1
- package/dist/browser-process/browser.d.ts +39 -0
- package/dist/browser-process/browser.d.ts.map +1 -1
- package/dist/browser-process/browser.js +125 -16
- package/dist/browser-process/browser.js.map +1 -1
- package/dist/browser-process/process.d.ts +9 -0
- package/dist/browser-process/process.d.ts.map +1 -1
- package/dist/browser-process/process.js +100 -6
- package/dist/browser-process/process.js.map +1 -1
- package/dist/browser-profiles/chrome/default.d.ts +116 -0
- package/dist/browser-profiles/chrome/default.d.ts.map +1 -1
- package/dist/browser-profiles/chrome/default.js +118 -1
- package/dist/browser-profiles/chrome/default.js.map +1 -1
- package/dist/browser-profiles/chrome/populate-profile.d.ts +76 -0
- package/dist/browser-profiles/chrome/populate-profile.d.ts.map +1 -0
- package/dist/browser-profiles/chrome/populate-profile.js +300 -0
- package/dist/browser-profiles/chrome/populate-profile.js.map +1 -0
- package/dist/browser-profiles/index.d.ts +1 -0
- package/dist/browser-profiles/index.d.ts.map +1 -1
- package/dist/browser-profiles/index.js +2 -0
- package/dist/browser-profiles/index.js.map +1 -1
- package/dist/crawler.d.ts +81 -9
- package/dist/crawler.d.ts.map +1 -1
- package/dist/crawler.js +26 -10
- package/dist/crawler.js.map +1 -1
- package/dist/fingerprinting/all-fingerprint-defender/_locales/en/messages.json +95 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-16ff15da.d.ts +2 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-16ff15da.d.ts.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-16ff15da.js +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-16ff15da.js.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-1a1456ec.d.ts +2 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-1a1456ec.d.ts.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-1a1456ec.js +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-1a1456ec.js.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-3a7b59dd.d.ts +83 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-3a7b59dd.d.ts.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-3a7b59dd.js +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-3a7b59dd.js.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-7ce85519.d.ts +2 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-7ce85519.d.ts.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-7ce85519.js +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-7ce85519.js.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-aaea1190.d.ts +2 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-aaea1190.d.ts.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-aaea1190.js +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-aaea1190.js.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-b4410958.d.ts +2 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-b4410958.d.ts.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-b4410958.js +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-b4410958.js.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-dfed3562.d.ts +2 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-dfed3562.d.ts.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-dfed3562.js +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-dfed3562.js.map +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/debounce-09920c81.css +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/options-fe2fb5aa.css +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/assets/popup-1886d2ef.css +1 -0
- package/dist/fingerprinting/all-fingerprint-defender/img/icon-128.png +0 -0
- package/dist/fingerprinting/all-fingerprint-defender/img/icon-16.png +0 -0
- package/dist/fingerprinting/all-fingerprint-defender/img/icon-24.png +0 -0
- package/dist/fingerprinting/all-fingerprint-defender/img/icon-32-disabled.png +0 -0
- package/dist/fingerprinting/all-fingerprint-defender/img/icon-32.png +0 -0
- package/dist/fingerprinting/all-fingerprint-defender/img/icon-48.png +0 -0
- package/dist/fingerprinting/all-fingerprint-defender/manifest.json +83 -0
- package/dist/fingerprinting/all-fingerprint-defender/options.html +17 -0
- package/dist/fingerprinting/all-fingerprint-defender/popup.html +23 -0
- package/dist/fingerprinting/anti-webgpu/background.d.ts +2 -0
- package/dist/fingerprinting/anti-webgpu/background.d.ts.map +1 -0
- package/dist/fingerprinting/anti-webgpu/background.js +4 -0
- package/dist/fingerprinting/anti-webgpu/background.js.map +1 -0
- package/dist/fingerprinting/anti-webgpu/data/content_script/inject.d.ts +2 -0
- package/dist/fingerprinting/anti-webgpu/data/content_script/inject.d.ts.map +1 -0
- package/dist/fingerprinting/anti-webgpu/data/content_script/inject.js +50 -0
- package/dist/fingerprinting/anti-webgpu/data/content_script/inject.js.map +1 -0
- package/dist/fingerprinting/anti-webgpu/data/content_script/page_context/inject.d.ts +2 -0
- package/dist/fingerprinting/anti-webgpu/data/content_script/page_context/inject.d.ts.map +1 -0
- package/dist/fingerprinting/anti-webgpu/data/content_script/page_context/inject.js +172 -0
- package/dist/fingerprinting/anti-webgpu/data/content_script/page_context/inject.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/fingerprinting/anti-webgpu/data/popup/popup.d.ts.map +1 -0
- package/dist/fingerprinting/anti-webgpu/data/popup/popup.html +58 -0
- package/dist/fingerprinting/anti-webgpu/data/popup/popup.js +96 -0
- package/dist/fingerprinting/anti-webgpu/data/popup/popup.js.map +1 -0
- package/dist/fingerprinting/anti-webgpu/lib/chrome.d.ts +2 -0
- package/dist/fingerprinting/anti-webgpu/lib/chrome.d.ts.map +1 -0
- package/dist/fingerprinting/anti-webgpu/lib/chrome.js +249 -0
- package/dist/fingerprinting/anti-webgpu/lib/chrome.js.map +1 -0
- package/dist/fingerprinting/anti-webgpu/lib/common.d.ts +2 -0
- package/dist/fingerprinting/anti-webgpu/lib/common.d.ts.map +1 -0
- package/dist/fingerprinting/anti-webgpu/lib/common.js +86 -0
- package/dist/fingerprinting/anti-webgpu/lib/common.js.map +1 -0
- package/dist/fingerprinting/anti-webgpu/lib/config.d.ts +2 -0
- package/dist/fingerprinting/anti-webgpu/lib/config.d.ts.map +1 -0
- package/dist/fingerprinting/anti-webgpu/lib/config.js +14 -0
- package/dist/fingerprinting/anti-webgpu/lib/config.js.map +1 -0
- package/dist/fingerprinting/anti-webgpu/lib/runtime.d.ts +2 -0
- package/dist/fingerprinting/anti-webgpu/lib/runtime.d.ts.map +1 -0
- package/dist/fingerprinting/anti-webgpu/lib/runtime.js +107 -0
- package/dist/fingerprinting/anti-webgpu/lib/runtime.js.map +1 -0
- package/dist/fingerprinting/anti-webgpu/manifest.json +58 -0
- package/dist/fingerprinting/custom-fingerprint-injector.d.ts +87 -0
- package/dist/fingerprinting/custom-fingerprint-injector.d.ts.map +1 -0
- package/dist/fingerprinting/custom-fingerprint-injector.js +342 -0
- package/dist/fingerprinting/custom-fingerprint-injector.js.map +1 -0
- package/dist/fingerprinting/fingerprint-injector.d.ts +157 -0
- package/dist/fingerprinting/fingerprint-injector.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-injector.js +632 -0
- package/dist/fingerprinting/fingerprint-injector.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/audio-spoofing.d.ts +6 -0
- package/dist/fingerprinting/fingerprint-overrides/audio-spoofing.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/audio-spoofing.js +119 -0
- package/dist/fingerprinting/fingerprint-overrides/audio-spoofing.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/canvas-protection.d.ts +6 -0
- package/dist/fingerprinting/fingerprint-overrides/canvas-protection.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/canvas-protection.js +149 -0
- package/dist/fingerprinting/fingerprint-overrides/canvas-protection.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/cdp-detection-bypass.d.ts +14 -0
- package/dist/fingerprinting/fingerprint-overrides/cdp-detection-bypass.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/cdp-detection-bypass.js +763 -0
- package/dist/fingerprinting/fingerprint-overrides/cdp-detection-bypass.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/client-rect-spoofing.d.ts +6 -0
- package/dist/fingerprinting/fingerprint-overrides/client-rect-spoofing.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/client-rect-spoofing.js +195 -0
- package/dist/fingerprinting/fingerprint-overrides/client-rect-spoofing.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/coalesced-events-spoofing.d.ts +10 -0
- package/dist/fingerprinting/fingerprint-overrides/coalesced-events-spoofing.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/coalesced-events-spoofing.js +195 -0
- package/dist/fingerprinting/fingerprint-overrides/coalesced-events-spoofing.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts +28 -0
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js +1181 -0
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/font-spoofing.d.ts +7 -0
- package/dist/fingerprinting/fingerprint-overrides/font-spoofing.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/font-spoofing.js +171 -0
- package/dist/fingerprinting/fingerprint-overrides/font-spoofing.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/index.d.ts +36 -0
- package/dist/fingerprinting/fingerprint-overrides/index.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/index.js +40 -0
- package/dist/fingerprinting/fingerprint-overrides/index.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.d.ts +45 -0
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js +268 -0
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.d.ts +6 -0
- package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.js +301 -0
- package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/mouse-humanization.d.ts +7 -0
- package/dist/fingerprinting/fingerprint-overrides/mouse-humanization.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/mouse-humanization.js +58 -0
- package/dist/fingerprinting/fingerprint-overrides/mouse-humanization.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/performance-spoofing.d.ts +6 -0
- package/dist/fingerprinting/fingerprint-overrides/performance-spoofing.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/performance-spoofing.js +249 -0
- package/dist/fingerprinting/fingerprint-overrides/performance-spoofing.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.d.ts +33 -0
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js +618 -0
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/prototype-integrity.d.ts +13 -0
- package/dist/fingerprinting/fingerprint-overrides/prototype-integrity.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/prototype-integrity.js +356 -0
- package/dist/fingerprinting/fingerprint-overrides/prototype-integrity.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/runtime-enable-bypass.d.ts +18 -0
- package/dist/fingerprinting/fingerprint-overrides/runtime-enable-bypass.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/runtime-enable-bypass.js +171 -0
- package/dist/fingerprinting/fingerprint-overrides/runtime-enable-bypass.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.d.ts +55 -0
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js +244 -0
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.d.ts +14 -0
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.js +925 -0
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/storage-consistency.d.ts +13 -0
- package/dist/fingerprinting/fingerprint-overrides/storage-consistency.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/storage-consistency.js +346 -0
- package/dist/fingerprinting/fingerprint-overrides/storage-consistency.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/timing-consistency.d.ts +13 -0
- package/dist/fingerprinting/fingerprint-overrides/timing-consistency.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/timing-consistency.js +264 -0
- package/dist/fingerprinting/fingerprint-overrides/timing-consistency.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts +27 -0
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.js +213 -0
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/utils.d.ts +12 -0
- package/dist/fingerprinting/fingerprint-overrides/utils.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/utils.js +517 -0
- package/dist/fingerprinting/fingerprint-overrides/utils.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.d.ts +12 -0
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js +215 -0
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/webgpu-spoofing.d.ts +6 -0
- package/dist/fingerprinting/fingerprint-overrides/webgpu-spoofing.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/webgpu-spoofing.js +202 -0
- package/dist/fingerprinting/fingerprint-overrides/webgpu-spoofing.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.d.ts +6 -0
- package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.js +188 -0
- package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/launcher-wrap.d.ts +2 -2
- package/dist/launcher-wrap.d.ts.map +1 -1
- package/dist/launcher-wrap.js.map +1 -1
- package/dist/page.d.ts +160 -13
- package/dist/page.d.ts.map +1 -1
- package/dist/page.js +1027 -42
- package/dist/page.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +8 -4
package/dist/page.js
CHANGED
|
@@ -1,8 +1,115 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
+
import { load as cheerioLoad } from 'cheerio';
|
|
3
|
+
import CDP from 'chrome-remote-interface';
|
|
4
|
+
import { log } from 'crawlee';
|
|
2
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
|
|
22
|
+
/**
|
|
23
|
+
* Creates a proxy that wraps CdpPage methods with reconnection logic
|
|
24
|
+
|
|
25
|
+
*/
|
|
26
|
+
// @ts-expect-error not used now
|
|
27
|
+
function createReconnectingPageProxy(page) {
|
|
28
|
+
const MAX_RETRIES = 3;
|
|
29
|
+
const INITIAL_BACKOFF = 1000;
|
|
30
|
+
async function executeWithRetries(operation, methodName) {
|
|
31
|
+
let lastError = null;
|
|
32
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
33
|
+
try {
|
|
34
|
+
// Wait for any ongoing reconnection
|
|
35
|
+
while (page.isReconnecting) {
|
|
36
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
37
|
+
}
|
|
38
|
+
// @ts-expect-error Accessing internal WebSocket
|
|
39
|
+
const ws = page._rawClient._ws;
|
|
40
|
+
// Check connection state before method call
|
|
41
|
+
if (ws && (ws.readyState !== ws.OPEN || ws.readyState === ws.CLOSING)) {
|
|
42
|
+
const backoff = INITIAL_BACKOFF * 2 ** attempt;
|
|
43
|
+
log.info(`WebSocket not connected before ${methodName}, attempting to reconnect... (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
44
|
+
try {
|
|
45
|
+
await page.reconnectSession();
|
|
46
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
47
|
+
}
|
|
48
|
+
catch (reconnectError) {
|
|
49
|
+
const error = reconnectError instanceof Error ? reconnectError : new Error(String(reconnectError));
|
|
50
|
+
log.error('Reconnection failed:', { error });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return await operation();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
58
|
+
if (error instanceof Error && error.message?.includes('WebSocket is not open')) {
|
|
59
|
+
const backoff = INITIAL_BACKOFF * 2 ** attempt;
|
|
60
|
+
log.info(`WebSocket error during ${methodName}, attempting to reconnect... (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
61
|
+
try {
|
|
62
|
+
await page.reconnectSession();
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
catch (reconnectError) {
|
|
67
|
+
const reconnectErr = reconnectError instanceof Error ? reconnectError : new Error(String(reconnectError));
|
|
68
|
+
log.error('Reconnection failed:', { error: reconnectErr });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
throw lastError || new Error(`Failed to execute ${methodName} after ${MAX_RETRIES} attempts`);
|
|
75
|
+
}
|
|
76
|
+
return new Proxy(page, {
|
|
77
|
+
get(target, property) {
|
|
78
|
+
const original = target[property];
|
|
79
|
+
const propertyName = String(property);
|
|
80
|
+
// Special handling for close method
|
|
81
|
+
if (propertyName === 'close') {
|
|
82
|
+
return original;
|
|
83
|
+
}
|
|
84
|
+
// Only proxy public methods that return promises
|
|
85
|
+
if (typeof original === 'function' &&
|
|
86
|
+
!propertyName.startsWith('_') &&
|
|
87
|
+
!propertyName.startsWith('on') &&
|
|
88
|
+
propertyName !== 'emit' &&
|
|
89
|
+
propertyName !== 'addListener' &&
|
|
90
|
+
propertyName !== 'removeListener') {
|
|
91
|
+
return async (...args) => {
|
|
92
|
+
// @ts-expect-error Accessing private property
|
|
93
|
+
if (target.isClosed) {
|
|
94
|
+
log.debug(`Operation '${propertyName}' skipped - page is closed`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
return executeWithRetries(() => original.apply(target, args), propertyName);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return original;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
3
104
|
export default class CdpPage extends EventEmitter {
|
|
4
105
|
constructor(client) {
|
|
5
106
|
super();
|
|
107
|
+
Object.defineProperty(this, "_rawClient", {
|
|
108
|
+
enumerable: true,
|
|
109
|
+
configurable: true,
|
|
110
|
+
writable: true,
|
|
111
|
+
value: void 0
|
|
112
|
+
});
|
|
6
113
|
Object.defineProperty(this, "client", {
|
|
7
114
|
enumerable: true,
|
|
8
115
|
configurable: true,
|
|
@@ -15,30 +122,143 @@ export default class CdpPage extends EventEmitter {
|
|
|
15
122
|
writable: true,
|
|
16
123
|
value: null
|
|
17
124
|
});
|
|
125
|
+
Object.defineProperty(this, "isReconnecting", {
|
|
126
|
+
enumerable: true,
|
|
127
|
+
configurable: true,
|
|
128
|
+
writable: true,
|
|
129
|
+
value: false
|
|
130
|
+
});
|
|
131
|
+
Object.defineProperty(this, "enabledDomains", {
|
|
132
|
+
enumerable: true,
|
|
133
|
+
configurable: true,
|
|
134
|
+
writable: true,
|
|
135
|
+
value: new Set()
|
|
136
|
+
});
|
|
137
|
+
Object.defineProperty(this, "connectionMonitor", {
|
|
138
|
+
enumerable: true,
|
|
139
|
+
configurable: true,
|
|
140
|
+
writable: true,
|
|
141
|
+
value: void 0
|
|
142
|
+
});
|
|
143
|
+
Object.defineProperty(this, "isClosed", {
|
|
144
|
+
enumerable: true,
|
|
145
|
+
configurable: true,
|
|
146
|
+
writable: true,
|
|
147
|
+
value: false
|
|
148
|
+
});
|
|
149
|
+
this._rawClient = client;
|
|
18
150
|
this.client = client;
|
|
19
151
|
this.emit(PAGE_CREATED);
|
|
152
|
+
// this._attachConnectionMonitor();
|
|
153
|
+
}
|
|
154
|
+
_attachConnectionMonitor() {
|
|
155
|
+
if (this.connectionMonitor) {
|
|
156
|
+
clearInterval(this.connectionMonitor);
|
|
157
|
+
}
|
|
158
|
+
// @ts-expect-error Accessing internal WebSocket
|
|
159
|
+
const ws = this._rawClient._ws;
|
|
160
|
+
if (!ws)
|
|
161
|
+
return;
|
|
162
|
+
ws.on('error', (error) => {
|
|
163
|
+
log.error('WebSocket error occurred:', { error });
|
|
164
|
+
});
|
|
165
|
+
// Only monitor connection health
|
|
166
|
+
this.connectionMonitor = setInterval(() => {
|
|
167
|
+
if (ws.readyState === ws.OPEN) {
|
|
168
|
+
ws.ping(() => { });
|
|
169
|
+
}
|
|
170
|
+
else if (!this.isReconnecting && ws.readyState !== ws.CONNECTING) {
|
|
171
|
+
log.debug('Connection health check failed, clearing monitor');
|
|
172
|
+
if (this.connectionMonitor)
|
|
173
|
+
clearInterval(this.connectionMonitor);
|
|
174
|
+
}
|
|
175
|
+
}, 5000);
|
|
176
|
+
this.once(PAGE_CLOSED, () => {
|
|
177
|
+
if (this.connectionMonitor)
|
|
178
|
+
clearInterval(this.connectionMonitor);
|
|
179
|
+
});
|
|
20
180
|
}
|
|
21
181
|
// @TODO: The enabling of API can cause blocking.
|
|
22
182
|
// We need to discover this more and add options to the crawler.
|
|
23
183
|
static async create(client) {
|
|
24
184
|
const page = new CdpPage(client);
|
|
25
185
|
await page.initialize();
|
|
186
|
+
// const proxiedPage = createReconnectingPageProxy(page);
|
|
187
|
+
log.debug('Created proxied page instance');
|
|
26
188
|
return page;
|
|
27
189
|
}
|
|
28
190
|
async initialize() {
|
|
29
|
-
//
|
|
30
|
-
//
|
|
191
|
+
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
192
|
+
// @TODO: NEVER ENABLE DOMAINS. THIS CAUSES BLOCKING. ALWAYS ENABLE DOMAINS ONLY IF NEEDED
|
|
193
|
+
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
194
|
+
// const { Page, DOM, Network } = this.client;
|
|
195
|
+
// Enable essential domains by default with proper error handling
|
|
196
|
+
// await Promise.all([
|
|
197
|
+
// Page.enable().catch(e => log.error('Failed to enable Page:', e)),
|
|
198
|
+
// DOM.enable().catch(e => log.error('Failed to enable DOM:', e)),
|
|
199
|
+
// Network.enable().catch(e => log.error('Failed to enable Network:', e)),
|
|
200
|
+
// ]);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Lazily enable Page domain only when needed for script injection.
|
|
204
|
+
* This avoids enabling Page upfront which is a CDP detection vector.
|
|
205
|
+
* Once enabled, subsequent calls are no-ops.
|
|
206
|
+
*/
|
|
207
|
+
async ensurePageEnabled() {
|
|
208
|
+
if (this.enabledDomains.has('Page')) {
|
|
209
|
+
return; // Already enabled
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
await this.client.Page.enable();
|
|
213
|
+
this.enabledDomains.add('Page');
|
|
214
|
+
log.debug('Page domain enabled lazily for script injection');
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
log.error('Failed to enable Page domain:', { error });
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Lazily enable Network domain only when needed for request interception/monitoring.
|
|
223
|
+
* This avoids enabling Network upfront which is a CDP detection vector.
|
|
224
|
+
* Once enabled, subsequent calls are no-ops.
|
|
225
|
+
*/
|
|
226
|
+
async ensureNetworkEnabled() {
|
|
227
|
+
if (this.enabledDomains.has('Network')) {
|
|
228
|
+
return; // Already enabled
|
|
229
|
+
}
|
|
230
|
+
await this.client.Network.enable();
|
|
231
|
+
this.enabledDomains.add('Network');
|
|
232
|
+
log.debug('Network domain enabled (lazy)');
|
|
31
233
|
}
|
|
32
234
|
async url() {
|
|
33
235
|
if (!this.loadedUrl) {
|
|
34
|
-
return await this.
|
|
236
|
+
return await this._getCurrentUrlWithoutRuntime();
|
|
35
237
|
}
|
|
36
238
|
return this.loadedUrl;
|
|
37
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Get current URL without activating the Runtime domain.
|
|
242
|
+
* Uses Page.getNavigationHistory which only requires the Page domain.
|
|
243
|
+
*/
|
|
244
|
+
async _getCurrentUrlWithoutRuntime() {
|
|
245
|
+
try {
|
|
246
|
+
await this.ensurePageEnabled();
|
|
247
|
+
const { currentIndex, entries } = await this.client.Page.getNavigationHistory();
|
|
248
|
+
if (entries && entries.length > 0 && currentIndex >= 0) {
|
|
249
|
+
return entries[currentIndex].url;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Fallback: if Page.getNavigationHistory fails, try evaluate
|
|
254
|
+
log.debug('getNavigationHistory failed, falling back to evaluate');
|
|
255
|
+
}
|
|
256
|
+
return await this.evaluate('window.location.href');
|
|
257
|
+
}
|
|
38
258
|
// Navigation method
|
|
39
259
|
async goto(url, options) {
|
|
40
|
-
const { Page
|
|
41
|
-
const timeout = options?.timeout ??
|
|
260
|
+
const { Page } = this.client;
|
|
261
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
42
262
|
await Page.navigate({ url });
|
|
43
263
|
const waitUntil = options?.waitUntil ?? 'load';
|
|
44
264
|
let waitEvent;
|
|
@@ -56,12 +276,41 @@ export default class CdpPage extends EventEmitter {
|
|
|
56
276
|
waitEvent,
|
|
57
277
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Navigation timeout')), timeout)),
|
|
58
278
|
]);
|
|
59
|
-
|
|
60
|
-
|
|
279
|
+
// Use Page.getNavigationHistory instead of Runtime.evaluate to avoid
|
|
280
|
+
// activating the Runtime domain (which is detection vector #1)
|
|
281
|
+
this.loadedUrl = await this._getCurrentUrlWithoutRuntime();
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Reloads the current page.
|
|
285
|
+
* @param options - Options for waiting and timeout, similar to goto.
|
|
286
|
+
*/
|
|
287
|
+
async reload(options) {
|
|
288
|
+
const { Page } = this.client;
|
|
289
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
290
|
+
const waitUntil = options?.waitUntil ?? 'load';
|
|
291
|
+
await Page.reload({});
|
|
292
|
+
let waitEvent;
|
|
293
|
+
switch (waitUntil) {
|
|
294
|
+
case 'load':
|
|
295
|
+
waitEvent = new Promise((resolve) => Page.loadEventFired(() => resolve()));
|
|
296
|
+
break;
|
|
297
|
+
case 'domcontentloaded':
|
|
298
|
+
waitEvent = new Promise((resolve) => Page.domContentEventFired(() => resolve()));
|
|
299
|
+
break;
|
|
300
|
+
default:
|
|
301
|
+
throw new Error(`Unsupported waitUntil event: ${waitUntil}`);
|
|
302
|
+
}
|
|
303
|
+
await Promise.race([
|
|
304
|
+
waitEvent,
|
|
305
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Reload timeout')), timeout)),
|
|
306
|
+
]);
|
|
307
|
+
// Use Page.getNavigationHistory instead of Runtime.evaluate
|
|
308
|
+
this.loadedUrl = await this._getCurrentUrlWithoutRuntime();
|
|
61
309
|
}
|
|
62
310
|
// Evaluate method
|
|
63
311
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
64
312
|
async evaluate(pageFunction, ...args) {
|
|
313
|
+
await this._enableDomain('Runtime');
|
|
65
314
|
const { Runtime } = this.client;
|
|
66
315
|
let expression;
|
|
67
316
|
if (typeof pageFunction === 'function') {
|
|
@@ -86,19 +335,349 @@ export default class CdpPage extends EventEmitter {
|
|
|
86
335
|
return result.value;
|
|
87
336
|
}
|
|
88
337
|
// Click method
|
|
89
|
-
async click(selector) {
|
|
90
|
-
await
|
|
338
|
+
async click(selector, options) {
|
|
339
|
+
await Promise.all([
|
|
340
|
+
this._enableDomain('DOM'),
|
|
341
|
+
this._enableDomain('Input'),
|
|
342
|
+
]);
|
|
91
343
|
const { DOM, Input } = this.client;
|
|
344
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
345
|
+
const force = options?.force ?? false;
|
|
346
|
+
const delay = options?.delay ?? 0;
|
|
347
|
+
const button = options?.button ?? 'left';
|
|
348
|
+
const clickCount = options?.clickCount ?? 1;
|
|
349
|
+
// Wait for element to exist
|
|
350
|
+
await this.waitForSelector(selector, { timeout });
|
|
351
|
+
// Get element details and check visibility
|
|
352
|
+
const elementInfo = await this._getElementInfo(selector);
|
|
353
|
+
if (!elementInfo) {
|
|
354
|
+
throw new Error(`Element not found: ${selector}`);
|
|
355
|
+
}
|
|
356
|
+
// Check if element is visible (unless force is true)
|
|
357
|
+
if (!force) {
|
|
358
|
+
const isVisible = await this._isElementVisible(selector);
|
|
359
|
+
if (!isVisible) {
|
|
360
|
+
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.`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Check if element is clickable (unless force is true)
|
|
364
|
+
if (!force) {
|
|
365
|
+
const isClickable = await this._isElementClickable(selector);
|
|
366
|
+
if (!isClickable) {
|
|
367
|
+
throw new Error(`Element is not clickable: ${selector}. Use force: true to bypass this check.`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Scroll element into view if needed
|
|
371
|
+
await this._scrollElementIntoView(selector);
|
|
372
|
+
// Wait for scroll animations to complete by polling for position stabilization
|
|
373
|
+
// Wait for the element's position to stabilize after scrolling.
|
|
374
|
+
// This is necessary because scroll animations or layout changes may cause the element's position
|
|
375
|
+
// to shift for a short period. If we attempt to click before the position is stable, the click
|
|
376
|
+
// may be sent to the wrong location, potentially missing the target element or causing unreliable
|
|
377
|
+
// automation. Polling for position stabilization ensures the click is performed accurately.
|
|
378
|
+
await this._waitForElementPositionToStabilize(selector, POSITION_STABILITY_TIMEOUT_MS, POSITION_CHECK_INTERVAL_MS);
|
|
379
|
+
// Get updated element position after scrolling
|
|
92
380
|
const { nodeId } = await this.getNodeId(selector);
|
|
93
|
-
|
|
381
|
+
// For hidden elements, we need to handle the case where box model can't be computed
|
|
382
|
+
let model;
|
|
383
|
+
try {
|
|
384
|
+
const boxModelResult = await DOM.getBoxModel({ nodeId });
|
|
385
|
+
model = boxModelResult.model;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
if (force) {
|
|
389
|
+
// If force is true, try to get a default position
|
|
390
|
+
const fallbackElementInfo = await this._getElementInfo(selector);
|
|
391
|
+
if (fallbackElementInfo) {
|
|
392
|
+
const x = fallbackElementInfo.boundingBox.x + fallbackElementInfo.boundingBox.width / 2;
|
|
393
|
+
const y = fallbackElementInfo.boundingBox.y + fallbackElementInfo.boundingBox.height / 2;
|
|
394
|
+
await this._performClick(Input, x, y, button, clickCount, delay);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
throw new Error(`Could not get box model for node: ${nodeId}. Element might be hidden or not rendered.`);
|
|
399
|
+
}
|
|
94
400
|
if (!model) {
|
|
95
401
|
throw new Error(`Could not get box model for node: ${nodeId}`);
|
|
96
402
|
}
|
|
403
|
+
// Calculate click coordinates (center of the element)
|
|
97
404
|
const x = (model.content[0] + model.content[4]) / 2;
|
|
98
405
|
const y = (model.content[1] + model.content[5]) / 2;
|
|
406
|
+
// Verify element is still in viewport after scrolling (with some tolerance)
|
|
407
|
+
if (!force) {
|
|
408
|
+
const isInViewport = await this._isElementInViewport(selector);
|
|
409
|
+
if (!isInViewport) {
|
|
410
|
+
// Give a bit more time for scroll animations to complete
|
|
411
|
+
await new Promise(resolve => setTimeout(resolve, HUMAN_DELAY_MS));
|
|
412
|
+
const isInViewportAfterDelay = await this._isElementInViewport(selector);
|
|
413
|
+
if (!isInViewportAfterDelay) {
|
|
414
|
+
throw new Error(`Element is not in viewport after scrolling: ${selector}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Perform the click with human-like interaction
|
|
419
|
+
await this._performClick(Input, x, y, button, clickCount, delay);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Performs a human-like click with proper mouse events.
|
|
423
|
+
* Note: screenX/screenY compensation is handled browser-side via fingerprint
|
|
424
|
+
* overrides that set realistic window.screenX/screenY values, which Chrome
|
|
425
|
+
* uses when constructing MouseEvent from CDP dispatch.
|
|
426
|
+
*/
|
|
427
|
+
async _performClick(Input, x, y, button, clickCount, delay) {
|
|
428
|
+
// Move mouse cursor to target coordinates (x, y)
|
|
99
429
|
await Input.dispatchMouseEvent({ type: 'mouseMoved', x, y });
|
|
100
|
-
|
|
101
|
-
|
|
430
|
+
// Small delay to simulate human behavior
|
|
431
|
+
if (delay > 0) {
|
|
432
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
433
|
+
}
|
|
434
|
+
// Press mouse button
|
|
435
|
+
await Input.dispatchMouseEvent({
|
|
436
|
+
type: 'mousePressed',
|
|
437
|
+
x,
|
|
438
|
+
y,
|
|
439
|
+
button,
|
|
440
|
+
clickCount,
|
|
441
|
+
});
|
|
442
|
+
// Small delay between press and release
|
|
443
|
+
await new Promise(resolve => setTimeout(resolve, PRESS_RELEASE_DELAY_MS));
|
|
444
|
+
// Release mouse button
|
|
445
|
+
await Input.dispatchMouseEvent({
|
|
446
|
+
type: 'mouseReleased',
|
|
447
|
+
x,
|
|
448
|
+
y,
|
|
449
|
+
button,
|
|
450
|
+
clickCount,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Gets detailed information about an element
|
|
455
|
+
*/
|
|
456
|
+
async _getElementInfo(selector) {
|
|
457
|
+
return await this.evaluate((sel) => {
|
|
458
|
+
const element = document.querySelector(sel);
|
|
459
|
+
if (!element)
|
|
460
|
+
return null;
|
|
461
|
+
const rect = element.getBoundingClientRect();
|
|
462
|
+
const style = window.getComputedStyle(element);
|
|
463
|
+
// Check if element is visible
|
|
464
|
+
const isVisible = style.display !== 'none' &&
|
|
465
|
+
style.visibility !== 'hidden' &&
|
|
466
|
+
rect.width > 0 &&
|
|
467
|
+
rect.height > 0;
|
|
468
|
+
// Check if element is clickable
|
|
469
|
+
const isClickable = !element.hasAttribute('disabled') &&
|
|
470
|
+
!element.classList.contains('disabled') &&
|
|
471
|
+
style.pointerEvents !== 'none' &&
|
|
472
|
+
rect.width > 0 &&
|
|
473
|
+
rect.height > 0;
|
|
474
|
+
return {
|
|
475
|
+
nodeId: 0, // Will be set by CDP
|
|
476
|
+
boundingBox: {
|
|
477
|
+
x: rect.left,
|
|
478
|
+
y: rect.top,
|
|
479
|
+
width: rect.width,
|
|
480
|
+
height: rect.height,
|
|
481
|
+
},
|
|
482
|
+
isVisible,
|
|
483
|
+
isClickable,
|
|
484
|
+
};
|
|
485
|
+
}, selector);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Enhanced visibility check that considers more factors
|
|
489
|
+
*/
|
|
490
|
+
async _isElementVisible(selector) {
|
|
491
|
+
return await this.evaluate((sel) => {
|
|
492
|
+
const element = document.querySelector(sel);
|
|
493
|
+
if (!element)
|
|
494
|
+
return false;
|
|
495
|
+
const rect = element.getBoundingClientRect();
|
|
496
|
+
const style = window.getComputedStyle(element);
|
|
497
|
+
// Basic visibility checks
|
|
498
|
+
if (style.display === 'none' || style.visibility === 'hidden') {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
// Check if element has dimensions
|
|
502
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
// Check if element is not transparent
|
|
506
|
+
if (parseFloat(style.opacity) === 0) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
// Check if element is not clipped by overflow
|
|
510
|
+
const parent = element.parentElement;
|
|
511
|
+
if (parent) {
|
|
512
|
+
const parentStyle = window.getComputedStyle(parent);
|
|
513
|
+
if (parentStyle.overflow === 'hidden') {
|
|
514
|
+
const parentRect = parent.getBoundingClientRect();
|
|
515
|
+
if (rect.left >= parentRect.right ||
|
|
516
|
+
rect.right <= parentRect.left ||
|
|
517
|
+
rect.top >= parentRect.bottom ||
|
|
518
|
+
rect.bottom <= parentRect.top) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return true;
|
|
524
|
+
}, selector);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Enhanced clickable check that considers more factors
|
|
528
|
+
*/
|
|
529
|
+
async _isElementClickable(selector) {
|
|
530
|
+
return await this.evaluate((sel) => {
|
|
531
|
+
const element = document.querySelector(sel);
|
|
532
|
+
if (!element)
|
|
533
|
+
return false;
|
|
534
|
+
const rect = element.getBoundingClientRect();
|
|
535
|
+
const style = window.getComputedStyle(element);
|
|
536
|
+
// Check if element is disabled
|
|
537
|
+
if (element.hasAttribute('disabled') || element.classList.contains('disabled')) {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
// Check if pointer events are disabled
|
|
541
|
+
if (style.pointerEvents === 'none') {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
// Check if element has dimensions
|
|
545
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
// Check if element is not transparent
|
|
549
|
+
if (parseFloat(style.opacity) === 0) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
// Check if element is not read-only (for form elements)
|
|
553
|
+
if (element.hasAttribute('readonly')) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
return true;
|
|
557
|
+
}, selector);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Enhanced scroll into view with better positioning
|
|
561
|
+
*/
|
|
562
|
+
async _scrollElementIntoView(selector) {
|
|
563
|
+
await this.evaluate((sel, tolerance) => {
|
|
564
|
+
const element = document.querySelector(sel);
|
|
565
|
+
if (!element)
|
|
566
|
+
return;
|
|
567
|
+
const rect = element.getBoundingClientRect();
|
|
568
|
+
const viewportHeight = window.innerHeight;
|
|
569
|
+
const viewportWidth = window.innerWidth;
|
|
570
|
+
// Check if element is already in viewport (with tolerance)
|
|
571
|
+
const isInViewport = rect.top >= -tolerance &&
|
|
572
|
+
rect.left >= -tolerance &&
|
|
573
|
+
rect.bottom <= viewportHeight + tolerance &&
|
|
574
|
+
rect.right <= viewportWidth + tolerance;
|
|
575
|
+
if (!isInViewport) {
|
|
576
|
+
// Scroll element into view with better positioning
|
|
577
|
+
element.scrollIntoView({
|
|
578
|
+
behavior: 'smooth',
|
|
579
|
+
block: 'center',
|
|
580
|
+
inline: 'center',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}, selector, VIEWPORT_TOLERANCE_PX);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Waits for an element's position to stabilize by polling its position over time.
|
|
587
|
+
* This is useful for waiting for scroll animations or dynamic layout changes to complete.
|
|
588
|
+
*
|
|
589
|
+
* @param selector - The CSS selector for the element to monitor
|
|
590
|
+
* @param timeout - Maximum time to wait in milliseconds (default: 2000)
|
|
591
|
+
* @param checkInterval - How often to check position in milliseconds (default: 100)
|
|
592
|
+
* @param stabilityThreshold - Number of consecutive stable checks required (default: 3)
|
|
593
|
+
* @param tolerance - Maximum pixel difference to consider "stable" (default: 1)
|
|
594
|
+
*/
|
|
595
|
+
async _waitForElementPositionToStabilize(selector, timeout = POSITION_STABILITY_TIMEOUT_MS, checkInterval = POSITION_CHECK_INTERVAL_MS, stabilityThreshold = POSITION_STABILITY_THRESHOLD, tolerance = POSITION_TOLERANCE_PX) {
|
|
596
|
+
const startTime = Date.now();
|
|
597
|
+
let stableCount = 0;
|
|
598
|
+
let lastPosition = null;
|
|
599
|
+
while (Date.now() - startTime < timeout) {
|
|
600
|
+
try {
|
|
601
|
+
const currentPosition = await this.evaluate((sel) => {
|
|
602
|
+
const element = document.querySelector(sel);
|
|
603
|
+
if (!element)
|
|
604
|
+
return null;
|
|
605
|
+
const rect = element.getBoundingClientRect();
|
|
606
|
+
return {
|
|
607
|
+
x: Math.round(rect.left),
|
|
608
|
+
y: Math.round(rect.top),
|
|
609
|
+
width: Math.round(rect.width),
|
|
610
|
+
height: Math.round(rect.height),
|
|
611
|
+
};
|
|
612
|
+
}, selector);
|
|
613
|
+
if (!currentPosition) {
|
|
614
|
+
// Element not found, reset stability counter
|
|
615
|
+
stableCount = 0;
|
|
616
|
+
lastPosition = null;
|
|
617
|
+
}
|
|
618
|
+
else if (!lastPosition) {
|
|
619
|
+
// First position reading
|
|
620
|
+
lastPosition = currentPosition;
|
|
621
|
+
stableCount = 1;
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
// Check if position is stable (within tolerance)
|
|
625
|
+
const deltaX = Math.abs(currentPosition.x - lastPosition.x);
|
|
626
|
+
const deltaY = Math.abs(currentPosition.y - lastPosition.y);
|
|
627
|
+
const deltaWidth = Math.abs(currentPosition.width - lastPosition.width);
|
|
628
|
+
const deltaHeight = Math.abs(currentPosition.height - lastPosition.height);
|
|
629
|
+
const isStable = deltaX <= tolerance &&
|
|
630
|
+
deltaY <= tolerance &&
|
|
631
|
+
deltaWidth <= tolerance &&
|
|
632
|
+
deltaHeight <= tolerance;
|
|
633
|
+
if (isStable) {
|
|
634
|
+
stableCount++;
|
|
635
|
+
if (stableCount >= stabilityThreshold) {
|
|
636
|
+
// Position has been stable for required number of checks
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
// Position changed, reset counter
|
|
642
|
+
stableCount = 1;
|
|
643
|
+
}
|
|
644
|
+
lastPosition = currentPosition;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
// Error reading position, reset stability counter
|
|
649
|
+
stableCount = 0;
|
|
650
|
+
lastPosition = null;
|
|
651
|
+
}
|
|
652
|
+
// Wait before next check
|
|
653
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
654
|
+
}
|
|
655
|
+
// If we reach here, we timed out - this is acceptable as some elements may never stabilize
|
|
656
|
+
// If we reach here, we timed out. This means the element's position did not stabilize within the allotted time.
|
|
657
|
+
// Proceeding may cause interaction failures if the element is still moving or changing position.
|
|
658
|
+
// Callers should be aware that stabilization was not achieved and handle this case appropriately.
|
|
659
|
+
}
|
|
660
|
+
// Public wrapper for tests and external use
|
|
661
|
+
async waitForElementPositionToStabilize(selector, timeout = POSITION_STABILITY_TIMEOUT_MS, checkInterval = POSITION_CHECK_INTERVAL_MS, stabilityThreshold = POSITION_STABILITY_THRESHOLD, tolerance = POSITION_TOLERANCE_PX) {
|
|
662
|
+
return this._waitForElementPositionToStabilize(selector, timeout, checkInterval, stabilityThreshold, tolerance);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Check if element is within the viewport (with some tolerance)
|
|
666
|
+
*/
|
|
667
|
+
async _isElementInViewport(selector) {
|
|
668
|
+
return await this.evaluate((sel, tolerance) => {
|
|
669
|
+
const element = document.querySelector(sel);
|
|
670
|
+
if (!element)
|
|
671
|
+
return false;
|
|
672
|
+
const rect = element.getBoundingClientRect();
|
|
673
|
+
const viewportHeight = window.innerHeight;
|
|
674
|
+
const viewportWidth = window.innerWidth;
|
|
675
|
+
// Add some tolerance for scroll positioning
|
|
676
|
+
return rect.top >= -tolerance &&
|
|
677
|
+
rect.left >= -tolerance &&
|
|
678
|
+
rect.bottom <= viewportHeight + tolerance &&
|
|
679
|
+
rect.right <= viewportWidth + tolerance;
|
|
680
|
+
}, selector, VIEWPORT_TOLERANCE_PX);
|
|
102
681
|
}
|
|
103
682
|
// Type method
|
|
104
683
|
async type(selector, text, options) {
|
|
@@ -113,6 +692,17 @@ export default class CdpPage extends EventEmitter {
|
|
|
113
692
|
}
|
|
114
693
|
}
|
|
115
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* Deletes (clears) the value of an input field specified by selector.
|
|
697
|
+
* @param selector - The CSS selector for the input element.
|
|
698
|
+
*/
|
|
699
|
+
async deleteInput(selector) {
|
|
700
|
+
await this.evaluate((sel) => {
|
|
701
|
+
const input = document.querySelector(sel);
|
|
702
|
+
if (input)
|
|
703
|
+
input.value = '';
|
|
704
|
+
}, selector);
|
|
705
|
+
}
|
|
116
706
|
// Set viewport
|
|
117
707
|
async setViewport(viewport) {
|
|
118
708
|
const { Emulation } = this.client;
|
|
@@ -135,23 +725,22 @@ export default class CdpPage extends EventEmitter {
|
|
|
135
725
|
}
|
|
136
726
|
// Wait for selector
|
|
137
727
|
async waitForSelector(selector, options) {
|
|
138
|
-
const timeout = options?.timeout ??
|
|
139
|
-
const
|
|
140
|
-
const maxAttempts = Math.ceil(timeout / pollingInterval);
|
|
728
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
729
|
+
const maxAttempts = Math.ceil(timeout / POLLING_INTERVAL_MS);
|
|
141
730
|
let attempts = 0;
|
|
142
731
|
while (attempts < maxAttempts) {
|
|
143
732
|
const exists = await this.elementExists(selector);
|
|
144
733
|
if (exists) {
|
|
145
734
|
return;
|
|
146
735
|
}
|
|
147
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
736
|
+
await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL_MS));
|
|
148
737
|
attempts++;
|
|
149
738
|
}
|
|
150
739
|
throw new Error(`Timeout waiting for selector: ${selector}`);
|
|
151
740
|
}
|
|
152
741
|
// Wait for response
|
|
153
742
|
async waitForResponse(urlPart, statusCode = 200, timeout = 180000) {
|
|
154
|
-
await this.
|
|
743
|
+
await this.ensureNetworkEnabled(); // Lazy enable Network only when needed
|
|
155
744
|
return new Promise((resolve, reject) => {
|
|
156
745
|
let timer;
|
|
157
746
|
const onResponseReceived = async ({ requestId, response }) => {
|
|
@@ -168,7 +757,7 @@ export default class CdpPage extends EventEmitter {
|
|
|
168
757
|
// Retry logic with exponential backoff
|
|
169
758
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
170
759
|
try {
|
|
171
|
-
await this.
|
|
760
|
+
await this.ensureNetworkEnabled();
|
|
172
761
|
await new Promise((resolveTimeout) => setTimeout(resolveTimeout, 1000 * 2 ** attempt)); // Exponential backoff
|
|
173
762
|
const { body } = await this.client.Network.getResponseBody({ requestId });
|
|
174
763
|
resolve({
|
|
@@ -192,14 +781,15 @@ export default class CdpPage extends EventEmitter {
|
|
|
192
781
|
}, timeout);
|
|
193
782
|
});
|
|
194
783
|
}
|
|
195
|
-
// Screenshot method
|
|
196
784
|
async screenshot(options) {
|
|
197
785
|
const { Page } = this.client;
|
|
786
|
+
const format = options?.format ?? 'png';
|
|
787
|
+
// Use captureBeyondViewport for full page screenshots to avoid changing viewport
|
|
788
|
+
const screenshotOptions = { format };
|
|
198
789
|
if (options?.fullPage) {
|
|
199
|
-
|
|
200
|
-
await this.setViewport({ width: contentSize.width, height: contentSize.height });
|
|
790
|
+
screenshotOptions.captureBeyondViewport = true;
|
|
201
791
|
}
|
|
202
|
-
const { data } = await Page.captureScreenshot(
|
|
792
|
+
const { data } = await Page.captureScreenshot(screenshotOptions);
|
|
203
793
|
const buffer = Buffer.from(data, 'base64');
|
|
204
794
|
if (options?.path) {
|
|
205
795
|
const fs = await import('fs');
|
|
@@ -211,9 +801,19 @@ export default class CdpPage extends EventEmitter {
|
|
|
211
801
|
async content() {
|
|
212
802
|
return await this.evaluate(() => document.documentElement.outerHTML);
|
|
213
803
|
}
|
|
804
|
+
/* Converts the current page content to a Cheerio instance.
|
|
805
|
+
* @returns { Promise<cheerio.CheerioAPI> } A Promise that resolves to a Cheerio API instance.
|
|
806
|
+
*/
|
|
807
|
+
async toCheerio() {
|
|
808
|
+
const html = await this.content();
|
|
809
|
+
const loaded = cheerioLoad(html);
|
|
810
|
+
return loaded;
|
|
811
|
+
;
|
|
812
|
+
}
|
|
214
813
|
// Close method
|
|
215
814
|
async close() {
|
|
216
815
|
const { Page } = this.client;
|
|
816
|
+
this.isClosed = true;
|
|
217
817
|
await Page.close();
|
|
218
818
|
this.emit(PAGE_CLOSED);
|
|
219
819
|
}
|
|
@@ -246,10 +846,51 @@ export default class CdpPage extends EventEmitter {
|
|
|
246
846
|
}
|
|
247
847
|
}, selector);
|
|
248
848
|
}
|
|
849
|
+
// Add proper Fetch Metadata headers to avoid CDP detection
|
|
850
|
+
async setupFetchMetadataHeaders() {
|
|
851
|
+
const { Network } = this.client;
|
|
852
|
+
await this._enableDomain('Network');
|
|
853
|
+
await Network.setRequestInterception({ patterns: [{ urlPattern: '*' }] });
|
|
854
|
+
Network.on('requestIntercepted', async (params) => {
|
|
855
|
+
const { request, interceptionId } = params;
|
|
856
|
+
const url = new URL(request.url);
|
|
857
|
+
// Add proper fetch metadata headers
|
|
858
|
+
const headers = { ...request.headers };
|
|
859
|
+
// Set sec-fetch-* headers based on request type and destination
|
|
860
|
+
if (request.method === 'GET' && !headers['sec-fetch-dest']) {
|
|
861
|
+
if (url.pathname.endsWith('.js')) {
|
|
862
|
+
headers['sec-fetch-dest'] = 'script';
|
|
863
|
+
headers['sec-fetch-mode'] = 'no-cors';
|
|
864
|
+
headers['sec-fetch-site'] = url.origin === headers.origin ? 'same-origin' : 'cross-site';
|
|
865
|
+
}
|
|
866
|
+
else if (url.pathname.endsWith('.css')) {
|
|
867
|
+
headers['sec-fetch-dest'] = 'style';
|
|
868
|
+
headers['sec-fetch-mode'] = 'no-cors';
|
|
869
|
+
headers['sec-fetch-site'] = url.origin === headers.origin ? 'same-origin' : 'cross-site';
|
|
870
|
+
}
|
|
871
|
+
else if (url.pathname.match(/\.(png|jpg|jpeg|gif|webp|svg)$/i)) {
|
|
872
|
+
headers['sec-fetch-dest'] = 'image';
|
|
873
|
+
headers['sec-fetch-mode'] = 'no-cors';
|
|
874
|
+
headers['sec-fetch-site'] = url.origin === headers.origin ? 'same-origin' : 'cross-site';
|
|
875
|
+
}
|
|
876
|
+
else if (url.pathname === '/' || !url.pathname.includes('.')) {
|
|
877
|
+
headers['sec-fetch-dest'] = 'document';
|
|
878
|
+
headers['sec-fetch-mode'] = 'navigate';
|
|
879
|
+
headers['sec-fetch-site'] = 'none';
|
|
880
|
+
headers['sec-fetch-user'] = '?1';
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// Continue the request with modified headers
|
|
884
|
+
await Network.continueInterceptedRequest({
|
|
885
|
+
interceptionId,
|
|
886
|
+
headers,
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
}
|
|
249
890
|
// Block requests matching URL patterns
|
|
250
891
|
async blockRequests(urlPatterns) {
|
|
251
892
|
const { Network } = this.client;
|
|
252
|
-
await
|
|
893
|
+
await this.ensureNetworkEnabled(); // Lazy enable Network only when needed
|
|
253
894
|
await Network.setRequestInterception({ patterns: [{ urlPattern: '*' }] });
|
|
254
895
|
Network.on('requestIntercepted', async (params) => {
|
|
255
896
|
const shouldBlock = urlPatterns.some((pattern) => {
|
|
@@ -319,30 +960,374 @@ export default class CdpPage extends EventEmitter {
|
|
|
319
960
|
}, selector, pageFunction.toString(), ...args);
|
|
320
961
|
}
|
|
321
962
|
/**
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
963
|
+
* Enables a CDP domain only if it hasn't been enabled yet
|
|
964
|
+
* @private
|
|
965
|
+
*/
|
|
966
|
+
async _enableDomain(domain) {
|
|
967
|
+
if (this.enabledDomains.has(domain))
|
|
968
|
+
return;
|
|
969
|
+
const domainApi = this.client[domain];
|
|
970
|
+
if (domainApi?.enable) {
|
|
971
|
+
await domainApi.enable();
|
|
972
|
+
this.enabledDomains.add(domain);
|
|
973
|
+
log.debug(`Enabled ${domain} domain`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async reconnectSession(retries = 3) {
|
|
977
|
+
if (this.isReconnecting) {
|
|
978
|
+
log.debug('Reconnection skipped - already in progress');
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (this.isClosed) {
|
|
982
|
+
log.debug('Reconnection skipped - page is closed');
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
let attempt = 0;
|
|
986
|
+
this.isReconnecting = true;
|
|
987
|
+
try {
|
|
988
|
+
while (attempt < retries) {
|
|
989
|
+
try {
|
|
990
|
+
// Wait with exponential backoff before retrying
|
|
991
|
+
if (attempt > 0) {
|
|
992
|
+
const backoffMs = Math.min(1000 * (2 ** attempt), 10000);
|
|
993
|
+
log.info(`Waiting ${backoffMs}ms before reconnection attempt ${attempt + 1}/${retries}`);
|
|
994
|
+
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
995
|
+
}
|
|
996
|
+
// Create a fresh CDP connection
|
|
997
|
+
const newSession = await CDP({
|
|
998
|
+
port: this._rawClient.port,
|
|
999
|
+
local: true,
|
|
1000
|
+
});
|
|
1001
|
+
try {
|
|
1002
|
+
// Get available targets
|
|
1003
|
+
const { targetInfos } = await newSession.Target.getTargets();
|
|
1004
|
+
const pageTarget = targetInfos.find(t => t.type === 'page' && !t.url.includes('chrome://'));
|
|
1005
|
+
if (!pageTarget) {
|
|
1006
|
+
log.debug('No valid page target found, stopping reconnection');
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
// Connect to the specific page target
|
|
1010
|
+
const pageSession = await CDP({
|
|
1011
|
+
target: pageTarget.targetId,
|
|
1012
|
+
port: this._rawClient.port,
|
|
1013
|
+
local: true,
|
|
1014
|
+
});
|
|
1015
|
+
// Re-enable only the domains that were previously enabled
|
|
1016
|
+
const enablePromises = Array.from(this.enabledDomains).map(domain => pageSession[domain].enable());
|
|
1017
|
+
await Promise.all(enablePromises);
|
|
1018
|
+
// Update references
|
|
1019
|
+
this._rawClient = pageSession;
|
|
1020
|
+
this.client = pageSession;
|
|
1021
|
+
this._attachConnectionMonitor();
|
|
1022
|
+
log.info(`Page session reconnected successfully on attempt ${attempt + 1}`);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
catch (error) {
|
|
1026
|
+
// Clean up the session if anything fails
|
|
1027
|
+
await newSession.close().catch(() => { });
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
attempt++;
|
|
1033
|
+
if (attempt === retries) {
|
|
1034
|
+
log.debug('Reconnection stopped - max retries reached');
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
log.error('Failed to reconnect page session', { error });
|
|
1038
|
+
}
|
|
336
1039
|
}
|
|
337
|
-
scriptText = bodyMatch[1];
|
|
338
1040
|
}
|
|
339
|
-
|
|
340
|
-
|
|
1041
|
+
finally {
|
|
1042
|
+
this.isReconnecting = false;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Selects one or more options from a select element or dropdown.
|
|
1047
|
+
*
|
|
1048
|
+
* Automatically handles both regular HTML select elements and custom dropdowns:
|
|
1049
|
+
* - For <select> elements: Directly selects options within the select
|
|
1050
|
+
* - For custom dropdowns: Automatically detects and clicks triggers to open dropdowns,
|
|
1051
|
+
* then selects the specified options with intelligent scrolling for virtualized lists
|
|
1052
|
+
*
|
|
1053
|
+
* @param dropdownSelector - CSS selector for the select element or dropdown container
|
|
1054
|
+
* @param optionSelector - CSS selector(s) for the option(s) to select. Can be a single selector or array
|
|
1055
|
+
* @param options - Optional configuration for timeout, scrolling, visibility checks, etc.
|
|
1056
|
+
*
|
|
1057
|
+
* @example
|
|
1058
|
+
* // Regular HTML select
|
|
1059
|
+
* await page.selectOption('select#country', 'option[value="us"]');
|
|
1060
|
+
*
|
|
1061
|
+
* // Custom dropdown (automatically finds and clicks trigger)
|
|
1062
|
+
* await page.selectOption('#dropdown-menu', '[data-value="premium"]');
|
|
1063
|
+
*
|
|
1064
|
+
* // Multiple selection
|
|
1065
|
+
* await page.selectOption('select#languages', ['option[value="en"]', 'option[value="es"]']);
|
|
1066
|
+
*/
|
|
1067
|
+
async selectOption(dropdownSelector, optionSelector, options) {
|
|
1068
|
+
await Promise.all([
|
|
1069
|
+
this._enableDomain('DOM'),
|
|
1070
|
+
this._enableDomain('Input'),
|
|
1071
|
+
]);
|
|
1072
|
+
// DOM and Input domains are enabled and actively used in this method
|
|
1073
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
1074
|
+
const force = options?.force ?? false;
|
|
1075
|
+
const waitForOptions = options?.waitForOptions ?? true;
|
|
1076
|
+
const maxScrollAttempts = options?.maxScrollAttempts ?? 10;
|
|
1077
|
+
// Normalize target selectors to array
|
|
1078
|
+
const targetSelectors = Array.isArray(optionSelector) ? optionSelector : [optionSelector];
|
|
1079
|
+
// Wait for dropdown element to exist
|
|
1080
|
+
await this.waitForSelector(dropdownSelector, { timeout });
|
|
1081
|
+
// Check if this is a regular select element
|
|
1082
|
+
const isSelectElement = await this.evaluate((sel) => {
|
|
1083
|
+
const element = document.querySelector(sel);
|
|
1084
|
+
return element?.tagName === 'SELECT';
|
|
1085
|
+
}, dropdownSelector);
|
|
1086
|
+
// For regular select elements, do visibility and disabled checks upfront
|
|
1087
|
+
if (isSelectElement) {
|
|
1088
|
+
// Check if element is visible (unless force is true)
|
|
1089
|
+
if (!force) {
|
|
1090
|
+
const isVisible = await this._isElementVisible(dropdownSelector);
|
|
1091
|
+
if (!isVisible) {
|
|
1092
|
+
throw new Error(`Element is not visible: ${dropdownSelector}. Use force: true to bypass this check.`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// Check if element is disabled (unless force is true)
|
|
1096
|
+
if (!force) {
|
|
1097
|
+
const isDisabled = await this.evaluate((sel) => {
|
|
1098
|
+
const element = document.querySelector(sel);
|
|
1099
|
+
return element?.hasAttribute('disabled') || element?.getAttribute('disabled') === 'true';
|
|
1100
|
+
}, dropdownSelector);
|
|
1101
|
+
if (isDisabled) {
|
|
1102
|
+
throw new Error(`Element is disabled: ${dropdownSelector}. Use force: true to bypass this check.`);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// Scroll element into view if needed
|
|
1106
|
+
await this._scrollElementIntoView(dropdownSelector);
|
|
1107
|
+
// Wait for scroll animations to complete
|
|
1108
|
+
await new Promise(resolve => setTimeout(resolve, SCROLL_ANIMATION_DELAY_MS));
|
|
1109
|
+
}
|
|
1110
|
+
if (isSelectElement) {
|
|
1111
|
+
// Handle regular select element
|
|
1112
|
+
await this._handleRegularSelect(dropdownSelector, targetSelectors, {
|
|
1113
|
+
timeout,
|
|
1114
|
+
force,
|
|
1115
|
+
});
|
|
341
1116
|
}
|
|
342
1117
|
else {
|
|
343
|
-
|
|
1118
|
+
// Handle custom dropdown - the dropdownSelector is the container
|
|
1119
|
+
await this._handleCustomDropdown(dropdownSelector, targetSelectors, {
|
|
1120
|
+
timeout,
|
|
1121
|
+
force,
|
|
1122
|
+
waitForOptions,
|
|
1123
|
+
maxScrollAttempts,
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Handles selection for regular HTML select elements
|
|
1129
|
+
*/
|
|
1130
|
+
async _handleRegularSelect(selectSelector, targetSelectors, _options) {
|
|
1131
|
+
const result = await this.evaluate((sel, targetSels) => {
|
|
1132
|
+
const select = document.querySelector(sel);
|
|
1133
|
+
if (!select) {
|
|
1134
|
+
throw new Error(`Select element not found: ${sel}`);
|
|
1135
|
+
}
|
|
1136
|
+
// Check if multiple selection is allowed
|
|
1137
|
+
if (!select.multiple && targetSels.length > 1) {
|
|
1138
|
+
throw new Error(`Select element does not support multiple selection. Found ${targetSels.length} selectors but multiple is false.`);
|
|
1139
|
+
}
|
|
1140
|
+
// Clear previous selections
|
|
1141
|
+
if (select.multiple) {
|
|
1142
|
+
Array.from(select.options).forEach(option => {
|
|
1143
|
+
option.selected = false;
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
let foundCount = 0;
|
|
1147
|
+
const foundSelectors = [];
|
|
1148
|
+
// Select the options by CSS selector
|
|
1149
|
+
for (const targetSelector of targetSels) {
|
|
1150
|
+
let optionFound = false;
|
|
1151
|
+
// Find option by CSS selector within the select element
|
|
1152
|
+
const option = select.querySelector(targetSelector);
|
|
1153
|
+
if (option) {
|
|
1154
|
+
option.selected = true;
|
|
1155
|
+
optionFound = true;
|
|
1156
|
+
foundSelectors.push(targetSelector);
|
|
1157
|
+
}
|
|
1158
|
+
if (optionFound) {
|
|
1159
|
+
foundCount++;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// Trigger change event
|
|
1163
|
+
const event = new Event('change', { bubbles: true });
|
|
1164
|
+
select.dispatchEvent(event);
|
|
1165
|
+
return { foundCount, foundSelectors, totalOptions: select.options.length };
|
|
1166
|
+
}, selectSelector, targetSelectors);
|
|
1167
|
+
if (result.foundCount === 0) {
|
|
1168
|
+
throw new Error(`No options found for selectors: ${targetSelectors.join(', ')}. Available options: ${result.totalOptions}`);
|
|
344
1169
|
}
|
|
345
|
-
|
|
1170
|
+
if (result.foundCount < targetSelectors.length) {
|
|
1171
|
+
const missingSelectors = targetSelectors.filter(s => !result.foundSelectors.includes(s));
|
|
1172
|
+
throw new Error(`Some selectors not found: ${missingSelectors.join(', ')}. Found: ${result.foundSelectors.join(', ')}`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Handles selection for custom dropdown elements.
|
|
1177
|
+
* Automatically detects and clicks triggers using multiple strategies:
|
|
1178
|
+
* - Common trigger patterns ([aria-haspopup], .dropdown-trigger, etc.)
|
|
1179
|
+
* - Previous sibling elements
|
|
1180
|
+
* - ID pattern matching (menu → trigger)
|
|
1181
|
+
*/
|
|
1182
|
+
async _handleCustomDropdown(dropdownContainer, targetSelectors, options) {
|
|
1183
|
+
const { timeout, force, waitForOptions, maxScrollAttempts, dropdownOpenDelay = DROPDOWN_OPEN_DELAY_MS } = options;
|
|
1184
|
+
// For custom dropdowns, always try to find and click a trigger to open
|
|
1185
|
+
// Most custom dropdowns need a trigger click to become visible
|
|
1186
|
+
await this.evaluate((containerSel) => {
|
|
1187
|
+
const container = document.querySelector(containerSel);
|
|
1188
|
+
if (!container)
|
|
1189
|
+
return false;
|
|
1190
|
+
// Strategy 1: Look for sibling trigger elements with common patterns
|
|
1191
|
+
const parent = container.parentElement;
|
|
1192
|
+
if (parent) {
|
|
1193
|
+
const triggers = parent.querySelectorAll('[role="button"], button, [data-toggle], [aria-haspopup], .dropdown-trigger, .select-trigger');
|
|
1194
|
+
for (const trigger of triggers) {
|
|
1195
|
+
if (trigger !== container) {
|
|
1196
|
+
trigger.click();
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
// Strategy 2: Look for previous sibling that might be the trigger
|
|
1201
|
+
const prevSibling = container.previousElementSibling;
|
|
1202
|
+
if (prevSibling && prevSibling.tagName !== 'LABEL') {
|
|
1203
|
+
prevSibling.click();
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
// Strategy 3: Look for element with similar ID (e.g., menu -> trigger)
|
|
1208
|
+
const containerId = container.id;
|
|
1209
|
+
if (containerId) {
|
|
1210
|
+
const triggerPatterns = [
|
|
1211
|
+
containerId.replace('menu', 'trigger').replace('dropdown', 'trigger'),
|
|
1212
|
+
containerId.replace('-menu', '-trigger'),
|
|
1213
|
+
containerId.replace('_menu', '_trigger'),
|
|
1214
|
+
containerId + '-trigger',
|
|
1215
|
+
'trigger-' + containerId,
|
|
1216
|
+
];
|
|
1217
|
+
for (const triggerId of triggerPatterns) {
|
|
1218
|
+
const trigger = document.getElementById(triggerId);
|
|
1219
|
+
if (trigger && trigger !== container) {
|
|
1220
|
+
trigger.click();
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return false;
|
|
1226
|
+
}, dropdownContainer);
|
|
1227
|
+
// Wait for dropdown to open
|
|
1228
|
+
await new Promise(resolve => setTimeout(resolve, dropdownOpenDelay));
|
|
1229
|
+
// Wait for dropdown to be visible
|
|
1230
|
+
if (waitForOptions) {
|
|
1231
|
+
await this.waitForSelector(dropdownContainer, { timeout });
|
|
1232
|
+
}
|
|
1233
|
+
// Now check if dropdown is visible (unless force is true)
|
|
1234
|
+
if (!force) {
|
|
1235
|
+
const isVisible = await this._isElementVisible(dropdownContainer);
|
|
1236
|
+
if (!isVisible) {
|
|
1237
|
+
throw new Error(`Dropdown container is not visible after trigger click: ${dropdownContainer}. Use force: true to bypass this check.`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
// Select each target selector
|
|
1241
|
+
for (const targetSelector of targetSelectors) {
|
|
1242
|
+
await this._selectOptionFromDropdown(dropdownContainer, targetSelector, {
|
|
1243
|
+
timeout,
|
|
1244
|
+
force,
|
|
1245
|
+
maxScrollAttempts,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Selects a single option from a dropdown container.
|
|
1251
|
+
* Handles virtualized lists with intelligent scrolling to find options that may not be initially visible.
|
|
1252
|
+
*/
|
|
1253
|
+
async _selectOptionFromDropdown(dropdownSelector, targetSelector, dropdownOptions) {
|
|
1254
|
+
const { timeout, maxScrollAttempts } = dropdownOptions;
|
|
1255
|
+
const startTime = Date.now();
|
|
1256
|
+
let scrollAttempts = 0;
|
|
1257
|
+
let optionFound = false;
|
|
1258
|
+
while (scrollAttempts < maxScrollAttempts && !optionFound && (Date.now() - startTime) < timeout) {
|
|
1259
|
+
// Try to find and click the option
|
|
1260
|
+
const result = await this.evaluate((dropdownSel, targetSel) => {
|
|
1261
|
+
const dropdown = document.querySelector(dropdownSel);
|
|
1262
|
+
if (!dropdown)
|
|
1263
|
+
return { found: false, needsScroll: false };
|
|
1264
|
+
// Try to find the target element directly first
|
|
1265
|
+
let targetOption = dropdown.querySelector(targetSel);
|
|
1266
|
+
if (!targetOption) {
|
|
1267
|
+
// Check if we need to scroll to find more options
|
|
1268
|
+
const isScrollable = dropdown.scrollHeight > dropdown.clientHeight;
|
|
1269
|
+
const isAtBottom = dropdown.scrollTop + dropdown.clientHeight >= dropdown.scrollHeight - 1;
|
|
1270
|
+
return { found: false, needsScroll: isScrollable && !isAtBottom };
|
|
1271
|
+
}
|
|
1272
|
+
// Check if option is visible within the dropdown
|
|
1273
|
+
const rect = targetOption.getBoundingClientRect();
|
|
1274
|
+
const dropdownRect = dropdown.getBoundingClientRect();
|
|
1275
|
+
const isVisible = rect.top >= dropdownRect.top &&
|
|
1276
|
+
rect.bottom <= dropdownRect.bottom &&
|
|
1277
|
+
rect.left >= dropdownRect.left &&
|
|
1278
|
+
rect.right <= dropdownRect.right;
|
|
1279
|
+
if (!isVisible) {
|
|
1280
|
+
// Scroll to make the option visible
|
|
1281
|
+
targetOption.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1282
|
+
return { found: false, needsScroll: false, scrolled: true };
|
|
1283
|
+
}
|
|
1284
|
+
// Click the option
|
|
1285
|
+
targetOption.click();
|
|
1286
|
+
return { found: true, needsScroll: false };
|
|
1287
|
+
}, dropdownSelector, targetSelector);
|
|
1288
|
+
if (result.found) {
|
|
1289
|
+
optionFound = true;
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
if (result.needsScroll) {
|
|
1293
|
+
// Scroll down to load more options
|
|
1294
|
+
await this.evaluate((dropdownSel) => {
|
|
1295
|
+
const dropdown = document.querySelector(dropdownSel);
|
|
1296
|
+
if (dropdown) {
|
|
1297
|
+
dropdown.scrollTop += dropdown.clientHeight * 0.8;
|
|
1298
|
+
}
|
|
1299
|
+
}, dropdownSelector);
|
|
1300
|
+
scrollAttempts++;
|
|
1301
|
+
await new Promise(resolve => setTimeout(resolve, OPTION_LOAD_DELAY_MS)); // Wait for new options to load
|
|
1302
|
+
}
|
|
1303
|
+
else if (result.scrolled) {
|
|
1304
|
+
// Option was scrolled into view, try clicking it now
|
|
1305
|
+
await new Promise(resolve => setTimeout(resolve, SCROLL_COMPLETE_DELAY_MS)); // Wait for scroll to complete
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
// No more scrolling needed, option not found
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (!optionFound) {
|
|
1313
|
+
throw new Error(`Option "${targetSelector}" not found in dropdown "${dropdownSelector}"`);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Checks if the element specified by selector is visible (not display:none and not visibility:hidden).
|
|
1318
|
+
* The selector should be the root item which can be hidden, otherwise this function could return a false positive.
|
|
1319
|
+
* @param selector - The CSS selector for the element.
|
|
1320
|
+
* @returns true if the element is visible, false otherwise.
|
|
1321
|
+
*/
|
|
1322
|
+
async isVisible(selector) {
|
|
1323
|
+
return await this.evaluate((s) => {
|
|
1324
|
+
const el = document.querySelector(s);
|
|
1325
|
+
if (!el) {
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
const style = window.getComputedStyle(el);
|
|
1329
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
1330
|
+
}, selector);
|
|
346
1331
|
}
|
|
347
1332
|
}
|
|
348
1333
|
//# sourceMappingURL=page.js.map
|