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.
Files changed (230) hide show
  1. package/dist/browser-controller.d.ts +22 -3
  2. package/dist/browser-controller.d.ts.map +1 -1
  3. package/dist/browser-controller.js +316 -71
  4. package/dist/browser-controller.js.map +1 -1
  5. package/dist/browser-plugin.d.ts +56 -7
  6. package/dist/browser-plugin.d.ts.map +1 -1
  7. package/dist/browser-plugin.js +268 -56
  8. package/dist/browser-plugin.js.map +1 -1
  9. package/dist/browser-process/browser.d.ts +39 -0
  10. package/dist/browser-process/browser.d.ts.map +1 -1
  11. package/dist/browser-process/browser.js +125 -16
  12. package/dist/browser-process/browser.js.map +1 -1
  13. package/dist/browser-process/process.d.ts +9 -0
  14. package/dist/browser-process/process.d.ts.map +1 -1
  15. package/dist/browser-process/process.js +100 -6
  16. package/dist/browser-process/process.js.map +1 -1
  17. package/dist/browser-profiles/chrome/default.d.ts +116 -0
  18. package/dist/browser-profiles/chrome/default.d.ts.map +1 -1
  19. package/dist/browser-profiles/chrome/default.js +118 -1
  20. package/dist/browser-profiles/chrome/default.js.map +1 -1
  21. package/dist/browser-profiles/chrome/populate-profile.d.ts +76 -0
  22. package/dist/browser-profiles/chrome/populate-profile.d.ts.map +1 -0
  23. package/dist/browser-profiles/chrome/populate-profile.js +300 -0
  24. package/dist/browser-profiles/chrome/populate-profile.js.map +1 -0
  25. package/dist/browser-profiles/index.d.ts +1 -0
  26. package/dist/browser-profiles/index.d.ts.map +1 -1
  27. package/dist/browser-profiles/index.js +2 -0
  28. package/dist/browser-profiles/index.js.map +1 -1
  29. package/dist/crawler.d.ts +81 -9
  30. package/dist/crawler.d.ts.map +1 -1
  31. package/dist/crawler.js +26 -10
  32. package/dist/crawler.js.map +1 -1
  33. package/dist/fingerprinting/all-fingerprint-defender/_locales/en/messages.json +95 -0
  34. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-16ff15da.d.ts +2 -0
  35. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-16ff15da.d.ts.map +1 -0
  36. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-16ff15da.js +1 -0
  37. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-16ff15da.js.map +1 -0
  38. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-1a1456ec.d.ts +2 -0
  39. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-1a1456ec.d.ts.map +1 -0
  40. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-1a1456ec.js +1 -0
  41. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-1a1456ec.js.map +1 -0
  42. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-3a7b59dd.d.ts +83 -0
  43. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-3a7b59dd.d.ts.map +1 -0
  44. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-3a7b59dd.js +1 -0
  45. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-3a7b59dd.js.map +1 -0
  46. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-7ce85519.d.ts +2 -0
  47. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-7ce85519.d.ts.map +1 -0
  48. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-7ce85519.js +1 -0
  49. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-7ce85519.js.map +1 -0
  50. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-aaea1190.d.ts +2 -0
  51. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-aaea1190.d.ts.map +1 -0
  52. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-aaea1190.js +1 -0
  53. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-aaea1190.js.map +1 -0
  54. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-b4410958.d.ts +2 -0
  55. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-b4410958.d.ts.map +1 -0
  56. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-b4410958.js +1 -0
  57. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-b4410958.js.map +1 -0
  58. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-dfed3562.d.ts +2 -0
  59. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-dfed3562.d.ts.map +1 -0
  60. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-dfed3562.js +1 -0
  61. package/dist/fingerprinting/all-fingerprint-defender/assets/chunk-dfed3562.js.map +1 -0
  62. package/dist/fingerprinting/all-fingerprint-defender/assets/debounce-09920c81.css +1 -0
  63. package/dist/fingerprinting/all-fingerprint-defender/assets/options-fe2fb5aa.css +1 -0
  64. package/dist/fingerprinting/all-fingerprint-defender/assets/popup-1886d2ef.css +1 -0
  65. package/dist/fingerprinting/all-fingerprint-defender/img/icon-128.png +0 -0
  66. package/dist/fingerprinting/all-fingerprint-defender/img/icon-16.png +0 -0
  67. package/dist/fingerprinting/all-fingerprint-defender/img/icon-24.png +0 -0
  68. package/dist/fingerprinting/all-fingerprint-defender/img/icon-32-disabled.png +0 -0
  69. package/dist/fingerprinting/all-fingerprint-defender/img/icon-32.png +0 -0
  70. package/dist/fingerprinting/all-fingerprint-defender/img/icon-48.png +0 -0
  71. package/dist/fingerprinting/all-fingerprint-defender/manifest.json +83 -0
  72. package/dist/fingerprinting/all-fingerprint-defender/options.html +17 -0
  73. package/dist/fingerprinting/all-fingerprint-defender/popup.html +23 -0
  74. package/dist/fingerprinting/anti-webgpu/background.d.ts +2 -0
  75. package/dist/fingerprinting/anti-webgpu/background.d.ts.map +1 -0
  76. package/dist/fingerprinting/anti-webgpu/background.js +4 -0
  77. package/dist/fingerprinting/anti-webgpu/background.js.map +1 -0
  78. package/dist/fingerprinting/anti-webgpu/data/content_script/inject.d.ts +2 -0
  79. package/dist/fingerprinting/anti-webgpu/data/content_script/inject.d.ts.map +1 -0
  80. package/dist/fingerprinting/anti-webgpu/data/content_script/inject.js +50 -0
  81. package/dist/fingerprinting/anti-webgpu/data/content_script/inject.js.map +1 -0
  82. package/dist/fingerprinting/anti-webgpu/data/content_script/page_context/inject.d.ts +2 -0
  83. package/dist/fingerprinting/anti-webgpu/data/content_script/page_context/inject.d.ts.map +1 -0
  84. package/dist/fingerprinting/anti-webgpu/data/content_script/page_context/inject.js +172 -0
  85. package/dist/fingerprinting/anti-webgpu/data/content_script/page_context/inject.js.map +1 -0
  86. package/dist/fingerprinting/anti-webgpu/data/icons/128.png +0 -0
  87. package/dist/fingerprinting/anti-webgpu/data/icons/16.png +0 -0
  88. package/dist/fingerprinting/anti-webgpu/data/icons/32.png +0 -0
  89. package/dist/fingerprinting/anti-webgpu/data/icons/48.png +0 -0
  90. package/dist/fingerprinting/anti-webgpu/data/icons/64.png +0 -0
  91. package/dist/fingerprinting/anti-webgpu/data/popup/popup.css +88 -0
  92. package/dist/fingerprinting/anti-webgpu/data/popup/popup.d.ts +2 -0
  93. package/dist/fingerprinting/anti-webgpu/data/popup/popup.d.ts.map +1 -0
  94. package/dist/fingerprinting/anti-webgpu/data/popup/popup.html +58 -0
  95. package/dist/fingerprinting/anti-webgpu/data/popup/popup.js +96 -0
  96. package/dist/fingerprinting/anti-webgpu/data/popup/popup.js.map +1 -0
  97. package/dist/fingerprinting/anti-webgpu/lib/chrome.d.ts +2 -0
  98. package/dist/fingerprinting/anti-webgpu/lib/chrome.d.ts.map +1 -0
  99. package/dist/fingerprinting/anti-webgpu/lib/chrome.js +249 -0
  100. package/dist/fingerprinting/anti-webgpu/lib/chrome.js.map +1 -0
  101. package/dist/fingerprinting/anti-webgpu/lib/common.d.ts +2 -0
  102. package/dist/fingerprinting/anti-webgpu/lib/common.d.ts.map +1 -0
  103. package/dist/fingerprinting/anti-webgpu/lib/common.js +86 -0
  104. package/dist/fingerprinting/anti-webgpu/lib/common.js.map +1 -0
  105. package/dist/fingerprinting/anti-webgpu/lib/config.d.ts +2 -0
  106. package/dist/fingerprinting/anti-webgpu/lib/config.d.ts.map +1 -0
  107. package/dist/fingerprinting/anti-webgpu/lib/config.js +14 -0
  108. package/dist/fingerprinting/anti-webgpu/lib/config.js.map +1 -0
  109. package/dist/fingerprinting/anti-webgpu/lib/runtime.d.ts +2 -0
  110. package/dist/fingerprinting/anti-webgpu/lib/runtime.d.ts.map +1 -0
  111. package/dist/fingerprinting/anti-webgpu/lib/runtime.js +107 -0
  112. package/dist/fingerprinting/anti-webgpu/lib/runtime.js.map +1 -0
  113. package/dist/fingerprinting/anti-webgpu/manifest.json +58 -0
  114. package/dist/fingerprinting/custom-fingerprint-injector.d.ts +87 -0
  115. package/dist/fingerprinting/custom-fingerprint-injector.d.ts.map +1 -0
  116. package/dist/fingerprinting/custom-fingerprint-injector.js +342 -0
  117. package/dist/fingerprinting/custom-fingerprint-injector.js.map +1 -0
  118. package/dist/fingerprinting/fingerprint-injector.d.ts +157 -0
  119. package/dist/fingerprinting/fingerprint-injector.d.ts.map +1 -0
  120. package/dist/fingerprinting/fingerprint-injector.js +632 -0
  121. package/dist/fingerprinting/fingerprint-injector.js.map +1 -0
  122. package/dist/fingerprinting/fingerprint-overrides/audio-spoofing.d.ts +6 -0
  123. package/dist/fingerprinting/fingerprint-overrides/audio-spoofing.d.ts.map +1 -0
  124. package/dist/fingerprinting/fingerprint-overrides/audio-spoofing.js +119 -0
  125. package/dist/fingerprinting/fingerprint-overrides/audio-spoofing.js.map +1 -0
  126. package/dist/fingerprinting/fingerprint-overrides/canvas-protection.d.ts +6 -0
  127. package/dist/fingerprinting/fingerprint-overrides/canvas-protection.d.ts.map +1 -0
  128. package/dist/fingerprinting/fingerprint-overrides/canvas-protection.js +149 -0
  129. package/dist/fingerprinting/fingerprint-overrides/canvas-protection.js.map +1 -0
  130. package/dist/fingerprinting/fingerprint-overrides/cdp-detection-bypass.d.ts +14 -0
  131. package/dist/fingerprinting/fingerprint-overrides/cdp-detection-bypass.d.ts.map +1 -0
  132. package/dist/fingerprinting/fingerprint-overrides/cdp-detection-bypass.js +763 -0
  133. package/dist/fingerprinting/fingerprint-overrides/cdp-detection-bypass.js.map +1 -0
  134. package/dist/fingerprinting/fingerprint-overrides/client-rect-spoofing.d.ts +6 -0
  135. package/dist/fingerprinting/fingerprint-overrides/client-rect-spoofing.d.ts.map +1 -0
  136. package/dist/fingerprinting/fingerprint-overrides/client-rect-spoofing.js +195 -0
  137. package/dist/fingerprinting/fingerprint-overrides/client-rect-spoofing.js.map +1 -0
  138. package/dist/fingerprinting/fingerprint-overrides/coalesced-events-spoofing.d.ts +10 -0
  139. package/dist/fingerprinting/fingerprint-overrides/coalesced-events-spoofing.d.ts.map +1 -0
  140. package/dist/fingerprinting/fingerprint-overrides/coalesced-events-spoofing.js +195 -0
  141. package/dist/fingerprinting/fingerprint-overrides/coalesced-events-spoofing.js.map +1 -0
  142. package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts +28 -0
  143. package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts.map +1 -0
  144. package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js +1181 -0
  145. package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js.map +1 -0
  146. package/dist/fingerprinting/fingerprint-overrides/font-spoofing.d.ts +7 -0
  147. package/dist/fingerprinting/fingerprint-overrides/font-spoofing.d.ts.map +1 -0
  148. package/dist/fingerprinting/fingerprint-overrides/font-spoofing.js +171 -0
  149. package/dist/fingerprinting/fingerprint-overrides/font-spoofing.js.map +1 -0
  150. package/dist/fingerprinting/fingerprint-overrides/index.d.ts +36 -0
  151. package/dist/fingerprinting/fingerprint-overrides/index.d.ts.map +1 -0
  152. package/dist/fingerprinting/fingerprint-overrides/index.js +40 -0
  153. package/dist/fingerprinting/fingerprint-overrides/index.js.map +1 -0
  154. package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.d.ts +45 -0
  155. package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.d.ts.map +1 -0
  156. package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js +268 -0
  157. package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js.map +1 -0
  158. package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.d.ts +6 -0
  159. package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.d.ts.map +1 -0
  160. package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.js +301 -0
  161. package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.js.map +1 -0
  162. package/dist/fingerprinting/fingerprint-overrides/mouse-humanization.d.ts +7 -0
  163. package/dist/fingerprinting/fingerprint-overrides/mouse-humanization.d.ts.map +1 -0
  164. package/dist/fingerprinting/fingerprint-overrides/mouse-humanization.js +58 -0
  165. package/dist/fingerprinting/fingerprint-overrides/mouse-humanization.js.map +1 -0
  166. package/dist/fingerprinting/fingerprint-overrides/performance-spoofing.d.ts +6 -0
  167. package/dist/fingerprinting/fingerprint-overrides/performance-spoofing.d.ts.map +1 -0
  168. package/dist/fingerprinting/fingerprint-overrides/performance-spoofing.js +249 -0
  169. package/dist/fingerprinting/fingerprint-overrides/performance-spoofing.js.map +1 -0
  170. package/dist/fingerprinting/fingerprint-overrides/platform-consistency.d.ts +33 -0
  171. package/dist/fingerprinting/fingerprint-overrides/platform-consistency.d.ts.map +1 -0
  172. package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js +618 -0
  173. package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js.map +1 -0
  174. package/dist/fingerprinting/fingerprint-overrides/prototype-integrity.d.ts +13 -0
  175. package/dist/fingerprinting/fingerprint-overrides/prototype-integrity.d.ts.map +1 -0
  176. package/dist/fingerprinting/fingerprint-overrides/prototype-integrity.js +356 -0
  177. package/dist/fingerprinting/fingerprint-overrides/prototype-integrity.js.map +1 -0
  178. package/dist/fingerprinting/fingerprint-overrides/runtime-enable-bypass.d.ts +18 -0
  179. package/dist/fingerprinting/fingerprint-overrides/runtime-enable-bypass.d.ts.map +1 -0
  180. package/dist/fingerprinting/fingerprint-overrides/runtime-enable-bypass.js +171 -0
  181. package/dist/fingerprinting/fingerprint-overrides/runtime-enable-bypass.js.map +1 -0
  182. package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.d.ts +55 -0
  183. package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.d.ts.map +1 -0
  184. package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js +244 -0
  185. package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js.map +1 -0
  186. package/dist/fingerprinting/fingerprint-overrides/stealth-script.d.ts +14 -0
  187. package/dist/fingerprinting/fingerprint-overrides/stealth-script.d.ts.map +1 -0
  188. package/dist/fingerprinting/fingerprint-overrides/stealth-script.js +925 -0
  189. package/dist/fingerprinting/fingerprint-overrides/stealth-script.js.map +1 -0
  190. package/dist/fingerprinting/fingerprint-overrides/storage-consistency.d.ts +13 -0
  191. package/dist/fingerprinting/fingerprint-overrides/storage-consistency.d.ts.map +1 -0
  192. package/dist/fingerprinting/fingerprint-overrides/storage-consistency.js +346 -0
  193. package/dist/fingerprinting/fingerprint-overrides/storage-consistency.js.map +1 -0
  194. package/dist/fingerprinting/fingerprint-overrides/timing-consistency.d.ts +13 -0
  195. package/dist/fingerprinting/fingerprint-overrides/timing-consistency.d.ts.map +1 -0
  196. package/dist/fingerprinting/fingerprint-overrides/timing-consistency.js +264 -0
  197. package/dist/fingerprinting/fingerprint-overrides/timing-consistency.js.map +1 -0
  198. package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts +27 -0
  199. package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts.map +1 -0
  200. package/dist/fingerprinting/fingerprint-overrides/ua-ch.js +213 -0
  201. package/dist/fingerprinting/fingerprint-overrides/ua-ch.js.map +1 -0
  202. package/dist/fingerprinting/fingerprint-overrides/utils.d.ts +12 -0
  203. package/dist/fingerprinting/fingerprint-overrides/utils.d.ts.map +1 -0
  204. package/dist/fingerprinting/fingerprint-overrides/utils.js +517 -0
  205. package/dist/fingerprinting/fingerprint-overrides/utils.js.map +1 -0
  206. package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.d.ts +12 -0
  207. package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.d.ts.map +1 -0
  208. package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js +215 -0
  209. package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js.map +1 -0
  210. package/dist/fingerprinting/fingerprint-overrides/webgpu-spoofing.d.ts +6 -0
  211. package/dist/fingerprinting/fingerprint-overrides/webgpu-spoofing.d.ts.map +1 -0
  212. package/dist/fingerprinting/fingerprint-overrides/webgpu-spoofing.js +202 -0
  213. package/dist/fingerprinting/fingerprint-overrides/webgpu-spoofing.js.map +1 -0
  214. package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.d.ts +6 -0
  215. package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.d.ts.map +1 -0
  216. package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.js +188 -0
  217. package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.js.map +1 -0
  218. package/dist/index.d.ts +4 -0
  219. package/dist/index.d.ts.map +1 -1
  220. package/dist/index.js +2 -0
  221. package/dist/index.js.map +1 -1
  222. package/dist/launcher-wrap.d.ts +2 -2
  223. package/dist/launcher-wrap.d.ts.map +1 -1
  224. package/dist/launcher-wrap.js.map +1 -1
  225. package/dist/page.d.ts +160 -13
  226. package/dist/page.d.ts.map +1 -1
  227. package/dist/page.js +1027 -42
  228. package/dist/page.js.map +1 -1
  229. package/dist/tsconfig.build.tsbuildinfo +1 -1
  230. 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
