@detiq/crawler 1.1.9 → 1.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/accessibility-auditor.js +54 -74
  2. package/dist/ai-login.js +227 -249
  3. package/dist/auth-token-rotator.js +57 -67
  4. package/dist/auth.js +299 -336
  5. package/dist/browser-extension.js +104 -142
  6. package/dist/captcha-solver.js +98 -125
  7. package/dist/color-contrast.js +77 -89
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +54 -71
  10. package/dist/cookie-consent.js +68 -83
  11. package/dist/cookie-jar.js +53 -60
  12. package/dist/crawler.js +4494 -5353
  13. package/dist/custom-js-injector.js +45 -61
  14. package/dist/dark-mode-capture.js +67 -81
  15. package/dist/dead-link-detector.js +47 -55
  16. package/dist/element-hash.js +8 -12
  17. package/dist/email-otp.js +154 -195
  18. package/dist/engine-router.js +87 -98
  19. package/dist/export/bulk-export.js +70 -55
  20. package/dist/export/vector-export.js +77 -105
  21. package/dist/extractors/advanced.js +249 -266
  22. package/dist/extractors/artifact-envelope.js +59 -58
  23. package/dist/failure-analysis-agent.js +37 -33
  24. package/dist/feature-flag-detector.js +51 -66
  25. package/dist/form-detector.js +55 -75
  26. package/dist/frontier.js +76 -101
  27. package/dist/gap-pass.js +68 -89
  28. package/dist/graphql-detector.js +62 -72
  29. package/dist/http-engine.js +282 -330
  30. package/dist/incremental-diff.js +54 -87
  31. package/dist/index.js +67 -28
  32. package/dist/input-fuzzer.js +93 -97
  33. package/dist/interactive-probe.js +715 -827
  34. package/dist/job-queue.js +2151 -2169
  35. package/dist/js-coverage.js +52 -56
  36. package/dist/js-router-detector.js +86 -117
  37. package/dist/language-detector.js +51 -63
  38. package/dist/link-classifier.js +32 -50
  39. package/dist/live-view.js +91 -108
  40. package/dist/locator-extractor.js +151 -187
  41. package/dist/logger.js +9 -7
  42. package/dist/markdown-extract.js +48 -61
  43. package/dist/native-crawler.js +780 -946
  44. package/dist/network-observer.js +135 -156
  45. package/dist/network-throttle.js +29 -40
  46. package/dist/openapi-generator.js +77 -92
  47. package/dist/pdf-extractor.js +72 -90
  48. package/dist/performance-metrics.js +80 -102
  49. package/dist/phash.js +125 -144
  50. package/dist/pii-scrubber.js +65 -68
  51. package/dist/playwright-recorder.js +70 -58
  52. package/dist/policies/crawl-safety.js +31 -34
  53. package/dist/profiles/domain-profile.js +130 -125
  54. package/dist/proxy.js +146 -172
  55. package/dist/rate-limiter.js +58 -65
  56. package/dist/redirect-tracker.js +60 -64
  57. package/dist/redis-frontier.js +116 -136
  58. package/dist/redis-queue.js +157 -144
  59. package/dist/response-cache.js +127 -140
  60. package/dist/responsive-detector.js +42 -46
  61. package/dist/retry-policy.js +46 -50
  62. package/dist/robots-txt.js +69 -83
  63. package/dist/screen-reader.js +45 -70
  64. package/dist/screenshot-masker.js +59 -77
  65. package/dist/seo-checker.js +88 -107
  66. package/dist/session-capture.js +192 -212
  67. package/dist/session-export.js +107 -133
  68. package/dist/sitemap-writer.js +38 -41
  69. package/dist/sitemap.js +141 -202
  70. package/dist/skyvern.js +123 -148
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +74 -90
  73. package/dist/stealth/human-behavior.js +156 -173
  74. package/dist/telemetry.js +68 -82
  75. package/dist/test-executor.js +309 -335
  76. package/dist/tls-fingerprint.js +77 -88
  77. package/dist/url-pattern-completer.js +71 -90
  78. package/dist/visual-diff-verdict.js +23 -38
  79. package/dist/webhook-notifier.js +71 -69
  80. package/dist/websocket-detector.js +56 -68
  81. package/dist/worker.js +29 -38
  82. package/package.json +1 -1
  83. package/src/job-queue.ts +136 -0
