@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.
- package/dist/accessibility-auditor.js +54 -74
- package/dist/ai-login.js +227 -249
- package/dist/auth-token-rotator.js +57 -67
- package/dist/auth.js +299 -336
- package/dist/browser-extension.js +104 -142
- package/dist/captcha-solver.js +98 -125
- package/dist/color-contrast.js +77 -89
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +54 -71
- package/dist/cookie-consent.js +68 -83
- package/dist/cookie-jar.js +53 -60
- package/dist/crawler.js +4494 -5353
- package/dist/custom-js-injector.js +45 -61
- package/dist/dark-mode-capture.js +67 -81
- package/dist/dead-link-detector.js +47 -55
- package/dist/element-hash.js +8 -12
- package/dist/email-otp.js +154 -195
- package/dist/engine-router.js +87 -98
- package/dist/export/bulk-export.js +70 -55
- package/dist/export/vector-export.js +77 -105
- package/dist/extractors/advanced.js +249 -266
- package/dist/extractors/artifact-envelope.js +59 -58
- package/dist/failure-analysis-agent.js +37 -33
- package/dist/feature-flag-detector.js +51 -66
- package/dist/form-detector.js +55 -75
- package/dist/frontier.js +76 -101
- package/dist/gap-pass.js +68 -89
- package/dist/graphql-detector.js +62 -72
- package/dist/http-engine.js +282 -330
- package/dist/incremental-diff.js +54 -87
- package/dist/index.js +67 -28
- package/dist/input-fuzzer.js +93 -97
- package/dist/interactive-probe.js +715 -827
- package/dist/job-queue.js +2151 -2169
- package/dist/js-coverage.js +52 -56
- package/dist/js-router-detector.js +86 -117
- package/dist/language-detector.js +51 -63
- package/dist/link-classifier.js +32 -50
- package/dist/live-view.js +91 -108
- package/dist/locator-extractor.js +151 -187
- package/dist/logger.js +9 -7
- package/dist/markdown-extract.js +48 -61
- package/dist/native-crawler.js +780 -946
- package/dist/network-observer.js +135 -156
- package/dist/network-throttle.js +29 -40
- package/dist/openapi-generator.js +77 -92
- package/dist/pdf-extractor.js +72 -90
- package/dist/performance-metrics.js +80 -102
- package/dist/phash.js +125 -144
- package/dist/pii-scrubber.js +65 -68
- package/dist/playwright-recorder.js +70 -58
- package/dist/policies/crawl-safety.js +31 -34
- package/dist/profiles/domain-profile.js +130 -125
- package/dist/proxy.js +146 -172
- package/dist/rate-limiter.js +58 -65
- package/dist/redirect-tracker.js +60 -64
- package/dist/redis-frontier.js +116 -136
- package/dist/redis-queue.js +157 -144
- package/dist/response-cache.js +127 -140
- package/dist/responsive-detector.js +42 -46
- package/dist/retry-policy.js +46 -50
- package/dist/robots-txt.js +69 -83
- package/dist/screen-reader.js +45 -70
- package/dist/screenshot-masker.js +59 -77
- package/dist/seo-checker.js +88 -107
- package/dist/session-capture.js +192 -212
- package/dist/session-export.js +107 -133
- package/dist/sitemap-writer.js +38 -41
- package/dist/sitemap.js +141 -202
- package/dist/skyvern.js +123 -148
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +74 -90
- package/dist/stealth/human-behavior.js +156 -173
- package/dist/telemetry.js +68 -82
- package/dist/test-executor.js +309 -335
- package/dist/tls-fingerprint.js +77 -88
- package/dist/url-pattern-completer.js +71 -90
- package/dist/visual-diff-verdict.js +23 -38
- package/dist/webhook-notifier.js +71 -69
- package/dist/websocket-detector.js +56 -68
- package/dist/worker.js +29 -38
- package/package.json +1 -1
- package/src/job-queue.ts +136 -0
|
@@ -1,62 +1,63 @@
|
|
|
1
|
-
import crypto from
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
function sha256(input) {
|
|
3
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
4
4
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
2
|
-
import { askTrackedWithImages, askTracked, deterministicSeed } from
|
|
3
|
-
import { AppBrain } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
package/dist/form-detector.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|