@taodigital/crawler 0.0.0-stage → 1.2.0
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/Dockerfile +82 -0
- package/README.md +10 -2
- package/dist/accessibility-auditor.d.ts +29 -0
- package/dist/accessibility-auditor.js +57 -0
- package/dist/accessibility-auditor.js.map +7 -0
- package/dist/accessibility-tree-crawler.d.ts +44 -0
- package/dist/accessibility-tree-crawler.js +156 -0
- package/dist/accessibility-tree-crawler.js.map +7 -0
- package/dist/advanced-ui-probes.d.ts +47 -0
- package/dist/advanced-ui-probes.js +310 -0
- package/dist/advanced-ui-probes.js.map +7 -0
- package/dist/ai-login.d.ts +32 -0
- package/dist/ai-login.js +250 -0
- package/dist/ai-login.js.map +7 -0
- package/dist/auth-token-rotator.d.ts +47 -0
- package/dist/auth-token-rotator.js +58 -0
- package/dist/auth-token-rotator.js.map +7 -0
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +348 -0
- package/dist/auth.js.map +7 -0
- package/dist/browser-extension.d.ts +37 -0
- package/dist/browser-extension.js +113 -0
- package/dist/browser-extension.js.map +7 -0
- package/dist/captcha-solver.d.ts +16 -0
- package/dist/captcha-solver.js +105 -0
- package/dist/captcha-solver.js.map +7 -0
- package/dist/color-contrast.d.ts +30 -0
- package/dist/color-contrast.js +78 -0
- package/dist/color-contrast.js.map +7 -0
- package/dist/content-chunker.d.ts +18 -0
- package/dist/content-chunker.js +152 -0
- package/dist/content-chunker.js.map +7 -0
- package/dist/content-dedup.d.ts +23 -0
- package/dist/content-dedup.js +60 -0
- package/dist/content-dedup.js.map +7 -0
- package/dist/cookie-consent.d.ts +25 -0
- package/dist/cookie-consent.js +78 -0
- package/dist/cookie-consent.js.map +7 -0
- package/dist/cookie-jar.d.ts +20 -0
- package/dist/cookie-jar.js +61 -0
- package/dist/cookie-jar.js.map +7 -0
- package/dist/crawl-session-manager.d.ts +44 -0
- package/dist/crawl-session-manager.js +145 -0
- package/dist/crawl-session-manager.js.map +7 -0
- package/dist/crawler.d.ts +233 -0
- package/dist/crawler.js +6239 -0
- package/dist/crawler.js.map +7 -0
- package/dist/crud-interaction.d.ts +31 -0
- package/dist/crud-interaction.js +264 -0
- package/dist/crud-interaction.js.map +7 -0
- package/dist/custom-js-injector.d.ts +42 -0
- package/dist/custom-js-injector.js +50 -0
- package/dist/custom-js-injector.js.map +7 -0
- package/dist/dark-mode-capture.d.ts +23 -0
- package/dist/dark-mode-capture.js +75 -0
- package/dist/dark-mode-capture.js.map +7 -0
- package/dist/dead-link-detector.d.ts +37 -0
- package/dist/dead-link-detector.js +50 -0
- package/dist/dead-link-detector.js.map +7 -0
- package/dist/dropdown-enumerator.d.ts +20 -0
- package/dist/dropdown-enumerator.js +230 -0
- package/dist/dropdown-enumerator.js.map +7 -0
- package/dist/element-hash.d.ts +9 -0
- package/dist/element-hash.js +9 -0
- package/dist/element-hash.js.map +7 -0
- package/dist/email-otp.d.ts +42 -0
- package/dist/email-otp.js +177 -0
- package/dist/email-otp.js.map +7 -0
- package/dist/engine-router.d.ts +47 -0
- package/dist/engine-router.js +94 -0
- package/dist/engine-router.js.map +7 -0
- package/dist/export/bulk-export.d.ts +26 -0
- package/dist/export/bulk-export.js +76 -0
- package/dist/export/bulk-export.js.map +7 -0
- package/dist/export/vector-export.d.ts +76 -0
- package/dist/export/vector-export.js +85 -0
- package/dist/export/vector-export.js.map +7 -0
- package/dist/extractors/advanced.d.ts +64 -0
- package/dist/extractors/advanced.js +265 -0
- package/dist/extractors/advanced.js.map +7 -0
- package/dist/extractors/artifact-envelope.d.ts +63 -0
- package/dist/extractors/artifact-envelope.js +63 -0
- package/dist/extractors/artifact-envelope.js.map +7 -0
- package/dist/extractors/branding-extractor.d.ts +46 -0
- package/dist/extractors/branding-extractor.js +104 -0
- package/dist/extractors/branding-extractor.js.map +7 -0
- package/dist/extractors/summary-extractor.d.ts +10 -0
- package/dist/extractors/summary-extractor.js +36 -0
- package/dist/extractors/summary-extractor.js.map +7 -0
- package/dist/failure-analysis-agent.d.ts +16 -0
- package/dist/failure-analysis-agent.js +42 -0
- package/dist/failure-analysis-agent.js.map +7 -0
- package/dist/feature-flag-detector.d.ts +30 -0
- package/dist/feature-flag-detector.js +55 -0
- package/dist/feature-flag-detector.js.map +7 -0
- package/dist/filter-pagination-crawler.d.ts +52 -0
- package/dist/filter-pagination-crawler.js +299 -0
- package/dist/filter-pagination-crawler.js.map +7 -0
- package/dist/form-detector.d.ts +34 -0
- package/dist/form-detector.js +58 -0
- package/dist/form-detector.js.map +7 -0
- package/dist/frontier.d.ts +40 -0
- package/dist/frontier.js +103 -0
- package/dist/frontier.js.map +7 -0
- package/dist/gap-pass.d.ts +36 -0
- package/dist/gap-pass.js +115 -0
- package/dist/gap-pass.js.map +7 -0
- package/dist/graphql-detector.d.ts +39 -0
- package/dist/graphql-detector.js +86 -0
- package/dist/graphql-detector.js.map +7 -0
- package/dist/http-engine.d.ts +80 -0
- package/dist/http-engine.js +318 -0
- package/dist/http-engine.js.map +7 -0
- package/dist/incremental-diff.d.ts +116 -0
- package/dist/incremental-diff.js +137 -0
- package/dist/incremental-diff.js.map +7 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +7 -0
- package/dist/input-fuzzer.d.ts +30 -0
- package/dist/input-fuzzer.js +105 -0
- package/dist/input-fuzzer.js.map +7 -0
- package/dist/interactive-probe.d.ts +144 -0
- package/dist/interactive-probe.js +865 -0
- package/dist/interactive-probe.js.map +7 -0
- package/dist/job-queue.d.ts +297 -0
- package/dist/job-queue.js +3812 -0
- package/dist/job-queue.js.map +7 -0
- package/dist/js-coverage.d.ts +29 -0
- package/dist/js-coverage.js +56 -0
- package/dist/js-coverage.js.map +7 -0
- package/dist/js-router-detector.d.ts +17 -0
- package/dist/js-router-detector.js +90 -0
- package/dist/js-router-detector.js.map +7 -0
- package/dist/language-detector.d.ts +22 -0
- package/dist/language-detector.js +57 -0
- package/dist/language-detector.js.map +7 -0
- package/dist/link-classifier.d.ts +23 -0
- package/dist/link-classifier.js +40 -0
- package/dist/link-classifier.js.map +7 -0
- package/dist/live-view.d.ts +36 -0
- package/dist/live-view.js +105 -0
- package/dist/live-view.js.map +7 -0
- package/dist/locator-extractor.d.ts +46 -0
- package/dist/locator-extractor.js +220 -0
- package/dist/locator-extractor.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +10 -0
- package/dist/logger.js.map +7 -0
- package/dist/markdown-extract.d.ts +8 -0
- package/dist/markdown-extract.js +54 -0
- package/dist/markdown-extract.js.map +7 -0
- package/dist/multi-context-handler.d.ts +55 -0
- package/dist/multi-context-handler.js +182 -0
- package/dist/multi-context-handler.js.map +7 -0
- package/dist/native-crawler.d.ts +69 -0
- package/dist/native-crawler.js +839 -0
- package/dist/native-crawler.js.map +7 -0
- package/dist/nested-window-crawler.d.ts +41 -0
- package/dist/nested-window-crawler.js +172 -0
- package/dist/nested-window-crawler.js.map +7 -0
- package/dist/network-action-correlator.d.ts +47 -0
- package/dist/network-action-correlator.js +209 -0
- package/dist/network-action-correlator.js.map +7 -0
- package/dist/network-observer.d.ts +49 -0
- package/dist/network-observer.js +149 -0
- package/dist/network-observer.js.map +7 -0
- package/dist/network-throttle.d.ts +25 -0
- package/dist/network-throttle.js +34 -0
- package/dist/network-throttle.js.map +7 -0
- package/dist/openapi-generator.d.ts +49 -0
- package/dist/openapi-generator.js +89 -0
- package/dist/openapi-generator.js.map +7 -0
- package/dist/pdf-extractor.d.ts +28 -0
- package/dist/pdf-extractor.js +81 -0
- package/dist/pdf-extractor.js.map +7 -0
- package/dist/performance-metrics-collector.d.ts +50 -0
- package/dist/performance-metrics-collector.js +155 -0
- package/dist/performance-metrics-collector.js.map +7 -0
- package/dist/performance-metrics.d.ts +52 -0
- package/dist/performance-metrics.js +83 -0
- package/dist/performance-metrics.js.map +7 -0
- package/dist/phash.d.ts +50 -0
- package/dist/phash.js +136 -0
- package/dist/phash.js.map +7 -0
- package/dist/pii-scrubber.d.ts +21 -0
- package/dist/pii-scrubber.js +75 -0
- package/dist/pii-scrubber.js.map +7 -0
- package/dist/playwright-recorder.d.ts +39 -0
- package/dist/playwright-recorder.js +123 -0
- package/dist/playwright-recorder.js.map +7 -0
- package/dist/policies/crawl-safety.d.ts +9 -0
- package/dist/policies/crawl-safety.js +41 -0
- package/dist/policies/crawl-safety.js.map +7 -0
- package/dist/profiles/domain-profile.d.ts +19 -0
- package/dist/profiles/domain-profile.js +160 -0
- package/dist/profiles/domain-profile.js.map +7 -0
- package/dist/proxy.d.ts +50 -0
- package/dist/proxy.js +165 -0
- package/dist/proxy.js.map +7 -0
- package/dist/rate-limiter.d.ts +31 -0
- package/dist/rate-limiter.js +64 -0
- package/dist/rate-limiter.js.map +7 -0
- package/dist/redirect-tracker.d.ts +37 -0
- package/dist/redirect-tracker.js +65 -0
- package/dist/redirect-tracker.js.map +7 -0
- package/dist/redis-frontier.d.ts +48 -0
- package/dist/redis-frontier.js +126 -0
- package/dist/redis-frontier.js.map +7 -0
- package/dist/redis-queue.d.ts +48 -0
- package/dist/redis-queue.js +165 -0
- package/dist/redis-queue.js.map +7 -0
- package/dist/response-cache.d.ts +38 -0
- package/dist/response-cache.js +140 -0
- package/dist/response-cache.js.map +7 -0
- package/dist/responsive-detector.d.ts +29 -0
- package/dist/responsive-detector.js +46 -0
- package/dist/responsive-detector.js.map +7 -0
- package/dist/retry-policy.d.ts +20 -0
- package/dist/retry-policy.js +54 -0
- package/dist/retry-policy.js.map +7 -0
- package/dist/robots-txt.d.ts +9 -0
- package/dist/robots-txt.js +77 -0
- package/dist/robots-txt.js.map +7 -0
- package/dist/screen-reader.d.ts +15 -0
- package/dist/screen-reader.js +48 -0
- package/dist/screen-reader.js.map +7 -0
- package/dist/screenshot-masker.d.ts +28 -0
- package/dist/screenshot-masker.js +62 -0
- package/dist/screenshot-masker.js.map +7 -0
- package/dist/seo-checker.d.ts +33 -0
- package/dist/seo-checker.js +91 -0
- package/dist/seo-checker.js.map +7 -0
- package/dist/service-worker-interceptor.d.ts +36 -0
- package/dist/service-worker-interceptor.js +136 -0
- package/dist/service-worker-interceptor.js.map +7 -0
- package/dist/session-capture.d.ts +39 -0
- package/dist/session-capture.js +274 -0
- package/dist/session-capture.js.map +7 -0
- package/dist/session-export.d.ts +49 -0
- package/dist/session-export.js +117 -0
- package/dist/session-export.js.map +7 -0
- package/dist/shadow-dom-crawler.d.ts +35 -0
- package/dist/shadow-dom-crawler.js +132 -0
- package/dist/shadow-dom-crawler.js.map +7 -0
- package/dist/sitemap-writer.d.ts +21 -0
- package/dist/sitemap-writer.js +47 -0
- package/dist/sitemap-writer.js.map +7 -0
- package/dist/sitemap.d.ts +51 -0
- package/dist/sitemap.js +160 -0
- package/dist/sitemap.js.map +7 -0
- package/dist/skyvern.d.ts +56 -0
- package/dist/skyvern.js +129 -0
- package/dist/skyvern.js.map +7 -0
- package/dist/sqs-queue.d.ts +13 -0
- package/dist/sqs-queue.js +115 -0
- package/dist/sqs-queue.js.map +7 -0
- package/dist/sso-token.d.ts +29 -0
- package/dist/sso-token.js +76 -0
- package/dist/sso-token.js.map +7 -0
- package/dist/state-graph.d.ts +59 -0
- package/dist/state-graph.js +193 -0
- package/dist/state-graph.js.map +7 -0
- package/dist/stealth/human-behavior.d.ts +20 -0
- package/dist/stealth/human-behavior.js +202 -0
- package/dist/stealth/human-behavior.js.map +7 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +77 -0
- package/dist/telemetry.js.map +7 -0
- package/dist/test-executor.d.ts +81 -0
- package/dist/test-executor.js +443 -0
- package/dist/test-executor.js.map +7 -0
- package/dist/tls-fingerprint.d.ts +50 -0
- package/dist/tls-fingerprint.js +81 -0
- package/dist/tls-fingerprint.js.map +7 -0
- package/dist/url-pattern-completer.d.ts +28 -0
- package/dist/url-pattern-completer.js +122 -0
- package/dist/url-pattern-completer.js.map +7 -0
- package/dist/visual-diff-verdict.d.ts +40 -0
- package/dist/visual-diff-verdict.js +27 -0
- package/dist/visual-diff-verdict.js.map +7 -0
- package/dist/webhook-notifier.d.ts +30 -0
- package/dist/webhook-notifier.js +76 -0
- package/dist/webhook-notifier.js.map +7 -0
- package/dist/websocket-detector.d.ts +24 -0
- package/dist/websocket-detector.js +60 -0
- package/dist/websocket-detector.js.map +7 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +33 -0
- package/dist/worker.js.map +7 -0
- package/package.json +49 -4
- package/src/accessibility-auditor.ts +102 -0
- package/src/accessibility-tree-crawler.ts +246 -0
- package/src/advanced-ui-probes.ts +470 -0
- package/src/ai-login.ts +298 -0
- package/src/auth-token-rotator.ts +89 -0
- package/src/auth.ts +418 -0
- package/src/browser-extension.ts +186 -0
- package/src/captcha-solver.ts +143 -0
- package/src/color-contrast.ts +116 -0
- package/src/content-chunker.ts +170 -0
- package/src/content-dedup.ts +99 -0
- package/src/cookie-consent.ts +106 -0
- package/src/cookie-jar.ts +91 -0
- package/src/crawl-session-manager.ts +231 -0
- package/src/crawler.ts +7123 -0
- package/src/crawler.ts.rej +12 -0
- package/src/crud-interaction.ts +386 -0
- package/src/custom-js-injector.ts +93 -0
- package/src/dark-mode-capture.ts +110 -0
- package/src/dead-link-detector.ts +76 -0
- package/src/dropdown-enumerator.ts +312 -0
- package/src/element-hash.ts +16 -0
- package/src/email-otp.ts +252 -0
- package/src/engine-router.ts +141 -0
- package/src/export/bulk-export.ts +85 -0
- package/src/export/vector-export.ts +160 -0
- package/src/extractors/advanced.ts +309 -0
- package/src/extractors/artifact-envelope.ts +111 -0
- package/src/extractors/branding-extractor.ts +159 -0
- package/src/extractors/summary-extractor.ts +49 -0
- package/src/failure-analysis-agent.ts +51 -0
- package/src/feature-flag-detector.ts +97 -0
- package/src/filter-pagination-crawler.ts +500 -0
- package/src/form-detector.ts +103 -0
- package/src/frontier.test.ts +64 -0
- package/src/frontier.ts +144 -0
- package/src/gap-pass.ts +165 -0
- package/src/graphql-detector.ts +129 -0
- package/src/http-engine.ts +453 -0
- package/src/incremental-diff.ts +259 -0
- package/src/index.ts +52 -0
- package/src/input-fuzzer.ts +129 -0
- package/src/interactive-probe.ts +1136 -0
- package/src/job-queue.test.ts +187 -0
- package/src/job-queue.ts +4504 -0
- package/src/js-coverage.ts +87 -0
- package/src/js-router-detector.ts +112 -0
- package/src/language-detector.ts +82 -0
- package/src/link-classifier.ts +65 -0
- package/src/live-view.ts +146 -0
- package/src/locator-extractor.ts +309 -0
- package/src/logger.ts +9 -0
- package/src/markdown-extract.test.ts +73 -0
- package/src/markdown-extract.ts +76 -0
- package/src/multi-context-handler.ts +296 -0
- package/src/native-crawler.ts +1108 -0
- package/src/nested-window-crawler.ts +310 -0
- package/src/network-action-correlator.ts +320 -0
- package/src/network-observer.ts +204 -0
- package/src/network-throttle.ts +69 -0
- package/src/openapi-generator.ts +135 -0
- package/src/pdf-extractor.ts +123 -0
- package/src/performance-metrics-collector.ts +243 -0
- package/src/performance-metrics.ts +134 -0
- package/src/phash.ts +184 -0
- package/src/pii-scrubber.ts +99 -0
- package/src/playwright-recorder.ts +170 -0
- package/src/policies/crawl-safety.ts +52 -0
- package/src/profiles/domain-profile.ts +190 -0
- package/src/proxy.ts +238 -0
- package/src/rate-limiter.ts +87 -0
- package/src/redirect-tracker.ts +97 -0
- package/src/redis-frontier.ts +143 -0
- package/src/redis-queue.ts +208 -0
- package/src/response-cache.ts +150 -0
- package/src/responsive-detector.ts +92 -0
- package/src/retry-policy.ts +87 -0
- package/src/robots-txt.ts +96 -0
- package/src/screen-reader.ts +98 -0
- package/src/screenshot-masker.ts +98 -0
- package/src/seo-checker.ts +134 -0
- package/src/service-worker-interceptor.ts +185 -0
- package/src/session-capture.ts +335 -0
- package/src/session-export.test.ts +75 -0
- package/src/session-export.ts +172 -0
- package/src/shadow-dom-crawler.ts +198 -0
- package/src/sitemap-writer.ts +65 -0
- package/src/sitemap.test.ts +127 -0
- package/src/sitemap.ts +233 -0
- package/src/skyvern.ts +199 -0
- package/src/sqs-queue.ts +129 -0
- package/src/sso-token.ts +120 -0
- package/src/state-graph.ts +280 -0
- package/src/stealth/human-behavior.ts +227 -0
- package/src/telemetry.ts +122 -0
- package/src/test-executor.test.ts +72 -0
- package/src/test-executor.ts +578 -0
- package/src/tls-fingerprint.ts +111 -0
- package/src/url-pattern-completer.ts +161 -0
- package/src/vendor.d.ts +20 -0
- package/src/visual-diff-verdict.test.ts +80 -0
- package/src/visual-diff-verdict.ts +59 -0
- package/src/webhook-notifier.ts +109 -0
- package/src/websocket-detector.ts +83 -0
- package/src/worker.ts +50 -0
- package/tsconfig.json +13 -0
- package/types/accessibility-auditor.d.ts +29 -0
- package/types/accessibility-tree-crawler.d.ts +44 -0
- package/types/advanced-ui-probes.d.ts +47 -0
- package/types/ai-login.d.ts +32 -0
- package/types/auth-token-rotator.d.ts +47 -0
- package/types/auth.d.ts +20 -0
- package/types/browser-extension.d.ts +37 -0
- package/types/bull-worker.d.ts +6 -0
- package/types/captcha-solver.d.ts +16 -0
- package/types/color-contrast.d.ts +30 -0
- package/types/content-chunker.d.ts +18 -0
- package/types/content-dedup.d.ts +23 -0
- package/types/cookie-consent.d.ts +25 -0
- package/types/cookie-jar.d.ts +20 -0
- package/types/crawl-session-manager.d.ts +44 -0
- package/types/crawler.d.ts +233 -0
- package/types/crud-interaction.d.ts +31 -0
- package/types/custom-js-injector.d.ts +42 -0
- package/types/dark-mode-capture.d.ts +23 -0
- package/types/dead-link-detector.d.ts +37 -0
- package/types/dropdown-enumerator.d.ts +20 -0
- package/types/element-hash.d.ts +9 -0
- package/types/email-otp.d.ts +42 -0
- package/types/engine-router.d.ts +47 -0
- package/types/export/bulk-export.d.ts +26 -0
- package/types/export/vector-export.d.ts +76 -0
- package/types/extractors/advanced.d.ts +64 -0
- package/types/extractors/artifact-envelope.d.ts +63 -0
- package/types/extractors/branding-extractor.d.ts +46 -0
- package/types/extractors/summary-extractor.d.ts +10 -0
- package/types/failure-analysis-agent.d.ts +16 -0
- package/types/feature-flag-detector.d.ts +30 -0
- package/types/filter-pagination-crawler.d.ts +52 -0
- package/types/form-detector.d.ts +34 -0
- package/types/frontier.d.ts +40 -0
- package/types/gap-pass.d.ts +36 -0
- package/types/graphql-detector.d.ts +39 -0
- package/types/http-engine.d.ts +80 -0
- package/types/incremental-diff.d.ts +116 -0
- package/types/index.d.ts +52 -0
- package/types/input-fuzzer.d.ts +30 -0
- package/types/interactive-probe.d.ts +144 -0
- package/types/job-queue.d.ts +308 -0
- package/types/js-coverage.d.ts +29 -0
- package/types/js-router-detector.d.ts +17 -0
- package/types/language-detector.d.ts +22 -0
- package/types/link-classifier.d.ts +23 -0
- package/types/live-view.d.ts +36 -0
- package/types/locator-extractor.d.ts +46 -0
- package/types/logger.d.ts +2 -0
- package/types/markdown-extract.d.ts +8 -0
- package/types/multi-context-handler.d.ts +55 -0
- package/types/native-crawler.d.ts +69 -0
- package/types/nested-window-crawler.d.ts +41 -0
- package/types/network-action-correlator.d.ts +47 -0
- package/types/network-observer.d.ts +49 -0
- package/types/network-throttle.d.ts +25 -0
- package/types/openapi-generator.d.ts +49 -0
- package/types/pdf-extractor.d.ts +28 -0
- package/types/performance-metrics-collector.d.ts +50 -0
- package/types/performance-metrics.d.ts +52 -0
- package/types/phash.d.ts +50 -0
- package/types/pii-scrubber.d.ts +21 -0
- package/types/playwright-recorder.d.ts +39 -0
- package/types/policies/crawl-safety.d.ts +9 -0
- package/types/profiles/domain-profile.d.ts +19 -0
- package/types/proxy.d.ts +50 -0
- package/types/rate-limiter.d.ts +31 -0
- package/types/redirect-tracker.d.ts +37 -0
- package/types/redis-frontier.d.ts +48 -0
- package/types/redis-queue.d.ts +48 -0
- package/types/response-cache.d.ts +38 -0
- package/types/responsive-detector.d.ts +29 -0
- package/types/retry-policy.d.ts +20 -0
- package/types/robots-txt.d.ts +9 -0
- package/types/screen-reader.d.ts +15 -0
- package/types/screenshot-masker.d.ts +28 -0
- package/types/seo-checker.d.ts +33 -0
- package/types/service-worker-interceptor.d.ts +36 -0
- package/types/session-capture.d.ts +39 -0
- package/types/session-export.d.ts +49 -0
- package/types/shadow-dom-crawler.d.ts +35 -0
- package/types/sitemap-writer.d.ts +21 -0
- package/types/sitemap.d.ts +51 -0
- package/types/skyvern.d.ts +56 -0
- package/types/sqs-queue.d.ts +13 -0
- package/types/sso-token.d.ts +29 -0
- package/types/state-graph.d.ts +59 -0
- package/types/stealth/human-behavior.d.ts +20 -0
- package/types/telemetry.d.ts +34 -0
- package/types/test-executor.d.ts +84 -0
- package/types/tls-fingerprint.d.ts +50 -0
- package/types/url-pattern-completer.d.ts +28 -0
- package/types/visual-diff-verdict.d.ts +40 -0
- package/types/webhook-notifier.d.ts +30 -0
- package/types/websocket-detector.d.ts +24 -0
- package/types/worker.d.ts +1 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter state crawling and pagination capture.
|
|
3
|
+
*
|
|
4
|
+
* Discovers filter controls (select, checkbox-group, radio-group, search),
|
|
5
|
+
* applies each meaningful option and records result counts and first-row
|
|
6
|
+
* values, then detects and walks pagination (numbered, load-more, infinite
|
|
7
|
+
* scroll). Also captures sort column behaviour.
|
|
8
|
+
*
|
|
9
|
+
* All per-filter / per-page errors are caught and skipped so one bad control
|
|
10
|
+
* never aborts the full crawl.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Page } from 'playwright';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Public interfaces
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export interface FilterOption {
|
|
20
|
+
value: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FilterDef {
|
|
25
|
+
kind: 'select' | 'checkbox-group' | 'radio-group' | 'search';
|
|
26
|
+
label: string;
|
|
27
|
+
locatorHint: string;
|
|
28
|
+
options: FilterOption[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FilterState {
|
|
32
|
+
filterLabel: string;
|
|
33
|
+
filterValue: string;
|
|
34
|
+
resultCount: number | null;
|
|
35
|
+
firstRowValues: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PaginationCapture {
|
|
39
|
+
kind: 'numbered' | 'load-more' | 'infinite-scroll' | 'none';
|
|
40
|
+
totalPages?: number;
|
|
41
|
+
totalItems?: number;
|
|
42
|
+
pageSizes?: number[];
|
|
43
|
+
pages: Array<{ pageNum: number; rowValues: string[] }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FilterPaginationResult {
|
|
47
|
+
filters: FilterDef[];
|
|
48
|
+
filterStates: FilterState[];
|
|
49
|
+
pagination: PaginationCapture;
|
|
50
|
+
sortColumns: Array<{ header: string; ariaSortAfterClick: string | null }>;
|
|
51
|
+
searchCapture?: { noResultsText: string | null; sampleSuggestions: string[] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Internal helpers
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/** Pause for up to `ms` ms or until network is idle, whichever comes first. */
|
|
59
|
+
async function waitForSettle(page: Page, ms = 1500): Promise<void> {
|
|
60
|
+
await Promise.race([
|
|
61
|
+
page.waitForLoadState('networkidle').catch(() => undefined),
|
|
62
|
+
new Promise<void>(r => setTimeout(r, ms)),
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Extract visible cell text from the first tbody row, if any. */
|
|
67
|
+
async function captureFirstRow(page: Page): Promise<string[]> {
|
|
68
|
+
try {
|
|
69
|
+
return await page.evaluate(() => {
|
|
70
|
+
const row = document.querySelector('tbody tr');
|
|
71
|
+
if (!row) return [];
|
|
72
|
+
return Array.from(row.querySelectorAll('td')).map(td => td.innerText.trim());
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Read a result-count hint from visible page text using common patterns. */
|
|
80
|
+
async function readResultCount(page: Page): Promise<number | null> {
|
|
81
|
+
try {
|
|
82
|
+
const text = await page.evaluate(() => document.body.innerText);
|
|
83
|
+
const match = text.match(/(\d[\d,]*)\s*(result|item|entr|record)/i);
|
|
84
|
+
if (!match) return null;
|
|
85
|
+
return parseInt(match[1].replace(/,/g, ''), 10);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// discoverFilters
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
async function discoverFilters(page: Page): Promise<FilterDef[]> {
|
|
96
|
+
const filters: FilterDef[] = [];
|
|
97
|
+
|
|
98
|
+
// --- native <select> elements outside dialogs ---------------------------------
|
|
99
|
+
try {
|
|
100
|
+
const selectHandles = await page.locator('select:not([role="dialog"] select)').all();
|
|
101
|
+
for (const handle of selectHandles.slice(0, 10)) {
|
|
102
|
+
try {
|
|
103
|
+
const id = await handle.getAttribute('id').catch(() => null);
|
|
104
|
+
const name = await handle.getAttribute('name').catch(() => null);
|
|
105
|
+
const locatorHint = id ? `#${id}` : name ? `select[name="${name}"]` : 'select';
|
|
106
|
+
|
|
107
|
+
// Find associated label
|
|
108
|
+
let label = '';
|
|
109
|
+
if (id) {
|
|
110
|
+
label = await page.locator(`label[for="${id}"]`).innerText().catch(() => '');
|
|
111
|
+
}
|
|
112
|
+
if (!label) {
|
|
113
|
+
label = await handle.evaluate((el: HTMLSelectElement) => {
|
|
114
|
+
const lbl = el.closest('label') ?? el.previousElementSibling;
|
|
115
|
+
return (lbl as HTMLElement | null)?.innerText?.trim() ?? '';
|
|
116
|
+
}).catch(() => '');
|
|
117
|
+
}
|
|
118
|
+
if (!label) label = name ?? id ?? 'filter';
|
|
119
|
+
|
|
120
|
+
// Collect options, skipping default/placeholder entries
|
|
121
|
+
const rawOptions: FilterOption[] = await handle.evaluate((el: HTMLSelectElement) =>
|
|
122
|
+
Array.from(el.options).map(o => ({ value: o.value, label: o.text.trim() })),
|
|
123
|
+
).catch(() => []);
|
|
124
|
+
|
|
125
|
+
const options = rawOptions.filter(
|
|
126
|
+
o => !(/^all$|^any$|^select|^--/i.test(o.label) || o.value === ''),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (options.length === 0) continue;
|
|
130
|
+
|
|
131
|
+
filters.push({ kind: 'select', label, locatorHint, options });
|
|
132
|
+
if (filters.length >= 10) break;
|
|
133
|
+
} catch {
|
|
134
|
+
// skip this select
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// page.locator failed
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- search inputs ------------------------------------------------------------
|
|
142
|
+
if (filters.length < 10) {
|
|
143
|
+
try {
|
|
144
|
+
const searchHandles = await page
|
|
145
|
+
.locator(
|
|
146
|
+
'input[type="search"]:not([role="dialog"] input), ' +
|
|
147
|
+
'input[placeholder*="search" i]:not([role="dialog"] input)',
|
|
148
|
+
)
|
|
149
|
+
.all();
|
|
150
|
+
|
|
151
|
+
for (const handle of searchHandles.slice(0, 10 - filters.length)) {
|
|
152
|
+
try {
|
|
153
|
+
const id = await handle.getAttribute('id').catch(() => null);
|
|
154
|
+
const placeholder = await handle.getAttribute('placeholder').catch(() => null);
|
|
155
|
+
const locatorHint = id ? `#${id}` : placeholder ? `input[placeholder="${placeholder}"]` : 'input[type="search"]';
|
|
156
|
+
|
|
157
|
+
let label = '';
|
|
158
|
+
if (id) {
|
|
159
|
+
label = await page.locator(`label[for="${id}"]`).innerText().catch(() => '');
|
|
160
|
+
}
|
|
161
|
+
if (!label) label = placeholder ?? 'search';
|
|
162
|
+
|
|
163
|
+
filters.push({ kind: 'search', label, locatorHint, options: [] });
|
|
164
|
+
if (filters.length >= 10) break;
|
|
165
|
+
} catch {
|
|
166
|
+
// skip
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// locator failed
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return filters;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// applyFilterAndCapture
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
async function applyFilterAndCapture(
|
|
182
|
+
page: Page,
|
|
183
|
+
filter: FilterDef,
|
|
184
|
+
option: FilterOption,
|
|
185
|
+
): Promise<FilterState> {
|
|
186
|
+
const state: FilterState = {
|
|
187
|
+
filterLabel: filter.label,
|
|
188
|
+
filterValue: option.value,
|
|
189
|
+
resultCount: null,
|
|
190
|
+
firstRowValues: [],
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
if (filter.kind === 'select') {
|
|
195
|
+
await page.locator(filter.locatorHint).selectOption(option.value);
|
|
196
|
+
} else if (filter.kind === 'search') {
|
|
197
|
+
await page.locator(filter.locatorHint).fill(option.value);
|
|
198
|
+
await page.locator(filter.locatorHint).press('Enter');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await waitForSettle(page, 1500);
|
|
202
|
+
|
|
203
|
+
state.resultCount = await readResultCount(page);
|
|
204
|
+
state.firstRowValues = await captureFirstRow(page);
|
|
205
|
+
} catch {
|
|
206
|
+
// non-fatal: return partial state
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Reset filter
|
|
210
|
+
try {
|
|
211
|
+
if (filter.kind === 'select') {
|
|
212
|
+
const firstOption = await page.locator(filter.locatorHint).evaluate(
|
|
213
|
+
(el: HTMLSelectElement) => el.options[0]?.value ?? '',
|
|
214
|
+
);
|
|
215
|
+
await page.locator(filter.locatorHint).selectOption(firstOption);
|
|
216
|
+
} else if (filter.kind === 'search') {
|
|
217
|
+
await page.locator(filter.locatorHint).fill('');
|
|
218
|
+
await page.locator(filter.locatorHint).press('Enter');
|
|
219
|
+
}
|
|
220
|
+
await waitForSettle(page, 800);
|
|
221
|
+
} catch {
|
|
222
|
+
// ignore reset errors
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return state;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// crawlFilterStates
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
async function crawlFilterStates(page: Page, filters: FilterDef[]): Promise<FilterState[]> {
|
|
233
|
+
const states: FilterState[] = [];
|
|
234
|
+
let totalStates = 0;
|
|
235
|
+
|
|
236
|
+
for (const filter of filters) {
|
|
237
|
+
if (totalStates >= 15) break;
|
|
238
|
+
|
|
239
|
+
const { options } = filter;
|
|
240
|
+
if (options.length === 0) continue;
|
|
241
|
+
|
|
242
|
+
// Pick first, middle, last — deduplicated, max 3
|
|
243
|
+
const indices = new Set<number>([
|
|
244
|
+
0,
|
|
245
|
+
Math.floor((options.length - 1) / 2),
|
|
246
|
+
options.length - 1,
|
|
247
|
+
]);
|
|
248
|
+
const picked = Array.from(indices).slice(0, 3);
|
|
249
|
+
|
|
250
|
+
for (const idx of picked) {
|
|
251
|
+
if (totalStates >= 15) break;
|
|
252
|
+
const option = options[idx];
|
|
253
|
+
try {
|
|
254
|
+
const s = await applyFilterAndCapture(page, filter, option);
|
|
255
|
+
states.push(s);
|
|
256
|
+
} catch {
|
|
257
|
+
// skip
|
|
258
|
+
}
|
|
259
|
+
totalStates++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return states;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// detectPaginationKind
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
async function detectPaginationKind(
|
|
271
|
+
page: Page,
|
|
272
|
+
): Promise<'numbered' | 'load-more' | 'infinite-scroll' | 'none'> {
|
|
273
|
+
try {
|
|
274
|
+
// Numbered pagination: ARIA page buttons or .pagination
|
|
275
|
+
const numberedCount = await page
|
|
276
|
+
.locator('[aria-label*="page" i], .pagination button, [role="navigation"] button')
|
|
277
|
+
.count()
|
|
278
|
+
.catch(() => 0);
|
|
279
|
+
if (numberedCount > 1) return 'numbered';
|
|
280
|
+
|
|
281
|
+
// Load-more button
|
|
282
|
+
const loadMore = await page
|
|
283
|
+
.locator('button:has-text("Load more"), button:has-text("Show more"), a:has-text("Load more")')
|
|
284
|
+
.count()
|
|
285
|
+
.catch(() => 0);
|
|
286
|
+
if (loadMore > 0) return 'load-more';
|
|
287
|
+
|
|
288
|
+
// Infinite scroll: scrollable content significantly taller than viewport
|
|
289
|
+
const isInfinite = await page.evaluate(() => {
|
|
290
|
+
return document.documentElement.scrollHeight > window.innerHeight * 2;
|
|
291
|
+
}).catch(() => false);
|
|
292
|
+
if (isInfinite) return 'infinite-scroll';
|
|
293
|
+
} catch {
|
|
294
|
+
// fall through
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return 'none';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// crawlNumberedPagination
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
async function crawlNumberedPagination(page: Page): Promise<PaginationCapture> {
|
|
305
|
+
const pages: Array<{ pageNum: number; rowValues: string[] }> = [];
|
|
306
|
+
const pageSizes: number[] = [];
|
|
307
|
+
|
|
308
|
+
// Read page size selector if present
|
|
309
|
+
try {
|
|
310
|
+
const sizeSelect = page.locator('select[name*="size" i], select[name*="per" i], select[aria-label*="per page" i]');
|
|
311
|
+
const sizeCount = await sizeSelect.count();
|
|
312
|
+
if (sizeCount > 0) {
|
|
313
|
+
const rawSizes = await sizeSelect.first().evaluate((el: HTMLSelectElement) =>
|
|
314
|
+
Array.from(el.options).map(o => parseInt(o.value, 10)).filter(n => !isNaN(n)),
|
|
315
|
+
);
|
|
316
|
+
pageSizes.push(...rawSizes);
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
// ignore
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Determine total pages from pagination widget
|
|
323
|
+
let totalPages: number | undefined;
|
|
324
|
+
try {
|
|
325
|
+
totalPages = await page.evaluate(() => {
|
|
326
|
+
const buttons = Array.from(
|
|
327
|
+
document.querySelectorAll('[aria-label*="page" i], .pagination button'),
|
|
328
|
+
);
|
|
329
|
+
const nums = buttons
|
|
330
|
+
.map(b => parseInt((b as HTMLElement).innerText.trim(), 10))
|
|
331
|
+
.filter(n => !isNaN(n));
|
|
332
|
+
return nums.length > 0 ? Math.max(...nums) : undefined;
|
|
333
|
+
});
|
|
334
|
+
} catch {
|
|
335
|
+
// leave undefined
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Capture first page immediately
|
|
339
|
+
pages.push({ pageNum: 1, rowValues: await captureFirstRow(page) });
|
|
340
|
+
|
|
341
|
+
// Walk pages by clicking Next
|
|
342
|
+
for (let pageNum = 2; pageNum <= 20; pageNum++) {
|
|
343
|
+
try {
|
|
344
|
+
const nextBtn = page.locator(
|
|
345
|
+
'button[aria-label*="next" i], a[aria-label*="next" i], [role="navigation"] button:last-child',
|
|
346
|
+
).first();
|
|
347
|
+
|
|
348
|
+
const isDisabled = await nextBtn.isDisabled().catch(() => true);
|
|
349
|
+
if (isDisabled) break;
|
|
350
|
+
|
|
351
|
+
await nextBtn.click();
|
|
352
|
+
await waitForSettle(page, 2000);
|
|
353
|
+
|
|
354
|
+
const rowValues = await captureFirstRow(page);
|
|
355
|
+
pages.push({ pageNum, rowValues });
|
|
356
|
+
} catch {
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
kind: 'numbered',
|
|
363
|
+
totalPages,
|
|
364
|
+
pageSizes: pageSizes.length > 0 ? pageSizes : undefined,
|
|
365
|
+
pages,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// captureInfiniteScroll
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
async function captureInfiniteScroll(page: Page): Promise<PaginationCapture> {
|
|
374
|
+
const pages: Array<{ pageNum: number; rowValues: string[] }> = [];
|
|
375
|
+
|
|
376
|
+
let prevRowCount = -1;
|
|
377
|
+
let iteration = 0;
|
|
378
|
+
|
|
379
|
+
while (iteration < 10) {
|
|
380
|
+
try {
|
|
381
|
+
const rowCount = await page.evaluate(() =>
|
|
382
|
+
document.querySelectorAll('tbody tr').length,
|
|
383
|
+
).catch(() => 0);
|
|
384
|
+
|
|
385
|
+
if (rowCount === prevRowCount) break; // stable
|
|
386
|
+
prevRowCount = rowCount;
|
|
387
|
+
|
|
388
|
+
// Record current first-row snapshot per scroll step
|
|
389
|
+
pages.push({ pageNum: iteration + 1, rowValues: await captureFirstRow(page) });
|
|
390
|
+
|
|
391
|
+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
|
392
|
+
await new Promise<void>(r => setTimeout(r, 800));
|
|
393
|
+
} catch {
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
iteration++;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const totalItems = await page.evaluate(() =>
|
|
400
|
+
document.querySelectorAll('tbody tr').length,
|
|
401
|
+
).catch(() => undefined);
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
kind: 'infinite-scroll',
|
|
405
|
+
totalItems: typeof totalItems === 'number' ? totalItems : undefined,
|
|
406
|
+
pages,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// captureSortColumns
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
async function captureSortColumns(
|
|
415
|
+
page: Page,
|
|
416
|
+
): Promise<Array<{ header: string; ariaSortAfterClick: string | null }>> {
|
|
417
|
+
const results: Array<{ header: string; ariaSortAfterClick: string | null }> = [];
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const sortable = await page.locator('[aria-sort]').all();
|
|
421
|
+
for (const th of sortable.slice(0, 5)) {
|
|
422
|
+
try {
|
|
423
|
+
const header = await th.innerText().catch(() => '');
|
|
424
|
+
await th.click();
|
|
425
|
+
await waitForSettle(page, 800);
|
|
426
|
+
const ariaSortAfterClick = await th.getAttribute('aria-sort').catch(() => null);
|
|
427
|
+
results.push({ header: header.trim(), ariaSortAfterClick });
|
|
428
|
+
|
|
429
|
+
// Reset by clicking again (most tables toggle back)
|
|
430
|
+
await th.click().catch(() => undefined);
|
|
431
|
+
await waitForSettle(page, 600);
|
|
432
|
+
} catch {
|
|
433
|
+
// skip this column
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
// no sortable columns found
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return results;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// crawlFiltersAndPagination — public orchestrator
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
export async function crawlFiltersAndPagination(page: Page): Promise<FilterPaginationResult> {
|
|
448
|
+
// 1. Discover filters
|
|
449
|
+
let filters: FilterDef[] = [];
|
|
450
|
+
try {
|
|
451
|
+
filters = await discoverFilters(page);
|
|
452
|
+
} catch {
|
|
453
|
+
// proceed with empty filters
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 2. Apply filters and capture states
|
|
457
|
+
let filterStates: FilterState[] = [];
|
|
458
|
+
try {
|
|
459
|
+
filterStates = await crawlFilterStates(page, filters);
|
|
460
|
+
} catch {
|
|
461
|
+
// proceed with empty states
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 3. Detect and crawl pagination
|
|
465
|
+
let pagination: PaginationCapture = { kind: 'none', pages: [] };
|
|
466
|
+
try {
|
|
467
|
+
const kind = await detectPaginationKind(page);
|
|
468
|
+
|
|
469
|
+
if (kind === 'numbered') {
|
|
470
|
+
pagination = await crawlNumberedPagination(page);
|
|
471
|
+
} else if (kind === 'load-more') {
|
|
472
|
+
// Treat load-more similarly to infinite scroll capture
|
|
473
|
+
pagination = await captureInfiniteScroll(page);
|
|
474
|
+
pagination = { ...pagination, kind: 'load-more' };
|
|
475
|
+
} else if (kind === 'infinite-scroll') {
|
|
476
|
+
pagination = await captureInfiniteScroll(page);
|
|
477
|
+
} else {
|
|
478
|
+
// Capture single-page rows
|
|
479
|
+
const rowValues = await captureFirstRow(page);
|
|
480
|
+
pagination = { kind: 'none', pages: rowValues.length > 0 ? [{ pageNum: 1, rowValues }] : [] };
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
// leave pagination as none
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 4. Sort columns
|
|
487
|
+
let sortColumns: Array<{ header: string; ariaSortAfterClick: string | null }> = [];
|
|
488
|
+
try {
|
|
489
|
+
sortColumns = await captureSortColumns(page);
|
|
490
|
+
} catch {
|
|
491
|
+
// proceed with empty sort
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
filters,
|
|
496
|
+
filterStates,
|
|
497
|
+
pagination,
|
|
498
|
+
sortColumns,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form detector — extract form metadata from crawled pages.
|
|
3
|
+
*
|
|
4
|
+
* For each form found on a page, captures: action URL, method, all input fields
|
|
5
|
+
* with their types/names/required/validation constraints, and submit button text.
|
|
6
|
+
*
|
|
7
|
+
* This data feeds ZeTa's test generator to create form validation test cases
|
|
8
|
+
* automatically (happy path + boundary/error cases per field type).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Page } from 'playwright';
|
|
12
|
+
|
|
13
|
+
export interface FormField {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string; // text, email, password, number, select, textarea, checkbox, radio, file, date
|
|
16
|
+
label?: string;
|
|
17
|
+
required: boolean;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
minLength?: number;
|
|
20
|
+
maxLength?: number;
|
|
21
|
+
min?: string;
|
|
22
|
+
max?: string;
|
|
23
|
+
pattern?: string;
|
|
24
|
+
options?: string[]; // for select/radio
|
|
25
|
+
autocomplete?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DetectedForm {
|
|
29
|
+
index: number;
|
|
30
|
+
action?: string;
|
|
31
|
+
method: string;
|
|
32
|
+
fields: FormField[];
|
|
33
|
+
submitText?: string;
|
|
34
|
+
hasFileUpload: boolean;
|
|
35
|
+
purpose: 'login' | 'search' | 'registration' | 'contact' | 'checkout' | 'settings' | 'unknown';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function guessPurpose(fields: FormField[], action?: string): DetectedForm['purpose'] {
|
|
39
|
+
const names = fields.map(f => f.name?.toLowerCase() + (f.label ?? '').toLowerCase()).join(' ');
|
|
40
|
+
if (/password/.test(names) && /email|username|user/.test(names) && fields.length <= 4) return 'login';
|
|
41
|
+
if (/password/.test(names) && /confirm|repeat/.test(names)) return 'registration';
|
|
42
|
+
if (/search|query|q\b/.test(names) && fields.length <= 3) return 'search';
|
|
43
|
+
if (/card|payment|cvv|expir/.test(names)) return 'checkout';
|
|
44
|
+
if (/message|subject|contact/.test(names)) return 'contact';
|
|
45
|
+
if (/setting|preference|profile/.test(names)) return 'settings';
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function detectForms(page: Page): Promise<DetectedForm[]> {
|
|
50
|
+
try {
|
|
51
|
+
return await page.$$eval('form', (forms) => forms.map((form: any, index: number) => {
|
|
52
|
+
const inputs = Array.from(form.querySelectorAll('input,select,textarea,button[type="submit"]'));
|
|
53
|
+
const fields: any[] = [];
|
|
54
|
+
|
|
55
|
+
for (const input of inputs as any[]) {
|
|
56
|
+
const tag = input.tagName.toLowerCase();
|
|
57
|
+
const type = tag === 'button' ? 'submit' : (input.type || tag);
|
|
58
|
+
if (['submit', 'reset', 'button', 'hidden'].includes(type)) continue;
|
|
59
|
+
|
|
60
|
+
// Find associated label
|
|
61
|
+
let label = '';
|
|
62
|
+
if (input.id) {
|
|
63
|
+
const lbl = form.querySelector(`label[for="${input.id}"]`);
|
|
64
|
+
if (lbl) label = (lbl.textContent ?? '').trim();
|
|
65
|
+
}
|
|
66
|
+
if (!label) {
|
|
67
|
+
const parent = input.closest('label');
|
|
68
|
+
if (parent) label = (parent.textContent ?? '').replace(input.value ?? '', '').trim().slice(0, 80);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const options = tag === 'select'
|
|
72
|
+
? Array.from(input.options ?? []).map((o: any) => o.text?.trim()).filter(Boolean).slice(0, 20)
|
|
73
|
+
: tag === 'input' && type === 'radio'
|
|
74
|
+
? [input.value]
|
|
75
|
+
: undefined;
|
|
76
|
+
|
|
77
|
+
fields.push({
|
|
78
|
+
name: input.name || input.id || '',
|
|
79
|
+
type,
|
|
80
|
+
label: label || undefined,
|
|
81
|
+
required: !!input.required,
|
|
82
|
+
placeholder: input.placeholder || undefined,
|
|
83
|
+
minLength: input.minLength > 0 ? input.minLength : undefined,
|
|
84
|
+
maxLength: input.maxLength > 0 && input.maxLength < 524288 ? input.maxLength : undefined,
|
|
85
|
+
min: input.min || undefined,
|
|
86
|
+
max: input.max || undefined,
|
|
87
|
+
pattern: input.pattern || undefined,
|
|
88
|
+
options,
|
|
89
|
+
autocomplete: input.autocomplete || undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const submitBtn = form.querySelector('button[type="submit"],input[type="submit"]') as any;
|
|
94
|
+
const hasFileUpload = !!form.querySelector('input[type="file"]');
|
|
95
|
+
const action = form.action || undefined;
|
|
96
|
+
const method = (form.method || 'get').toUpperCase();
|
|
97
|
+
|
|
98
|
+
return { index, action, method, fields, submitText: submitBtn?.textContent?.trim() || submitBtn?.value || undefined, hasFileUpload };
|
|
99
|
+
})).then(forms => forms.map((f: any) => ({ ...f, purpose: guessPurpose(f.fields, f.action) })));
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { CrawlFrontier } from './frontier.js';
|
|
4
|
+
|
|
5
|
+
test('CrawlFrontier: BFS dequeues in FIFO order, identical to a plain array shift()', () => {
|
|
6
|
+
const f = new CrawlFrontier('BFS', ['https://a.com/1', 'https://a.com/2']);
|
|
7
|
+
f.push('https://a.com/3');
|
|
8
|
+
assert.equal(f.shift(), 'https://a.com/1');
|
|
9
|
+
assert.equal(f.shift(), 'https://a.com/2');
|
|
10
|
+
assert.equal(f.shift(), 'https://a.com/3');
|
|
11
|
+
assert.equal(f.shift(), undefined);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('CrawlFrontier: DFS dequeues in LIFO order', () => {
|
|
15
|
+
const f = new CrawlFrontier('DFS', ['https://a.com/1', 'https://a.com/2']);
|
|
16
|
+
f.push('https://a.com/3');
|
|
17
|
+
assert.equal(f.shift(), 'https://a.com/3');
|
|
18
|
+
assert.equal(f.shift(), 'https://a.com/2');
|
|
19
|
+
assert.equal(f.shift(), 'https://a.com/1');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('CrawlFrontier: BEST_FIRST prefers shallower paths over deeper ones', () => {
|
|
23
|
+
const f = new CrawlFrontier('BEST_FIRST', [
|
|
24
|
+
'https://a.com/products/electronics/phones/model-x',
|
|
25
|
+
'https://a.com/about',
|
|
26
|
+
]);
|
|
27
|
+
assert.equal(f.shift(), 'https://a.com/about');
|
|
28
|
+
assert.equal(f.shift(), 'https://a.com/products/electronics/phones/model-x');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('CrawlFrontier: BEST_FIRST boosts URLs matching keywords ahead of shallower non-matching ones', () => {
|
|
32
|
+
const f = new CrawlFrontier('BEST_FIRST', ['https://a.com/'], ['pricing']);
|
|
33
|
+
f.push('https://a.com/deep/nested/pricing/plans');
|
|
34
|
+
// "/" has depth 0 (score 0); the pricing URL has depth 4 (score -4) but +10 for the
|
|
35
|
+
// keyword match, netting +6 — the keyword-matching page should win despite depth.
|
|
36
|
+
assert.equal(f.shift(), 'https://a.com/deep/nested/pricing/plans');
|
|
37
|
+
assert.equal(f.shift(), 'https://a.com/');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('CrawlFrontier: length reflects push/shift correctly across all strategies', () => {
|
|
41
|
+
for (const strategy of ['BFS', 'DFS', 'BEST_FIRST'] as const) {
|
|
42
|
+
const f = new CrawlFrontier(strategy, ['https://a.com/1']);
|
|
43
|
+
assert.equal(f.length, 1);
|
|
44
|
+
f.push('https://a.com/2');
|
|
45
|
+
assert.equal(f.length, 2);
|
|
46
|
+
f.shift();
|
|
47
|
+
assert.equal(f.length, 1);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('CrawlFrontier: filter returns URLs matching the predicate without mutating the frontier', () => {
|
|
52
|
+
const f = new CrawlFrontier('BFS', ['https://a.com/1', 'https://a.com/2', 'https://a.com/3']);
|
|
53
|
+
const matched = f.filter((u) => u.endsWith('/2'));
|
|
54
|
+
assert.deepEqual(matched, ['https://a.com/2']);
|
|
55
|
+
assert.equal(f.length, 3); // unchanged
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('CrawlFrontier: malformed URLs never throw when scored under BEST_FIRST', () => {
|
|
59
|
+
assert.doesNotThrow(() => {
|
|
60
|
+
const f = new CrawlFrontier('BEST_FIRST', ['not-a-valid-url', 'https://a.com/ok']);
|
|
61
|
+
f.shift();
|
|
62
|
+
f.shift();
|
|
63
|
+
});
|
|
64
|
+
});
|