@@ -1,62 +1,63 @@
1
- import crypto from 'node:crypto';
2
- export function sha256(input) {
3
- return crypto.createHash('sha256').update(input).digest('hex');
1
+ import crypto from "node:crypto";
2
+ function sha256(input) {
3
+ return crypto.createHash("sha256").update(input).digest("hex");
4
4
  }
5
- export function buildFitMarkdown(markdown, keywords = []) {
6
- const lines = markdown
7
- .split('\n')
8
- .map((line) => line.trimEnd())
9
- .filter((line) => line.trim().length > 0);
10
- const lowerKeywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);
11
- const filtered = lines.filter((line) => {
12
- const plain = line.replace(/[#*_`>\-[\]()]/g, '').trim();
13
- if (plain.length < 3)
14
- return false;
15
- if (/^(cookie|privacy|terms|copyright|all rights reserved|subscribe|newsletter)$/i.test(plain))
16
- return false;
17
- if (lowerKeywords.length === 0)
18
- return true;
19
- const lower = plain.toLowerCase();
20
- return lowerKeywords.some((kw) => lower.includes(kw)) || /^#{1,4}\s/.test(line);
21
- });
22
- const candidate = filtered.join('\n');
23
- return candidate.length >= 200 ? candidate : markdown;
5
+ function buildFitMarkdown(markdown, keywords = []) {
6
+ const lines = markdown.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
7
+ const lowerKeywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);
8
+ const filtered = lines.filter((line) => {
9
+ const plain = line.replace(/[#*_`>\-[\]()]/g, "").trim();
10
+ if (plain.length < 3) return false;
11
+ if (/^(cookie|privacy|terms|copyright|all rights reserved|subscribe|newsletter)$/i.test(plain)) return false;
12
+ if (lowerKeywords.length === 0) return true;
13
+ const lower = plain.toLowerCase();
14
+ return lowerKeywords.some((kw) => lower.includes(kw)) || /^#{1,4}\s/.test(line);
15
+ });
16
+ const candidate = filtered.join("\n");
17
+ return candidate.length >= 200 ? candidate : markdown;
24
18
  }
25
- export function summarizeStructuredElements(elements) {
26
- const forms = elements.filter((e) => ['email', 'password', 'text', 'tel', 'date', 'number', 'select', 'textarea'].includes(e.role));
27
- const actions = elements.filter((e) => e.role === 'button' || e.role === 'submit' || e.role === 'link' || e.role === 'a');
28
- return {
29
- elementCount: elements.length,
30
- formFieldCount: forms.length,
31
- actionCount: actions.length,
32
- fields: forms.slice(0, 50).map((e) => ({ label: e.label, role: e.role, expectedData: e.expectedData })),
33
- actions: actions.slice(0, 50).map((e) => ({ label: e.label, role: e.role })),
34
- };
19
+ function summarizeStructuredElements(elements) {
20
+ const forms = elements.filter((e) => ["email", "password", "text", "tel", "date", "number", "select", "textarea"].includes(e.role));
21
+ const actions = elements.filter((e) => e.role === "button" || e.role === "submit" || e.role === "link" || e.role === "a");
22
+ return {
23
+ elementCount: elements.length,
24
+ formFieldCount: forms.length,
25
+ actionCount: actions.length,
26
+ fields: forms.slice(0, 50).map((e) => ({ label: e.label, role: e.role, expectedData: e.expectedData })),
27
+ actions: actions.slice(0, 50).map((e) => ({ label: e.label, role: e.role }))
28
+ };
35
29
  }
36
- export function buildCrawlArtifactEnvelope(input) {
37
- const accessibilityTree = input.accessibilityElements ? JSON.stringify(input.accessibilityElements) : undefined;
38
- return {
39
- tenantId: input.tenantId,
40
- projectId: input.projectId,
41
- jobId: input.jobId,
42
- url: input.url,
43
- canonicalUrl: input.canonicalUrl,
44
- screenId: input.screenId,
45
- status: 'ok',
46
- outputs: {
47
- screenshot: input.screenshotPath,
48
- html: input.domPath,
49
- markdown: input.markdown,
50
- fitMarkdown: input.fitMarkdown,
51
- accessibilityTree,
52
- structuredJson: input.structuredJson,
53
- },
54
- fingerprints: {
55
- textHash: input.markdown ? sha256(input.markdown) : undefined,
56
- domHash: input.domContent ? sha256(input.domContent) : undefined,
57
- accessibilityHash: accessibilityTree ? sha256(accessibilityTree) : undefined,
58
- urlCanonicalHash: sha256(input.canonicalUrl),
59
- },
60
- evidence: input.evidence ?? [],
61
- };
30
+ function buildCrawlArtifactEnvelope(input) {
31
+ const accessibilityTree = input.accessibilityElements ? JSON.stringify(input.accessibilityElements) : void 0;
32
+ return {
33
+ tenantId: input.tenantId,
34
+ projectId: input.projectId,
35
+ jobId: input.jobId,
36
+ url: input.url,
37
+ canonicalUrl: input.canonicalUrl,
38
+ screenId: input.screenId,
39
+ status: "ok",
40
+ outputs: {
41
+ screenshot: input.screenshotPath,
42
+ html: input.domPath,
43
+ markdown: input.markdown,
44
+ fitMarkdown: input.fitMarkdown,
45
+ accessibilityTree,
46
+ structuredJson: input.structuredJson
47
+ },
48
+ fingerprints: {
49
+ textHash: input.markdown ? sha256(input.markdown) : void 0,
50
+ domHash: input.domContent ? sha256(input.domContent) : void 0,
51
+ accessibilityHash: accessibilityTree ? sha256(accessibilityTree) : void 0,
52
+ urlCanonicalHash: sha256(input.canonicalUrl)
53
+ },
54
+ evidence: input.evidence ?? []
55
+ };
62
56
  }
57
+ export {
58
+ buildCrawlArtifactEnvelope,
59
+ buildFitMarkdown,
60
+ sha256,
61
+ summarizeStructuredElements
62
+ };
63
+ //# sourceMappingURL=artifact-envelope.js.map
@@ -1,37 +1,41 @@
1
- import { readFileSync } from 'node:fs';
2
- import { askTrackedWithImages, askTracked, deterministicSeed } from '@detiq/agents';
3
- import { AppBrain } from '@detiq/app-brain';
4
- export async function analyzeFailure(ctx) {
5
- try {
6
- let llmCfg;
7
- if (ctx.tenantId) {
8
- llmCfg = await AppBrain.getTaskLLMConfig(ctx.tenantId, 'testRunner').catch(() => undefined) ?? undefined;
9
- }
10
- if (!llmCfg)
11
- return null;
12
- const consoleSection = ctx.consoleLogs?.length
13
- ? `\n\nBrowser console (last ${ctx.consoleLogs.length} entries):\n${ctx.consoleLogs.map(l => `[${l.type}] ${l.text}`).join('\n')}`
14
- : '';
15
- const networkSection = ctx.networkErrors?.length
16
- ? `\n\nFailed network requests:\n${ctx.networkErrors.map(n => `${n.method} ${n.url} → ${n.status}`).join('\n')}`
17
- : '';
18
- const userText = `A Playwright automated test failed. Identify the root cause in 2-3 sentences. Be specific — name the failing element, API endpoint, or error type. Do not suggest fixes.
1
+ import { readFileSync } from "node:fs";
2
+ import { askTrackedWithImages, askTracked, deterministicSeed } from "@detiq/agents";
3
+ import { AppBrain } from "@detiq/app-brain";
4
+ async function analyzeFailure(ctx) {
5
+ try {
6
+ let llmCfg;
7
+ if (ctx.tenantId) {
8
+ llmCfg = await AppBrain.getTaskLLMConfig(ctx.tenantId, "testRunner").catch(() => void 0) ?? void 0;
9
+ }
10
+ if (!llmCfg) return null;
11
+ const consoleSection = ctx.consoleLogs?.length ? `
12
+
13
+ Browser console (last ${ctx.consoleLogs.length} entries):
14
+ ${ctx.consoleLogs.map((l) => `[${l.type}] ${l.text}`).join("\n")}` : "";
15
+ const networkSection = ctx.networkErrors?.length ? `
19
16
 
20
- Test step: ${ctx.stepDescription ?? 'unknown'}
17
+ Failed network requests:
18
+ ${ctx.networkErrors.map((n) => `${n.method} ${n.url} \u2192 ${n.status}`).join("\n")}` : "";
19
+ const userText = `A Playwright automated test failed. Identify the root cause in 2-3 sentences. Be specific \u2014 name the failing element, API endpoint, or error type. Do not suggest fixes.
20
+
21
+ Test step: ${ctx.stepDescription ?? "unknown"}
21
22
  Error: ${ctx.error}${consoleSection}${networkSection}`;
22
- if (ctx.screenshotPath) {
23
- try {
24
- const img = readFileSync(ctx.screenshotPath);
25
- const result = await askTrackedWithImages('', userText, [{ base64: img.toString('base64'), mediaType: 'image/png' }], llmCfg);
26
- return result.text || null;
27
- }
28
- catch { /* fall through to text-only */ }
29
- }
30
- const failureSeed = deterministicSeed(`${ctx.tenantId ?? ''}:failure:${(ctx.stepDescription ?? '').slice(0, 100)}:${ctx.error.slice(0, 200)}`);
31
- const result = await askTracked('', userText, llmCfg, 256, undefined, { temperature: 0, seed: failureSeed });
32
- return result.text || null;
33
- }
34
- catch {
35
- return null;
23
+ if (ctx.screenshotPath) {
24
+ try {
25
+ const img = readFileSync(ctx.screenshotPath);
26
+ const result2 = await askTrackedWithImages("", userText, [{ base64: img.toString("base64"), mediaType: "image/png" }], llmCfg);
27
+ return result2.text || null;
28
+ } catch {
29
+ }
36
30
  }
31
+ const failureSeed = deterministicSeed(`${ctx.tenantId ?? ""}:failure:${(ctx.stepDescription ?? "").slice(0, 100)}:${ctx.error.slice(0, 200)}`);
32
+ const result = await askTracked("", userText, llmCfg, 256, void 0, { temperature: 0, seed: failureSeed });
33
+ return result.text || null;
34
+ } catch {
35
+ return null;
36
+ }
37
37
  }
38
+ export {
39
+ analyzeFailure
40
+ };
41
+ //# sourceMappingURL=failure-analysis-agent.js.map
@@ -1,70 +1,55 @@
1
- /**
2
- * Feature flag + A/B test detector.
3
- *
4
- * Detects popular feature flag / experiment SDKs loaded on the page:
5
- * LaunchDarkly, Optimizely, Split.io, Unleash, GrowthBook, Statsig, Flagsmith, Amplitude Experiment.
6
- *
7
- * Also detects common A/B test patterns: data-testid attributes, gtm.js experiment layers,
8
- * Optimizely variation IDs, VWO, etc.
9
- *
10
- * Why: ZeTa needs to know when a page shows different content to different users
11
- * so test generation can account for variants and not generate flaky tests.
12
- */
13
- // SDK detectors — checked against window object + script URLs
14
1
  const FLAG_SDK_CHECKS = [
15
- { sdk: 'LaunchDarkly', windowKeys: ['LDClient', 'LaunchDarkly', 'ldClient'], scriptPattern: /launchdarkly/i },
16
- { sdk: 'Optimizely', windowKeys: ['optimizely', 'optimizelySdk'], scriptPattern: /optimizely/i },
17
- { sdk: 'Split.io', windowKeys: ['splitio', 'SplitFactory'], scriptPattern: /split\.io/i },
18
- { sdk: 'GrowthBook', windowKeys: ['growthbook', 'GrowthBook'], scriptPattern: /growthbook/i },
19
- { sdk: 'Statsig', windowKeys: ['statsig', 'StatsigClient'], scriptPattern: /statsig/i },
20
- { sdk: 'Flagsmith', windowKeys: ['flagsmith'], scriptPattern: /flagsmith/i },
21
- { sdk: 'Unleash', windowKeys: ['unleash', 'UnleashClient'], scriptPattern: /unleash/i },
22
- { sdk: 'VWO', windowKeys: ['vwo', '_vwo'], scriptPattern: /vwo\.com/i },
23
- { sdk: 'Google Optimize', windowKeys: ['gaData', 'google_optimize'], scriptPattern: /googleoptimize/i },
24
- { sdk: 'AB Tasty', windowKeys: ['ABTasty'], scriptPattern: /abtasty/i },
2
+ { sdk: "LaunchDarkly", windowKeys: ["LDClient", "LaunchDarkly", "ldClient"], scriptPattern: /launchdarkly/i },
3
+ { sdk: "Optimizely", windowKeys: ["optimizely", "optimizelySdk"], scriptPattern: /optimizely/i },
4
+ { sdk: "Split.io", windowKeys: ["splitio", "SplitFactory"], scriptPattern: /split\.io/i },
5
+ { sdk: "GrowthBook", windowKeys: ["growthbook", "GrowthBook"], scriptPattern: /growthbook/i },
6
+ { sdk: "Statsig", windowKeys: ["statsig", "StatsigClient"], scriptPattern: /statsig/i },
7
+ { sdk: "Flagsmith", windowKeys: ["flagsmith"], scriptPattern: /flagsmith/i },
8
+ { sdk: "Unleash", windowKeys: ["unleash", "UnleashClient"], scriptPattern: /unleash/i },
9
+ { sdk: "VWO", windowKeys: ["vwo", "_vwo"], scriptPattern: /vwo\.com/i },
10
+ { sdk: "Google Optimize", windowKeys: ["gaData", "google_optimize"], scriptPattern: /googleoptimize/i },
11
+ { sdk: "AB Tasty", windowKeys: ["ABTasty"], scriptPattern: /abtasty/i }
25
12
  ];
26
- export async function detectFeatureFlags(page, url) {
27
- const detectedAt = new Date().toISOString();
28
- const result = await page.evaluate((checks) => {
29
- const flagSdks = [];
30
- const scripts = Array.from(document.querySelectorAll('script[src]')).map((s) => s.src ?? '');
31
- for (const check of checks) {
32
- let found = false;
33
- let confidence = 'low';
34
- // Check window keys
35
- if (check.windowKeys?.some((k) => k in window)) {
36
- found = true;
37
- confidence = 'high';
38
- }
39
- // Check script URLs
40
- if (!found && check.scriptPattern) {
41
- const re = new RegExp(check.scriptPattern.source, 'i');
42
- const matchingSrc = scripts.find(s => re.test(s));
43
- if (matchingSrc) {
44
- found = true;
45
- confidence = 'medium';
46
- }
47
- }
48
- if (found)
49
- flagSdks.push({ sdk: check.sdk, confidence, details: undefined });
13
+ async function detectFeatureFlags(page, url) {
14
+ const detectedAt = (/* @__PURE__ */ new Date()).toISOString();
15
+ const result = await page.evaluate((checks) => {
16
+ const flagSdks = [];
17
+ const scripts = Array.from(document.querySelectorAll("script[src]")).map((s) => s.src ?? "");
18
+ for (const check of checks) {
19
+ let found = false;
20
+ let confidence = "low";
21
+ if (check.windowKeys?.some((k) => k in window)) {
22
+ found = true;
23
+ confidence = "high";
24
+ }
25
+ if (!found && check.scriptPattern) {
26
+ const re = new RegExp(check.scriptPattern.source, "i");
27
+ const matchingSrc = scripts.find((s) => re.test(s));
28
+ if (matchingSrc) {
29
+ found = true;
30
+ confidence = "medium";
50
31
  }
51
- // A/B tools
52
- const abTools = [];
53
- if ('_vwo_code' in window || '_vis_opt_queue' in window)
54
- abTools.push({ tool: 'VWO', experiments: [] });
55
- if ('dataLayer' in window) {
56
- const dl = window.dataLayer ?? [];
57
- const expEvents = dl.filter((e) => e?.event?.includes('experiment') || e?.experimentId);
58
- if (expEvents.length > 0)
59
- abTools.push({ tool: 'Google Tag Manager Experiments', experiments: expEvents.slice(0, 5).map((e) => e.experimentId ?? '') });
60
- }
61
- return { flagSdks, abTools };
62
- }, FLAG_SDK_CHECKS).catch(() => ({ flagSdks: [], abTools: [] }));
63
- return {
64
- url,
65
- flagSdks: result.flagSdks,
66
- abTools: result.abTools,
67
- hasAnyFlags: result.flagSdks.length > 0 || result.abTools.length > 0,
68
- detectedAt,
69
- };
32
+ }
33
+ if (found) flagSdks.push({ sdk: check.sdk, confidence, details: void 0 });
34
+ }
35
+ const abTools = [];
36
+ if ("_vwo_code" in window || "_vis_opt_queue" in window) abTools.push({ tool: "VWO", experiments: [] });
37
+ if ("dataLayer" in window) {
38
+ const dl = window.dataLayer ?? [];
39
+ const expEvents = dl.filter((e) => e?.event?.includes("experiment") || e?.experimentId);
40
+ if (expEvents.length > 0) abTools.push({ tool: "Google Tag Manager Experiments", experiments: expEvents.slice(0, 5).map((e) => e.experimentId ?? "") });
41
+ }
42
+ return { flagSdks, abTools };
43
+ }, FLAG_SDK_CHECKS).catch(() => ({ flagSdks: [], abTools: [] }));
44
+ return {
45
+ url,
46
+ flagSdks: result.flagSdks,
47
+ abTools: result.abTools,
48
+ hasAnyFlags: result.flagSdks.length > 0 || result.abTools.length > 0,
49
+ detectedAt
50
+ };
70
51
  }
52
+ export {
53
+ detectFeatureFlags
54
+ };
55
+ //# sourceMappingURL=feature-flag-detector.js.map
@@ -1,78 +1,58 @@
1
- /**
2
- * Form detector — extract form metadata from crawled pages.
3
- *
4
- * For each form found on a page, captures: action URL, method, all input fields
5
- * with their types/names/required/validation constraints, and submit button text.
6
- *
7
- * This data feeds ZeTa's test generator to create form validation test cases
8
- * automatically (happy path + boundary/error cases per field type).
9
- */
10
1
  function guessPurpose(fields, action) {
11
- const names = fields.map(f => f.name?.toLowerCase() + (f.label ?? '').toLowerCase()).join(' ');
12
- if (/password/.test(names) && /email|username|user/.test(names) && fields.length <= 4)
13
- return 'login';
14
- if (/password/.test(names) && /confirm|repeat/.test(names))
15
- return 'registration';
16
- if (/search|query|q\b/.test(names) && fields.length <= 3)
17
- return 'search';
18
- if (/card|payment|cvv|expir/.test(names))
19
- return 'checkout';
20
- if (/message|subject|contact/.test(names))
21
- return 'contact';
22
- if (/setting|preference|profile/.test(names))
23
- return 'settings';
24
- return 'unknown';
2
+ const names = fields.map((f) => f.name?.toLowerCase() + (f.label ?? "").toLowerCase()).join(" ");
3
+ if (/password/.test(names) && /email|username|user/.test(names) && fields.length <= 4) return "login";
4
+ if (/password/.test(names) && /confirm|repeat/.test(names)) return "registration";
5
+ if (/search|query|q\b/.test(names) && fields.length <= 3) return "search";
6
+ if (/card|payment|cvv|expir/.test(names)) return "checkout";
7
+ if (/message|subject|contact/.test(names)) return "contact";
8
+ if (/setting|preference|profile/.test(names)) return "settings";
9
+ return "unknown";
25
10
  }
26
- export async function detectForms(page) {
27
- try {
28
- return await page.$$eval('form', (forms) => forms.map((form, index) => {
29
- const inputs = Array.from(form.querySelectorAll('input,select,textarea,button[type="submit"]'));
30
- const fields = [];
31
- for (const input of inputs) {
32
- const tag = input.tagName.toLowerCase();
33
- const type = tag === 'button' ? 'submit' : (input.type || tag);
34
- if (['submit', 'reset', 'button', 'hidden'].includes(type))
35
- continue;
36
- // Find associated label
37
- let label = '';
38
- if (input.id) {
39
- const lbl = form.querySelector(`label[for="${input.id}"]`);
40
- if (lbl)
41
- label = (lbl.textContent ?? '').trim();
42
- }
43
- if (!label) {
44
- const parent = input.closest('label');
45
- if (parent)
46
- label = (parent.textContent ?? '').replace(input.value ?? '', '').trim().slice(0, 80);
47
- }
48
- const options = tag === 'select'
49
- ? Array.from(input.options ?? []).map((o) => o.text?.trim()).filter(Boolean).slice(0, 20)
50
- : tag === 'input' && type === 'radio'
51
- ? [input.value]
52
- : undefined;
53
- fields.push({
54
- name: input.name || input.id || '',
55
- type,
56
- label: label || undefined,
57
- required: !!input.required,
58
- placeholder: input.placeholder || undefined,
59
- minLength: input.minLength > 0 ? input.minLength : undefined,
60
- maxLength: input.maxLength > 0 && input.maxLength < 524288 ? input.maxLength : undefined,
61
- min: input.min || undefined,
62
- max: input.max || undefined,
63
- pattern: input.pattern || undefined,
64
- options,
65
- autocomplete: input.autocomplete || undefined,
66
- });
67
- }
68
- const submitBtn = form.querySelector('button[type="submit"],input[type="submit"]');
69
- const hasFileUpload = !!form.querySelector('input[type="file"]');
70
- const action = form.action || undefined;
71
- const method = (form.method || 'get').toUpperCase();
72
- return { index, action, method, fields, submitText: submitBtn?.textContent?.trim() || submitBtn?.value || undefined, hasFileUpload };
73
- })).then(forms => forms.map((f) => ({ ...f, purpose: guessPurpose(f.fields, f.action) })));
74
- }
75
- catch {
76
- return [];
77
- }
11
+ async function detectForms(page) {
12
+ try {
13
+ return await page.$$eval("form", (forms) => forms.map((form, index) => {
14
+ const inputs = Array.from(form.querySelectorAll('input,select,textarea,button[type="submit"]'));
15
+ const fields = [];
16
+ for (const input of inputs) {
17
+ const tag = input.tagName.toLowerCase();
18
+ const type = tag === "button" ? "submit" : input.type || tag;
19
+ if (["submit", "reset", "button", "hidden"].includes(type)) continue;
20
+ let label = "";
21
+ if (input.id) {
22
+ const lbl = form.querySelector(`label[for="${input.id}"]`);
23
+ if (lbl) label = (lbl.textContent ?? "").trim();
24
+ }
25
+ if (!label) {
26
+ const parent = input.closest("label");
27
+ if (parent) label = (parent.textContent ?? "").replace(input.value ?? "", "").trim().slice(0, 80);
28
+ }
29
+ const options = tag === "select" ? Array.from(input.options ?? []).map((o) => o.text?.trim()).filter(Boolean).slice(0, 20) : tag === "input" && type === "radio" ? [input.value] : void 0;
30
+ fields.push({
31
+ name: input.name || input.id || "",
32
+ type,
33
+ label: label || void 0,
34
+ required: !!input.required,
35
+ placeholder: input.placeholder || void 0,
36
+ minLength: input.minLength > 0 ? input.minLength : void 0,
37
+ maxLength: input.maxLength > 0 && input.maxLength < 524288 ? input.maxLength : void 0,
38
+ min: input.min || void 0,
39
+ max: input.max || void 0,
40
+ pattern: input.pattern || void 0,
41
+ options,
42
+ autocomplete: input.autocomplete || void 0
43
+ });
44
+ }
45
+ const submitBtn = form.querySelector('button[type="submit"],input[type="submit"]');
46
+ const hasFileUpload = !!form.querySelector('input[type="file"]');
47
+ const action = form.action || void 0;
48
+ const method = (form.method || "get").toUpperCase();
49
+ return { index, action, method, fields, submitText: submitBtn?.textContent?.trim() || submitBtn?.value || void 0, hasFileUpload };
50
+ })).then((forms) => forms.map((f) => ({ ...f, purpose: guessPurpose(f.fields, f.action) })));
51
+ } catch {
52
+ return [];
53
+ }
78
54
  }
55
+ export {
56
+ detectForms
57
+ };
58
+ //# sourceMappingURL=form-detector.js.map