@taodigital/crawler 0.0.0-stage → 1.2.1
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,31 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
export type EditFlowType = 'inline' | 'modal' | 'navigation' | 'unknown';
|
|
3
|
+
export interface CrudFlowCapture {
|
|
4
|
+
entityName: string;
|
|
5
|
+
createTrigger?: string;
|
|
6
|
+
editFlowType: EditFlowType;
|
|
7
|
+
editFields: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
prefilledValue?: string;
|
|
11
|
+
}>;
|
|
12
|
+
deleteConfirmation?: {
|
|
13
|
+
message: string;
|
|
14
|
+
confirmLabel: string;
|
|
15
|
+
cancelLabel: string;
|
|
16
|
+
};
|
|
17
|
+
createSuccessToast?: string;
|
|
18
|
+
editSuccessToast?: string;
|
|
19
|
+
rowCount: number;
|
|
20
|
+
hasDeleteButton: boolean;
|
|
21
|
+
hasEditButton: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface CrudProbeResult {
|
|
24
|
+
flows: CrudFlowCapture[];
|
|
25
|
+
toastsObserved: Array<{
|
|
26
|
+
action: string;
|
|
27
|
+
text: string;
|
|
28
|
+
role: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export declare function probeCrudFlows(page: Page, entityName?: string): Promise<CrudProbeResult>;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
function injectToastObserver(page) {
|
|
2
|
+
page.evaluate(() => {
|
|
3
|
+
if (window.__toastObserver) return;
|
|
4
|
+
window.__capturedToasts = [];
|
|
5
|
+
const TOAST_CLASSES = ["toast", "snackbar", "notification"];
|
|
6
|
+
function matchesToast(el) {
|
|
7
|
+
const role = el.getAttribute("role") ?? "";
|
|
8
|
+
if (role === "alert" || role === "status") return true;
|
|
9
|
+
const cls = (el.getAttribute("class") ?? "").toLowerCase();
|
|
10
|
+
return TOAST_CLASSES.some((t) => cls.includes(t));
|
|
11
|
+
}
|
|
12
|
+
function captureNode(node) {
|
|
13
|
+
if (!(node instanceof Element)) return;
|
|
14
|
+
if (matchesToast(node)) {
|
|
15
|
+
const text = (node.innerText ?? "").replace(/\s+/g, " ").trim();
|
|
16
|
+
const role = node.getAttribute("role") ?? "alert";
|
|
17
|
+
if (text) window.__capturedToasts.push({ text, role });
|
|
18
|
+
}
|
|
19
|
+
node.querySelectorAll('[role="alert"],[role="status"],[class*="toast"],[class*="Toast"],[class*="snackbar"],[class*="notification"]').forEach((child) => {
|
|
20
|
+
const text = (child.innerText ?? "").replace(/\s+/g, " ").trim();
|
|
21
|
+
const role = child.getAttribute("role") ?? "alert";
|
|
22
|
+
if (text) window.__capturedToasts.push({ text, role });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const observer = new MutationObserver((mutations) => {
|
|
26
|
+
for (const m of mutations) {
|
|
27
|
+
m.addedNodes.forEach(captureNode);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
31
|
+
window.__toastObserver = observer;
|
|
32
|
+
}).catch(() => void 0);
|
|
33
|
+
}
|
|
34
|
+
async function collectCapturedToasts(page) {
|
|
35
|
+
try {
|
|
36
|
+
return await page.evaluate(() => {
|
|
37
|
+
const toasts = window.__capturedToasts ?? [];
|
|
38
|
+
window.__capturedToasts = [];
|
|
39
|
+
return toasts;
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function findCrudButtons(page) {
|
|
46
|
+
const editLocator = page.locator(
|
|
47
|
+
'button[aria-label*="edit" i], button[aria-label*="Edit"], button:has-text("Edit")'
|
|
48
|
+
);
|
|
49
|
+
const deleteLocator = page.locator(
|
|
50
|
+
'button[aria-label*="delete" i], button[aria-label*="Delete"], button:has-text("Delete")'
|
|
51
|
+
);
|
|
52
|
+
const createLocator = page.locator(
|
|
53
|
+
'button:has-text("New"), button:has-text("Create"), button:has-text("Add")'
|
|
54
|
+
);
|
|
55
|
+
const [editCount, deleteCount, createCount] = await Promise.all([
|
|
56
|
+
editLocator.count().catch(() => 0),
|
|
57
|
+
deleteLocator.count().catch(() => 0),
|
|
58
|
+
createLocator.count().catch(() => 0)
|
|
59
|
+
]);
|
|
60
|
+
const editBtns = [];
|
|
61
|
+
for (let i = 0; i < Math.min(editCount, 3); i++) {
|
|
62
|
+
editBtns.push(editLocator.nth(i));
|
|
63
|
+
}
|
|
64
|
+
const deleteBtns = [];
|
|
65
|
+
for (let i = 0; i < Math.min(deleteCount, 3); i++) {
|
|
66
|
+
deleteBtns.push(deleteLocator.nth(i));
|
|
67
|
+
}
|
|
68
|
+
const createBtn = createCount > 0 ? createLocator.first() : null;
|
|
69
|
+
return { editBtns, deleteBtns, createBtn };
|
|
70
|
+
}
|
|
71
|
+
async function detectEditFlowType(page, editButton) {
|
|
72
|
+
try {
|
|
73
|
+
const urlBefore = page.url();
|
|
74
|
+
await editButton.click({ timeout: 2e3 });
|
|
75
|
+
await page.waitForTimeout(600);
|
|
76
|
+
const urlAfter = page.url();
|
|
77
|
+
if (urlAfter !== urlBefore) {
|
|
78
|
+
await page.goBack().catch(() => void 0);
|
|
79
|
+
await page.waitForTimeout(300);
|
|
80
|
+
return "navigation";
|
|
81
|
+
}
|
|
82
|
+
const dialogVisible = await page.locator('[role="dialog"]').first().isVisible().catch(() => false);
|
|
83
|
+
if (dialogVisible) {
|
|
84
|
+
await page.keyboard.press("Escape");
|
|
85
|
+
await page.waitForTimeout(300);
|
|
86
|
+
return "modal";
|
|
87
|
+
}
|
|
88
|
+
const inlineInputCount = await page.locator("input:visible, textarea:visible").count().catch(() => 0);
|
|
89
|
+
if (inlineInputCount > 0) {
|
|
90
|
+
await page.keyboard.press("Escape");
|
|
91
|
+
await page.waitForTimeout(300);
|
|
92
|
+
return "inline";
|
|
93
|
+
}
|
|
94
|
+
await page.keyboard.press("Escape");
|
|
95
|
+
await page.waitForTimeout(300);
|
|
96
|
+
return "unknown";
|
|
97
|
+
} catch {
|
|
98
|
+
try {
|
|
99
|
+
await page.keyboard.press("Escape");
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
return "unknown";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function captureEditModal(page) {
|
|
106
|
+
try {
|
|
107
|
+
const dialog = page.locator('[role="dialog"]').first();
|
|
108
|
+
const fields = dialog.locator("input, textarea, select");
|
|
109
|
+
const count = Math.min(await fields.count().catch(() => 0), 20);
|
|
110
|
+
const results = [];
|
|
111
|
+
for (let i = 0; i < count; i++) {
|
|
112
|
+
try {
|
|
113
|
+
const field = fields.nth(i);
|
|
114
|
+
const tagName = await field.evaluate((el) => el.tagName.toLowerCase()).catch(() => "input");
|
|
115
|
+
const inputType = await field.getAttribute("type").catch(() => null) ?? tagName;
|
|
116
|
+
const name = await field.getAttribute("name").catch(() => null) ?? await field.getAttribute("id").catch(() => null) ?? await field.getAttribute("aria-label").catch(() => null) ?? `field_${i}`;
|
|
117
|
+
const prefilledValue = await field.inputValue().catch(() => void 0);
|
|
118
|
+
results.push({
|
|
119
|
+
name,
|
|
120
|
+
type: inputType,
|
|
121
|
+
prefilledValue: prefilledValue || void 0
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function captureDeleteConfirmation(page, deleteBtn) {
|
|
132
|
+
try {
|
|
133
|
+
await deleteBtn.click({ timeout: 2e3 });
|
|
134
|
+
const confirmDialog = page.locator('[role="alertdialog"], [role="dialog"]').first();
|
|
135
|
+
try {
|
|
136
|
+
await confirmDialog.waitFor({ state: "visible", timeout: 2e3 });
|
|
137
|
+
} catch {
|
|
138
|
+
await page.keyboard.press("Escape");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const dialogText = await confirmDialog.evaluate(
|
|
142
|
+
(el) => el.innerText?.replace(/\s+/g, " ").trim()
|
|
143
|
+
).catch(() => "");
|
|
144
|
+
const isDeleteDialog = /delete|remove|are you sure|confirm/i.test(dialogText);
|
|
145
|
+
if (!isDeleteDialog) {
|
|
146
|
+
await page.keyboard.press("Escape");
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const buttons = confirmDialog.locator("button");
|
|
150
|
+
const btnCount = await buttons.count().catch(() => 0);
|
|
151
|
+
const btnLabels = [];
|
|
152
|
+
for (let i = 0; i < btnCount; i++) {
|
|
153
|
+
try {
|
|
154
|
+
const label = await buttons.nth(i).innerText();
|
|
155
|
+
btnLabels.push(label.trim());
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const confirmLabel = btnLabels.find((l) => /delete|remove|yes|confirm/i.test(l)) ?? btnLabels[0] ?? "Confirm";
|
|
160
|
+
const cancelLabel = btnLabels.find((l) => /cancel|no|keep|dismiss/i.test(l)) ?? btnLabels[btnLabels.length - 1] ?? "Cancel";
|
|
161
|
+
await page.keyboard.press("Escape");
|
|
162
|
+
await page.waitForTimeout(300);
|
|
163
|
+
return {
|
|
164
|
+
message: dialogText,
|
|
165
|
+
confirmLabel,
|
|
166
|
+
cancelLabel
|
|
167
|
+
};
|
|
168
|
+
} catch {
|
|
169
|
+
try {
|
|
170
|
+
await page.keyboard.press("Escape");
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function detectRowCount(page) {
|
|
177
|
+
try {
|
|
178
|
+
const tableRows = await page.locator("table tbody tr").count().catch(() => 0);
|
|
179
|
+
if (tableRows > 0) return tableRows;
|
|
180
|
+
const listItems = await page.locator('[role="row"]').count().catch(() => 0);
|
|
181
|
+
if (listItems > 0) return listItems;
|
|
182
|
+
const dataItems = await page.locator('[data-testid*="row"], [class*="row" i], [class*="item" i]').count().catch(() => 0);
|
|
183
|
+
return dataItems;
|
|
184
|
+
} catch {
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function getCreateTriggerLabel(createBtn) {
|
|
189
|
+
if (!createBtn) return void 0;
|
|
190
|
+
try {
|
|
191
|
+
const label = await createBtn.innerText();
|
|
192
|
+
return label.trim() || void 0;
|
|
193
|
+
} catch {
|
|
194
|
+
return void 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function probeCrudFlows(page, entityName = "Entity") {
|
|
198
|
+
const toastsObserved = [];
|
|
199
|
+
injectToastObserver(page);
|
|
200
|
+
const { editBtns, deleteBtns, createBtn } = await findCrudButtons(page);
|
|
201
|
+
const rowCount = await detectRowCount(page);
|
|
202
|
+
const createTrigger = await getCreateTriggerLabel(createBtn);
|
|
203
|
+
let editFlowType = "unknown";
|
|
204
|
+
let editFields = [];
|
|
205
|
+
if (editBtns.length > 0) {
|
|
206
|
+
try {
|
|
207
|
+
editFlowType = await detectEditFlowType(page, editBtns[0]);
|
|
208
|
+
if (editFlowType === "modal") {
|
|
209
|
+
try {
|
|
210
|
+
await editBtns[0].click({ timeout: 2e3 });
|
|
211
|
+
await page.waitForTimeout(400);
|
|
212
|
+
const dialogVisible = await page.locator('[role="dialog"]').first().isVisible().catch(() => false);
|
|
213
|
+
if (dialogVisible) {
|
|
214
|
+
editFields = await captureEditModal(page);
|
|
215
|
+
const editToasts = await collectCapturedToasts(page);
|
|
216
|
+
for (const t of editToasts) {
|
|
217
|
+
toastsObserved.push({ action: "edit_open", ...t });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
await page.keyboard.press("Escape");
|
|
221
|
+
await page.waitForTimeout(300);
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let deleteConfirmation;
|
|
229
|
+
if (deleteBtns.length > 0) {
|
|
230
|
+
try {
|
|
231
|
+
const result = await captureDeleteConfirmation(page, deleteBtns[0]);
|
|
232
|
+
if (result) {
|
|
233
|
+
deleteConfirmation = result;
|
|
234
|
+
const deleteToasts = await collectCapturedToasts(page);
|
|
235
|
+
for (const t of deleteToasts) {
|
|
236
|
+
toastsObserved.push({ action: "delete_cancel", ...t });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const remainingToasts = await collectCapturedToasts(page);
|
|
243
|
+
for (const t of remainingToasts) {
|
|
244
|
+
toastsObserved.push({ action: "ambient", ...t });
|
|
245
|
+
}
|
|
246
|
+
const flow = {
|
|
247
|
+
entityName,
|
|
248
|
+
createTrigger,
|
|
249
|
+
editFlowType,
|
|
250
|
+
editFields,
|
|
251
|
+
deleteConfirmation,
|
|
252
|
+
rowCount,
|
|
253
|
+
hasDeleteButton: deleteBtns.length > 0,
|
|
254
|
+
hasEditButton: editBtns.length > 0
|
|
255
|
+
};
|
|
256
|
+
return {
|
|
257
|
+
flows: [flow],
|
|
258
|
+
toastsObserved
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
export {
|
|
262
|
+
probeCrudFlows
|
|
263
|
+
};
|
|
264
|
+
//# sourceMappingURL=crud-interaction.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/crud-interaction.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Page, Locator } from 'playwright';\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type EditFlowType = 'inline' | 'modal' | 'navigation' | 'unknown';\n\nexport interface CrudFlowCapture {\n entityName: string;\n createTrigger?: string;\n editFlowType: EditFlowType;\n editFields: Array<{ name: string; type: string; prefilledValue?: string }>;\n deleteConfirmation?: { message: string; confirmLabel: string; cancelLabel: string };\n createSuccessToast?: string;\n editSuccessToast?: string;\n rowCount: number;\n hasDeleteButton: boolean;\n hasEditButton: boolean;\n}\n\nexport interface CrudProbeResult {\n flows: CrudFlowCapture[];\n toastsObserved: Array<{ action: string; text: string; role: string }>;\n}\n\n// \u2500\u2500 Internal: Toast observer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction injectToastObserver(page: Page): void {\n page.evaluate(() => {\n if ((window as any).__toastObserver) return;\n\n (window as any).__capturedToasts = [] as Array<{ text: string; role: string }>;\n\n const TOAST_CLASSES = ['toast', 'snackbar', 'notification'];\n\n function matchesToast(el: Element): boolean {\n const role = el.getAttribute('role') ?? '';\n if (role === 'alert' || role === 'status') return true;\n const cls = (el.getAttribute('class') ?? '').toLowerCase();\n return TOAST_CLASSES.some((t) => cls.includes(t));\n }\n\n function captureNode(node: Node): void {\n if (!(node instanceof Element)) return;\n if (matchesToast(node)) {\n const text = ((node as HTMLElement).innerText ?? '').replace(/\\s+/g, ' ').trim();\n const role = node.getAttribute('role') ?? 'alert';\n if (text) (window as any).__capturedToasts.push({ text, role });\n }\n node.querySelectorAll('[role=\"alert\"],[role=\"status\"],[class*=\"toast\"],[class*=\"Toast\"],[class*=\"snackbar\"],[class*=\"notification\"]').forEach((child) => {\n const text = ((child as HTMLElement).innerText ?? '').replace(/\\s+/g, ' ').trim();\n const role = child.getAttribute('role') ?? 'alert';\n if (text) (window as any).__capturedToasts.push({ text, role });\n });\n }\n\n const observer = new MutationObserver((mutations) => {\n for (const m of mutations) {\n m.addedNodes.forEach(captureNode);\n }\n });\n\n observer.observe(document.body, { childList: true, subtree: true });\n (window as any).__toastObserver = observer;\n }).catch(() => undefined);\n}\n\nasync function collectCapturedToasts(page: Page): Promise<Array<{ text: string; role: string }>> {\n try {\n return await page.evaluate(() => {\n const toasts: Array<{ text: string; role: string }> = (window as any).__capturedToasts ?? [];\n (window as any).__capturedToasts = [];\n return toasts;\n });\n } catch {\n return [];\n }\n}\n\n// \u2500\u2500 Internal: Button discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface CrudButtons {\n editBtns: Locator[];\n deleteBtns: Locator[];\n createBtn: Locator | null;\n}\n\nasync function findCrudButtons(page: Page): Promise<CrudButtons> {\n const editLocator = page.locator(\n 'button[aria-label*=\"edit\" i], button[aria-label*=\"Edit\"], button:has-text(\"Edit\")'\n );\n const deleteLocator = page.locator(\n 'button[aria-label*=\"delete\" i], button[aria-label*=\"Delete\"], button:has-text(\"Delete\")'\n );\n const createLocator = page.locator(\n 'button:has-text(\"New\"), button:has-text(\"Create\"), button:has-text(\"Add\")'\n );\n\n const [editCount, deleteCount, createCount] = await Promise.all([\n editLocator.count().catch(() => 0),\n deleteLocator.count().catch(() => 0),\n createLocator.count().catch(() => 0),\n ]);\n\n const editBtns: Locator[] = [];\n for (let i = 0; i < Math.min(editCount, 3); i++) {\n editBtns.push(editLocator.nth(i));\n }\n\n const deleteBtns: Locator[] = [];\n for (let i = 0; i < Math.min(deleteCount, 3); i++) {\n deleteBtns.push(deleteLocator.nth(i));\n }\n\n const createBtn = createCount > 0 ? createLocator.first() : null;\n\n return { editBtns, deleteBtns, createBtn };\n}\n\n// \u2500\u2500 Internal: Edit flow detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function detectEditFlowType(page: Page, editButton: Locator): Promise<EditFlowType> {\n try {\n const urlBefore = page.url();\n await editButton.click({ timeout: 2000 });\n await page.waitForTimeout(600);\n\n const urlAfter = page.url();\n if (urlAfter !== urlBefore) {\n await page.goBack().catch(() => undefined);\n await page.waitForTimeout(300);\n return 'navigation';\n }\n\n const dialogVisible = await page.locator('[role=\"dialog\"]').first().isVisible().catch(() => false);\n if (dialogVisible) {\n await page.keyboard.press('Escape');\n await page.waitForTimeout(300);\n return 'modal';\n }\n\n const inlineInputCount = await page.locator('input:visible, textarea:visible').count().catch(() => 0);\n if (inlineInputCount > 0) {\n await page.keyboard.press('Escape');\n await page.waitForTimeout(300);\n return 'inline';\n }\n\n await page.keyboard.press('Escape');\n await page.waitForTimeout(300);\n return 'unknown';\n } catch {\n try {\n await page.keyboard.press('Escape');\n } catch { /* ignore */ }\n return 'unknown';\n }\n}\n\n// \u2500\u2500 Internal: Modal field capture \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function captureEditModal(\n page: Page\n): Promise<Array<{ name: string; type: string; prefilledValue?: string }>> {\n try {\n const dialog = page.locator('[role=\"dialog\"]').first();\n const fields = dialog.locator('input, textarea, select');\n const count = Math.min(await fields.count().catch(() => 0), 20);\n\n const results: Array<{ name: string; type: string; prefilledValue?: string }> = [];\n\n for (let i = 0; i < count; i++) {\n try {\n const field = fields.nth(i);\n const tagName = await field.evaluate((el) => el.tagName.toLowerCase()).catch(() => 'input');\n const inputType = await field.getAttribute('type').catch(() => null) ?? tagName;\n const name =\n (await field.getAttribute('name').catch(() => null)) ??\n (await field.getAttribute('id').catch(() => null)) ??\n (await field.getAttribute('aria-label').catch(() => null)) ??\n `field_${i}`;\n const prefilledValue = await field.inputValue().catch(() => undefined);\n\n results.push({\n name,\n type: inputType,\n prefilledValue: prefilledValue || undefined,\n });\n } catch { /* skip individual field errors */ }\n }\n\n return results;\n } catch {\n return [];\n }\n}\n\n// \u2500\u2500 Internal: Delete confirmation capture \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function captureDeleteConfirmation(\n page: Page,\n deleteBtn: Locator\n): Promise<{ message: string; confirmLabel: string; cancelLabel: string } | null> {\n try {\n await deleteBtn.click({ timeout: 2000 });\n\n const confirmDialog = page.locator('[role=\"alertdialog\"], [role=\"dialog\"]').first();\n try {\n await confirmDialog.waitFor({ state: 'visible', timeout: 2000 });\n } catch {\n await page.keyboard.press('Escape');\n return null;\n }\n\n const dialogText = await confirmDialog.evaluate((el) =>\n (el as HTMLElement).innerText?.replace(/\\s+/g, ' ').trim()\n ).catch(() => '');\n\n const isDeleteDialog =\n /delete|remove|are you sure|confirm/i.test(dialogText);\n\n if (!isDeleteDialog) {\n await page.keyboard.press('Escape');\n return null;\n }\n\n // Capture button labels inside the dialog\n const buttons = confirmDialog.locator('button');\n const btnCount = await buttons.count().catch(() => 0);\n const btnLabels: string[] = [];\n for (let i = 0; i < btnCount; i++) {\n try {\n const label = await buttons.nth(i).innerText();\n btnLabels.push(label.trim());\n } catch { /* skip */ }\n }\n\n // Heuristic: confirm = first destructive-looking label, cancel = cancel/no label\n const confirmLabel =\n btnLabels.find((l) => /delete|remove|yes|confirm/i.test(l)) ??\n btnLabels[0] ??\n 'Confirm';\n const cancelLabel =\n btnLabels.find((l) => /cancel|no|keep|dismiss/i.test(l)) ??\n btnLabels[btnLabels.length - 1] ??\n 'Cancel';\n\n // CRITICAL: Always cancel \u2014 never actually delete data\n await page.keyboard.press('Escape');\n await page.waitForTimeout(300);\n\n return {\n message: dialogText,\n confirmLabel,\n cancelLabel,\n };\n } catch {\n try {\n await page.keyboard.press('Escape');\n } catch { /* ignore */ }\n return null;\n }\n}\n\n// \u2500\u2500 Internal: Row count \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function detectRowCount(page: Page): Promise<number> {\n try {\n // Try common table row patterns\n const tableRows = await page.locator('table tbody tr').count().catch(() => 0);\n if (tableRows > 0) return tableRows;\n\n const listItems = await page.locator('[role=\"row\"]').count().catch(() => 0);\n if (listItems > 0) return listItems;\n\n const dataItems = await page\n .locator('[data-testid*=\"row\"], [class*=\"row\" i], [class*=\"item\" i]')\n .count()\n .catch(() => 0);\n return dataItems;\n } catch {\n return 0;\n }\n}\n\n// \u2500\u2500 Internal: Create trigger label \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function getCreateTriggerLabel(createBtn: Locator | null): Promise<string | undefined> {\n if (!createBtn) return undefined;\n try {\n const label = await createBtn.innerText();\n return label.trim() || undefined;\n } catch {\n return undefined;\n }\n}\n\n// \u2500\u2500 Public: probeCrudFlows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport async function probeCrudFlows(\n page: Page,\n entityName = 'Entity'\n): Promise<CrudProbeResult> {\n const toastsObserved: Array<{ action: string; text: string; role: string }> = [];\n\n // Step 1: Inject toast observer\n injectToastObserver(page);\n\n // Step 2: Find CRUD buttons\n const { editBtns, deleteBtns, createBtn } = await findCrudButtons(page);\n\n // Step 3: Row count\n const rowCount = await detectRowCount(page);\n\n // Step 4: Create trigger label\n const createTrigger = await getCreateTriggerLabel(createBtn);\n\n // Step 5: Probe edit flow (use first edit button)\n let editFlowType: EditFlowType = 'unknown';\n let editFields: Array<{ name: string; type: string; prefilledValue?: string }> = [];\n\n if (editBtns.length > 0) {\n try {\n editFlowType = await detectEditFlowType(page, editBtns[0]);\n\n if (editFlowType === 'modal') {\n // Re-open to capture fields\n try {\n await editBtns[0].click({ timeout: 2000 });\n await page.waitForTimeout(400);\n const dialogVisible = await page\n .locator('[role=\"dialog\"]')\n .first()\n .isVisible()\n .catch(() => false);\n if (dialogVisible) {\n editFields = await captureEditModal(page);\n const editToasts = await collectCapturedToasts(page);\n for (const t of editToasts) {\n toastsObserved.push({ action: 'edit_open', ...t });\n }\n }\n await page.keyboard.press('Escape');\n await page.waitForTimeout(300);\n } catch { /* non-fatal */ }\n }\n } catch { /* non-fatal */ }\n }\n\n // Step 6: Probe delete confirmation (NEVER actually delete)\n let deleteConfirmation: CrudFlowCapture['deleteConfirmation'];\n\n if (deleteBtns.length > 0) {\n try {\n const result = await captureDeleteConfirmation(page, deleteBtns[0]);\n if (result) {\n deleteConfirmation = result;\n const deleteToasts = await collectCapturedToasts(page);\n for (const t of deleteToasts) {\n toastsObserved.push({ action: 'delete_cancel', ...t });\n }\n }\n } catch { /* non-fatal */ }\n }\n\n // Step 7: Collect any remaining toasts\n const remainingToasts = await collectCapturedToasts(page);\n for (const t of remainingToasts) {\n toastsObserved.push({ action: 'ambient', ...t });\n }\n\n const flow: CrudFlowCapture = {\n entityName,\n createTrigger,\n editFlowType,\n editFields,\n deleteConfirmation,\n rowCount,\n hasDeleteButton: deleteBtns.length > 0,\n hasEditButton: editBtns.length > 0,\n };\n\n return {\n flows: [flow],\n toastsObserved,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom JS injector — run user-provided JavaScript in the context of each crawled page.
|
|
3
|
+
*
|
|
4
|
+
* Use cases:
|
|
5
|
+
* - Extract custom data points not captured by standard extractors
|
|
6
|
+
* - Set localStorage/sessionStorage values before crawl
|
|
7
|
+
* - Override fetch() to intercept API calls
|
|
8
|
+
* - Simulate user events (keydown, focus, etc.)
|
|
9
|
+
* - Bypass specific anti-bot checks
|
|
10
|
+
*
|
|
11
|
+
* Security: scripts run in the page context (sandboxed by Chromium's renderer).
|
|
12
|
+
* They cannot access the Node.js process or file system.
|
|
13
|
+
* ZeTa validates scripts are strings before injection.
|
|
14
|
+
*/
|
|
15
|
+
import type { Page } from 'playwright';
|
|
16
|
+
export interface JsInjectionOpts {
|
|
17
|
+
/** Script to run on every page BEFORE navigation (via page.addInitScript) */
|
|
18
|
+
initScript?: string;
|
|
19
|
+
/** Script to run on every page AFTER navigation completes (via page.evaluate) */
|
|
20
|
+
postLoadScript?: string;
|
|
21
|
+
/** Key-value pairs to set in localStorage before page load */
|
|
22
|
+
localStorage?: Record<string, string>;
|
|
23
|
+
/** Key-value pairs to set in sessionStorage before page load */
|
|
24
|
+
sessionStorage?: Record<string, string>;
|
|
25
|
+
/** Cookie key-value pairs to add before page load */
|
|
26
|
+
cookies?: Array<{
|
|
27
|
+
name: string;
|
|
28
|
+
value: string;
|
|
29
|
+
domain?: string;
|
|
30
|
+
path?: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export interface JsInjectionResult {
|
|
34
|
+
initScriptApplied: boolean;
|
|
35
|
+
postLoadResult: unknown;
|
|
36
|
+
storageSet: boolean;
|
|
37
|
+
cookiesSet: number;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function injectCustomJs(page: Page, opts: JsInjectionOpts): Promise<JsInjectionResult>;
|
|
41
|
+
/** Safe string validator — ensures script is a string, not an object/function reference */
|
|
42
|
+
export declare function validateScript(script: unknown): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
async function injectCustomJs(page, opts) {
|
|
2
|
+
const result = {
|
|
3
|
+
initScriptApplied: false,
|
|
4
|
+
postLoadResult: null,
|
|
5
|
+
storageSet: false,
|
|
6
|
+
cookiesSet: 0
|
|
7
|
+
};
|
|
8
|
+
if (opts.cookies?.length) {
|
|
9
|
+
try {
|
|
10
|
+
await page.context().addCookies(
|
|
11
|
+
opts.cookies.map((c) => ({ name: c.name, value: c.value, domain: c.domain ?? "", path: c.path ?? "/", url: page.url() || void 0 }))
|
|
12
|
+
);
|
|
13
|
+
result.cookiesSet = opts.cookies.length;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
result.error = `Cookie injection failed: ${e.message}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (opts.initScript || opts.localStorage || opts.sessionStorage) {
|
|
19
|
+
const storageSetup = [
|
|
20
|
+
opts.localStorage ? `Object.entries(${JSON.stringify(opts.localStorage)}).forEach(([k,v])=>localStorage.setItem(k,v));` : "",
|
|
21
|
+
opts.sessionStorage ? `Object.entries(${JSON.stringify(opts.sessionStorage)}).forEach(([k,v])=>sessionStorage.setItem(k,v));` : ""
|
|
22
|
+
].filter(Boolean).join("\n");
|
|
23
|
+
const combinedInit = [storageSetup, opts.initScript ?? ""].filter(Boolean).join("\n");
|
|
24
|
+
try {
|
|
25
|
+
await page.addInitScript(combinedInit);
|
|
26
|
+
result.initScriptApplied = true;
|
|
27
|
+
result.storageSet = !!(opts.localStorage || opts.sessionStorage);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
result.error = `Init script failed: ${e.message}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (opts.postLoadScript) {
|
|
33
|
+
try {
|
|
34
|
+
result.postLoadResult = await page.evaluate(opts.postLoadScript);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
result.error = `Post-load script failed: ${e.message}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function validateScript(script) {
|
|
42
|
+
if (typeof script !== "string") throw new Error("Script must be a string");
|
|
43
|
+
if (script.length > 5e4) throw new Error("Script too large (>50KB)");
|
|
44
|
+
return script;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
injectCustomJs,
|
|
48
|
+
validateScript
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=custom-js-injector.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/custom-js-injector.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Custom JS injector \u2014 run user-provided JavaScript in the context of each crawled page.\n *\n * Use cases:\n * - Extract custom data points not captured by standard extractors\n * - Set localStorage/sessionStorage values before crawl\n * - Override fetch() to intercept API calls\n * - Simulate user events (keydown, focus, etc.)\n * - Bypass specific anti-bot checks\n *\n * Security: scripts run in the page context (sandboxed by Chromium's renderer).\n * They cannot access the Node.js process or file system.\n * ZeTa validates scripts are strings before injection.\n */\n\nimport type { Page } from 'playwright';\n\nexport interface JsInjectionOpts {\n /** Script to run on every page BEFORE navigation (via page.addInitScript) */\n initScript?: string;\n /** Script to run on every page AFTER navigation completes (via page.evaluate) */\n postLoadScript?: string;\n /** Key-value pairs to set in localStorage before page load */\n localStorage?: Record<string, string>;\n /** Key-value pairs to set in sessionStorage before page load */\n sessionStorage?: Record<string, string>;\n /** Cookie key-value pairs to add before page load */\n cookies?: Array<{ name: string; value: string; domain?: string; path?: string }>;\n}\n\nexport interface JsInjectionResult {\n initScriptApplied: boolean;\n postLoadResult: unknown;\n storageSet: boolean;\n cookiesSet: number;\n error?: string;\n}\n\nexport async function injectCustomJs(page: Page, opts: JsInjectionOpts): Promise<JsInjectionResult> {\n const result: JsInjectionResult = {\n initScriptApplied: false,\n postLoadResult: null,\n storageSet: false,\n cookiesSet: 0,\n };\n\n // Set cookies via Playwright context\n if (opts.cookies?.length) {\n try {\n await page.context().addCookies(\n opts.cookies.map(c => ({ name: c.name, value: c.value, domain: c.domain ?? '', path: c.path ?? '/', url: page.url() || undefined }))\n );\n result.cookiesSet = opts.cookies.length;\n } catch (e: any) {\n result.error = `Cookie injection failed: ${e.message}`;\n }\n }\n\n // Init script (runs before page load on every navigation)\n if (opts.initScript || opts.localStorage || opts.sessionStorage) {\n const storageSetup = [\n opts.localStorage ? `Object.entries(${JSON.stringify(opts.localStorage)}).forEach(([k,v])=>localStorage.setItem(k,v));` : '',\n opts.sessionStorage ? `Object.entries(${JSON.stringify(opts.sessionStorage)}).forEach(([k,v])=>sessionStorage.setItem(k,v));` : '',\n ].filter(Boolean).join('\\n');\n\n const combinedInit = [storageSetup, opts.initScript ?? ''].filter(Boolean).join('\\n');\n try {\n await page.addInitScript(combinedInit);\n result.initScriptApplied = true;\n result.storageSet = !!(opts.localStorage || opts.sessionStorage);\n } catch (e: any) {\n result.error = `Init script failed: ${e.message}`;\n }\n }\n\n // Post-load script (runs after navigation)\n if (opts.postLoadScript) {\n try {\n result.postLoadResult = await page.evaluate(opts.postLoadScript);\n } catch (e: any) {\n result.error = `Post-load script failed: ${e.message}`;\n }\n }\n\n return result;\n}\n\n/** Safe string validator \u2014 ensures script is a string, not an object/function reference */\nexport function validateScript(script: unknown): string {\n if (typeof script !== 'string') throw new Error('Script must be a string');\n if (script.length > 50_000) throw new Error('Script too large (>50KB)');\n return script;\n}\n"],
|
|
5
|
+
"mappings": "AAsCA,eAAsB,eAAe,MAAY,MAAmD;AAClG,QAAM,SAA4B;AAAA,IAChC,mBAAmB;AAAA,IACnB,gBAAgB;AAAA,IAChB,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAGA,MAAI,KAAK,SAAS,QAAQ;AACxB,QAAI;AACF,YAAM,KAAK,QAAQ,EAAE;AAAA,QACnB,KAAK,QAAQ,IAAI,QAAM,EAAE,MAAM,EAAE,MAAM,OAAO,EAAE,OAAO,QAAQ,EAAE,UAAU,IAAI,MAAM,EAAE,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,OAAU,EAAE;AAAA,MACrI;AACA,aAAO,aAAa,KAAK,QAAQ;AAAA,IACnC,SAAS,GAAQ;AACf,aAAO,QAAQ,4BAA4B,EAAE,OAAO;AAAA,IACtD;AAAA,EACF;AAGA,MAAI,KAAK,cAAc,KAAK,gBAAgB,KAAK,gBAAgB;AAC/D,UAAM,eAAe;AAAA,MACnB,KAAK,eAAe,kBAAkB,KAAK,UAAU,KAAK,YAAY,CAAC,mDAAmD;AAAA,MAC1H,KAAK,iBAAiB,kBAAkB,KAAK,UAAU,KAAK,cAAc,CAAC,qDAAqD;AAAA,IAClI,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAE3B,UAAM,eAAe,CAAC,cAAc,KAAK,cAAc,EAAE,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AACpF,QAAI;AACF,YAAM,KAAK,cAAc,YAAY;AACrC,aAAO,oBAAoB;AAC3B,aAAO,aAAa,CAAC,EAAE,KAAK,gBAAgB,KAAK;AAAA,IACnD,SAAS,GAAQ;AACf,aAAO,QAAQ,uBAAuB,EAAE,OAAO;AAAA,IACjD;AAAA,EACF;AAGA,MAAI,KAAK,gBAAgB;AACvB,QAAI;AACF,aAAO,iBAAiB,MAAM,KAAK,SAAS,KAAK,cAAc;AAAA,IACjE,SAAS,GAAQ;AACf,aAAO,QAAQ,4BAA4B,EAAE,OAAO;AAAA,IACtD;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,eAAe,QAAyB;AACtD,MAAI,OAAO,WAAW,SAAU,OAAM,IAAI,MAAM,yBAAyB;AACzE,MAAI,OAAO,SAAS,IAAQ,OAAM,IAAI,MAAM,0BAA0B;AACtE,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dark mode capture — screenshot pages in both light and dark color schemes.
|
|
3
|
+
*
|
|
4
|
+
* Uses Playwright's page.emulateMedia({ colorScheme: 'dark' }) to force dark mode,
|
|
5
|
+
* then captures a screenshot. Compares with light mode to determine if dark mode
|
|
6
|
+
* is supported (significant visual difference).
|
|
7
|
+
*
|
|
8
|
+
* Also detects prefers-color-scheme media query support in CSS.
|
|
9
|
+
*/
|
|
10
|
+
import type { Page } from 'playwright';
|
|
11
|
+
export interface DarkModeCapture {
|
|
12
|
+
url: string;
|
|
13
|
+
lightScreenshot?: Buffer;
|
|
14
|
+
darkScreenshot?: Buffer;
|
|
15
|
+
hasDarkMode: boolean;
|
|
16
|
+
hasCssDarkMode: boolean;
|
|
17
|
+
backgroundColorLight?: string;
|
|
18
|
+
backgroundColorDark?: string;
|
|
19
|
+
capturedAt: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function captureDarkMode(page: Page, url: string, opts?: {
|
|
22
|
+
takeScreenshots?: boolean;
|
|
23
|
+
}): Promise<DarkModeCapture>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
async function getBackgroundColor(page) {
|
|
2
|
+
try {
|
|
3
|
+
return await page.evaluate(() => {
|
|
4
|
+
const body = document.body;
|
|
5
|
+
return window.getComputedStyle(body).backgroundColor;
|
|
6
|
+
});
|
|
7
|
+
} catch {
|
|
8
|
+
return "unknown";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function colorsAreDifferent(a, b) {
|
|
12
|
+
if (a === b) return false;
|
|
13
|
+
if (a === "unknown" || b === "unknown") return false;
|
|
14
|
+
const parse = (c) => {
|
|
15
|
+
const m = c.match(/\d+/g);
|
|
16
|
+
return m ? m.map(Number) : [0, 0, 0];
|
|
17
|
+
};
|
|
18
|
+
const [ar, ag, ab] = parse(a);
|
|
19
|
+
const [br, bg, bb] = parse(b);
|
|
20
|
+
const lumA = 0.299 * ar + 0.587 * ag + 0.114 * ab;
|
|
21
|
+
const lumB = 0.299 * br + 0.587 * bg + 0.114 * bb;
|
|
22
|
+
return Math.abs(lumA - lumB) > 30;
|
|
23
|
+
}
|
|
24
|
+
async function hasCssDarkModeSupport(page) {
|
|
25
|
+
try {
|
|
26
|
+
return await page.evaluate(() => {
|
|
27
|
+
const sheets = Array.from(document.styleSheets);
|
|
28
|
+
for (const sheet of sheets) {
|
|
29
|
+
try {
|
|
30
|
+
const rules = Array.from(sheet.cssRules ?? []);
|
|
31
|
+
for (const rule of rules) {
|
|
32
|
+
if (rule instanceof CSSMediaRule && rule.conditionText?.includes("prefers-color-scheme")) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function captureDarkMode(page, url, opts = {}) {
|
|
46
|
+
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
47
|
+
const { takeScreenshots = false } = opts;
|
|
48
|
+
await page.emulateMedia({ colorScheme: "light" }).catch(() => {
|
|
49
|
+
});
|
|
50
|
+
const backgroundColorLight = await getBackgroundColor(page);
|
|
51
|
+
const hasCssDark = await hasCssDarkModeSupport(page);
|
|
52
|
+
const lightScreenshot = takeScreenshots ? await page.screenshot({ type: "jpeg", quality: 60 }).catch(() => void 0) : void 0;
|
|
53
|
+
await page.emulateMedia({ colorScheme: "dark" }).catch(() => {
|
|
54
|
+
});
|
|
55
|
+
await page.waitForTimeout(300);
|
|
56
|
+
const backgroundColorDark = await getBackgroundColor(page);
|
|
57
|
+
const darkScreenshot = takeScreenshots ? await page.screenshot({ type: "jpeg", quality: 60 }).catch(() => void 0) : void 0;
|
|
58
|
+
await page.emulateMedia({ colorScheme: "light" }).catch(() => {
|
|
59
|
+
});
|
|
60
|
+
const hasDarkMode = colorsAreDifferent(backgroundColorLight, backgroundColorDark) || hasCssDark;
|
|
61
|
+
return {
|
|
62
|
+
url,
|
|
63
|
+
lightScreenshot,
|
|
64
|
+
darkScreenshot,
|
|
65
|
+
hasDarkMode,
|
|
66
|
+
hasCssDarkMode: hasCssDark,
|
|
67
|
+
backgroundColorLight,
|
|
68
|
+
backgroundColorDark,
|
|
69
|
+
capturedAt
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
captureDarkMode
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=dark-mode-capture.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/dark-mode-capture.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Dark mode capture \u2014 screenshot pages in both light and dark color schemes.\n *\n * Uses Playwright's page.emulateMedia({ colorScheme: 'dark' }) to force dark mode,\n * then captures a screenshot. Compares with light mode to determine if dark mode\n * is supported (significant visual difference).\n *\n * Also detects prefers-color-scheme media query support in CSS.\n */\n\nimport type { Page } from 'playwright';\n\nexport interface DarkModeCapture {\n url: string;\n lightScreenshot?: Buffer;\n darkScreenshot?: Buffer;\n hasDarkMode: boolean;\n hasCssDarkMode: boolean; // @media (prefers-color-scheme: dark) detected in stylesheets\n backgroundColorLight?: string;\n backgroundColorDark?: string;\n capturedAt: string;\n}\n\nasync function getBackgroundColor(page: Page): Promise<string> {\n try {\n return await page.evaluate(() => {\n const body = document.body;\n return window.getComputedStyle(body).backgroundColor;\n });\n } catch {\n return 'unknown';\n }\n}\n\nfunction colorsAreDifferent(a: string, b: string): boolean {\n if (a === b) return false;\n if (a === 'unknown' || b === 'unknown') return false;\n // Parse rgb(r, g, b) and compare luminance\n const parse = (c: string) => {\n const m = c.match(/\\d+/g);\n return m ? m.map(Number) : [0, 0, 0];\n };\n const [ar, ag, ab] = parse(a);\n const [br, bg, bb] = parse(b);\n const lumA = 0.299 * ar + 0.587 * ag + 0.114 * ab;\n const lumB = 0.299 * br + 0.587 * bg + 0.114 * bb;\n return Math.abs(lumA - lumB) > 30; // significant luminance difference\n}\n\nasync function hasCssDarkModeSupport(page: Page): Promise<boolean> {\n try {\n return await page.evaluate(() => {\n const sheets = Array.from(document.styleSheets);\n for (const sheet of sheets) {\n try {\n const rules = Array.from(sheet.cssRules ?? []);\n for (const rule of rules) {\n if (rule instanceof CSSMediaRule && rule.conditionText?.includes('prefers-color-scheme')) {\n return true;\n }\n }\n } catch { /* cross-origin stylesheet \u2014 skip */ }\n }\n return false;\n });\n } catch {\n return false;\n }\n}\n\nexport async function captureDarkMode(\n page: Page,\n url: string,\n opts: { takeScreenshots?: boolean } = {},\n): Promise<DarkModeCapture> {\n const capturedAt = new Date().toISOString();\n const { takeScreenshots = false } = opts;\n\n // Light mode\n await page.emulateMedia({ colorScheme: 'light' }).catch(() => {});\n const backgroundColorLight = await getBackgroundColor(page);\n const hasCssDark = await hasCssDarkModeSupport(page);\n const lightScreenshot = takeScreenshots\n ? await page.screenshot({ type: 'jpeg', quality: 60 }).catch(() => undefined)\n : undefined;\n\n // Dark mode\n await page.emulateMedia({ colorScheme: 'dark' }).catch(() => {});\n await page.waitForTimeout(300); // allow CSS transitions\n const backgroundColorDark = await getBackgroundColor(page);\n const darkScreenshot = takeScreenshots\n ? await page.screenshot({ type: 'jpeg', quality: 60 }).catch(() => undefined)\n : undefined;\n\n // Restore light mode\n await page.emulateMedia({ colorScheme: 'light' }).catch(() => {});\n\n const hasDarkMode = colorsAreDifferent(backgroundColorLight, backgroundColorDark) || hasCssDark;\n\n return {\n url,\n lightScreenshot,\n darkScreenshot,\n hasDarkMode,\n hasCssDarkMode: hasCssDark,\n backgroundColorLight,\n backgroundColorDark,\n capturedAt,\n };\n}\n"],
|
|
5
|
+
"mappings": "AAuBA,eAAe,mBAAmB,MAA6B;AAC7D,MAAI;AACF,WAAO,MAAM,KAAK,SAAS,MAAM;AAC/B,YAAM,OAAO,SAAS;AACtB,aAAO,OAAO,iBAAiB,IAAI,EAAE;AAAA,IACvC,CAAC;AAAA,EACH,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,mBAAmB,GAAW,GAAoB;AACzD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,MAAM,aAAa,MAAM,UAAW,QAAO;AAE/C,QAAM,QAAQ,CAAC,MAAc;AAC3B,UAAM,IAAI,EAAE,MAAM,MAAM;AACxB,WAAO,IAAI,EAAE,IAAI,MAAM,IAAI,CAAC,GAAG,GAAG,CAAC;AAAA,EACrC;AACA,QAAM,CAAC,IAAI,IAAI,EAAE,IAAI,MAAM,CAAC;AAC5B,QAAM,CAAC,IAAI,IAAI,EAAE,IAAI,MAAM,CAAC;AAC5B,QAAM,OAAO,QAAQ,KAAK,QAAQ,KAAK,QAAQ;AAC/C,QAAM,OAAO,QAAQ,KAAK,QAAQ,KAAK,QAAQ;AAC/C,SAAO,KAAK,IAAI,OAAO,IAAI,IAAI;AACjC;AAEA,eAAe,sBAAsB,MAA8B;AACjE,MAAI;AACF,WAAO,MAAM,KAAK,SAAS,MAAM;AAC/B,YAAM,SAAS,MAAM,KAAK,SAAS,WAAW;AAC9C,iBAAW,SAAS,QAAQ;AAC1B,YAAI;AACF,gBAAM,QAAQ,MAAM,KAAK,MAAM,YAAY,CAAC,CAAC;AAC7C,qBAAW,QAAQ,OAAO;AACxB,gBAAI,gBAAgB,gBAAgB,KAAK,eAAe,SAAS,sBAAsB,GAAG;AACxF,qBAAO;AAAA,YACT;AAAA,UACF;AAAA,QACF,QAAQ;AAAA,QAAuC;AAAA,MACjD;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,gBACpB,MACA,KACA,OAAsC,CAAC,GACb;AAC1B,QAAM,cAAa,oBAAI,KAAK,GAAE,YAAY;AAC1C,QAAM,EAAE,kBAAkB,MAAM,IAAI;AAGpC,QAAM,KAAK,aAAa,EAAE,aAAa,QAAQ,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AAChE,QAAM,uBAAuB,MAAM,mBAAmB,IAAI;AAC1D,QAAM,aAAa,MAAM,sBAAsB,IAAI;AACnD,QAAM,kBAAkB,kBACpB,MAAM,KAAK,WAAW,EAAE,MAAM,QAAQ,SAAS,GAAG,CAAC,EAAE,MAAM,MAAM,MAAS,IAC1E;AAGJ,QAAM,KAAK,aAAa,EAAE,aAAa,OAAO,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AAC/D,QAAM,KAAK,eAAe,GAAG;AAC7B,QAAM,sBAAsB,MAAM,mBAAmB,IAAI;AACzD,QAAM,iBAAiB,kBACnB,MAAM,KAAK,WAAW,EAAE,MAAM,QAAQ,SAAS,GAAG,CAAC,EAAE,MAAM,MAAM,MAAS,IAC1E;AAGJ,QAAM,KAAK,aAAa,EAAE,aAAa,QAAQ,CAAC,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AAEhE,QAAM,cAAc,mBAAmB,sBAAsB,mBAAmB,KAAK;AAErF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead link detector — tracks broken links discovered during crawl.
|
|
3
|
+
*
|
|
4
|
+
* A "dead link" is a URL that:
|
|
5
|
+
* - Returns 404, 410, or other 4xx/5xx responses
|
|
6
|
+
* - Times out after maxRetries
|
|
7
|
+
* - Redirects to an error page (detected by title heuristic)
|
|
8
|
+
*
|
|
9
|
+
* Results: list of broken links with source page, status code, and anchor text.
|
|
10
|
+
* Used for: SEO audit export, test report enrichment.
|
|
11
|
+
*/
|
|
12
|
+
export interface DeadLink {
|
|
13
|
+
url: string;
|
|
14
|
+
statusCode: number;
|
|
15
|
+
sourceUrl?: string;
|
|
16
|
+
anchorText?: string;
|
|
17
|
+
checkedAt: string;
|
|
18
|
+
reason: 'http-error' | 'timeout' | 'dns-error';
|
|
19
|
+
}
|
|
20
|
+
export declare class DeadLinkTracker {
|
|
21
|
+
private deadLinks;
|
|
22
|
+
private checked;
|
|
23
|
+
record(url: string, statusCode: number, source?: {
|
|
24
|
+
sourceUrl?: string;
|
|
25
|
+
anchorText?: string;
|
|
26
|
+
}): void;
|
|
27
|
+
recordTimeout(url: string, sourceUrl?: string): void;
|
|
28
|
+
getDeadLinks(): DeadLink[];
|
|
29
|
+
isKnownBroken(url: string): boolean;
|
|
30
|
+
stats(): {
|
|
31
|
+
total: number;
|
|
32
|
+
by404: number;
|
|
33
|
+
by5xx: number;
|
|
34
|
+
byTimeout: number;
|
|
35
|
+
};
|
|
36
|
+
toCsv(): string;
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class DeadLinkTracker {
|
|
2
|
+
deadLinks = [];
|
|
3
|
+
checked = /* @__PURE__ */ new Map();
|
|
4
|
+
// url → statusCode
|
|
5
|
+
record(url, statusCode, source) {
|
|
6
|
+
if (this.checked.has(url)) return;
|
|
7
|
+
this.checked.set(url, statusCode);
|
|
8
|
+
if (statusCode >= 400) {
|
|
9
|
+
this.deadLinks.push({
|
|
10
|
+
url,
|
|
11
|
+
statusCode,
|
|
12
|
+
sourceUrl: source?.sourceUrl,
|
|
13
|
+
anchorText: source?.anchorText,
|
|
14
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15
|
+
reason: statusCode === 0 ? "timeout" : "http-error"
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
recordTimeout(url, sourceUrl) {
|
|
20
|
+
if (this.checked.has(url)) return;
|
|
21
|
+
this.checked.set(url, 0);
|
|
22
|
+
this.deadLinks.push({ url, statusCode: 0, sourceUrl, checkedAt: (/* @__PURE__ */ new Date()).toISOString(), reason: "timeout" });
|
|
23
|
+
}
|
|
24
|
+
getDeadLinks() {
|
|
25
|
+
return [...this.deadLinks];
|
|
26
|
+
}
|
|
27
|
+
isKnownBroken(url) {
|
|
28
|
+
const code = this.checked.get(url);
|
|
29
|
+
return code !== void 0 && code >= 400;
|
|
30
|
+
}
|
|
31
|
+
stats() {
|
|
32
|
+
return {
|
|
33
|
+
total: this.deadLinks.length,
|
|
34
|
+
by404: this.deadLinks.filter((l) => l.statusCode === 404).length,
|
|
35
|
+
by5xx: this.deadLinks.filter((l) => l.statusCode >= 500).length,
|
|
36
|
+
byTimeout: this.deadLinks.filter((l) => l.reason === "timeout").length
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
toCsv() {
|
|
40
|
+
const rows = [["URL", "Status", "Source URL", "Anchor Text", "Checked At"]];
|
|
41
|
+
for (const l of this.deadLinks) {
|
|
42
|
+
rows.push([l.url, String(l.statusCode), l.sourceUrl ?? "", l.anchorText ?? "", l.checkedAt]);
|
|
43
|
+
}
|
|
44
|
+
return rows.map((r) => r.map((c) => `"${c.replace(/"/g, '""')}"`).join(",")).join("\n");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
DeadLinkTracker
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=dead-link-detector.js.map
|