- // const { Page, DOM, Network, Runtime } = this.client;
30
- // await Promise.all([Page.enable(), DOM.enable(), Network.enable(), Runtime.enable()]);
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.evaluate("window.location.href");
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, Runtime } = this.client;
41
- const timeout = options?.timeout ?? 30000;
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
- const { result } = await Runtime.evaluate({ expression: 'window.location.href' });
60
- this.loadedUrl = result.value;
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 this.waitForSelector(selector);
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
- const { model } = await DOM.getBoxModel({ nodeId });
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
- await Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
101
- await Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
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 ?? 30000;
139
- const pollingInterval = 100;
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, pollingInterval));
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.client.Network.enable(); // Ensure Network monitoring is enabled
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.client.Network.enable();
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
- const { contentSize } = await Page.getLayoutMetrics();
200
- await this.setViewport({ width: contentSize.width, height: contentSize.height });
790
+ screenshotOptions.captureBeyondViewport = true;
201
791
  }
202
- const { data } = await Page.captureScreenshot({ format: 'png' });
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 Network.enable();
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
- * Adds a script to be evaluated on every new document initialization.
323
- *
324
- * @param {string | Function} script - The JavaScript code or function to inject.
325
- * @returns {Promise<void>} - Resolves when the script has been added.
326
- */
327
- async addInitScript(script) {
328
- const { Page } = this.client;
329
- let scriptText;
330
- if (typeof script === 'function') {
331
- // Convert the function to a string and extract its body
332
- const fnStr = script.toString();
333
- const bodyMatch = fnStr.match(/function\s*\(\)\s*{([\s\S]*)}/);
334
- if (!bodyMatch) {
335
- throw new Error('Invalid function format for init script.');
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
- else if (typeof script === 'string') {
340
- scriptText = script;
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
- throw new Error('Script must be a string or a function');
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
- await Page.addScriptToEvaluateOnNewDocument({ source: scriptText });
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