@detiq/crawler 0.1.3
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 +49 -0
- package/README.md +11 -0
- 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/ai-login.d.ts +32 -0
- package/dist/ai-login.js +177 -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 +329 -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/crawler.d.ts +179 -0
- package/dist/crawler.js +3672 -0
- package/dist/crawler.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/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 +54 -0
- package/dist/extractors/advanced.js +245 -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/failure-analysis-agent.d.ts +16 -0
- package/dist/failure-analysis-agent.js +40 -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/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 +32 -0
- package/dist/frontier.js +83 -0
- package/dist/frontier.js.map +7 -0
- package/dist/gap-pass.d.ts +35 -0
- package/dist/gap-pass.js +81 -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 +304 -0
- package/dist/http-engine.js.map +7 -0
- package/dist/incremental-diff.d.ts +59 -0
- package/dist/incremental-diff.js +61 -0
- package/dist/incremental-diff.js.map +7 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +61 -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 +104 -0
- package/dist/interactive-probe.js +442 -0
- package/dist/interactive-probe.js.map +7 -0
- package/dist/job-queue.d.ts +171 -0
- package/dist/job-queue.js +2256 -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 +100 -0
- package/dist/live-view.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/native-crawler.d.ts +69 -0
- package/dist/native-crawler.js +839 -0
- package/dist/native-crawler.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.d.ts +53 -0
- package/dist/performance-metrics.js +83 -0
- package/dist/performance-metrics.js.map +7 -0
- package/dist/phash.d.ts +38 -0
- package/dist/phash.js +115 -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 +37 -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 +140 -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 +16 -0
- package/dist/screen-reader.js +46 -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/session-capture.d.ts +35 -0
- package/dist/session-capture.js +267 -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/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 +152 -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 +2 -0
- package/dist/sqs-queue.js +47 -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/stealth/human-behavior.d.ts +20 -0
- package/dist/stealth/human-behavior.js +168 -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 +67 -0
- package/dist/test-executor.js +311 -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 +87 -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 +25 -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 +45 -0
- package/src/accessibility-auditor.ts +102 -0
- package/src/ai-login.ts +218 -0
- package/src/auth-token-rotator.ts +89 -0
- package/src/auth.ts +396 -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/crawler.ts +4305 -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/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 +283 -0
- package/src/extractors/artifact-envelope.ts +111 -0
- package/src/failure-analysis-agent.ts +49 -0
- package/src/feature-flag-detector.ts +97 -0
- package/src/form-detector.ts +103 -0
- package/src/frontier.test.ts +64 -0
- package/src/frontier.ts +112 -0
- package/src/gap-pass.ts +122 -0
- package/src/graphql-detector.ts +129 -0
- package/src/http-engine.ts +433 -0
- package/src/incremental-diff.ts +121 -0
- package/src/index.ts +35 -0
- package/src/input-fuzzer.ts +129 -0
- package/src/interactive-probe.ts +623 -0
- package/src/job-queue.test.ts +120 -0
- package/src/job-queue.ts +2701 -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 +141 -0
- package/src/logger.ts +9 -0
- package/src/markdown-extract.test.ts +73 -0
- package/src/markdown-extract.ts +76 -0
- package/src/native-crawler.ts +1106 -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.ts +134 -0
- package/src/phash.ts +151 -0
- package/src/pii-scrubber.ts +99 -0
- package/src/playwright-recorder.ts +170 -0
- package/src/policies/crawl-safety.ts +46 -0
- package/src/profiles/domain-profile.ts +167 -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 +89 -0
- package/src/screenshot-masker.ts +98 -0
- package/src/seo-checker.ts +134 -0
- package/src/session-capture.ts +325 -0
- package/src/session-export.test.ts +75 -0
- package/src/session-export.ts +172 -0
- package/src/sitemap-writer.ts +65 -0
- package/src/sitemap.test.ts +131 -0
- package/src/sitemap.ts +224 -0
- package/src/skyvern.ts +199 -0
- package/src/sqs-queue.ts +49 -0
- package/src/sso-token.ts +120 -0
- package/src/stealth/human-behavior.ts +191 -0
- package/src/telemetry.ts +122 -0
- package/src/test-executor.test.ts +72 -0
- package/src/test-executor.ts +424 -0
- package/src/tls-fingerprint.ts +111 -0
- package/src/url-pattern-completer.ts +114 -0
- package/src/visual-diff-verdict.test.ts +80 -0
- package/src/visual-diff-verdict.ts +57 -0
- package/src/webhook-notifier.ts +109 -0
- package/src/websocket-detector.ts +83 -0
- package/src/worker.ts +48 -0
- package/tsconfig.json +13 -0
- package/types/accessibility-auditor.d.ts +29 -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/crawler.d.ts +179 -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/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 +54 -0
- package/types/extractors/artifact-envelope.d.ts +63 -0
- package/types/failure-analysis-agent.d.ts +16 -0
- package/types/feature-flag-detector.d.ts +30 -0
- package/types/form-detector.d.ts +34 -0
- package/types/frontier.d.ts +32 -0
- package/types/gap-pass.d.ts +35 -0
- package/types/graphql-detector.d.ts +39 -0
- package/types/http-engine.d.ts +80 -0
- package/types/incremental-diff.d.ts +59 -0
- package/types/index.d.ts +35 -0
- package/types/input-fuzzer.d.ts +30 -0
- package/types/interactive-probe.d.ts +104 -0
- package/types/job-queue.d.ts +171 -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/logger.d.ts +2 -0
- package/types/markdown-extract.d.ts +8 -0
- package/types/native-crawler.d.ts +69 -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.d.ts +53 -0
- package/types/phash.d.ts +38 -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 +16 -0
- package/types/screenshot-masker.d.ts +28 -0
- package/types/seo-checker.d.ts +33 -0
- package/types/session-capture.d.ts +38 -0
- package/types/session-export.d.ts +49 -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 +2 -0
- package/types/sso-token.d.ts +29 -0
- package/types/stealth/human-behavior.d.ts +20 -0
- package/types/telemetry.d.ts +34 -0
- package/types/test-executor.d.ts +67 -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,442 @@
|
|
|
1
|
+
import { AppBrain } from "@qip/app-brain";
|
|
2
|
+
const ERROR_SELECTORS = [
|
|
3
|
+
'[role="alert"]',
|
|
4
|
+
'[aria-live="assertive"]',
|
|
5
|
+
'[aria-live="polite"]',
|
|
6
|
+
".invalid-feedback",
|
|
7
|
+
".field-error",
|
|
8
|
+
".error-message",
|
|
9
|
+
".form-error",
|
|
10
|
+
".validation-error",
|
|
11
|
+
'[class*="error" i]',
|
|
12
|
+
'[class*="invalid" i]',
|
|
13
|
+
'[class*="Error"]',
|
|
14
|
+
"p.text-red-500",
|
|
15
|
+
"p.text-red-600",
|
|
16
|
+
"p.text-destructive",
|
|
17
|
+
"p.text-danger",
|
|
18
|
+
"[data-error]",
|
|
19
|
+
"[data-invalid]",
|
|
20
|
+
".ant-form-item-explain-error",
|
|
21
|
+
// Ant Design
|
|
22
|
+
".el-form-item__error",
|
|
23
|
+
// Element UI
|
|
24
|
+
".chakra-form__error-message",
|
|
25
|
+
// Chakra UI
|
|
26
|
+
".mantine-InputWrapper-error"
|
|
27
|
+
// Mantine
|
|
28
|
+
].join(", ");
|
|
29
|
+
const TOAST_SELECTORS = [
|
|
30
|
+
'[role="status"]',
|
|
31
|
+
"[data-sonner-toast]",
|
|
32
|
+
"[data-toast]",
|
|
33
|
+
"[data-hot-toast]",
|
|
34
|
+
".Toastify__toast",
|
|
35
|
+
".toast-message",
|
|
36
|
+
".notification-body",
|
|
37
|
+
".notistack-snackbar",
|
|
38
|
+
'[class*="toast" i]',
|
|
39
|
+
'[class*="Toast"]',
|
|
40
|
+
'[class*="snackbar" i]',
|
|
41
|
+
'[class*="notification" i]'
|
|
42
|
+
].join(", ");
|
|
43
|
+
async function waitForAny(page, selector, maxMs = 2500) {
|
|
44
|
+
try {
|
|
45
|
+
await page.waitForSelector(selector, { timeout: maxMs, state: "visible" });
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return page.$$eval(
|
|
50
|
+
selector,
|
|
51
|
+
(els) => els.map((el) => el.innerText?.replace(/\s+/g, " ").trim()).filter(Boolean)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
async function collectErrors(page) {
|
|
55
|
+
try {
|
|
56
|
+
return page.$$eval(
|
|
57
|
+
ERROR_SELECTORS,
|
|
58
|
+
(els) => els.filter((el) => {
|
|
59
|
+
const style = window.getComputedStyle(el);
|
|
60
|
+
return style.display !== "none" && style.visibility !== "hidden" && el.innerText?.trim().length > 0;
|
|
61
|
+
}).map((el) => el.innerText?.replace(/\s+/g, " ").trim()).filter(Boolean)
|
|
62
|
+
);
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function collectToasts(page) {
|
|
68
|
+
try {
|
|
69
|
+
return page.$$eval(
|
|
70
|
+
TOAST_SELECTORS,
|
|
71
|
+
(els) => els.filter((el) => {
|
|
72
|
+
const style = window.getComputedStyle(el);
|
|
73
|
+
return style.display !== "none" && el.innerText?.trim().length > 0;
|
|
74
|
+
}).map((el) => el.innerText?.replace(/\s+/g, " ").trim()).filter(Boolean)
|
|
75
|
+
);
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function findSubmitButton(page) {
|
|
81
|
+
return page.evaluate(() => {
|
|
82
|
+
const SUBMIT_KW = /submit|send|save|create|confirm|continue|proceed|register|sign.?in|log.?in|apply|verify|search|next|finish|complete|order|buy|checkout/i;
|
|
83
|
+
const candidates = Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]'));
|
|
84
|
+
const primary = candidates.find((el) => {
|
|
85
|
+
const style = window.getComputedStyle(el);
|
|
86
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
87
|
+
const text = el.innerText || el.value || el.getAttribute("aria-label") || "";
|
|
88
|
+
return SUBMIT_KW.test(text);
|
|
89
|
+
});
|
|
90
|
+
if (!primary) return null;
|
|
91
|
+
return primary.getAttribute("data-testid") || primary.getAttribute("aria-label") || primary.innerText?.trim().slice(0, 60);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function findModalTriggers(page) {
|
|
95
|
+
return page.evaluate(() => {
|
|
96
|
+
const SKIP = /submit|send|save|create|log.?in|sign.?in|register|continue|checkout|buy|order|search|close|cancel|dismiss/i;
|
|
97
|
+
const NAV_PARENTS = /* @__PURE__ */ new Set(["NAV", "HEADER", "FOOTER"]);
|
|
98
|
+
return Array.from(document.querySelectorAll('button, [role="button"]')).filter((el) => {
|
|
99
|
+
const style = window.getComputedStyle(el);
|
|
100
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
101
|
+
const label = el.innerText?.trim() || el.getAttribute("aria-label") || "";
|
|
102
|
+
if (!label || SKIP.test(label)) return false;
|
|
103
|
+
let p = el.parentElement;
|
|
104
|
+
while (p) {
|
|
105
|
+
if (NAV_PARENTS.has(p.tagName)) return false;
|
|
106
|
+
p = p.parentElement;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}).slice(0, 8).map((el) => {
|
|
110
|
+
const label = el.innerText?.trim() || el.getAttribute("aria-label") || "button";
|
|
111
|
+
return { selector: `[aria-label="${label}"]`, label: label.slice(0, 60) };
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function probeConditionals(page) {
|
|
116
|
+
const results = [];
|
|
117
|
+
try {
|
|
118
|
+
const selects = await page.$$("select");
|
|
119
|
+
for (const sel of selects.slice(0, 5)) {
|
|
120
|
+
const label = await sel.getAttribute("aria-label") || await sel.getAttribute("name") || "select";
|
|
121
|
+
const options = await sel.$$eval("option", (opts) => opts.slice(1, 3).map((o) => ({ value: o.value, text: o.textContent?.trim() })));
|
|
122
|
+
for (const opt of options) {
|
|
123
|
+
if (!opt.value) continue;
|
|
124
|
+
const domBefore = await page.evaluate(() => document.body.innerText.length);
|
|
125
|
+
await sel.selectOption(opt.value);
|
|
126
|
+
await page.waitForTimeout(500);
|
|
127
|
+
const domAfter = await page.evaluate(() => document.body.innerText.length);
|
|
128
|
+
if (Math.abs(domAfter - domBefore) > 50) {
|
|
129
|
+
const revealed = await page.evaluate(() => {
|
|
130
|
+
return Array.from(document.querySelectorAll('[data-conditional],[data-show],[class*="conditional"]')).filter((el) => window.getComputedStyle(el).display !== "none").map((el) => el.innerText?.trim()).filter((t) => t && t.length > 10).slice(0, 3).join(" | ");
|
|
131
|
+
});
|
|
132
|
+
if (revealed) {
|
|
133
|
+
results.push({ triggerElement: label, triggerValue: opt.text || opt.value, revealedText: revealed.slice(0, 200) });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
async function probeScreen(page, screenId, url, elements) {
|
|
143
|
+
const result = {
|
|
144
|
+
screenId,
|
|
145
|
+
url,
|
|
146
|
+
validationErrors: [],
|
|
147
|
+
successMessages: [],
|
|
148
|
+
modals: [],
|
|
149
|
+
conditionals: [],
|
|
150
|
+
loadingPatterns: []
|
|
151
|
+
};
|
|
152
|
+
const formInputs = elements.filter(
|
|
153
|
+
(e) => /input|text|email|password|tel|search|textarea|select/i.test(e.role)
|
|
154
|
+
);
|
|
155
|
+
const hasForm = formInputs.length > 0;
|
|
156
|
+
if (hasForm) {
|
|
157
|
+
try {
|
|
158
|
+
const submitLabel = await findSubmitButton(page);
|
|
159
|
+
if (submitLabel) {
|
|
160
|
+
const submitBtn = page.locator(`button:has-text("${submitLabel}")`).first();
|
|
161
|
+
await submitBtn.click({ timeout: 2e3 });
|
|
162
|
+
await page.waitForTimeout(1500);
|
|
163
|
+
const errors = await collectErrors(page);
|
|
164
|
+
for (const errorText of errors) {
|
|
165
|
+
const matchedField = formInputs.find(
|
|
166
|
+
(el) => errorText.toLowerCase().includes(el.meaning.toLowerCase().split(" ")[0]) || el.meaning.toLowerCase().includes(errorText.toLowerCase().split(" ")[0])
|
|
167
|
+
);
|
|
168
|
+
result.validationErrors.push({
|
|
169
|
+
field: matchedField?.meaning ?? "unknown",
|
|
170
|
+
trigger: "empty_submit",
|
|
171
|
+
errorText: errorText.slice(0, 150)
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const loadingText = await page.evaluate(() => {
|
|
175
|
+
const loading = Array.from(document.querySelectorAll('button, [aria-busy="true"], [class*="loading" i], [class*="spinner" i]')).map((el) => el.innerText?.trim()).filter((t) => t && /loading|saving|submitting|sending|processing/i.test(t));
|
|
176
|
+
return loading.slice(0, 3);
|
|
177
|
+
});
|
|
178
|
+
result.loadingPatterns.push(...loadingText);
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (hasForm) {
|
|
184
|
+
try {
|
|
185
|
+
const emailField = formInputs.find((e) => e.expectedData === "valid_email" || /email/i.test(e.meaning));
|
|
186
|
+
const passwordField = formInputs.find((e) => e.expectedData === "password" || /password/i.test(e.meaning));
|
|
187
|
+
if (emailField) {
|
|
188
|
+
const emailLocator = page.locator('input[type="email"], input[name*="email"], input[placeholder*="email" i]').first();
|
|
189
|
+
try {
|
|
190
|
+
await emailLocator.fill("notanemail", { timeout: 1e3 });
|
|
191
|
+
await emailLocator.blur();
|
|
192
|
+
await page.waitForTimeout(600);
|
|
193
|
+
const errors = await collectErrors(page);
|
|
194
|
+
for (const errorText of errors) {
|
|
195
|
+
result.validationErrors.push({ field: emailField.meaning, trigger: "invalid_format", errorText: errorText.slice(0, 150) });
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (passwordField) {
|
|
201
|
+
const pwLocator = page.locator('input[type="password"]').first();
|
|
202
|
+
try {
|
|
203
|
+
await pwLocator.fill("123", { timeout: 1e3 });
|
|
204
|
+
await pwLocator.blur();
|
|
205
|
+
await page.waitForTimeout(600);
|
|
206
|
+
const errors = await collectErrors(page);
|
|
207
|
+
for (const errorText of errors) {
|
|
208
|
+
if (!result.validationErrors.some((e) => e.errorText === errorText)) {
|
|
209
|
+
result.validationErrors.push({ field: passwordField.meaning, trigger: "invalid_format", errorText: errorText.slice(0, 150) });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const triggers = await findModalTriggers(page);
|
|
220
|
+
for (const trigger of triggers) {
|
|
221
|
+
try {
|
|
222
|
+
const btn = page.locator(`button:has-text("${trigger.label}")`).first();
|
|
223
|
+
const domBefore = await page.evaluate(() => document.body.innerText.length);
|
|
224
|
+
await btn.click({ timeout: 1500 });
|
|
225
|
+
await page.waitForTimeout(1e3);
|
|
226
|
+
const dialogText = await page.evaluate(() => {
|
|
227
|
+
const DIALOG_SEL = '[role="dialog"], [role="alertdialog"], .modal, .Modal, [class*="modal" i], [class*="dialog" i], [class*="Dialog"]';
|
|
228
|
+
const dialogs = Array.from(document.querySelectorAll(DIALOG_SEL)).filter((el) => window.getComputedStyle(el).display !== "none");
|
|
229
|
+
if (dialogs.length === 0) return null;
|
|
230
|
+
const d = dialogs[0];
|
|
231
|
+
const title = d.querySelector('[role="heading"], h1, h2, h3')?.textContent?.trim() || "";
|
|
232
|
+
const content = d.innerText?.replace(/\s+/g, " ").trim().slice(0, 400);
|
|
233
|
+
const fields = Array.from(d.querySelectorAll("input, select, textarea, button")).map((f) => f.getAttribute("aria-label") || f.getAttribute("placeholder") || f.value || "").filter((t) => t.trim().length > 0);
|
|
234
|
+
return { title, content, fields };
|
|
235
|
+
});
|
|
236
|
+
if (dialogText && dialogText.content.length > 20) {
|
|
237
|
+
result.modals.push({
|
|
238
|
+
triggerElement: trigger.label,
|
|
239
|
+
title: dialogText.title,
|
|
240
|
+
content: dialogText.content,
|
|
241
|
+
fields: dialogText.fields.slice(0, 10)
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
await page.keyboard.press("Escape");
|
|
245
|
+
await page.waitForTimeout(400);
|
|
246
|
+
const toasts = await collectToasts(page);
|
|
247
|
+
for (const t of toasts) {
|
|
248
|
+
if (t.length > 5) result.successMessages.push({ type: "toast", content: t.slice(0, 200) });
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const conditionals = await probeConditionals(page);
|
|
257
|
+
result.conditionals.push(...conditionals);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
function probeResultToObservations(result) {
|
|
263
|
+
const obs = [];
|
|
264
|
+
for (const ve of result.validationErrors) {
|
|
265
|
+
obs.push({ elementMeaning: ve.field, observationType: "VALIDATION_ERROR", observedText: ve.errorText, triggeredBy: ve.trigger });
|
|
266
|
+
}
|
|
267
|
+
for (const sm of result.successMessages) {
|
|
268
|
+
obs.push({ elementMeaning: "(page)", observationType: "SUCCESS_TOAST", observedText: sm.content, triggeredBy: sm.type });
|
|
269
|
+
}
|
|
270
|
+
for (const m of result.modals) {
|
|
271
|
+
obs.push({ elementMeaning: m.triggerElement, observationType: "MODAL", observedText: [m.title, m.content].filter(Boolean).join(" \u2014 ").slice(0, 500), triggeredBy: `button_click:${m.triggerElement}` });
|
|
272
|
+
}
|
|
273
|
+
for (const c of result.conditionals) {
|
|
274
|
+
obs.push({ elementMeaning: c.triggerElement, observationType: "CONDITIONAL_REVEAL", observedText: c.revealedText, triggeredBy: `select_change:${c.triggerElement}:${c.triggerValue}` });
|
|
275
|
+
}
|
|
276
|
+
for (const lp of result.loadingPatterns) {
|
|
277
|
+
obs.push({ elementMeaning: "(submit button)", observationType: "LOADING_STATE", observedText: lp, triggeredBy: "form_submit" });
|
|
278
|
+
}
|
|
279
|
+
return obs;
|
|
280
|
+
}
|
|
281
|
+
function probeResultToMarkdown(result) {
|
|
282
|
+
const lines = [`## Probe Observations \u2014 ${result.url}
|
|
283
|
+
`];
|
|
284
|
+
if (result.validationErrors.length > 0) {
|
|
285
|
+
lines.push("### Validation Errors (observed)");
|
|
286
|
+
for (const ve of result.validationErrors) {
|
|
287
|
+
lines.push(`- **${ve.field}** (${ve.trigger}): "${ve.errorText}"`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (result.successMessages.length > 0) {
|
|
291
|
+
lines.push("\n### Success / Feedback Messages (observed)");
|
|
292
|
+
for (const sm of result.successMessages) {
|
|
293
|
+
lines.push(`- ${sm.type}: "${sm.content}"`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (result.modals.length > 0) {
|
|
297
|
+
lines.push("\n### Modal / Dialog Contents (observed)");
|
|
298
|
+
for (const m of result.modals) {
|
|
299
|
+
lines.push(`- **Trigger:** ${m.triggerElement}`);
|
|
300
|
+
if (m.title) lines.push(` - Title: ${m.title}`);
|
|
301
|
+
lines.push(` - Content: ${m.content.slice(0, 200)}`);
|
|
302
|
+
if (m.fields.length > 0) lines.push(` - Fields: ${m.fields.join(", ")}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (result.conditionals.length > 0) {
|
|
306
|
+
lines.push("\n### Conditional UI (observed)");
|
|
307
|
+
for (const c of result.conditionals) {
|
|
308
|
+
lines.push(`- **${c.triggerElement}** = "${c.triggerValue}" reveals: ${c.revealedText}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (result.loadingPatterns.length > 0) {
|
|
312
|
+
lines.push("\n### Loading State Text (observed)");
|
|
313
|
+
for (const lp of result.loadingPatterns) lines.push(`- "${lp}"`);
|
|
314
|
+
}
|
|
315
|
+
const full = lines.join("\n");
|
|
316
|
+
const errorSection = result.validationErrors.length > 0 ? `## Real Validation Errors \u2014 ${result.url}
|
|
317
|
+
` + result.validationErrors.map((e) => `- **${e.field}**: "${e.errorText}"`).join("\n") : "";
|
|
318
|
+
const interactionSection = [
|
|
319
|
+
result.successMessages.length > 0 ? `## Success Messages \u2014 ${result.url}
|
|
320
|
+
` + result.successMessages.map((s) => `- ${s.type}: "${s.content}"`).join("\n") : "",
|
|
321
|
+
result.loadingPatterns.length > 0 ? `## Loading Patterns \u2014 ${result.url}
|
|
322
|
+
` + result.loadingPatterns.map((lp) => `- "${lp}"`).join("\n") : "",
|
|
323
|
+
result.modals.length > 0 ? `## Modal Contents \u2014 ${result.url}
|
|
324
|
+
` + result.modals.map((m) => `### ${m.triggerElement}
|
|
325
|
+
${m.content}`).join("\n") : ""
|
|
326
|
+
].filter(Boolean).join("\n\n");
|
|
327
|
+
return {
|
|
328
|
+
errorCatalogueChunk: errorSection,
|
|
329
|
+
interactionPatternsChunk: interactionSection
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async function probeAllScreens(page, tenantId, projectId, discovered, onProgress) {
|
|
333
|
+
const probeable = discovered.filter((d) => d.screenId && d.url && !d.url.includes("#"));
|
|
334
|
+
if (probeable.length === 0) return;
|
|
335
|
+
const errorCatalogueChunks = [];
|
|
336
|
+
const interactionChunks = [];
|
|
337
|
+
for (let i = 0; i < probeable.length; i++) {
|
|
338
|
+
const screen = probeable[i];
|
|
339
|
+
const pct = 88 + Math.round(i / probeable.length * 10);
|
|
340
|
+
await onProgress?.(pct, `Probing screen ${i + 1}/${probeable.length}: ${screen.url}`);
|
|
341
|
+
try {
|
|
342
|
+
if (page.url() !== screen.url) {
|
|
343
|
+
await page.goto(screen.url, { waitUntil: "domcontentloaded" });
|
|
344
|
+
await page.waitForTimeout(1500);
|
|
345
|
+
}
|
|
346
|
+
const result = await probeScreen(page, screen.screenId, screen.url, screen.elements ?? []);
|
|
347
|
+
const observations = probeResultToObservations(result);
|
|
348
|
+
if (observations.length > 0) {
|
|
349
|
+
await AppBrain.saveProbeObservations(tenantId, projectId, screen.screenId, observations).catch(console.warn);
|
|
350
|
+
}
|
|
351
|
+
const md = probeResultToMarkdown(result);
|
|
352
|
+
if (md.errorCatalogueChunk) errorCatalogueChunks.push(md.errorCatalogueChunk);
|
|
353
|
+
if (md.interactionPatternsChunk) interactionChunks.push(md.interactionPatternsChunk);
|
|
354
|
+
const probeMarkdown = [md.errorCatalogueChunk, md.interactionPatternsChunk].filter(Boolean).join("\n\n");
|
|
355
|
+
if (probeMarkdown.length > 50) {
|
|
356
|
+
await AppBrain.saveKnowledgeChunk(tenantId, projectId, "probe", screen.screenId, probeMarkdown, `Probe \u2014 ${screen.url}`).catch(console.warn);
|
|
357
|
+
}
|
|
358
|
+
if (result.validationErrors.length > 0) {
|
|
359
|
+
console.log(`[probe] ${screen.url}: ${result.validationErrors.length} validation errors, ${result.modals.length} modals`);
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.warn(`[probe] \u2717 ${screen.url}:`, err instanceof Error ? err.message : err);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (errorCatalogueChunks.length > 0) {
|
|
366
|
+
const combined = `# Error Catalogue (Real Observed \u2014 Interactive Probe)
|
|
367
|
+
|
|
368
|
+
${errorCatalogueChunks.join("\n\n")}`;
|
|
369
|
+
await AppBrain.upsertSkillFile(tenantId, projectId, "ERROR_CATALOGUE", combined, {
|
|
370
|
+
summary: `Real validation error messages observed via interactive probe across ${errorCatalogueChunks.length} screens`,
|
|
371
|
+
scope: "PROJECT"
|
|
372
|
+
}).catch(console.warn);
|
|
373
|
+
}
|
|
374
|
+
if (interactionChunks.length > 0) {
|
|
375
|
+
const combined = `# Interaction Patterns (Real Observed \u2014 Interactive Probe)
|
|
376
|
+
|
|
377
|
+
${interactionChunks.join("\n\n")}`;
|
|
378
|
+
await AppBrain.upsertSkillFile(tenantId, projectId, "INTERACTION_PATTERNS", combined, {
|
|
379
|
+
summary: `Real loading states, success messages, and modal contents observed via interactive probe`,
|
|
380
|
+
scope: "PROJECT"
|
|
381
|
+
}).catch(console.warn);
|
|
382
|
+
}
|
|
383
|
+
await onProgress?.(98, "Interactive probe complete");
|
|
384
|
+
console.log(`[probe] Probed ${probeable.length} screens. Errors: ${errorCatalogueChunks.length}, Interactions: ${interactionChunks.length}`);
|
|
385
|
+
}
|
|
386
|
+
async function probeKeyboardNav(page) {
|
|
387
|
+
const sequence = [];
|
|
388
|
+
const issues = [];
|
|
389
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
390
|
+
let firstKey = null;
|
|
391
|
+
for (let i = 0; i < 50; i++) {
|
|
392
|
+
await page.keyboard.press("Tab");
|
|
393
|
+
const el = await page.evaluate(() => {
|
|
394
|
+
const active = document.activeElement;
|
|
395
|
+
if (!active || active === document.body) return null;
|
|
396
|
+
const outline = window.getComputedStyle(active).outline;
|
|
397
|
+
return {
|
|
398
|
+
tag: active.tagName.toLowerCase(),
|
|
399
|
+
role: active.getAttribute("role"),
|
|
400
|
+
ariaLabel: active.getAttribute("aria-label"),
|
|
401
|
+
text: (active.innerText ?? "").trim().slice(0, 60) || null,
|
|
402
|
+
outline,
|
|
403
|
+
tabIndex: active.tabIndex ?? null
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
if (!el) break;
|
|
407
|
+
const hasFocusIndicator = el.outline !== "none" && el.outline !== "" && !el.outline.startsWith("0px") && !el.outline.startsWith("rgba(0, 0, 0, 0)") && !el.outline.startsWith("rgba(0,0,0,0)");
|
|
408
|
+
const focusable = {
|
|
409
|
+
tag: el.tag,
|
|
410
|
+
role: el.role,
|
|
411
|
+
ariaLabel: el.ariaLabel,
|
|
412
|
+
text: el.text,
|
|
413
|
+
hasFocusIndicator,
|
|
414
|
+
tabIndex: el.tabIndex
|
|
415
|
+
};
|
|
416
|
+
const key = `${el.tag}|${el.text ?? ""}|${el.role ?? ""}`;
|
|
417
|
+
if (i === 0) {
|
|
418
|
+
firstKey = key;
|
|
419
|
+
} else if (key === firstKey || seenKeys.has(key)) {
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
seenKeys.add(key);
|
|
423
|
+
sequence.push(focusable);
|
|
424
|
+
if (!hasFocusIndicator) {
|
|
425
|
+
issues.push(`Element #${i + 1} (${el.tag}) has no visible focus indicator`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
focusableCount: sequence.length,
|
|
430
|
+
elementsWithNoFocusIndicator: issues.length,
|
|
431
|
+
sequence,
|
|
432
|
+
issues
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
export {
|
|
436
|
+
probeAllScreens,
|
|
437
|
+
probeKeyboardNav,
|
|
438
|
+
probeResultToMarkdown,
|
|
439
|
+
probeResultToObservations,
|
|
440
|
+
probeScreen
|
|
441
|
+
};
|
|
442
|
+
//# sourceMappingURL=interactive-probe.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/interactive-probe.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Interactive Probe \u2014 Phase 2 Dynamic Observation\n *\n * The static crawl captures DOM structure at rest. This module runs AFTER the static crawl\n * to observe dynamic behaviour by actually interacting with the UI:\n *\n * 1. Empty submit \u2192 captures required-field validation error messages\n * 2. Invalid format \u2192 captures format validation errors (bad email, short password)\n * 3. Button clicks \u2192 captures modal/dialog contents\n * 4. Select changes \u2192 captures conditional reveals (show/hide logic)\n * 5. Success paths \u2192 captures toast/banner messages (where input is safe)\n *\n * Results are stored as KnowledgeChunks (sourceType='probe') and update the\n * ERROR_CATALOGUE + INTERACTION_PATTERNS skill files with real observed data\n * instead of LLM inference.\n *\n * Call signature: probeScreen(page, screenId, elements, opts)\n * Called from crawlProject() after static element extraction when opts.interactive=true.\n */\n\nimport type { Page } from 'playwright';\nimport { AppBrain } from '@qip/app-brain';\n\nexport interface ProbeElementFact {\n meaning: string;\n role: string;\n expectedData?: string;\n}\n\nexport interface ValidationError {\n field: string;\n trigger: 'empty_submit' | 'invalid_format' | 'boundary';\n errorText: string;\n}\n\nexport interface SuccessMessage {\n type: 'toast' | 'inline_success' | 'redirect';\n content: string;\n}\n\nexport interface ModalObservation {\n triggerElement: string;\n title: string;\n content: string;\n fields: string[];\n}\n\nexport interface ConditionalReveal {\n triggerElement: string;\n triggerValue: string;\n revealedText: string;\n}\n\nexport interface ProbeResult {\n screenId: string;\n url: string;\n validationErrors: ValidationError[];\n successMessages: SuccessMessage[];\n modals: ModalObservation[];\n conditionals: ConditionalReveal[];\n loadingPatterns: string[];\n}\n\n// \u2500\u2500 CSS selectors that reliably find error messages across frameworks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst ERROR_SELECTORS = [\n '[role=\"alert\"]',\n '[aria-live=\"assertive\"]',\n '[aria-live=\"polite\"]',\n '.invalid-feedback',\n '.field-error',\n '.error-message',\n '.form-error',\n '.validation-error',\n '[class*=\"error\" i]',\n '[class*=\"invalid\" i]',\n '[class*=\"Error\"]',\n 'p.text-red-500', 'p.text-red-600', 'p.text-destructive', 'p.text-danger',\n '[data-error]', '[data-invalid]',\n '.ant-form-item-explain-error', // Ant Design\n '.el-form-item__error', // Element UI\n '.chakra-form__error-message', // Chakra UI\n '.mantine-InputWrapper-error', // Mantine\n].join(', ');\n\n// \u2500\u2500 Toast / notification selectors \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\nconst TOAST_SELECTORS = [\n '[role=\"status\"]',\n '[data-sonner-toast]',\n '[data-toast]',\n '[data-hot-toast]',\n '.Toastify__toast',\n '.toast-message',\n '.notification-body',\n '.notistack-snackbar',\n '[class*=\"toast\" i]',\n '[class*=\"Toast\"]',\n '[class*=\"snackbar\" i]',\n '[class*=\"notification\" i]',\n].join(', ');\n\n/** Wait up to `maxMs` for any selector to appear, then return its text content. */\nasync function waitForAny(page: Page, selector: string, maxMs = 2500): Promise<string[]> {\n try {\n await page.waitForSelector(selector, { timeout: maxMs, state: 'visible' });\n } catch { return []; }\n return page.$$eval(selector, (els) =>\n els.map((el) => (el as HTMLElement).innerText?.replace(/\\s+/g, ' ').trim()).filter(Boolean)\n );\n}\n\n/** Collect all currently-visible error messages. */\nasync function collectErrors(page: Page): Promise<string[]> {\n try {\n return page.$$eval(ERROR_SELECTORS, (els) =>\n els\n .filter((el) => {\n const style = window.getComputedStyle(el);\n return style.display !== 'none' && style.visibility !== 'hidden' && (el as HTMLElement).innerText?.trim().length > 0;\n })\n .map((el) => (el as HTMLElement).innerText?.replace(/\\s+/g, ' ').trim())\n .filter(Boolean)\n );\n } catch { return []; }\n}\n\n/** Collect any visible toast / notification content. */\nasync function collectToasts(page: Page): Promise<string[]> {\n try {\n return page.$$eval(TOAST_SELECTORS, (els) =>\n els\n .filter((el) => {\n const style = window.getComputedStyle(el);\n return style.display !== 'none' && (el as HTMLElement).innerText?.trim().length > 0;\n })\n .map((el) => (el as HTMLElement).innerText?.replace(/\\s+/g, ' ').trim())\n .filter(Boolean)\n );\n } catch { return []; }\n}\n\n/** Find the primary submit button on the page. */\nasync function findSubmitButton(page: Page): Promise<string | null> {\n return page.evaluate(() => {\n const SUBMIT_KW = /submit|send|save|create|confirm|continue|proceed|register|sign.?in|log.?in|apply|verify|search|next|finish|complete|order|buy|checkout/i;\n const candidates = Array.from(document.querySelectorAll('button, [role=\"button\"], input[type=\"submit\"]')) as HTMLElement[];\n const primary = candidates.find((el) => {\n const style = window.getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden') return false;\n const text = el.innerText || (el as HTMLInputElement).value || el.getAttribute('aria-label') || '';\n return SUBMIT_KW.test(text);\n });\n if (!primary) return null;\n return primary.getAttribute('data-testid') || primary.getAttribute('aria-label') || primary.innerText?.trim().slice(0, 60);\n });\n}\n\n/** Find all modal-trigger buttons (non-submit buttons). */\nasync function findModalTriggers(page: Page): Promise<Array<{ selector: string; label: string }>> {\n return page.evaluate(() => {\n const SKIP = /submit|send|save|create|log.?in|sign.?in|register|continue|checkout|buy|order|search|close|cancel|dismiss/i;\n const NAV_PARENTS = new Set(['NAV', 'HEADER', 'FOOTER']);\n return Array.from(document.querySelectorAll('button, [role=\"button\"]'))\n .filter((el) => {\n const style = window.getComputedStyle(el as HTMLElement);\n if (style.display === 'none' || style.visibility === 'hidden') return false;\n const label = (el as HTMLElement).innerText?.trim() || el.getAttribute('aria-label') || '';\n if (!label || SKIP.test(label)) return false;\n // Skip nav chrome\n let p = el.parentElement;\n while (p) { if (NAV_PARENTS.has(p.tagName)) return false; p = p.parentElement; }\n return true;\n })\n .slice(0, 8) // probe at most 8 buttons per screen to keep it fast\n .map((el) => {\n const label = (el as HTMLElement).innerText?.trim() || el.getAttribute('aria-label') || 'button';\n return { selector: `[aria-label=\"${label}\"]`, label: label.slice(0, 60) };\n });\n });\n}\n\n/** Try to interact with select elements and observe conditional reveals. */\nasync function probeConditionals(page: Page): Promise<ConditionalReveal[]> {\n const results: ConditionalReveal[] = [];\n try {\n const selects = await page.$$('select');\n for (const sel of selects.slice(0, 5)) {\n const label = await sel.getAttribute('aria-label') || await sel.getAttribute('name') || 'select';\n const options = await sel.$$eval('option', (opts) => opts.slice(1, 3).map((o) => ({ value: o.value, text: o.textContent?.trim() })));\n for (const opt of options) {\n if (!opt.value) continue;\n const domBefore = await page.evaluate(() => document.body.innerText.length);\n await sel.selectOption(opt.value);\n await page.waitForTimeout(500);\n const domAfter = await page.evaluate(() => document.body.innerText.length);\n if (Math.abs(domAfter - domBefore) > 50) {\n const revealed = await page.evaluate(() => {\n // Grab text from any newly-visible sections\n return Array.from(document.querySelectorAll('[data-conditional],[data-show],[class*=\"conditional\"]'))\n .filter((el) => window.getComputedStyle(el).display !== 'none')\n .map((el) => (el as HTMLElement).innerText?.trim())\n .filter((t) => t && t.length > 10)\n .slice(0, 3)\n .join(' | ');\n });\n if (revealed) {\n results.push({ triggerElement: label, triggerValue: opt.text || opt.value, revealedText: revealed.slice(0, 200) });\n }\n }\n }\n }\n } catch { /* non-fatal */ }\n return results;\n}\n\n/**\n * Run interactive probing on a single screen page.\n * The page must already be navigated to the screen URL.\n * `elements` is the output of the static extractElements pass.\n */\nexport async function probeScreen(\n page: Page,\n screenId: string,\n url: string,\n elements: ProbeElementFact[],\n): Promise<ProbeResult> {\n const result: ProbeResult = {\n screenId, url,\n validationErrors: [], successMessages: [], modals: [], conditionals: [], loadingPatterns: [],\n };\n\n const formInputs = elements.filter((e) =>\n /input|text|email|password|tel|search|textarea|select/i.test(e.role)\n );\n const hasForm = formInputs.length > 0;\n\n // \u2500\u2500 Probe 1: Empty submit \u2192 capture required-field errors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (hasForm) {\n try {\n const submitLabel = await findSubmitButton(page);\n if (submitLabel) {\n // Click submit with no data filled in\n const submitBtn = page.locator(`button:has-text(\"${submitLabel}\")`).first();\n await submitBtn.click({ timeout: 2000 });\n await page.waitForTimeout(1500);\n\n const errors = await collectErrors(page);\n for (const errorText of errors) {\n // Try to match error back to a field\n const matchedField = formInputs.find((el) =>\n errorText.toLowerCase().includes(el.meaning.toLowerCase().split(' ')[0]) ||\n el.meaning.toLowerCase().includes(errorText.toLowerCase().split(' ')[0])\n );\n result.validationErrors.push({\n field: matchedField?.meaning ?? 'unknown',\n trigger: 'empty_submit',\n errorText: errorText.slice(0, 150),\n });\n }\n\n // Check for loading state text during submit\n const loadingText = await page.evaluate(() => {\n const loading = Array.from(document.querySelectorAll('button, [aria-busy=\"true\"], [class*=\"loading\" i], [class*=\"spinner\" i]'))\n .map((el) => (el as HTMLElement).innerText?.trim())\n .filter((t) => t && /loading|saving|submitting|sending|processing/i.test(t));\n return loading.slice(0, 3);\n });\n result.loadingPatterns.push(...loadingText);\n }\n } catch { /* non-fatal \u2014 form probe failed, continue */ }\n }\n\n // \u2500\u2500 Probe 2: Invalid format inputs \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 if (hasForm) {\n try {\n const emailField = formInputs.find((e) => e.expectedData === 'valid_email' || /email/i.test(e.meaning));\n const passwordField = formInputs.find((e) => e.expectedData === 'password' || /password/i.test(e.meaning));\n\n if (emailField) {\n const emailLocator = page.locator('input[type=\"email\"], input[name*=\"email\"], input[placeholder*=\"email\" i]').first();\n try {\n await emailLocator.fill('notanemail', { timeout: 1000 });\n await emailLocator.blur();\n await page.waitForTimeout(600);\n const errors = await collectErrors(page);\n for (const errorText of errors) {\n result.validationErrors.push({ field: emailField.meaning, trigger: 'invalid_format', errorText: errorText.slice(0, 150) });\n }\n } catch { /* locator not found */ }\n }\n\n if (passwordField) {\n const pwLocator = page.locator('input[type=\"password\"]').first();\n try {\n await pwLocator.fill('123', { timeout: 1000 });\n await pwLocator.blur();\n await page.waitForTimeout(600);\n const errors = await collectErrors(page);\n for (const errorText of errors) {\n if (!result.validationErrors.some((e) => e.errorText === errorText)) {\n result.validationErrors.push({ field: passwordField.meaning, trigger: 'invalid_format', errorText: errorText.slice(0, 150) });\n }\n }\n } catch { /* locator not found */ }\n }\n } catch { /* non-fatal */ }\n }\n\n // \u2500\u2500 Probe 3: Button clicks \u2192 discover modal/dialog contents \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n try {\n const triggers = await findModalTriggers(page);\n for (const trigger of triggers) {\n try {\n const btn = page.locator(`button:has-text(\"${trigger.label}\")`).first();\n const domBefore = await page.evaluate(() => document.body.innerText.length);\n\n await btn.click({ timeout: 1500 });\n await page.waitForTimeout(1000);\n\n // Check if a dialog/modal appeared\n const dialogText = await page.evaluate(() => {\n const DIALOG_SEL = '[role=\"dialog\"], [role=\"alertdialog\"], .modal, .Modal, [class*=\"modal\" i], [class*=\"dialog\" i], [class*=\"Dialog\"]';\n const dialogs = Array.from(document.querySelectorAll(DIALOG_SEL))\n .filter((el) => window.getComputedStyle(el).display !== 'none');\n if (dialogs.length === 0) return null;\n const d = dialogs[0] as HTMLElement;\n const title = d.querySelector('[role=\"heading\"], h1, h2, h3')?.textContent?.trim() || '';\n const content = d.innerText?.replace(/\\s+/g, ' ').trim().slice(0, 400);\n const fields = Array.from(d.querySelectorAll('input, select, textarea, button'))\n .map((f) => (f as HTMLElement).getAttribute('aria-label') || (f as HTMLElement).getAttribute('placeholder') || (f as HTMLInputElement).value || '')\n .filter((t) => t.trim().length > 0);\n return { title, content, fields };\n });\n\n if (dialogText && dialogText.content.length > 20) {\n result.modals.push({\n triggerElement: trigger.label,\n title: dialogText.title,\n content: dialogText.content,\n fields: dialogText.fields.slice(0, 10),\n });\n }\n\n // Dismiss: Escape key first, then close button\n await page.keyboard.press('Escape');\n await page.waitForTimeout(400);\n\n // Check if toast appeared after the click\n const toasts = await collectToasts(page);\n for (const t of toasts) {\n if (t.length > 5) result.successMessages.push({ type: 'toast', content: t.slice(0, 200) });\n }\n } catch { /* this button's probe failed, try next */ }\n }\n } catch { /* non-fatal */ }\n\n // \u2500\u2500 Probe 4: Select/radio \u2192 conditional reveals \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 try {\n const conditionals = await probeConditionals(page);\n result.conditionals.push(...conditionals);\n } catch { /* non-fatal */ }\n\n return result;\n}\n\n/** Phase 5: Convert ProbeResult to typed ProbeObservationFact rows for direct DB storage. */\nexport function probeResultToObservations(result: ProbeResult): Array<{\n elementMeaning: string; observationType: string; observedText: string; triggeredBy: string;\n}> {\n const obs: Array<{ elementMeaning: string; observationType: string; observedText: string; triggeredBy: string }> = [];\n for (const ve of result.validationErrors) {\n obs.push({ elementMeaning: ve.field, observationType: 'VALIDATION_ERROR', observedText: ve.errorText, triggeredBy: ve.trigger });\n }\n for (const sm of result.successMessages) {\n obs.push({ elementMeaning: '(page)', observationType: 'SUCCESS_TOAST', observedText: sm.content, triggeredBy: sm.type });\n }\n for (const m of result.modals) {\n obs.push({ elementMeaning: m.triggerElement, observationType: 'MODAL', observedText: [m.title, m.content].filter(Boolean).join(' \u2014 ').slice(0, 500), triggeredBy: `button_click:${m.triggerElement}` });\n }\n for (const c of result.conditionals) {\n obs.push({ elementMeaning: c.triggerElement, observationType: 'CONDITIONAL_REVEAL', observedText: c.revealedText, triggeredBy: `select_change:${c.triggerElement}:${c.triggerValue}` });\n }\n for (const lp of result.loadingPatterns) {\n obs.push({ elementMeaning: '(submit button)', observationType: 'LOADING_STATE', observedText: lp, triggeredBy: 'form_submit' });\n }\n return obs;\n}\n\n/**\n * Convert a ProbeResult into markdown to store as KnowledgeChunks\n * and update ERROR_CATALOGUE + INTERACTION_PATTERNS skill files.\n */\nexport function probeResultToMarkdown(result: ProbeResult): {\n errorCatalogueChunk: string;\n interactionPatternsChunk: string;\n} {\n const lines: string[] = [`## Probe Observations \u2014 ${result.url}\\n`];\n\n if (result.validationErrors.length > 0) {\n lines.push('### Validation Errors (observed)');\n for (const ve of result.validationErrors) {\n lines.push(`- **${ve.field}** (${ve.trigger}): \"${ve.errorText}\"`);\n }\n }\n\n if (result.successMessages.length > 0) {\n lines.push('\\n### Success / Feedback Messages (observed)');\n for (const sm of result.successMessages) {\n lines.push(`- ${sm.type}: \"${sm.content}\"`);\n }\n }\n\n if (result.modals.length > 0) {\n lines.push('\\n### Modal / Dialog Contents (observed)');\n for (const m of result.modals) {\n lines.push(`- **Trigger:** ${m.triggerElement}`);\n if (m.title) lines.push(` - Title: ${m.title}`);\n lines.push(` - Content: ${m.content.slice(0, 200)}`);\n if (m.fields.length > 0) lines.push(` - Fields: ${m.fields.join(', ')}`);\n }\n }\n\n if (result.conditionals.length > 0) {\n lines.push('\\n### Conditional UI (observed)');\n for (const c of result.conditionals) {\n lines.push(`- **${c.triggerElement}** = \"${c.triggerValue}\" reveals: ${c.revealedText}`);\n }\n }\n\n if (result.loadingPatterns.length > 0) {\n lines.push('\\n### Loading State Text (observed)');\n for (const lp of result.loadingPatterns) lines.push(`- \"${lp}\"`);\n }\n\n const full = lines.join('\\n');\n\n const errorSection = result.validationErrors.length > 0\n ? `## Real Validation Errors \u2014 ${result.url}\\n` +\n result.validationErrors.map((e) => `- **${e.field}**: \"${e.errorText}\"`).join('\\n')\n : '';\n\n const interactionSection = [\n result.successMessages.length > 0\n ? `## Success Messages \u2014 ${result.url}\\n` + result.successMessages.map((s) => `- ${s.type}: \"${s.content}\"`).join('\\n')\n : '',\n result.loadingPatterns.length > 0\n ? `## Loading Patterns \u2014 ${result.url}\\n` + result.loadingPatterns.map((lp) => `- \"${lp}\"`).join('\\n')\n : '',\n result.modals.length > 0\n ? `## Modal Contents \u2014 ${result.url}\\n` + result.modals.map((m) => `### ${m.triggerElement}\\n${m.content}`).join('\\n')\n : '',\n ].filter(Boolean).join('\\n\\n');\n\n return {\n errorCatalogueChunk: errorSection,\n interactionPatternsChunk: interactionSection,\n };\n}\n\n/**\n * Run interactive probing for all screens discovered in a crawl session.\n * Called from crawlProject() when opts.interactive = true.\n */\nexport async function probeAllScreens(\n page: any,\n tenantId: string,\n projectId: string,\n discovered: Array<{ url: string; screenId?: string; elements?: any[] }>,\n onProgress?: (pct: number, step: string) => Promise<void>,\n): Promise<void> {\n const probeable = discovered.filter((d) => d.screenId && d.url && !d.url.includes('#'));\n if (probeable.length === 0) return;\n\n const errorCatalogueChunks: string[] = [];\n const interactionChunks: string[] = [];\n\n for (let i = 0; i < probeable.length; i++) {\n const screen = probeable[i];\n const pct = 88 + Math.round((i / probeable.length) * 10);\n await onProgress?.(pct, `Probing screen ${i + 1}/${probeable.length}: ${screen.url}`);\n\n try {\n // Navigate to screen\n if (page.url() !== screen.url) {\n await page.goto(screen.url, { waitUntil: 'domcontentloaded' });\n await page.waitForTimeout(1500); // SPA hydration\n }\n\n const result = await probeScreen(page, screen.screenId!, screen.url, screen.elements ?? []);\n\n // Phase 5: save structured probe observations (replaces per-re-probe)\n const observations = probeResultToObservations(result);\n if (observations.length > 0) {\n await AppBrain.saveProbeObservations(tenantId, projectId, screen.screenId!, observations).catch(console.warn);\n }\n\n // Store raw probe result as KnowledgeChunk (kept for BM25 / oversized projects)\n const md = probeResultToMarkdown(result);\n if (md.errorCatalogueChunk) errorCatalogueChunks.push(md.errorCatalogueChunk);\n if (md.interactionPatternsChunk) interactionChunks.push(md.interactionPatternsChunk);\n\n const probeMarkdown = [md.errorCatalogueChunk, md.interactionPatternsChunk].filter(Boolean).join('\\n\\n');\n if (probeMarkdown.length > 50) {\n await AppBrain.saveKnowledgeChunk(tenantId, projectId, 'probe', screen.screenId!, probeMarkdown, `Probe \u2014 ${screen.url}`).catch(console.warn);\n }\n\n if (result.validationErrors.length > 0) {\n console.log(`[probe] ${screen.url}: ${result.validationErrors.length} validation errors, ${result.modals.length} modals`);\n }\n } catch (err) {\n console.warn(`[probe] \u2717 ${screen.url}:`, err instanceof Error ? err.message : err);\n }\n }\n\n // Update ERROR_CATALOGUE skill file with all real observed errors\n if (errorCatalogueChunks.length > 0) {\n const combined = `# Error Catalogue (Real Observed \u2014 Interactive Probe)\\n\\n${errorCatalogueChunks.join('\\n\\n')}`;\n await AppBrain.upsertSkillFile(tenantId, projectId, 'ERROR_CATALOGUE', combined, {\n summary: `Real validation error messages observed via interactive probe across ${errorCatalogueChunks.length} screens`,\n scope: 'PROJECT',\n }).catch(console.warn);\n }\n\n // Update INTERACTION_PATTERNS skill file with real observed patterns\n if (interactionChunks.length > 0) {\n const combined = `# Interaction Patterns (Real Observed \u2014 Interactive Probe)\\n\\n${interactionChunks.join('\\n\\n')}`;\n await AppBrain.upsertSkillFile(tenantId, projectId, 'INTERACTION_PATTERNS', combined, {\n summary: `Real loading states, success messages, and modal contents observed via interactive probe`,\n scope: 'PROJECT',\n }).catch(console.warn);\n }\n\n await onProgress?.(98, 'Interactive probe complete');\n console.log(`[probe] Probed ${probeable.length} screens. Errors: ${errorCatalogueChunks.length}, Interactions: ${interactionChunks.length}`);\n}\n\n// \u2500\u2500 Keyboard Navigation Probe \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 interface FocusableElement {\n tag: string;\n role: string | null;\n ariaLabel: string | null;\n text: string | null;\n hasFocusIndicator: boolean;\n tabIndex: number | null;\n}\n\nexport interface KeyboardNavResult {\n focusableCount: number;\n elementsWithNoFocusIndicator: number;\n sequence: FocusableElement[];\n issues: string[];\n}\n\n/**\n * Probe keyboard navigation accessibility by pressing Tab up to 50 times and\n * recording each focused element's properties and focus-indicator visibility.\n * Stops early if focus wraps back to the start or hits the same element twice.\n */\nexport async function probeKeyboardNav(page: Page): Promise<KeyboardNavResult> {\n const sequence: FocusableElement[] = [];\n const issues: string[] = [];\n const seenKeys = new Set<string>();\n let firstKey: string | null = null;\n\n for (let i = 0; i < 50; i++) {\n await page.keyboard.press('Tab');\n\n const el = await page.evaluate(() => {\n const active = document.activeElement as HTMLElement | null;\n if (!active || active === document.body) return null;\n const outline = window.getComputedStyle(active).outline;\n return {\n tag: active.tagName.toLowerCase(),\n role: active.getAttribute('role'),\n ariaLabel: active.getAttribute('aria-label'),\n text: (active.innerText ?? '').trim().slice(0, 60) || null,\n outline,\n tabIndex: active.tabIndex ?? null,\n };\n });\n\n if (!el) break;\n\n const hasFocusIndicator =\n el.outline !== 'none' &&\n el.outline !== '' &&\n !el.outline.startsWith('0px') &&\n !el.outline.startsWith('rgba(0, 0, 0, 0)') &&\n !el.outline.startsWith('rgba(0,0,0,0)');\n\n const focusable: FocusableElement = {\n tag: el.tag,\n role: el.role,\n ariaLabel: el.ariaLabel,\n text: el.text,\n hasFocusIndicator,\n tabIndex: el.tabIndex,\n };\n\n const key = `${el.tag}|${el.text ?? ''}|${el.role ?? ''}`;\n\n if (i === 0) {\n firstKey = key;\n } else if (key === firstKey || seenKeys.has(key)) {\n // Focus wrapped back to start or hit a duplicate \u2014 stop probing\n break;\n }\n\n seenKeys.add(key);\n sequence.push(focusable);\n\n if (!hasFocusIndicator) {\n issues.push(`Element #${i + 1} (${el.tag}) has no visible focus indicator`);\n }\n }\n\n return {\n focusableCount: sequence.length,\n elementsWithNoFocusIndicator: issues.length,\n sequence,\n issues,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|