jumpy-lion 0.1.6-beta.9 → 1.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -607
- package/browser/fonts/macos-bundle/41-macos-remaining.conf +157 -0
- package/browser/fonts/macos-bundle/README.md +24 -0
- package/browser/fonts/macos-bundle/build-metric-fonts.py +1010 -0
- package/browser/fonts/macos-bundle/generated/AmericanTypewriter-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AmericanTypewriter-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AndaleMono-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AndaleMono-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AndaleMono-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AndaleMono-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AppleChancery-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AppleChancery-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AppleChancery-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AppleChancery-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Arial-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Arial-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Arial-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Arial-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ArialBlack-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ArialBlack-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ArialBlack-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ArialBlack-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ArialNarrow-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ArialNarrow-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ArialNarrow-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ArialNarrow-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Avenir-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Avenir-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Avenir-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Avenir-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AvenirNext-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AvenirNext-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AvenirNext-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AvenirNext-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AvenirNextCondensed-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AvenirNextCondensed-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AvenirNextCondensed-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/AvenirNextCondensed-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Baskerville-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Baskerville-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Baskerville-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Baskerville-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BigCaslon-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BigCaslon-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BigCaslon-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BigCaslon-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BlinkMacSystemFont-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BlinkMacSystemFont-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BlinkMacSystemFont-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BlinkMacSystemFont-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72Oldstyle-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72Oldstyle-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72Oldstyle-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72Oldstyle-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72Smallcaps-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72Smallcaps-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72Smallcaps-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Bodoni72Smallcaps-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BradleyHand-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/BrushScriptMT-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Chalkboard-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Chalkboard-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Chalkboard-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Chalkboard-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ChalkboardSE-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ChalkboardSE-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ChalkboardSE-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ChalkboardSE-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Chalkduster-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Chalkduster-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Chalkduster-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Chalkduster-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Charter-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Charter-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Charter-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Charter-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Cochin-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Cochin-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Cochin-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Cochin-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ComicSansMS-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ComicSansMS-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ComicSansMS-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/ComicSansMS-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Copperplate-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Copperplate-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Courier-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Courier-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Courier-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Courier-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/CourierNew-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Didot-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Didot-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Didot-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Didot-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Futura-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Futura-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Futura-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Futura-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Geneva-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Georgia-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Georgia-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Georgia-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Georgia-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/GillSans-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/GillSans-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/GillSans-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/GillSans-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Helvetica-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Helvetica-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Helvetica-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Helvetica-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/HelveticaNeue-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/HelveticaNeue-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/HelveticaNeue-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/HelveticaNeue-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Herculanum-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Herculanum-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Herculanum-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Herculanum-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/HoeflerText-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/HoeflerText-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/HoeflerText-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/HoeflerText-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Impact-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Impact-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Impact-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Impact-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/LucidaGrande-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/LucidaGrande-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/MarkerFelt-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/MarkerFelt-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/MarkerFelt-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/MarkerFelt-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Menlo-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Monaco-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Monaco-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Monaco-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Monaco-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Noteworthy-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Noteworthy-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Optima-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Optima-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Optima-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Optima-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Palatino-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Palatino-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Palatino-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Palatino-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Papyrus-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Papyrus-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Papyrus-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Papyrus-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/PartyLET-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/PartyLET-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/PartyLET-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/PartyLET-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Phosphate-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/README.md +132 -0
- package/browser/fonts/macos-bundle/generated/Rockwell-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Rockwell-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Rockwell-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Rockwell-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/SavoyeLET-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/SavoyeLET-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/SavoyeLET-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/SavoyeLET-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Seravek-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Seravek-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Seravek-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Seravek-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/SignPainter-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/SignPainter-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Skia-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Skia-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Skia-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Skia-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/SnellRoundhand-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/SnellRoundhand-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Tahoma-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Tahoma-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Times-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Times-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Times-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Times-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/TimesNewRoman-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/TimesNewRoman-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/TimesNewRoman-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/TimesNewRoman-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Trattatello-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/TrebuchetMS-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/TrebuchetMS-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/TrebuchetMS-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/TrebuchetMS-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Verdana-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Verdana-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Verdana-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Verdana-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Webdings-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Webdings-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Webdings-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Webdings-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Zapfino-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Zapfino-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Zapfino-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/Zapfino-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/manifest.json +2928 -0
- package/browser/fonts/macos-bundle/generated/systemui-Bold.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/systemui-BoldItalic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/systemui-Italic.ttf +0 -0
- package/browser/fonts/macos-bundle/generated/systemui-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/install.sh +136 -0
- package/browser/fonts/macos-bundle/vendor/README.md +30 -0
- package/browser/fonts/macos-bundle/vendor/anton/Anton-Regular.ttf +0 -0
- package/browser/fonts/macos-bundle/vendor/anton/OFL.txt +93 -0
- package/browser/fonts/macos-bundle/vendor/gelasio/Gelasio-Italic-VariableFont_wght.ttf +0 -0
- package/browser/fonts/macos-bundle/vendor/gelasio/Gelasio-VariableFont_wght.ttf +0 -0
- package/browser/fonts/macos-bundle/vendor/gelasio/OFL.txt +93 -0
- package/browser/fonts/macos-bundle/verify-metric-fonts.py +171 -0
- package/dist/browser-controller.d.ts +47 -4
- package/dist/browser-controller.d.ts.map +1 -1
- package/dist/browser-controller.js +219 -278
- package/dist/browser-controller.js.map +1 -1
- package/dist/browser-plugin.d.ts +82 -110
- package/dist/browser-plugin.d.ts.map +1 -1
- package/dist/browser-plugin.js +405 -379
- package/dist/browser-plugin.js.map +1 -1
- package/dist/browser-process/align-fingerprint-config.d.ts +36 -0
- package/dist/browser-process/align-fingerprint-config.d.ts.map +1 -0
- package/dist/browser-process/align-fingerprint-config.js +74 -0
- package/dist/browser-process/align-fingerprint-config.js.map +1 -0
- package/dist/browser-process/anti-detect-browser.d.ts +2 -2
- package/dist/browser-process/anti-detect-browser.js +7 -7
- package/dist/browser-process/anti-detect-config.d.ts +146 -2
- package/dist/browser-process/anti-detect-config.d.ts.map +1 -1
- package/dist/browser-process/anti-detect-config.js +432 -127
- package/dist/browser-process/anti-detect-config.js.map +1 -1
- package/dist/browser-process/browser.d.ts +25 -92
- package/dist/browser-process/browser.d.ts.map +1 -1
- package/dist/browser-process/browser.js +467 -79
- package/dist/browser-process/browser.js.map +1 -1
- package/dist/browser-process/fingerprint-config.d.ts +146 -0
- package/dist/browser-process/fingerprint-config.d.ts.map +1 -0
- package/dist/browser-process/fingerprint-config.js +351 -0
- package/dist/browser-process/fingerprint-config.js.map +1 -0
- package/dist/browser-process/get-chrome-executable.d.ts +6 -0
- package/dist/browser-process/get-chrome-executable.d.ts.map +1 -1
- package/dist/browser-process/get-chrome-executable.js +79 -4
- package/dist/browser-process/get-chrome-executable.js.map +1 -1
- package/dist/browser-process/gpu-family-profiles.d.ts +58 -0
- package/dist/browser-process/gpu-family-profiles.d.ts.map +1 -0
- package/dist/browser-process/gpu-family-profiles.js +480 -0
- package/dist/browser-process/gpu-family-profiles.js.map +1 -0
- package/dist/browser-process/launch-options.d.ts +301 -0
- package/dist/browser-process/launch-options.d.ts.map +1 -0
- package/dist/browser-process/launch-options.js +2 -0
- package/dist/browser-process/launch-options.js.map +1 -0
- package/dist/browser-process/port-allocator.d.ts +64 -0
- package/dist/browser-process/port-allocator.d.ts.map +1 -0
- package/dist/browser-process/port-allocator.js +198 -0
- package/dist/browser-process/port-allocator.js.map +1 -0
- package/dist/browser-process/process.d.ts +85 -0
- package/dist/browser-process/process.d.ts.map +1 -1
- package/dist/browser-process/process.js +235 -19
- package/dist/browser-process/process.js.map +1 -1
- package/dist/browser-process/signal-cleanup.d.ts +40 -0
- package/dist/browser-process/signal-cleanup.d.ts.map +1 -0
- package/dist/browser-process/signal-cleanup.js +105 -0
- package/dist/browser-process/signal-cleanup.js.map +1 -0
- package/dist/browser-process/widevine-cdm.d.ts +144 -0
- package/dist/browser-process/widevine-cdm.d.ts.map +1 -0
- package/dist/browser-process/widevine-cdm.js +298 -0
- package/dist/browser-process/widevine-cdm.js.map +1 -0
- package/dist/browser-profiles/chrome/populate-profile.d.ts.map +1 -1
- package/dist/browser-profiles/chrome/populate-profile.js +21 -22
- package/dist/browser-profiles/chrome/populate-profile.js.map +1 -1
- package/dist/connection/cdp-reconnection.d.ts +50 -0
- package/dist/connection/cdp-reconnection.d.ts.map +1 -0
- package/dist/connection/cdp-reconnection.js +75 -0
- package/dist/connection/cdp-reconnection.js.map +1 -0
- package/dist/connection/page-session.d.ts +114 -0
- package/dist/connection/page-session.d.ts.map +1 -0
- package/dist/connection/page-session.js +237 -0
- package/dist/connection/page-session.js.map +1 -0
- package/dist/connection/session-client.d.ts +34 -0
- package/dist/connection/session-client.d.ts.map +1 -0
- package/dist/connection/session-client.js +129 -0
- package/dist/connection/session-client.js.map +1 -0
- package/dist/crawler.d.ts +9 -88
- package/dist/crawler.d.ts.map +1 -1
- package/dist/crawler.js +157 -8
- package/dist/crawler.js.map +1 -1
- package/dist/fingerprinting/fingerprint-injector.d.ts +24 -114
- package/dist/fingerprinting/fingerprint-injector.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-injector.js +305 -611
- package/dist/fingerprinting/fingerprint-injector.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts +1 -1
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js +16 -14
- package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/index.d.ts +6 -2
- package/dist/fingerprinting/fingerprint-overrides/index.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/index.js +6 -2
- package/dist/fingerprinting/fingerprint-overrides/index.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.d.ts +1 -1
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js +4 -4
- package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.js +4 -4
- package/dist/fingerprinting/fingerprint-overrides/navigator-override.d.ts +18 -0
- package/dist/fingerprinting/fingerprint-overrides/navigator-override.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/navigator-override.js +136 -0
- package/dist/fingerprinting/fingerprint-overrides/navigator-override.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/override.d.ts +151 -0
- package/dist/fingerprinting/fingerprint-overrides/override.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/override.js +14 -0
- package/dist/fingerprinting/fingerprint-overrides/override.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js +46 -15
- package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/registry.d.ts +43 -0
- package/dist/fingerprinting/fingerprint-overrides/registry.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/registry.js +342 -0
- package/dist/fingerprinting/fingerprint-overrides/registry.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/screen-override.d.ts +16 -0
- package/dist/fingerprinting/fingerprint-overrides/screen-override.d.ts.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/screen-override.js +175 -0
- package/dist/fingerprinting/fingerprint-overrides/screen-override.js.map +1 -0
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.d.ts +1 -1
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js +4 -4
- package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.js +75 -9
- package/dist/fingerprinting/fingerprint-overrides/stealth-script.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts +1 -1
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.js +34 -20
- package/dist/fingerprinting/fingerprint-overrides/ua-ch.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.d.ts.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js +28 -9
- package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js.map +1 -1
- package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.js +1 -1
- package/dist/fingerprinting/hardware-realism.d.ts +74 -0
- package/dist/fingerprinting/hardware-realism.d.ts.map +1 -0
- package/dist/fingerprinting/hardware-realism.js +155 -0
- package/dist/fingerprinting/hardware-realism.js.map +1 -0
- package/dist/fingerprinting/injection-planner.d.ts +133 -0
- package/dist/fingerprinting/injection-planner.d.ts.map +1 -0
- package/dist/fingerprinting/injection-planner.js +497 -0
- package/dist/fingerprinting/injection-planner.js.map +1 -0
- package/dist/fingerprinting/locale-resolver.d.ts +8 -1
- package/dist/fingerprinting/locale-resolver.d.ts.map +1 -1
- package/dist/fingerprinting/locale-resolver.js +26 -3
- package/dist/fingerprinting/locale-resolver.js.map +1 -1
- package/dist/fingerprinting/non-apify-fingerprint-generator.d.ts +4 -4
- package/dist/fingerprinting/non-apify-fingerprint-generator.d.ts.map +1 -1
- package/dist/fingerprinting/non-apify-fingerprint-generator.js +35 -69
- package/dist/fingerprinting/non-apify-fingerprint-generator.js.map +1 -1
- package/dist/fingerprinting/non-apify-profiles.json +10515 -8494
- package/dist/fingerprinting/os-consistency.d.ts +28 -0
- package/dist/fingerprinting/os-consistency.d.ts.map +1 -0
- package/dist/fingerprinting/os-consistency.js +45 -0
- package/dist/fingerprinting/os-consistency.js.map +1 -0
- package/dist/fingerprinting/profile-quality.d.ts +25 -0
- package/dist/fingerprinting/profile-quality.d.ts.map +1 -0
- package/dist/fingerprinting/profile-quality.js +310 -0
- package/dist/fingerprinting/profile-quality.js.map +1 -0
- package/dist/fingerprinting/profile-selector.d.ts +102 -0
- package/dist/fingerprinting/profile-selector.d.ts.map +1 -0
- package/dist/fingerprinting/profile-selector.js +168 -0
- package/dist/fingerprinting/profile-selector.js.map +1 -0
- package/dist/fingerprinting/ua-alignment.d.ts +83 -0
- package/dist/fingerprinting/ua-alignment.d.ts.map +1 -0
- package/dist/fingerprinting/ua-alignment.js +197 -0
- package/dist/fingerprinting/ua-alignment.js.map +1 -0
- package/dist/fingerprinting/ua-ch-brands.d.ts +88 -0
- package/dist/fingerprinting/ua-ch-brands.d.ts.map +1 -0
- package/dist/fingerprinting/ua-ch-brands.js +174 -0
- package/dist/fingerprinting/ua-ch-brands.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/input/dropdown-selector.d.ts +90 -0
- package/dist/input/dropdown-selector.d.ts.map +1 -0
- package/dist/input/dropdown-selector.js +394 -0
- package/dist/input/dropdown-selector.js.map +1 -0
- package/dist/input/element-target.d.ts +152 -0
- package/dist/input/element-target.d.ts.map +1 -0
- package/dist/input/element-target.js +461 -0
- package/dist/input/element-target.js.map +1 -0
- package/dist/input/input-emulator.d.ts +85 -0
- package/dist/input/input-emulator.d.ts.map +1 -0
- package/dist/input/input-emulator.js +325 -0
- package/dist/input/input-emulator.js.map +1 -0
- package/dist/input/input-transport.d.ts +60 -0
- package/dist/input/input-transport.d.ts.map +1 -0
- package/dist/input/input-transport.js +28 -0
- package/dist/input/input-transport.js.map +1 -0
- package/dist/input/recording-transport.d.ts +32 -0
- package/dist/input/recording-transport.d.ts.map +1 -0
- package/dist/input/recording-transport.js +43 -0
- package/dist/input/recording-transport.js.map +1 -0
- package/dist/keyboard-layout.d.ts +2 -2
- package/dist/keyboard-layout.js +2 -2
- package/dist/navigation/page-navigation.d.ts +67 -0
- package/dist/navigation/page-navigation.d.ts.map +1 -0
- package/dist/navigation/page-navigation.js +122 -0
- package/dist/navigation/page-navigation.js.map +1 -0
- package/dist/network/network-watch.d.ts +110 -0
- package/dist/network/network-watch.d.ts.map +1 -0
- package/dist/network/network-watch.js +223 -0
- package/dist/network/network-watch.js.map +1 -0
- package/dist/page.d.ts +123 -104
- package/dist/page.d.ts.map +1 -1
- package/dist/page.js +348 -1129
- package/dist/page.js.map +1 -1
- package/dist/session/inject-hook.d.ts +24 -0
- package/dist/session/inject-hook.d.ts.map +1 -0
- package/dist/session/inject-hook.js +80 -0
- package/dist/session/inject-hook.js.map +1 -0
- package/dist/session/save.d.ts +21 -0
- package/dist/session/save.d.ts.map +1 -0
- package/dist/session/save.js +163 -0
- package/dist/session/save.js.map +1 -0
- package/dist/session/tar-data-dir.d.ts +18 -0
- package/dist/session/tar-data-dir.d.ts.map +1 -0
- package/dist/session/tar-data-dir.js +105 -0
- package/dist/session/tar-data-dir.js.map +1 -0
- package/dist/session/types.d.ts +80 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +29 -0
- package/dist/session/types.js.map +1 -0
- package/dist/session-profile.d.ts +102 -0
- package/dist/session-profile.d.ts.map +1 -0
- package/dist/session-profile.js +163 -0
- package/dist/session-profile.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +15 -6
- package/dist/fingerprinting/custom-fingerprint-injector.d.ts +0 -87
- package/dist/fingerprinting/custom-fingerprint-injector.d.ts.map +0 -1
- package/dist/fingerprinting/custom-fingerprint-injector.js +0 -342
- package/dist/fingerprinting/custom-fingerprint-injector.js.map +0 -1
- package/dist/launcher-wrap.d.ts +0 -10
- package/dist/launcher-wrap.d.ts.map +0 -1
- package/dist/launcher-wrap.js +0 -11
- package/dist/launcher-wrap.js.map +0 -1
package/dist/page.js
CHANGED
|
@@ -1,230 +1,255 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { load as cheerioLoad } from 'cheerio';
|
|
3
|
-
import CDP from 'chrome-remote-interface';
|
|
4
3
|
import { log } from 'crawlee';
|
|
4
|
+
import { PageSession } from './connection/page-session.js';
|
|
5
5
|
import { PAGE_CLOSED, PAGE_CREATED } from './events.js';
|
|
6
6
|
import { humanScrollToElement } from './fingerprinting/fingerprint-overrides/index.js';
|
|
7
|
-
import {
|
|
7
|
+
import { DropdownSelector } from './input/dropdown-selector.js';
|
|
8
|
+
import { ElementTarget } from './input/element-target.js';
|
|
9
|
+
import { InputEmulator } from './input/input-emulator.js';
|
|
10
|
+
import { CdpInputTransport } from './input/input-transport.js';
|
|
11
|
+
import { PageNavigation } from './navigation/page-navigation.js';
|
|
12
|
+
import { NetworkWatch } from './network/network-watch.js';
|
|
8
13
|
// Constants for timing and delays
|
|
9
|
-
|
|
10
|
-
const SCROLL_ANIMATION_DELAY_MS = 200; // ms to wait for scroll animations
|
|
11
|
-
const OPTION_LOAD_DELAY_MS = 300; // ms to wait for new options to load in dropdowns
|
|
12
|
-
const DROPDOWN_OPEN_DELAY_MS = 300; // ms to wait for dropdown to open
|
|
13
|
-
const HUMAN_DELAY_MS = 200; // ms delay for human-like interactions
|
|
14
|
-
const SCROLL_COMPLETE_DELAY_MS = 300; // ms to wait for scroll to complete
|
|
15
|
-
const HUMAN_KEYBOARD_DELAY_MIN_MS = 45;
|
|
16
|
-
const HUMAN_KEYBOARD_DELAY_MAX_MS = 160;
|
|
17
|
-
const HUMAN_MOUSE_MOVE_DELAY_MIN_MS = 6;
|
|
18
|
-
const HUMAN_MOUSE_MOVE_DELAY_MAX_MS = 18;
|
|
19
|
-
/** Bit values for `MouseEvent.buttons` per the UI Events spec. */
|
|
20
|
-
const MOUSE_BUTTON_BITS = {
|
|
21
|
-
left: 1,
|
|
22
|
-
right: 2,
|
|
23
|
-
middle: 4,
|
|
24
|
-
};
|
|
14
|
+
// (mouse/keyboard humanization timing + button bitmask moved to src/input/input-emulator.ts)
|
|
25
15
|
// Constants for stability and positioning
|
|
26
16
|
const DEFAULT_TIMEOUT_MS = 30000; // Default timeout for operations
|
|
27
17
|
const POSITION_STABILITY_TIMEOUT_MS = 2000; // Timeout for position stabilization
|
|
28
18
|
const POSITION_CHECK_INTERVAL_MS = 100; // Interval for position checks
|
|
29
19
|
const POSITION_STABILITY_THRESHOLD = 3; // Number of consecutive stable checks required
|
|
30
20
|
const POSITION_TOLERANCE_PX = 1; // Maximum pixel difference to consider "stable"
|
|
31
|
-
const VIEWPORT_TOLERANCE_PX = 50; // Tolerance for viewport positioning
|
|
32
|
-
// Constants for polling and retries
|
|
33
|
-
const POLLING_INTERVAL_MS = 100; // Default polling interval
|
|
34
21
|
/**
|
|
35
|
-
*
|
|
36
|
-
|
|
22
|
+
* Transpiler helpers that a serialized function body can reference, mapped to a
|
|
23
|
+
* behaviourally equivalent definition for the page.
|
|
24
|
+
*
|
|
25
|
+
* A function handed to `evaluate` crosses into the page as SOURCE TEXT, so it
|
|
26
|
+
* carries whatever the consumer's transpiler emitted. esbuild's `keepNames`
|
|
27
|
+
* option - which `tsx` turns ON by default - rewrites every named function as
|
|
28
|
+
* `__name(function foo() {…}, "foo")`, and `__name` lives in the Node module
|
|
29
|
+
* scope, not in the page. Evaluating that body throws
|
|
30
|
+
* `ReferenceError: __name is not defined`, and because the failure is inside the
|
|
31
|
+
* page it surfaces as a generic "Evaluation failed" - which is how it spent a
|
|
32
|
+
* platform run looking like a Google block: clicks and keystrokes silently did
|
|
33
|
+
* nothing while the page rendered perfectly.
|
|
34
|
+
*
|
|
35
|
+
* The shims are prepended only when the body actually mentions the helper, so a
|
|
36
|
+
* plain `tsc` build (what the published package ships) evaluates byte-identical
|
|
37
|
+
* source to before.
|
|
37
38
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (ws && (ws.readyState !== ws.OPEN || ws.readyState === ws.CLOSING)) {
|
|
54
|
-
const backoff = INITIAL_BACKOFF * 2 ** attempt;
|
|
55
|
-
log.info(`WebSocket not connected before ${methodName}, attempting to reconnect... (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
56
|
-
try {
|
|
57
|
-
await page.reconnectSession();
|
|
58
|
-
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
59
|
-
}
|
|
60
|
-
catch (reconnectError) {
|
|
61
|
-
const error = reconnectError instanceof Error ? reconnectError : new Error(String(reconnectError));
|
|
62
|
-
log.error('Reconnection failed:', { error });
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return await operation();
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
70
|
-
if (error instanceof Error && error.message?.includes('WebSocket is not open')) {
|
|
71
|
-
const backoff = INITIAL_BACKOFF * 2 ** attempt;
|
|
72
|
-
log.info(`WebSocket error during ${methodName}, attempting to reconnect... (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
73
|
-
try {
|
|
74
|
-
await page.reconnectSession();
|
|
75
|
-
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
catch (reconnectError) {
|
|
79
|
-
const reconnectErr = reconnectError instanceof Error ? reconnectError : new Error(String(reconnectError));
|
|
80
|
-
log.error('Reconnection failed:', { error: reconnectErr });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
throw lastError || new Error(`Failed to execute ${methodName} after ${MAX_RETRIES} attempts`);
|
|
87
|
-
}
|
|
88
|
-
return new Proxy(page, {
|
|
89
|
-
get(target, property) {
|
|
90
|
-
const original = target[property];
|
|
91
|
-
const propertyName = String(property);
|
|
92
|
-
// Special handling for close method
|
|
93
|
-
if (propertyName === 'close') {
|
|
94
|
-
return original;
|
|
95
|
-
}
|
|
96
|
-
// Only proxy public methods that return promises
|
|
97
|
-
if (typeof original === 'function' &&
|
|
98
|
-
!propertyName.startsWith('_') &&
|
|
99
|
-
!propertyName.startsWith('on') &&
|
|
100
|
-
propertyName !== 'emit' &&
|
|
101
|
-
propertyName !== 'addListener' &&
|
|
102
|
-
propertyName !== 'removeListener') {
|
|
103
|
-
return async (...args) => {
|
|
104
|
-
// @ts-expect-error Accessing private property
|
|
105
|
-
if (target.isClosed) {
|
|
106
|
-
log.debug(`Operation '${propertyName}' skipped - page is closed`);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
return executeWithRetries(() => original.apply(target, args), propertyName);
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
return original;
|
|
113
|
-
},
|
|
114
|
-
});
|
|
39
|
+
const TRANSPILER_HELPER_SHIMS = [
|
|
40
|
+
// esbuild keepNames: `__name(target, name)` defines `name` and returns target.
|
|
41
|
+
{ pattern: /\b__name\s*\(/, declaration: 'const __name = (target) => target;' },
|
|
42
|
+
// esbuild class-field lowering: `__publicField(obj, key, value)` assigns and returns the value.
|
|
43
|
+
{ pattern: /\b__publicField\s*\(/, declaration: 'const __publicField = (obj, key, value) => { obj[key] = value; return value; };' },
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Wrap an evaluate expression so transpiler helpers its source references are
|
|
47
|
+
* defined inside the page. Returns the expression unchanged when none apply.
|
|
48
|
+
*/
|
|
49
|
+
export function withTranspilerHelperShims(expression) {
|
|
50
|
+
const needed = TRANSPILER_HELPER_SHIMS.filter(({ pattern }) => pattern.test(expression));
|
|
51
|
+
if (needed.length === 0)
|
|
52
|
+
return expression;
|
|
53
|
+
return `(() => { ${needed.map((s) => s.declaration).join(' ')} return ${expression}; })()`;
|
|
115
54
|
}
|
|
116
55
|
export default class CdpPage extends EventEmitter {
|
|
117
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Navigation + load-wait protocol - navigate, race dom/load events vs
|
|
58
|
+
* timeout, reconcile URL via getNavigationHistory - lives in PageNavigation
|
|
59
|
+
* behind NavigationPort. The port closures read the current client through
|
|
60
|
+
* the session accessor, so a reconnect client swap is picked up. The
|
|
61
|
+
* `loadedUrl` cache lives inside the module, not on this object.
|
|
62
|
+
*/
|
|
63
|
+
get navigation() {
|
|
64
|
+
this._navigation ?? (this._navigation = new PageNavigation(this.navigationPort()));
|
|
65
|
+
return this._navigation;
|
|
66
|
+
}
|
|
67
|
+
navigationPort() {
|
|
68
|
+
return {
|
|
69
|
+
enablePage: async () => this.ensurePageEnabled(),
|
|
70
|
+
navigate: async (url) => {
|
|
71
|
+
this._documentNodeId = undefined;
|
|
72
|
+
await this.client.Page.navigate({ url });
|
|
73
|
+
},
|
|
74
|
+
reload: async () => {
|
|
75
|
+
this._documentNodeId = undefined;
|
|
76
|
+
await this.client.Page.reload({});
|
|
77
|
+
},
|
|
78
|
+
// chrome-remote-interface returns an unsubscribe fn from an event
|
|
79
|
+
// subscription; surface it so PageNavigation can detach the
|
|
80
|
+
// milestone listener after every navigation (incl. timeouts).
|
|
81
|
+
onDomContent: (handler) => this.client.Page.domContentEventFired(() => handler()),
|
|
82
|
+
onLoad: (handler) => this.client.Page.loadEventFired(() => handler()),
|
|
83
|
+
navigationHistory: async () => this.client.Page.getNavigationHistory(),
|
|
84
|
+
evaluateUrl: async () => this.evaluate('window.location.href'),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Network interception protocol - match/timeout/cleanup races live in
|
|
89
|
+
* NetworkWatch behind NetworkWatchPort. The port closures read
|
|
90
|
+
* `this.client` at call time, so a reconnectSession() client swap is
|
|
91
|
+
* picked up automatically.
|
|
92
|
+
*/
|
|
93
|
+
get networkWatch() {
|
|
94
|
+
this._networkWatch ?? (this._networkWatch = new NetworkWatch({
|
|
95
|
+
ensureNetworkEnabled: async () => this.ensureNetworkEnabled(),
|
|
96
|
+
onResponseReceived: (handler) => {
|
|
97
|
+
const client = this.client;
|
|
98
|
+
client.on('Network.responseReceived', handler);
|
|
99
|
+
return () => client.removeListener('Network.responseReceived', handler);
|
|
100
|
+
},
|
|
101
|
+
getResponseBody: async (requestId) => this.client.Network.getResponseBody({ requestId }),
|
|
102
|
+
setRequestInterception: async (patterns) => {
|
|
103
|
+
await this.client.Network.setRequestInterception({ patterns });
|
|
104
|
+
},
|
|
105
|
+
onRequestIntercepted: (handler) => {
|
|
106
|
+
this.client.Network.on('requestIntercepted', handler);
|
|
107
|
+
},
|
|
108
|
+
continueInterceptedRequest: async (params) => {
|
|
109
|
+
await this.client.Network.continueInterceptedRequest(params);
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
return this._networkWatch;
|
|
113
|
+
}
|
|
114
|
+
constructor(client, sessionOptions) {
|
|
118
115
|
super();
|
|
119
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Owns the live CDP session for this page: the current client, reconnect +
|
|
118
|
+
* heartbeat, the `isReconnecting` latch, and domain re-bind on swap. CDP
|
|
119
|
+
* methods route through `session.run(op)`; consumers read the current
|
|
120
|
+
* client through `session.client()`. See src/connection/page-session.ts.
|
|
121
|
+
*/
|
|
122
|
+
Object.defineProperty(this, "session", {
|
|
120
123
|
enumerable: true,
|
|
121
124
|
configurable: true,
|
|
122
125
|
writable: true,
|
|
123
126
|
value: void 0
|
|
124
127
|
});
|
|
125
|
-
Object.defineProperty(this, "
|
|
128
|
+
Object.defineProperty(this, "_navigation", {
|
|
126
129
|
enumerable: true,
|
|
127
130
|
configurable: true,
|
|
128
131
|
writable: true,
|
|
129
132
|
value: void 0
|
|
130
133
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Cached document root nodeId for {@link getNodeId}. Populated on first
|
|
136
|
+
* lookup and reused across clicks so we don't re-issue DOM.getDocument on
|
|
137
|
+
* every element resolution. Invalidated on main-frame navigation (the
|
|
138
|
+
* navigate/reload closures below) because a new document reassigns node
|
|
139
|
+
* IDs; getNodeId additionally self-heals with a fresh getDocument if a
|
|
140
|
+
* stale root slips through.
|
|
141
|
+
*/
|
|
142
|
+
Object.defineProperty(this, "_documentNodeId", {
|
|
138
143
|
enumerable: true,
|
|
139
144
|
configurable: true,
|
|
140
145
|
writable: true,
|
|
141
|
-
value:
|
|
146
|
+
value: void 0
|
|
142
147
|
});
|
|
143
|
-
Object.defineProperty(this, "
|
|
148
|
+
Object.defineProperty(this, "_networkWatch", {
|
|
144
149
|
enumerable: true,
|
|
145
150
|
configurable: true,
|
|
146
151
|
writable: true,
|
|
147
|
-
value:
|
|
152
|
+
value: void 0
|
|
148
153
|
});
|
|
149
|
-
Object.defineProperty(this, "
|
|
154
|
+
Object.defineProperty(this, "humanization", {
|
|
150
155
|
enumerable: true,
|
|
151
156
|
configurable: true,
|
|
152
157
|
writable: true,
|
|
153
|
-
value:
|
|
158
|
+
value: {
|
|
159
|
+
mouse: false,
|
|
160
|
+
keyboard: false,
|
|
161
|
+
scroll: false,
|
|
162
|
+
}
|
|
154
163
|
});
|
|
155
|
-
Object.defineProperty(this, "
|
|
164
|
+
Object.defineProperty(this, "launchSnapshot", {
|
|
156
165
|
enumerable: true,
|
|
157
166
|
configurable: true,
|
|
158
167
|
writable: true,
|
|
159
|
-
value:
|
|
168
|
+
value: {}
|
|
160
169
|
});
|
|
161
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Human-like input emulation (mouse, keyboard, IME) over a transport bound
|
|
172
|
+
* to the live CDP client - see src/input/input-emulator.ts. The lazy
|
|
173
|
+
* `() => this.client` accessor keeps the transport pointed at the current
|
|
174
|
+
* connection across session reconnects, which swap `this.client`.
|
|
175
|
+
*/
|
|
176
|
+
Object.defineProperty(this, "input", {
|
|
162
177
|
enumerable: true,
|
|
163
178
|
configurable: true,
|
|
164
179
|
writable: true,
|
|
165
|
-
value:
|
|
166
|
-
mouse: false,
|
|
167
|
-
keyboard: false,
|
|
168
|
-
scroll: false,
|
|
169
|
-
}
|
|
180
|
+
value: void 0
|
|
170
181
|
});
|
|
171
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Element targeting (geometry, visibility, scroll, position stabilization)
|
|
184
|
+
* over the narrow ElementTargetPort - see src/input/element-target.ts.
|
|
185
|
+
* Decides WHERE/WHEN to interact; `input` decides HOW.
|
|
186
|
+
*/
|
|
187
|
+
Object.defineProperty(this, "elements", {
|
|
172
188
|
enumerable: true,
|
|
173
189
|
configurable: true,
|
|
174
190
|
writable: true,
|
|
175
|
-
value:
|
|
191
|
+
value: void 0
|
|
176
192
|
});
|
|
177
193
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* this, every press reads `event.buttons === 0` — a one-line bot signal.
|
|
194
|
+
* Select/dropdown protocol (native <select>, custom-dropdown trigger
|
|
195
|
+
* detection, idempotent open, virtualized-list scrolling) over the same
|
|
196
|
+
* port - see src/input/dropdown-selector.ts.
|
|
182
197
|
*/
|
|
183
|
-
Object.defineProperty(this, "
|
|
198
|
+
Object.defineProperty(this, "dropdowns", {
|
|
184
199
|
enumerable: true,
|
|
185
200
|
configurable: true,
|
|
186
201
|
writable: true,
|
|
187
|
-
value:
|
|
202
|
+
value: void 0
|
|
188
203
|
});
|
|
189
|
-
this.
|
|
190
|
-
this.
|
|
204
|
+
this.session = new PageSession({ initialClient: client, ...sessionOptions });
|
|
205
|
+
this.input = new InputEmulator(new CdpInputTransport(() => this.client), this.humanization);
|
|
206
|
+
const targetPort = {
|
|
207
|
+
evaluate: async (fn, ...args) => this.evaluate(fn, ...args),
|
|
208
|
+
humanScrollToElement: async (selector) => {
|
|
209
|
+
await Promise.all([
|
|
210
|
+
this._enableDomain('DOM'),
|
|
211
|
+
this._enableDomain('Input'),
|
|
212
|
+
]);
|
|
213
|
+
await humanScrollToElement(this.client, selector);
|
|
214
|
+
},
|
|
215
|
+
humanization: () => this.humanization,
|
|
216
|
+
};
|
|
217
|
+
this.elements = new ElementTarget(targetPort);
|
|
218
|
+
this.dropdowns = new DropdownSelector({
|
|
219
|
+
...targetPort,
|
|
220
|
+
waitForSelector: async (selector, options) => this.waitForSelector(selector, options),
|
|
221
|
+
isLooselyVisible: async (selector) => this.isVisible(selector),
|
|
222
|
+
// Trusted coordinate click for dropdown triggers/options: the same
|
|
223
|
+
// CDP Input path CdpPage.click uses, so dropdown interactions carry
|
|
224
|
+
// isTrusted === true like every other click we make.
|
|
225
|
+
clickAt: async (x, y) => {
|
|
226
|
+
await Promise.all([
|
|
227
|
+
this._enableDomain('DOM'),
|
|
228
|
+
this._enableDomain('Input'),
|
|
229
|
+
]);
|
|
230
|
+
await this.input.clickAt(x, y);
|
|
231
|
+
},
|
|
232
|
+
}, this.elements);
|
|
191
233
|
this.emit(PAGE_CREATED);
|
|
192
|
-
// this._attachConnectionMonitor();
|
|
193
234
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// Only monitor connection health
|
|
206
|
-
this.connectionMonitor = setInterval(() => {
|
|
207
|
-
if (ws.readyState === ws.OPEN) {
|
|
208
|
-
ws.ping(() => { });
|
|
209
|
-
}
|
|
210
|
-
else if (!this.isReconnecting && ws.readyState !== ws.CONNECTING) {
|
|
211
|
-
log.debug('Connection health check failed, clearing monitor');
|
|
212
|
-
if (this.connectionMonitor)
|
|
213
|
-
clearInterval(this.connectionMonitor);
|
|
214
|
-
}
|
|
215
|
-
}, 5000);
|
|
216
|
-
this.once(PAGE_CLOSED, () => {
|
|
217
|
-
if (this.connectionMonitor)
|
|
218
|
-
clearInterval(this.connectionMonitor);
|
|
219
|
-
});
|
|
235
|
+
/** The current CDP client. Reads through PageSession so a reconnect swap is picked up. */
|
|
236
|
+
get client() {
|
|
237
|
+
return this.session.client();
|
|
238
|
+
}
|
|
239
|
+
/** The current raw CDP client (some consumers reach for `_ws` directly). */
|
|
240
|
+
get _rawClient() {
|
|
241
|
+
return this.session.client();
|
|
242
|
+
}
|
|
243
|
+
/** True while the page session is mid-reconnect. */
|
|
244
|
+
get isReconnecting() {
|
|
245
|
+
return this.session.isReconnecting;
|
|
220
246
|
}
|
|
221
247
|
// @TODO: The enabling of API can cause blocking.
|
|
222
248
|
// We need to discover this more and add options to the crawler.
|
|
223
|
-
static async create(client) {
|
|
224
|
-
const page = new CdpPage(client);
|
|
249
|
+
static async create(client, sessionOptions) {
|
|
250
|
+
const page = new CdpPage(client, sessionOptions);
|
|
225
251
|
await page.initialize();
|
|
226
|
-
|
|
227
|
-
log.debug('Created proxied page instance');
|
|
252
|
+
log.debug('Created page instance');
|
|
228
253
|
return page;
|
|
229
254
|
}
|
|
230
255
|
async initialize() {
|
|
@@ -245,13 +270,8 @@ export default class CdpPage extends EventEmitter {
|
|
|
245
270
|
* Once enabled, subsequent calls are no-ops.
|
|
246
271
|
*/
|
|
247
272
|
async ensurePageEnabled() {
|
|
248
|
-
if (this.enabledDomains.has('Page')) {
|
|
249
|
-
return; // Already enabled
|
|
250
|
-
}
|
|
251
273
|
try {
|
|
252
|
-
await this.
|
|
253
|
-
this.enabledDomains.add('Page');
|
|
254
|
-
log.debug('Page domain enabled lazily for script injection');
|
|
274
|
+
await this.session.enableDomain('Page');
|
|
255
275
|
}
|
|
256
276
|
catch (error) {
|
|
257
277
|
log.error('Failed to enable Page domain:', { error });
|
|
@@ -264,12 +284,7 @@ export default class CdpPage extends EventEmitter {
|
|
|
264
284
|
* Once enabled, subsequent calls are no-ops.
|
|
265
285
|
*/
|
|
266
286
|
async ensureNetworkEnabled() {
|
|
267
|
-
|
|
268
|
-
return; // Already enabled
|
|
269
|
-
}
|
|
270
|
-
await this.client.Network.enable();
|
|
271
|
-
this.enabledDomains.add('Network');
|
|
272
|
-
log.debug('Network domain enabled (lazy)');
|
|
287
|
+
await this.session.enableDomain('Network');
|
|
273
288
|
}
|
|
274
289
|
setHumanizationOptions(options) {
|
|
275
290
|
this.humanization = {
|
|
@@ -277,93 +292,40 @@ export default class CdpPage extends EventEmitter {
|
|
|
277
292
|
keyboard: options?.keyboard ?? false,
|
|
278
293
|
scroll: options?.scroll ?? false,
|
|
279
294
|
};
|
|
295
|
+
this.input.setHumanization(this.humanization);
|
|
280
296
|
}
|
|
281
297
|
async url() {
|
|
282
|
-
|
|
283
|
-
return await this._getCurrentUrlWithoutRuntime();
|
|
284
|
-
}
|
|
285
|
-
return this.loadedUrl;
|
|
298
|
+
return this.navigation.url();
|
|
286
299
|
}
|
|
287
|
-
|
|
288
|
-
* Get current URL without activating the Runtime domain.
|
|
289
|
-
* Uses Page.getNavigationHistory which only requires the Page domain.
|
|
290
|
-
*/
|
|
291
|
-
async _getCurrentUrlWithoutRuntime() {
|
|
292
|
-
try {
|
|
293
|
-
await this.ensurePageEnabled();
|
|
294
|
-
const { currentIndex, entries } = await this.client.Page.getNavigationHistory();
|
|
295
|
-
if (entries && entries.length > 0 && currentIndex >= 0) {
|
|
296
|
-
return entries[currentIndex].url;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
catch {
|
|
300
|
-
// Fallback: if Page.getNavigationHistory fails, try evaluate
|
|
301
|
-
log.debug('getNavigationHistory failed, falling back to evaluate');
|
|
302
|
-
}
|
|
303
|
-
return await this.evaluate('window.location.href');
|
|
304
|
-
}
|
|
305
|
-
// Navigation method
|
|
300
|
+
// Navigation method - protocol lives in PageNavigation (src/navigation/page-navigation.ts)
|
|
306
301
|
async goto(url, options) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
await Page.navigate({ url });
|
|
310
|
-
const waitUntil = options?.waitUntil ?? 'load';
|
|
311
|
-
let waitEvent;
|
|
312
|
-
switch (waitUntil) {
|
|
313
|
-
case 'load':
|
|
314
|
-
waitEvent = new Promise((resolve) => Page.loadEventFired(() => resolve()));
|
|
315
|
-
break;
|
|
316
|
-
case 'domcontentloaded':
|
|
317
|
-
waitEvent = new Promise((resolve) => Page.domContentEventFired(() => resolve()));
|
|
318
|
-
break;
|
|
319
|
-
default:
|
|
320
|
-
throw new Error(`Unsupported waitUntil event: ${waitUntil}`);
|
|
321
|
-
}
|
|
322
|
-
await Promise.race([
|
|
323
|
-
waitEvent,
|
|
324
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Navigation timeout')), timeout)),
|
|
325
|
-
]);
|
|
326
|
-
// Use Page.getNavigationHistory instead of Runtime.evaluate to avoid
|
|
327
|
-
// activating the Runtime domain (which is detection vector #1)
|
|
328
|
-
this.loadedUrl = await this._getCurrentUrlWithoutRuntime();
|
|
302
|
+
// Transparently heal a dropped CDP socket; a closed page is a no-op.
|
|
303
|
+
await this.session.run(async () => this.navigation.goto(url, options));
|
|
329
304
|
}
|
|
330
305
|
/**
|
|
331
306
|
* Reloads the current page.
|
|
332
307
|
* @param options - Options for waiting and timeout, similar to goto.
|
|
333
308
|
*/
|
|
334
309
|
async reload(options) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const waitUntil = options?.waitUntil ?? 'load';
|
|
338
|
-
await Page.reload({});
|
|
339
|
-
let waitEvent;
|
|
340
|
-
switch (waitUntil) {
|
|
341
|
-
case 'load':
|
|
342
|
-
waitEvent = new Promise((resolve) => Page.loadEventFired(() => resolve()));
|
|
343
|
-
break;
|
|
344
|
-
case 'domcontentloaded':
|
|
345
|
-
waitEvent = new Promise((resolve) => Page.domContentEventFired(() => resolve()));
|
|
346
|
-
break;
|
|
347
|
-
default:
|
|
348
|
-
throw new Error(`Unsupported waitUntil event: ${waitUntil}`);
|
|
349
|
-
}
|
|
350
|
-
await Promise.race([
|
|
351
|
-
waitEvent,
|
|
352
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Reload timeout')), timeout)),
|
|
353
|
-
]);
|
|
354
|
-
// Use Page.getNavigationHistory instead of Runtime.evaluate
|
|
355
|
-
this.loadedUrl = await this._getCurrentUrlWithoutRuntime();
|
|
310
|
+
// Transparently heal a dropped CDP socket; a closed page is a no-op.
|
|
311
|
+
await this.session.run(async () => this.navigation.reload(options));
|
|
356
312
|
}
|
|
357
313
|
// Evaluate method
|
|
358
314
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
359
315
|
async evaluate(pageFunction, ...args) {
|
|
316
|
+
// Transparently heal a dropped CDP socket; a closed page resolves to
|
|
317
|
+
// undefined rather than throwing.
|
|
318
|
+
return await this.session.run(async () => this._evaluateInner(pageFunction, ...args));
|
|
319
|
+
}
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
321
|
+
async _evaluateInner(pageFunction, ...args) {
|
|
360
322
|
await this._enableDomain('Runtime');
|
|
361
323
|
const { Runtime } = this.client;
|
|
362
324
|
let expression;
|
|
363
325
|
if (typeof pageFunction === 'function') {
|
|
364
326
|
const fnStr = pageFunction.toString();
|
|
365
327
|
const argsStr = args.map((a) => JSON.stringify(a)).join(',');
|
|
366
|
-
expression = `(${fnStr})(${argsStr})
|
|
328
|
+
expression = withTranspilerHelperShims(`(${fnStr})(${argsStr})`);
|
|
367
329
|
}
|
|
368
330
|
else if (typeof pageFunction === 'string') {
|
|
369
331
|
expression = pageFunction;
|
|
@@ -387,7 +349,6 @@ export default class CdpPage extends EventEmitter {
|
|
|
387
349
|
this._enableDomain('DOM'),
|
|
388
350
|
this._enableDomain('Input'),
|
|
389
351
|
]);
|
|
390
|
-
const { DOM, Input } = this.client;
|
|
391
352
|
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
392
353
|
const force = options?.force ?? false;
|
|
393
354
|
const delay = options?.delay ?? 0;
|
|
@@ -395,469 +356,73 @@ export default class CdpPage extends EventEmitter {
|
|
|
395
356
|
const clickCount = options?.clickCount ?? 1;
|
|
396
357
|
// Wait for element to exist
|
|
397
358
|
await this.waitForSelector(selector, { timeout });
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
// Check if element is visible (unless force is true)
|
|
404
|
-
if (!force) {
|
|
405
|
-
const isVisible = await this._isElementVisible(selector);
|
|
406
|
-
if (!isVisible) {
|
|
407
|
-
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.`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
// Check if element is clickable (unless force is true)
|
|
411
|
-
if (!force) {
|
|
412
|
-
const isClickable = await this._isElementClickable(selector);
|
|
413
|
-
if (!isClickable) {
|
|
414
|
-
throw new Error(`Element is not clickable: ${selector}. Use force: true to bypass this check.`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
// Scroll element into view if needed
|
|
418
|
-
await this._scrollElementIntoView(selector);
|
|
419
|
-
// Wait for scroll animations to complete by polling for position stabilization
|
|
420
|
-
// Wait for the element's position to stabilize after scrolling.
|
|
421
|
-
// This is necessary because scroll animations or layout changes may cause the element's position
|
|
422
|
-
// to shift for a short period. If we attempt to click before the position is stable, the click
|
|
423
|
-
// may be sent to the wrong location, potentially missing the target element or causing unreliable
|
|
424
|
-
// automation. Polling for position stabilization ensures the click is performed accurately.
|
|
425
|
-
await this._waitForElementPositionToStabilize(selector, POSITION_STABILITY_TIMEOUT_MS, POSITION_CHECK_INTERVAL_MS);
|
|
426
|
-
// Get updated element position after scrolling
|
|
427
|
-
const { nodeId } = await this.getNodeId(selector);
|
|
428
|
-
// For hidden elements, we need to handle the case where box model can't be computed
|
|
429
|
-
let model;
|
|
430
|
-
try {
|
|
431
|
-
const boxModelResult = await DOM.getBoxModel({ nodeId });
|
|
432
|
-
model = boxModelResult.model;
|
|
433
|
-
}
|
|
434
|
-
catch (error) {
|
|
435
|
-
if (force) {
|
|
436
|
-
// If force is true, try to get a default position
|
|
437
|
-
const fallbackElementInfo = await this._getElementInfo(selector);
|
|
438
|
-
if (fallbackElementInfo) {
|
|
439
|
-
const x = fallbackElementInfo.boundingBox.x + fallbackElementInfo.boundingBox.width / 2;
|
|
440
|
-
const y = fallbackElementInfo.boundingBox.y + fallbackElementInfo.boundingBox.height / 2;
|
|
441
|
-
await this._performClick(Input, x, y, button, clickCount, delay);
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
throw new Error(`Could not get box model for node: ${nodeId}. Element might be hidden or not rendered.`);
|
|
446
|
-
}
|
|
447
|
-
if (!model) {
|
|
448
|
-
throw new Error(`Could not get box model for node: ${nodeId}`);
|
|
449
|
-
}
|
|
450
|
-
// Calculate click coordinates (center of the element)
|
|
451
|
-
const x = (model.content[0] + model.content[4]) / 2;
|
|
452
|
-
const y = (model.content[1] + model.content[5]) / 2;
|
|
453
|
-
// Verify element is still in viewport after scrolling (with some tolerance)
|
|
454
|
-
if (!force) {
|
|
455
|
-
const isInViewport = await this._isElementInViewport(selector);
|
|
456
|
-
if (!isInViewport) {
|
|
457
|
-
// Give a bit more time for scroll animations to complete
|
|
458
|
-
await new Promise(resolve => setTimeout(resolve, HUMAN_DELAY_MS));
|
|
459
|
-
const isInViewportAfterDelay = await this._isElementInViewport(selector);
|
|
460
|
-
if (!isInViewportAfterDelay) {
|
|
461
|
-
throw new Error(`Element is not in viewport after scrolling: ${selector}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
359
|
+
// Resolve the selector into a stable, in-viewport coordinate target
|
|
360
|
+
// (visibility/clickability checks, scroll, position stabilization,
|
|
361
|
+
// humanized point selection) - see src/input/element-target.ts.
|
|
362
|
+
const { x, y } = await this.elements.acquire(selector, { force });
|
|
465
363
|
// Perform the click with human-like interaction
|
|
466
|
-
await this.
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Performs a human-like click with proper mouse events.
|
|
470
|
-
* Note: screenX/screenY compensation is handled browser-side via fingerprint
|
|
471
|
-
* overrides that set realistic window.screenX/screenY values, which Chrome
|
|
472
|
-
* uses when constructing MouseEvent from CDP dispatch.
|
|
473
|
-
*/
|
|
474
|
-
async _performClick(Input, x, y, button, clickCount, delay) {
|
|
475
|
-
await this._moveMouseTo(Input, x, y);
|
|
476
|
-
// Small delay to simulate human behavior
|
|
477
|
-
if (delay > 0) {
|
|
478
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
479
|
-
}
|
|
480
|
-
else if (this.humanization.mouse) {
|
|
481
|
-
await new Promise(resolve => setTimeout(resolve, 40 + Math.floor(Math.random() * 120)));
|
|
482
|
-
}
|
|
483
|
-
// Press: add to held set FIRST so `event.buttons` includes it on mousedown
|
|
484
|
-
// (matches real hardware where mousedown fires with the button already in the bitmask).
|
|
485
|
-
this.heldMouseButtons.add(button);
|
|
486
|
-
await Input.dispatchMouseEvent({
|
|
487
|
-
type: 'mousePressed',
|
|
488
|
-
x,
|
|
489
|
-
y,
|
|
490
|
-
button,
|
|
491
|
-
clickCount,
|
|
492
|
-
buttons: this._buttonsBitmask(),
|
|
493
|
-
pointerType: 'mouse',
|
|
494
|
-
});
|
|
495
|
-
// Small delay between press and release
|
|
496
|
-
await new Promise(resolve => setTimeout(resolve, PRESS_RELEASE_DELAY_MS));
|
|
497
|
-
// Release: remove from held set FIRST so `event.buttons` reflects the post-release
|
|
498
|
-
// state on mouseup (per UI Events spec — the released button is no longer pressed).
|
|
499
|
-
this.heldMouseButtons.delete(button);
|
|
500
|
-
await Input.dispatchMouseEvent({
|
|
501
|
-
type: 'mouseReleased',
|
|
502
|
-
x,
|
|
503
|
-
y,
|
|
504
|
-
button,
|
|
505
|
-
clickCount,
|
|
506
|
-
buttons: this._buttonsBitmask(),
|
|
507
|
-
pointerType: 'mouse',
|
|
508
|
-
});
|
|
509
|
-
this.lastMousePosition = { x, y };
|
|
510
|
-
}
|
|
511
|
-
_buttonsBitmask() {
|
|
512
|
-
let mask = 0;
|
|
513
|
-
for (const button of this.heldMouseButtons) {
|
|
514
|
-
mask += MOUSE_BUTTON_BITS[button];
|
|
515
|
-
}
|
|
516
|
-
return mask;
|
|
517
|
-
}
|
|
518
|
-
async _moveMouseTo(Input, x, y) {
|
|
519
|
-
if (!this.humanization.mouse) {
|
|
520
|
-
await Input.dispatchMouseEvent({
|
|
521
|
-
type: 'mouseMoved',
|
|
522
|
-
x,
|
|
523
|
-
y,
|
|
524
|
-
buttons: this._buttonsBitmask(),
|
|
525
|
-
pointerType: 'mouse',
|
|
526
|
-
});
|
|
527
|
-
this.lastMousePosition = { x, y };
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
const fromX = this.lastMousePosition.x;
|
|
531
|
-
const fromY = this.lastMousePosition.y;
|
|
532
|
-
const path = await this.evaluate((startX, startY, endX, endY) => {
|
|
533
|
-
const fallback = [{ x: endX, y: endY }];
|
|
534
|
-
const globalStealth = window.__stealth;
|
|
535
|
-
if (!globalStealth || typeof globalStealth.humanizeMouse !== 'function') {
|
|
536
|
-
return fallback;
|
|
537
|
-
}
|
|
538
|
-
const points = globalStealth.humanizeMouse(startX, startY, endX, endY);
|
|
539
|
-
return Array.isArray(points) && points.length > 0 ? points : fallback;
|
|
540
|
-
}, fromX, fromY, x, y);
|
|
541
|
-
for (const point of path) {
|
|
542
|
-
await Input.dispatchMouseEvent({
|
|
543
|
-
type: 'mouseMoved',
|
|
544
|
-
x: point.x,
|
|
545
|
-
y: point.y,
|
|
546
|
-
buttons: this._buttonsBitmask(),
|
|
547
|
-
pointerType: 'mouse',
|
|
548
|
-
});
|
|
549
|
-
const delay = HUMAN_MOUSE_MOVE_DELAY_MIN_MS
|
|
550
|
-
+ Math.floor(Math.random() * (HUMAN_MOUSE_MOVE_DELAY_MAX_MS - HUMAN_MOUSE_MOVE_DELAY_MIN_MS + 1));
|
|
551
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
552
|
-
}
|
|
553
|
-
this.lastMousePosition = { x, y };
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* Gets detailed information about an element
|
|
557
|
-
*/
|
|
558
|
-
async _getElementInfo(selector) {
|
|
559
|
-
return await this.evaluate((sel) => {
|
|
560
|
-
const element = document.querySelector(sel);
|
|
561
|
-
if (!element)
|
|
562
|
-
return null;
|
|
563
|
-
const rect = element.getBoundingClientRect();
|
|
564
|
-
const style = window.getComputedStyle(element);
|
|
565
|
-
// Check if element is visible
|
|
566
|
-
const isVisible = style.display !== 'none' &&
|
|
567
|
-
style.visibility !== 'hidden' &&
|
|
568
|
-
rect.width > 0 &&
|
|
569
|
-
rect.height > 0;
|
|
570
|
-
// Check if element is clickable
|
|
571
|
-
const isClickable = !element.hasAttribute('disabled') &&
|
|
572
|
-
!element.classList.contains('disabled') &&
|
|
573
|
-
style.pointerEvents !== 'none' &&
|
|
574
|
-
rect.width > 0 &&
|
|
575
|
-
rect.height > 0;
|
|
576
|
-
return {
|
|
577
|
-
nodeId: 0, // Will be set by CDP
|
|
578
|
-
boundingBox: {
|
|
579
|
-
x: rect.left,
|
|
580
|
-
y: rect.top,
|
|
581
|
-
width: rect.width,
|
|
582
|
-
height: rect.height,
|
|
583
|
-
},
|
|
584
|
-
isVisible,
|
|
585
|
-
isClickable,
|
|
586
|
-
};
|
|
587
|
-
}, selector);
|
|
588
|
-
}
|
|
589
|
-
/**
|
|
590
|
-
* Enhanced visibility check that considers more factors
|
|
591
|
-
*/
|
|
592
|
-
async _isElementVisible(selector) {
|
|
593
|
-
return await this.evaluate((sel) => {
|
|
594
|
-
const element = document.querySelector(sel);
|
|
595
|
-
if (!element)
|
|
596
|
-
return false;
|
|
597
|
-
const rect = element.getBoundingClientRect();
|
|
598
|
-
const style = window.getComputedStyle(element);
|
|
599
|
-
// Basic visibility checks
|
|
600
|
-
if (style.display === 'none' || style.visibility === 'hidden') {
|
|
601
|
-
return false;
|
|
602
|
-
}
|
|
603
|
-
// Check if element has dimensions
|
|
604
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
605
|
-
return false;
|
|
606
|
-
}
|
|
607
|
-
// Check if element is not transparent
|
|
608
|
-
if (parseFloat(style.opacity) === 0) {
|
|
609
|
-
return false;
|
|
610
|
-
}
|
|
611
|
-
// Check if element is not clipped by overflow
|
|
612
|
-
const parent = element.parentElement;
|
|
613
|
-
if (parent) {
|
|
614
|
-
const parentStyle = window.getComputedStyle(parent);
|
|
615
|
-
if (parentStyle.overflow === 'hidden') {
|
|
616
|
-
const parentRect = parent.getBoundingClientRect();
|
|
617
|
-
if (rect.left >= parentRect.right ||
|
|
618
|
-
rect.right <= parentRect.left ||
|
|
619
|
-
rect.top >= parentRect.bottom ||
|
|
620
|
-
rect.bottom <= parentRect.top) {
|
|
621
|
-
return false;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
return true;
|
|
626
|
-
}, selector);
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Enhanced clickable check that considers more factors
|
|
630
|
-
*/
|
|
631
|
-
async _isElementClickable(selector) {
|
|
632
|
-
return await this.evaluate((sel) => {
|
|
633
|
-
const element = document.querySelector(sel);
|
|
634
|
-
if (!element)
|
|
635
|
-
return false;
|
|
636
|
-
const rect = element.getBoundingClientRect();
|
|
637
|
-
const style = window.getComputedStyle(element);
|
|
638
|
-
// Check if element is disabled
|
|
639
|
-
if (element.hasAttribute('disabled') || element.classList.contains('disabled')) {
|
|
640
|
-
return false;
|
|
641
|
-
}
|
|
642
|
-
// Check if pointer events are disabled
|
|
643
|
-
if (style.pointerEvents === 'none') {
|
|
644
|
-
return false;
|
|
645
|
-
}
|
|
646
|
-
// Check if element has dimensions
|
|
647
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
// Check if element is not transparent
|
|
651
|
-
if (parseFloat(style.opacity) === 0) {
|
|
652
|
-
return false;
|
|
653
|
-
}
|
|
654
|
-
// Check if element is not read-only (for form elements)
|
|
655
|
-
if (element.hasAttribute('readonly')) {
|
|
656
|
-
return false;
|
|
657
|
-
}
|
|
658
|
-
return true;
|
|
659
|
-
}, selector);
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Enhanced scroll into view with better positioning
|
|
663
|
-
*/
|
|
664
|
-
async _scrollElementIntoView(selector) {
|
|
665
|
-
if (this.humanization.scroll) {
|
|
666
|
-
try {
|
|
667
|
-
await Promise.all([
|
|
668
|
-
this._enableDomain('DOM'),
|
|
669
|
-
this._enableDomain('Input'),
|
|
670
|
-
]);
|
|
671
|
-
await humanScrollToElement(this.client, selector);
|
|
672
|
-
await this.evaluate((sel) => {
|
|
673
|
-
const element = document.querySelector(sel);
|
|
674
|
-
element?.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'center' });
|
|
675
|
-
}, selector);
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
catch {
|
|
679
|
-
// Some hidden/offscreen nodes do not expose a box model.
|
|
680
|
-
// Fall back to native DOM scrolling for robustness.
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
await this.evaluate((sel, tolerance) => {
|
|
684
|
-
const element = document.querySelector(sel);
|
|
685
|
-
if (!element)
|
|
686
|
-
return;
|
|
687
|
-
const rect = element.getBoundingClientRect();
|
|
688
|
-
const viewportHeight = window.innerHeight;
|
|
689
|
-
const viewportWidth = window.innerWidth;
|
|
690
|
-
// Check if element is already in viewport (with tolerance)
|
|
691
|
-
const isInViewport = rect.top >= -tolerance &&
|
|
692
|
-
rect.left >= -tolerance &&
|
|
693
|
-
rect.bottom <= viewportHeight + tolerance &&
|
|
694
|
-
rect.right <= viewportWidth + tolerance;
|
|
695
|
-
if (!isInViewport) {
|
|
696
|
-
// Scroll element into view with better positioning
|
|
697
|
-
element.scrollIntoView({
|
|
698
|
-
behavior: 'smooth',
|
|
699
|
-
block: 'center',
|
|
700
|
-
inline: 'center',
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
}, selector, VIEWPORT_TOLERANCE_PX);
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Waits for an element's position to stabilize by polling its position over time.
|
|
707
|
-
* This is useful for waiting for scroll animations or dynamic layout changes to complete.
|
|
708
|
-
*
|
|
709
|
-
* @param selector - The CSS selector for the element to monitor
|
|
710
|
-
* @param timeout - Maximum time to wait in milliseconds (default: 2000)
|
|
711
|
-
* @param checkInterval - How often to check position in milliseconds (default: 100)
|
|
712
|
-
* @param stabilityThreshold - Number of consecutive stable checks required (default: 3)
|
|
713
|
-
* @param tolerance - Maximum pixel difference to consider "stable" (default: 1)
|
|
714
|
-
*/
|
|
715
|
-
async _waitForElementPositionToStabilize(selector, timeout = POSITION_STABILITY_TIMEOUT_MS, checkInterval = POSITION_CHECK_INTERVAL_MS, stabilityThreshold = POSITION_STABILITY_THRESHOLD, tolerance = POSITION_TOLERANCE_PX) {
|
|
716
|
-
const startTime = Date.now();
|
|
717
|
-
let stableCount = 0;
|
|
718
|
-
let lastPosition = null;
|
|
719
|
-
while (Date.now() - startTime < timeout) {
|
|
720
|
-
try {
|
|
721
|
-
const currentPosition = await this.evaluate((sel) => {
|
|
722
|
-
const element = document.querySelector(sel);
|
|
723
|
-
if (!element)
|
|
724
|
-
return null;
|
|
725
|
-
const rect = element.getBoundingClientRect();
|
|
726
|
-
return {
|
|
727
|
-
x: Math.round(rect.left),
|
|
728
|
-
y: Math.round(rect.top),
|
|
729
|
-
width: Math.round(rect.width),
|
|
730
|
-
height: Math.round(rect.height),
|
|
731
|
-
};
|
|
732
|
-
}, selector);
|
|
733
|
-
if (!currentPosition) {
|
|
734
|
-
// Element not found, reset stability counter
|
|
735
|
-
stableCount = 0;
|
|
736
|
-
lastPosition = null;
|
|
737
|
-
}
|
|
738
|
-
else if (!lastPosition) {
|
|
739
|
-
// First position reading
|
|
740
|
-
lastPosition = currentPosition;
|
|
741
|
-
stableCount = 1;
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
// Check if position is stable (within tolerance)
|
|
745
|
-
const deltaX = Math.abs(currentPosition.x - lastPosition.x);
|
|
746
|
-
const deltaY = Math.abs(currentPosition.y - lastPosition.y);
|
|
747
|
-
const deltaWidth = Math.abs(currentPosition.width - lastPosition.width);
|
|
748
|
-
const deltaHeight = Math.abs(currentPosition.height - lastPosition.height);
|
|
749
|
-
const isStable = deltaX <= tolerance &&
|
|
750
|
-
deltaY <= tolerance &&
|
|
751
|
-
deltaWidth <= tolerance &&
|
|
752
|
-
deltaHeight <= tolerance;
|
|
753
|
-
if (isStable) {
|
|
754
|
-
stableCount++;
|
|
755
|
-
if (stableCount >= stabilityThreshold) {
|
|
756
|
-
// Position has been stable for required number of checks
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
else {
|
|
761
|
-
// Position changed, reset counter
|
|
762
|
-
stableCount = 1;
|
|
763
|
-
}
|
|
764
|
-
lastPosition = currentPosition;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
catch (error) {
|
|
768
|
-
// Error reading position, reset stability counter
|
|
769
|
-
stableCount = 0;
|
|
770
|
-
lastPosition = null;
|
|
771
|
-
}
|
|
772
|
-
// Wait before next check
|
|
773
|
-
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
774
|
-
}
|
|
775
|
-
// If we reach here, we timed out - this is acceptable as some elements may never stabilize
|
|
776
|
-
// If we reach here, we timed out. This means the element's position did not stabilize within the allotted time.
|
|
777
|
-
// Proceeding may cause interaction failures if the element is still moving or changing position.
|
|
778
|
-
// Callers should be aware that stabilization was not achieved and handle this case appropriately.
|
|
364
|
+
await this.input.clickAt(x, y, { button, clickCount, delay });
|
|
779
365
|
}
|
|
780
366
|
// Public wrapper for tests and external use
|
|
781
367
|
async waitForElementPositionToStabilize(selector, timeout = POSITION_STABILITY_TIMEOUT_MS, checkInterval = POSITION_CHECK_INTERVAL_MS, stabilityThreshold = POSITION_STABILITY_THRESHOLD, tolerance = POSITION_TOLERANCE_PX) {
|
|
782
|
-
return this.
|
|
783
|
-
}
|
|
784
|
-
/**
|
|
785
|
-
* Check if element is within the viewport (with some tolerance)
|
|
786
|
-
*/
|
|
787
|
-
async _isElementInViewport(selector) {
|
|
788
|
-
return await this.evaluate((sel, tolerance) => {
|
|
789
|
-
const element = document.querySelector(sel);
|
|
790
|
-
if (!element)
|
|
791
|
-
return false;
|
|
792
|
-
const rect = element.getBoundingClientRect();
|
|
793
|
-
const viewportHeight = window.innerHeight;
|
|
794
|
-
const viewportWidth = window.innerWidth;
|
|
795
|
-
// Add some tolerance for scroll positioning
|
|
796
|
-
return rect.top >= -tolerance &&
|
|
797
|
-
rect.left >= -tolerance &&
|
|
798
|
-
rect.bottom <= viewportHeight + tolerance &&
|
|
799
|
-
rect.right <= viewportWidth + tolerance;
|
|
800
|
-
}, selector, VIEWPORT_TOLERANCE_PX);
|
|
368
|
+
return this.elements.waitForPositionToStabilize(selector, timeout, checkInterval, stabilityThreshold, tolerance);
|
|
801
369
|
}
|
|
802
370
|
// Type method
|
|
803
371
|
async type(selector, text, options) {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
372
|
+
// Focus the field, then delegate keystroke emulation (ANSI key
|
|
373
|
+
// sequences, CJK IME composition, humanized cadence) to the
|
|
374
|
+
// InputEmulator - see src/input/input-emulator.ts. With mouse
|
|
375
|
+
// humanization on, focus goes through the full humanized click path;
|
|
376
|
+
// otherwise a single in-page element.focus() round-trip skips the
|
|
377
|
+
// mouse-move emulation and click dispatch entirely.
|
|
378
|
+
if (this.humanization.mouse) {
|
|
379
|
+
await this.click(selector);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
await this.focusElement(selector);
|
|
815
383
|
}
|
|
384
|
+
await this.input.typeText(text, { delay: options?.delay ?? 0 });
|
|
816
385
|
}
|
|
817
386
|
/**
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
* which still produces beforeinput/input but no keyboard events (matches IME
|
|
824
|
-
* behaviour for Unicode/emoji input).
|
|
387
|
+
* Focus an element in a single Runtime.evaluate round-trip. Mirrors the
|
|
388
|
+
* wait-then-act semantics the click path gets from `waitForSelector`
|
|
389
|
+
* (MutationObserver wait, same timeout, same timeout error for a missing
|
|
390
|
+
* element), but collapses the wait and the `element.focus()` call into
|
|
391
|
+
* one CDP round-trip.
|
|
825
392
|
*/
|
|
826
|
-
async
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
393
|
+
async focusElement(selector, timeout = DEFAULT_TIMEOUT_MS) {
|
|
394
|
+
const focused = await this.evaluate(async (sel, timeoutMs) => {
|
|
395
|
+
return new Promise((resolve) => {
|
|
396
|
+
const tryFocus = () => {
|
|
397
|
+
const el = document.querySelector(sel);
|
|
398
|
+
if (!el)
|
|
399
|
+
return false;
|
|
400
|
+
el.focus();
|
|
401
|
+
resolve(true);
|
|
402
|
+
return true;
|
|
403
|
+
};
|
|
404
|
+
if (tryFocus())
|
|
405
|
+
return;
|
|
406
|
+
let timer;
|
|
407
|
+
const observer = new MutationObserver(() => {
|
|
408
|
+
if (tryFocus()) {
|
|
409
|
+
clearTimeout(timer);
|
|
410
|
+
observer.disconnect();
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
// Same observation surface as waitForSelector: childList+subtree
|
|
414
|
+
// catches insertion; attributes+characterData catch an existing
|
|
415
|
+
// node becoming matching.
|
|
416
|
+
observer.observe(document, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
417
|
+
timer = setTimeout(() => {
|
|
418
|
+
observer.disconnect();
|
|
419
|
+
resolve(false);
|
|
420
|
+
}, timeoutMs);
|
|
421
|
+
});
|
|
422
|
+
}, selector, timeout);
|
|
423
|
+
if (!focused) {
|
|
424
|
+
throw new Error(`Timeout waiting for selector: ${selector}`);
|
|
831
425
|
}
|
|
832
|
-
const { descriptor, shift } = mapping;
|
|
833
|
-
const modifiers = shift ? MODIFIER_SHIFT : 0;
|
|
834
|
-
const text = descriptor.text ?? descriptor.key;
|
|
835
|
-
await Input.dispatchKeyEvent({
|
|
836
|
-
type: 'rawKeyDown',
|
|
837
|
-
modifiers,
|
|
838
|
-
key: descriptor.key,
|
|
839
|
-
code: descriptor.code,
|
|
840
|
-
windowsVirtualKeyCode: descriptor.keyCode,
|
|
841
|
-
location: descriptor.location ?? 0,
|
|
842
|
-
});
|
|
843
|
-
await Input.dispatchKeyEvent({
|
|
844
|
-
type: 'char',
|
|
845
|
-
modifiers,
|
|
846
|
-
key: descriptor.key,
|
|
847
|
-
code: descriptor.code,
|
|
848
|
-
text,
|
|
849
|
-
unmodifiedText: text,
|
|
850
|
-
windowsVirtualKeyCode: descriptor.keyCode,
|
|
851
|
-
location: descriptor.location ?? 0,
|
|
852
|
-
});
|
|
853
|
-
await Input.dispatchKeyEvent({
|
|
854
|
-
type: 'keyUp',
|
|
855
|
-
modifiers,
|
|
856
|
-
key: descriptor.key,
|
|
857
|
-
code: descriptor.code,
|
|
858
|
-
windowsVirtualKeyCode: descriptor.keyCode,
|
|
859
|
-
location: descriptor.location ?? 0,
|
|
860
|
-
});
|
|
861
426
|
}
|
|
862
427
|
/**
|
|
863
428
|
* Deletes (clears) the value of an input field specified by selector.
|
|
@@ -893,60 +458,42 @@ export default class CdpPage extends EventEmitter {
|
|
|
893
458
|
// Wait for selector
|
|
894
459
|
async waitForSelector(selector, options) {
|
|
895
460
|
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
461
|
+
// Single in-page round-trip: resolve as soon as the selector matches via
|
|
462
|
+
// a MutationObserver (with an initial querySelector for elements already
|
|
463
|
+
// present), instead of re-issuing an evaluate on a fixed poll interval.
|
|
464
|
+
// The in-page promise resolves true on match / false on timeout; the
|
|
465
|
+
// observer is always torn down before resolving.
|
|
466
|
+
const found = await this.evaluate(async (sel, timeoutMs) => {
|
|
467
|
+
return new Promise((resolve) => {
|
|
468
|
+
if (document.querySelector(sel)) {
|
|
469
|
+
resolve(true);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
let timer;
|
|
473
|
+
const observer = new MutationObserver(() => {
|
|
474
|
+
if (document.querySelector(sel)) {
|
|
475
|
+
clearTimeout(timer);
|
|
476
|
+
observer.disconnect();
|
|
477
|
+
resolve(true);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
// childList+subtree catches insertion; attributes+characterData catch
|
|
481
|
+
// an already-present node becoming matching via a class/attribute/text
|
|
482
|
+
// change - the old 100ms querySelector poll caught those too.
|
|
483
|
+
observer.observe(document, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
484
|
+
timer = setTimeout(() => {
|
|
485
|
+
observer.disconnect();
|
|
486
|
+
resolve(false);
|
|
487
|
+
}, timeoutMs);
|
|
488
|
+
});
|
|
489
|
+
}, selector, timeout);
|
|
490
|
+
if (!found) {
|
|
491
|
+
throw new Error(`Timeout waiting for selector: ${selector}`);
|
|
905
492
|
}
|
|
906
|
-
throw new Error(`Timeout waiting for selector: ${selector}`);
|
|
907
493
|
}
|
|
908
|
-
// Wait for response
|
|
494
|
+
// Wait for response - protocol lives in NetworkWatch (src/network/network-watch.ts)
|
|
909
495
|
async waitForResponse(urlPart, statusCode = 200, timeout = 180000) {
|
|
910
|
-
|
|
911
|
-
return new Promise((resolve, reject) => {
|
|
912
|
-
let timer;
|
|
913
|
-
const onResponseReceived = async ({ requestId, response }) => {
|
|
914
|
-
if (response.url.includes(urlPart) && response.status === statusCode) {
|
|
915
|
-
clearTimeout(timer);
|
|
916
|
-
try {
|
|
917
|
-
const { body } = await this.client.Network.getResponseBody({ requestId });
|
|
918
|
-
resolve({
|
|
919
|
-
...response,
|
|
920
|
-
body: JSON.parse(body),
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
catch (err) {
|
|
924
|
-
// Retry logic with exponential backoff
|
|
925
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
926
|
-
try {
|
|
927
|
-
await this.ensureNetworkEnabled();
|
|
928
|
-
await new Promise((resolveTimeout) => setTimeout(resolveTimeout, 1000 * 2 ** attempt)); // Exponential backoff
|
|
929
|
-
const { body } = await this.client.Network.getResponseBody({ requestId });
|
|
930
|
-
resolve({
|
|
931
|
-
...response,
|
|
932
|
-
body: JSON.parse(body),
|
|
933
|
-
});
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
catch (retryErr) {
|
|
937
|
-
if (attempt === 3)
|
|
938
|
-
reject(new Error(`Failed to get response body after ${attempt} attempts`));
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
};
|
|
944
|
-
// @ts-expect-error it exists
|
|
945
|
-
this.client.Network.on('responseReceived', onResponseReceived);
|
|
946
|
-
timer = setTimeout(() => {
|
|
947
|
-
reject(new Error(`Timeout after ${timeout / 1000} seconds`));
|
|
948
|
-
}, timeout);
|
|
949
|
-
});
|
|
496
|
+
return this.networkWatch.waitForResponse(urlPart, statusCode, timeout);
|
|
950
497
|
}
|
|
951
498
|
async screenshot(options) {
|
|
952
499
|
const { Page } = this.client;
|
|
@@ -980,7 +527,7 @@ export default class CdpPage extends EventEmitter {
|
|
|
980
527
|
// Close method
|
|
981
528
|
async close() {
|
|
982
529
|
const { Page } = this.client;
|
|
983
|
-
this.
|
|
530
|
+
this.session.markClosed();
|
|
984
531
|
await Page.close();
|
|
985
532
|
this.emit(PAGE_CLOSED);
|
|
986
533
|
}
|
|
@@ -1015,71 +562,42 @@ export default class CdpPage extends EventEmitter {
|
|
|
1015
562
|
}
|
|
1016
563
|
// Add proper Fetch Metadata headers to avoid CDP detection
|
|
1017
564
|
async setupFetchMetadataHeaders() {
|
|
1018
|
-
|
|
1019
|
-
await this._enableDomain('Network');
|
|
1020
|
-
await Network.setRequestInterception({ patterns: [{ urlPattern: '*' }] });
|
|
1021
|
-
Network.on('requestIntercepted', async (params) => {
|
|
1022
|
-
const { request, interceptionId } = params;
|
|
1023
|
-
const url = new URL(request.url);
|
|
1024
|
-
// Add proper fetch metadata headers
|
|
1025
|
-
const headers = { ...request.headers };
|
|
1026
|
-
// Set sec-fetch-* headers based on request type and destination
|
|
1027
|
-
if (request.method === 'GET' && !headers['sec-fetch-dest']) {
|
|
1028
|
-
if (url.pathname.endsWith('.js')) {
|
|
1029
|
-
headers['sec-fetch-dest'] = 'script';
|
|
1030
|
-
headers['sec-fetch-mode'] = 'no-cors';
|
|
1031
|
-
headers['sec-fetch-site'] = url.origin === headers.origin ? 'same-origin' : 'cross-site';
|
|
1032
|
-
}
|
|
1033
|
-
else if (url.pathname.endsWith('.css')) {
|
|
1034
|
-
headers['sec-fetch-dest'] = 'style';
|
|
1035
|
-
headers['sec-fetch-mode'] = 'no-cors';
|
|
1036
|
-
headers['sec-fetch-site'] = url.origin === headers.origin ? 'same-origin' : 'cross-site';
|
|
1037
|
-
}
|
|
1038
|
-
else if (url.pathname.match(/\.(png|jpg|jpeg|gif|webp|svg)$/i)) {
|
|
1039
|
-
headers['sec-fetch-dest'] = 'image';
|
|
1040
|
-
headers['sec-fetch-mode'] = 'no-cors';
|
|
1041
|
-
headers['sec-fetch-site'] = url.origin === headers.origin ? 'same-origin' : 'cross-site';
|
|
1042
|
-
}
|
|
1043
|
-
else if (url.pathname === '/' || !url.pathname.includes('.')) {
|
|
1044
|
-
headers['sec-fetch-dest'] = 'document';
|
|
1045
|
-
headers['sec-fetch-mode'] = 'navigate';
|
|
1046
|
-
headers['sec-fetch-site'] = 'none';
|
|
1047
|
-
headers['sec-fetch-user'] = '?1';
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
// Continue the request with modified headers
|
|
1051
|
-
await Network.continueInterceptedRequest({
|
|
1052
|
-
interceptionId,
|
|
1053
|
-
headers,
|
|
1054
|
-
});
|
|
1055
|
-
});
|
|
565
|
+
return this.networkWatch.setupFetchMetadataHeaders();
|
|
1056
566
|
}
|
|
1057
567
|
// Block requests matching URL patterns
|
|
1058
568
|
async blockRequests(urlPatterns) {
|
|
1059
|
-
|
|
1060
|
-
await this.ensureNetworkEnabled(); // Lazy enable Network only when needed
|
|
1061
|
-
await Network.setRequestInterception({ patterns: [{ urlPattern: '*' }] });
|
|
1062
|
-
Network.on('requestIntercepted', async (params) => {
|
|
1063
|
-
const shouldBlock = urlPatterns.some((pattern) => {
|
|
1064
|
-
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
1065
|
-
return regex.test(params.request.url);
|
|
1066
|
-
});
|
|
1067
|
-
if (shouldBlock) {
|
|
1068
|
-
await Network.continueInterceptedRequest({
|
|
1069
|
-
interceptionId: params.interceptionId,
|
|
1070
|
-
errorReason: 'Aborted',
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1073
|
-
else {
|
|
1074
|
-
await Network.continueInterceptedRequest({ interceptionId: params.interceptionId });
|
|
1075
|
-
}
|
|
1076
|
-
});
|
|
569
|
+
return this.networkWatch.blockRequests(urlPatterns);
|
|
1077
570
|
}
|
|
1078
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Resolve a CSS selector to a CDP DOM nodeId. Retained as public page API
|
|
573
|
+
* for callers that need a node handle for direct CDP DOM work; the click
|
|
574
|
+
* hot-path no longer uses it (geometry now comes from in-page
|
|
575
|
+
* getBoundingClientRect - see ElementTarget.acquire). Reuses a cached
|
|
576
|
+
* document root and self-heals a stale root with a fresh getDocument.
|
|
577
|
+
*/
|
|
1079
578
|
async getNodeId(selector) {
|
|
1080
579
|
const { DOM } = this.client;
|
|
1081
|
-
|
|
1082
|
-
|
|
580
|
+
// Reuse a cached document root nodeId to skip a per-call DOM.getDocument
|
|
581
|
+
// round-trip. querySelector still runs on every call.
|
|
582
|
+
if (this._documentNodeId === undefined) {
|
|
583
|
+
const { root: { nodeId } } = await DOM.getDocument();
|
|
584
|
+
this._documentNodeId = nodeId;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const { nodeId } = await DOM.querySelector({ selector, nodeId: this._documentNodeId });
|
|
588
|
+
if (nodeId) {
|
|
589
|
+
return { nodeId };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// Cached root may be stale (e.g. a navigation we did not observe).
|
|
594
|
+
// Fall through to the fresh-document retry below.
|
|
595
|
+
}
|
|
596
|
+
// Defensive retry: refetch the document root once and try again before
|
|
597
|
+
// reporting the selector as missing. Covers a stale cached root.
|
|
598
|
+
const { root: { nodeId: freshRoot } } = await DOM.getDocument();
|
|
599
|
+
this._documentNodeId = freshRoot;
|
|
600
|
+
const { nodeId } = await DOM.querySelector({ selector, nodeId: freshRoot });
|
|
1083
601
|
if (!nodeId) {
|
|
1084
602
|
throw new Error(`No node found for selector: ${selector}`);
|
|
1085
603
|
}
|
|
@@ -1089,9 +607,8 @@ export default class CdpPage extends EventEmitter {
|
|
|
1089
607
|
if (cookies.length === 0) {
|
|
1090
608
|
return;
|
|
1091
609
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}
|
|
610
|
+
// One batched CDP call instead of a round-trip per cookie.
|
|
611
|
+
await this.client.Network.setCookies({ cookies });
|
|
1095
612
|
}
|
|
1096
613
|
/**
|
|
1097
614
|
* Retrieves all cookies associated with the given URLs or all cookies for the current page if no URLs are provided.
|
|
@@ -1109,6 +626,17 @@ export default class CdpPage extends EventEmitter {
|
|
|
1109
626
|
const cookiesResult = await Network.getCookies({ urls });
|
|
1110
627
|
return cookiesResult.cookies;
|
|
1111
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* Records launch-time inputs (fingerprint config, resolved locale, user-data-dir, …)
|
|
631
|
+
* that `saveSession()` needs to assemble a SessionBundle. Called by the browser
|
|
632
|
+
* controller after fingerprint injection - not part of the user-facing API.
|
|
633
|
+
*/
|
|
634
|
+
setLaunchSnapshot(snapshot) {
|
|
635
|
+
this.launchSnapshot = { ...this.launchSnapshot, ...snapshot };
|
|
636
|
+
}
|
|
637
|
+
getLaunchSnapshot() {
|
|
638
|
+
return this.launchSnapshot;
|
|
639
|
+
}
|
|
1112
640
|
// Add this method to your CdpPage class
|
|
1113
641
|
/**
|
|
1114
642
|
* Evaluates a function across multiple elements matching the given selector.
|
|
@@ -1124,90 +652,23 @@ export default class CdpPage extends EventEmitter {
|
|
|
1124
652
|
// eslint-disable-next-line no-new-func
|
|
1125
653
|
const fn = new Function(`return ${fnStr}`)();
|
|
1126
654
|
return fn(elements, ...innerArgs);
|
|
1127
|
-
}, selector,
|
|
655
|
+
}, selector,
|
|
656
|
+
// Same transpiler-helper problem as `evaluate`: this body is rebuilt
|
|
657
|
+
// with `new Function` inside the page, so a keepNames-wrapped source
|
|
658
|
+
// would throw `__name is not defined` there.
|
|
659
|
+
withTranspilerHelperShims(pageFunction.toString()), ...args);
|
|
1128
660
|
}
|
|
1129
661
|
/**
|
|
1130
|
-
* Enables a CDP domain only if it hasn't been enabled yet
|
|
662
|
+
* Enables a CDP domain only if it hasn't been enabled yet. Tracking lives
|
|
663
|
+
* in PageSession so a reconnect re-binds exactly the enabled domains.
|
|
1131
664
|
* @private
|
|
1132
665
|
*/
|
|
1133
666
|
async _enableDomain(domain) {
|
|
1134
|
-
|
|
1135
|
-
return;
|
|
1136
|
-
const domainApi = this.client[domain];
|
|
1137
|
-
if (domainApi?.enable) {
|
|
1138
|
-
await domainApi.enable();
|
|
1139
|
-
this.enabledDomains.add(domain);
|
|
1140
|
-
log.debug(`Enabled ${domain} domain`);
|
|
1141
|
-
}
|
|
667
|
+
await this.session.enableDomain(domain);
|
|
1142
668
|
}
|
|
669
|
+
/** Re-bind the page's CDP session after a transport drop. Delegates to PageSession. */
|
|
1143
670
|
async reconnectSession(retries = 3) {
|
|
1144
|
-
|
|
1145
|
-
log.debug('Reconnection skipped - already in progress');
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
if (this.isClosed) {
|
|
1149
|
-
log.debug('Reconnection skipped - page is closed');
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
let attempt = 0;
|
|
1153
|
-
this.isReconnecting = true;
|
|
1154
|
-
try {
|
|
1155
|
-
while (attempt < retries) {
|
|
1156
|
-
try {
|
|
1157
|
-
// Wait with exponential backoff before retrying
|
|
1158
|
-
if (attempt > 0) {
|
|
1159
|
-
const backoffMs = Math.min(1000 * (2 ** attempt), 10000);
|
|
1160
|
-
log.info(`Waiting ${backoffMs}ms before reconnection attempt ${attempt + 1}/${retries}`);
|
|
1161
|
-
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
1162
|
-
}
|
|
1163
|
-
// Create a fresh CDP connection
|
|
1164
|
-
const newSession = await CDP({
|
|
1165
|
-
port: this._rawClient.port,
|
|
1166
|
-
local: true,
|
|
1167
|
-
});
|
|
1168
|
-
try {
|
|
1169
|
-
// Get available targets
|
|
1170
|
-
const { targetInfos } = await newSession.Target.getTargets();
|
|
1171
|
-
const pageTarget = targetInfos.find(t => t.type === 'page' && !t.url.includes('chrome://'));
|
|
1172
|
-
if (!pageTarget) {
|
|
1173
|
-
log.debug('No valid page target found, stopping reconnection');
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
// Connect to the specific page target
|
|
1177
|
-
const pageSession = await CDP({
|
|
1178
|
-
target: pageTarget.targetId,
|
|
1179
|
-
port: this._rawClient.port,
|
|
1180
|
-
local: true,
|
|
1181
|
-
});
|
|
1182
|
-
// Re-enable only the domains that were previously enabled
|
|
1183
|
-
const enablePromises = Array.from(this.enabledDomains).map(domain => pageSession[domain].enable());
|
|
1184
|
-
await Promise.all(enablePromises);
|
|
1185
|
-
// Update references
|
|
1186
|
-
this._rawClient = pageSession;
|
|
1187
|
-
this.client = pageSession;
|
|
1188
|
-
this._attachConnectionMonitor();
|
|
1189
|
-
log.info(`Page session reconnected successfully on attempt ${attempt + 1}`);
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
catch (error) {
|
|
1193
|
-
// Clean up the session if anything fails
|
|
1194
|
-
await newSession.close().catch(() => { });
|
|
1195
|
-
throw error;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
catch (error) {
|
|
1199
|
-
attempt++;
|
|
1200
|
-
if (attempt === retries) {
|
|
1201
|
-
log.debug('Reconnection stopped - max retries reached');
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
log.error('Failed to reconnect page session', { error });
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
finally {
|
|
1209
|
-
this.isReconnecting = false;
|
|
1210
|
-
}
|
|
671
|
+
await this.session.reconnect(retries);
|
|
1211
672
|
}
|
|
1212
673
|
/**
|
|
1213
674
|
* Selects one or more options from a select element or dropdown.
|
|
@@ -1236,249 +697,7 @@ export default class CdpPage extends EventEmitter {
|
|
|
1236
697
|
this._enableDomain('DOM'),
|
|
1237
698
|
this._enableDomain('Input'),
|
|
1238
699
|
]);
|
|
1239
|
-
|
|
1240
|
-
const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
1241
|
-
const force = options?.force ?? false;
|
|
1242
|
-
const waitForOptions = options?.waitForOptions ?? true;
|
|
1243
|
-
const maxScrollAttempts = options?.maxScrollAttempts ?? 10;
|
|
1244
|
-
// Normalize target selectors to array
|
|
1245
|
-
const targetSelectors = Array.isArray(optionSelector) ? optionSelector : [optionSelector];
|
|
1246
|
-
// Wait for dropdown element to exist
|
|
1247
|
-
await this.waitForSelector(dropdownSelector, { timeout });
|
|
1248
|
-
// Check if this is a regular select element
|
|
1249
|
-
const isSelectElement = await this.evaluate((sel) => {
|
|
1250
|
-
const element = document.querySelector(sel);
|
|
1251
|
-
return element?.tagName === 'SELECT';
|
|
1252
|
-
}, dropdownSelector);
|
|
1253
|
-
// For regular select elements, do visibility and disabled checks upfront
|
|
1254
|
-
if (isSelectElement) {
|
|
1255
|
-
// Check if element is visible (unless force is true)
|
|
1256
|
-
if (!force) {
|
|
1257
|
-
const isVisible = await this._isElementVisible(dropdownSelector);
|
|
1258
|
-
if (!isVisible) {
|
|
1259
|
-
throw new Error(`Element is not visible: ${dropdownSelector}. Use force: true to bypass this check.`);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
// Check if element is disabled (unless force is true)
|
|
1263
|
-
if (!force) {
|
|
1264
|
-
const isDisabled = await this.evaluate((sel) => {
|
|
1265
|
-
const element = document.querySelector(sel);
|
|
1266
|
-
return element?.hasAttribute('disabled') || element?.getAttribute('disabled') === 'true';
|
|
1267
|
-
}, dropdownSelector);
|
|
1268
|
-
if (isDisabled) {
|
|
1269
|
-
throw new Error(`Element is disabled: ${dropdownSelector}. Use force: true to bypass this check.`);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
// Scroll element into view if needed
|
|
1273
|
-
await this._scrollElementIntoView(dropdownSelector);
|
|
1274
|
-
// Wait for scroll animations to complete
|
|
1275
|
-
await new Promise(resolve => setTimeout(resolve, SCROLL_ANIMATION_DELAY_MS));
|
|
1276
|
-
}
|
|
1277
|
-
if (isSelectElement) {
|
|
1278
|
-
// Handle regular select element
|
|
1279
|
-
await this._handleRegularSelect(dropdownSelector, targetSelectors, {
|
|
1280
|
-
timeout,
|
|
1281
|
-
force,
|
|
1282
|
-
});
|
|
1283
|
-
}
|
|
1284
|
-
else {
|
|
1285
|
-
// Handle custom dropdown - the dropdownSelector is the container
|
|
1286
|
-
await this._handleCustomDropdown(dropdownSelector, targetSelectors, {
|
|
1287
|
-
timeout,
|
|
1288
|
-
force,
|
|
1289
|
-
waitForOptions,
|
|
1290
|
-
maxScrollAttempts,
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
/**
|
|
1295
|
-
* Handles selection for regular HTML select elements
|
|
1296
|
-
*/
|
|
1297
|
-
async _handleRegularSelect(selectSelector, targetSelectors, _options) {
|
|
1298
|
-
const result = await this.evaluate((sel, targetSels) => {
|
|
1299
|
-
const select = document.querySelector(sel);
|
|
1300
|
-
if (!select) {
|
|
1301
|
-
throw new Error(`Select element not found: ${sel}`);
|
|
1302
|
-
}
|
|
1303
|
-
// Check if multiple selection is allowed
|
|
1304
|
-
if (!select.multiple && targetSels.length > 1) {
|
|
1305
|
-
throw new Error(`Select element does not support multiple selection. Found ${targetSels.length} selectors but multiple is false.`);
|
|
1306
|
-
}
|
|
1307
|
-
// Clear previous selections
|
|
1308
|
-
if (select.multiple) {
|
|
1309
|
-
Array.from(select.options).forEach(option => {
|
|
1310
|
-
option.selected = false;
|
|
1311
|
-
});
|
|
1312
|
-
}
|
|
1313
|
-
let foundCount = 0;
|
|
1314
|
-
const foundSelectors = [];
|
|
1315
|
-
// Select the options by CSS selector
|
|
1316
|
-
for (const targetSelector of targetSels) {
|
|
1317
|
-
let optionFound = false;
|
|
1318
|
-
// Find option by CSS selector within the select element
|
|
1319
|
-
const option = select.querySelector(targetSelector);
|
|
1320
|
-
if (option) {
|
|
1321
|
-
option.selected = true;
|
|
1322
|
-
optionFound = true;
|
|
1323
|
-
foundSelectors.push(targetSelector);
|
|
1324
|
-
}
|
|
1325
|
-
if (optionFound) {
|
|
1326
|
-
foundCount++;
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
// Trigger change event
|
|
1330
|
-
const event = new Event('change', { bubbles: true });
|
|
1331
|
-
select.dispatchEvent(event);
|
|
1332
|
-
return { foundCount, foundSelectors, totalOptions: select.options.length };
|
|
1333
|
-
}, selectSelector, targetSelectors);
|
|
1334
|
-
if (result.foundCount === 0) {
|
|
1335
|
-
throw new Error(`No options found for selectors: ${targetSelectors.join(', ')}. Available options: ${result.totalOptions}`);
|
|
1336
|
-
}
|
|
1337
|
-
if (result.foundCount < targetSelectors.length) {
|
|
1338
|
-
const missingSelectors = targetSelectors.filter(s => !result.foundSelectors.includes(s));
|
|
1339
|
-
throw new Error(`Some selectors not found: ${missingSelectors.join(', ')}. Found: ${result.foundSelectors.join(', ')}`);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
/**
|
|
1343
|
-
* Handles selection for custom dropdown elements.
|
|
1344
|
-
* Automatically detects and clicks triggers using multiple strategies:
|
|
1345
|
-
* - Common trigger patterns ([aria-haspopup], .dropdown-trigger, etc.)
|
|
1346
|
-
* - Previous sibling elements
|
|
1347
|
-
* - ID pattern matching (menu → trigger)
|
|
1348
|
-
*/
|
|
1349
|
-
async _handleCustomDropdown(dropdownContainer, targetSelectors, options) {
|
|
1350
|
-
const { timeout, force, waitForOptions, maxScrollAttempts, dropdownOpenDelay = DROPDOWN_OPEN_DELAY_MS } = options;
|
|
1351
|
-
// For custom dropdowns, always try to find and click a trigger to open
|
|
1352
|
-
// Most custom dropdowns need a trigger click to become visible
|
|
1353
|
-
await this.evaluate((containerSel) => {
|
|
1354
|
-
const container = document.querySelector(containerSel);
|
|
1355
|
-
if (!container)
|
|
1356
|
-
return false;
|
|
1357
|
-
// Strategy 1: Look for sibling trigger elements with common patterns
|
|
1358
|
-
const parent = container.parentElement;
|
|
1359
|
-
if (parent) {
|
|
1360
|
-
const triggers = parent.querySelectorAll('[role="button"], button, [data-toggle], [aria-haspopup], .dropdown-trigger, .select-trigger');
|
|
1361
|
-
for (const trigger of triggers) {
|
|
1362
|
-
if (trigger !== container) {
|
|
1363
|
-
trigger.click();
|
|
1364
|
-
return true;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
// Strategy 2: Look for previous sibling that might be the trigger
|
|
1368
|
-
const prevSibling = container.previousElementSibling;
|
|
1369
|
-
if (prevSibling && prevSibling.tagName !== 'LABEL') {
|
|
1370
|
-
prevSibling.click();
|
|
1371
|
-
return true;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
// Strategy 3: Look for element with similar ID (e.g., menu -> trigger)
|
|
1375
|
-
const containerId = container.id;
|
|
1376
|
-
if (containerId) {
|
|
1377
|
-
const triggerPatterns = [
|
|
1378
|
-
containerId.replace('menu', 'trigger').replace('dropdown', 'trigger'),
|
|
1379
|
-
containerId.replace('-menu', '-trigger'),
|
|
1380
|
-
containerId.replace('_menu', '_trigger'),
|
|
1381
|
-
`${containerId}-trigger`,
|
|
1382
|
-
`trigger-${containerId}`,
|
|
1383
|
-
];
|
|
1384
|
-
for (const triggerId of triggerPatterns) {
|
|
1385
|
-
const trigger = document.getElementById(triggerId);
|
|
1386
|
-
if (trigger && trigger !== container) {
|
|
1387
|
-
trigger.click();
|
|
1388
|
-
return true;
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
return false;
|
|
1393
|
-
}, dropdownContainer);
|
|
1394
|
-
// Wait for dropdown to open
|
|
1395
|
-
await new Promise(resolve => setTimeout(resolve, dropdownOpenDelay));
|
|
1396
|
-
// Wait for dropdown to be visible
|
|
1397
|
-
if (waitForOptions) {
|
|
1398
|
-
await this.waitForSelector(dropdownContainer, { timeout });
|
|
1399
|
-
}
|
|
1400
|
-
// Now check if dropdown is visible (unless force is true)
|
|
1401
|
-
if (!force) {
|
|
1402
|
-
const isVisible = await this._isElementVisible(dropdownContainer);
|
|
1403
|
-
if (!isVisible) {
|
|
1404
|
-
throw new Error(`Dropdown container is not visible after trigger click: ${dropdownContainer}. Use force: true to bypass this check.`);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
// Select each target selector
|
|
1408
|
-
for (const targetSelector of targetSelectors) {
|
|
1409
|
-
await this._selectOptionFromDropdown(dropdownContainer, targetSelector, {
|
|
1410
|
-
timeout,
|
|
1411
|
-
force,
|
|
1412
|
-
maxScrollAttempts,
|
|
1413
|
-
});
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* Selects a single option from a dropdown container.
|
|
1418
|
-
* Handles virtualized lists with intelligent scrolling to find options that may not be initially visible.
|
|
1419
|
-
*/
|
|
1420
|
-
async _selectOptionFromDropdown(dropdownSelector, targetSelector, dropdownOptions) {
|
|
1421
|
-
const { timeout, maxScrollAttempts } = dropdownOptions;
|
|
1422
|
-
const startTime = Date.now();
|
|
1423
|
-
let scrollAttempts = 0;
|
|
1424
|
-
let optionFound = false;
|
|
1425
|
-
while (scrollAttempts < maxScrollAttempts && !optionFound && (Date.now() - startTime) < timeout) {
|
|
1426
|
-
// Try to find and click the option
|
|
1427
|
-
const result = await this.evaluate((dropdownSel, targetSel) => {
|
|
1428
|
-
const dropdown = document.querySelector(dropdownSel);
|
|
1429
|
-
if (!dropdown)
|
|
1430
|
-
return { found: false, needsScroll: false };
|
|
1431
|
-
// Try to find the target element directly first
|
|
1432
|
-
const targetOption = dropdown.querySelector(targetSel);
|
|
1433
|
-
if (!targetOption) {
|
|
1434
|
-
// Check if we need to scroll to find more options
|
|
1435
|
-
const isScrollable = dropdown.scrollHeight > dropdown.clientHeight;
|
|
1436
|
-
const isAtBottom = dropdown.scrollTop + dropdown.clientHeight >= dropdown.scrollHeight - 1;
|
|
1437
|
-
return { found: false, needsScroll: isScrollable && !isAtBottom };
|
|
1438
|
-
}
|
|
1439
|
-
// Check if option is visible within the dropdown
|
|
1440
|
-
const rect = targetOption.getBoundingClientRect();
|
|
1441
|
-
const dropdownRect = dropdown.getBoundingClientRect();
|
|
1442
|
-
const isVisible = rect.top >= dropdownRect.top &&
|
|
1443
|
-
rect.bottom <= dropdownRect.bottom &&
|
|
1444
|
-
rect.left >= dropdownRect.left &&
|
|
1445
|
-
rect.right <= dropdownRect.right;
|
|
1446
|
-
if (!isVisible) {
|
|
1447
|
-
// Scroll to make the option visible
|
|
1448
|
-
targetOption.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1449
|
-
return { found: false, needsScroll: false, scrolled: true };
|
|
1450
|
-
}
|
|
1451
|
-
// Click the option
|
|
1452
|
-
targetOption.click();
|
|
1453
|
-
return { found: true, needsScroll: false };
|
|
1454
|
-
}, dropdownSelector, targetSelector);
|
|
1455
|
-
if (result.found) {
|
|
1456
|
-
optionFound = true;
|
|
1457
|
-
break;
|
|
1458
|
-
}
|
|
1459
|
-
if (result.needsScroll) {
|
|
1460
|
-
// Scroll down to load more options
|
|
1461
|
-
await this.evaluate((dropdownSel) => {
|
|
1462
|
-
const dropdown = document.querySelector(dropdownSel);
|
|
1463
|
-
if (dropdown) {
|
|
1464
|
-
dropdown.scrollTop += dropdown.clientHeight * 0.8;
|
|
1465
|
-
}
|
|
1466
|
-
}, dropdownSelector);
|
|
1467
|
-
scrollAttempts++;
|
|
1468
|
-
await new Promise(resolve => setTimeout(resolve, OPTION_LOAD_DELAY_MS)); // Wait for new options to load
|
|
1469
|
-
}
|
|
1470
|
-
else if (result.scrolled) {
|
|
1471
|
-
// Option was scrolled into view, try clicking it now
|
|
1472
|
-
await new Promise(resolve => setTimeout(resolve, SCROLL_COMPLETE_DELAY_MS)); // Wait for scroll to complete
|
|
1473
|
-
}
|
|
1474
|
-
else {
|
|
1475
|
-
// No more scrolling needed, option not found
|
|
1476
|
-
break;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
if (!optionFound) {
|
|
1480
|
-
throw new Error(`Option "${targetSelector}" not found in dropdown "${dropdownSelector}"`);
|
|
1481
|
-
}
|
|
700
|
+
await this.dropdowns.selectOption(dropdownSelector, optionSelector, options);
|
|
1482
701
|
}
|
|
1483
702
|
/**
|
|
1484
703
|
* Checks if the element specified by selector is visible (not display:none and not visibility:hidden).
|