@detiq/crawler 1.0.4 → 1.0.5
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/crawler.d.ts +14 -0
- package/dist/crawler.js +170 -39
- package/dist/crawler.js.map +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/job-queue.d.ts +7 -0
- package/dist/job-queue.js +43 -8
- package/dist/job-queue.js.map +2 -2
- package/dist/phash.d.ts +12 -0
- package/dist/phash.js +21 -0
- package/dist/phash.js.map +2 -2
- package/dist/session-capture.d.ts +4 -1
- package/dist/stealth/human-behavior.js +12 -11
- package/dist/stealth/human-behavior.js.map +2 -2
- package/dist/test-executor.d.ts +8 -1
- package/dist/visual-diff-verdict.d.ts +1 -1
- package/package.json +1 -1
- package/src/crawler.ts +98 -27
- package/src/job-queue.ts +50 -4
- package/types/index.d.ts +2 -0
- package/types/job-queue.d.ts +7 -0
- package/types/phash.d.ts +12 -0
package/dist/crawler.d.ts
CHANGED
|
@@ -35,10 +35,24 @@ export declare function crawlProject(opts: {
|
|
|
35
35
|
tenantId: string;
|
|
36
36
|
projectId: string;
|
|
37
37
|
appUrl: string;
|
|
38
|
+
/** The viewport used for this crawl pass */
|
|
39
|
+
viewport?: 'DESKTOP' | 'MOBILE_PORTRAIT' | 'TABLET';
|
|
38
40
|
credentials?: Credentials;
|
|
39
41
|
/** Playwright storageState JSON string — if present, session is restored and AI login is skipped. */
|
|
40
42
|
storageState?: string;
|
|
41
43
|
screenshotDir?: string;
|
|
44
|
+
/** Cloud storage config for immediate screenshot upload. If set, screenshots are uploaded to cloud immediately after capture and cloudUrl is persisted. */
|
|
45
|
+
storageConfig?: {
|
|
46
|
+
provider: string;
|
|
47
|
+
bucket: string;
|
|
48
|
+
region?: string;
|
|
49
|
+
accountId?: string;
|
|
50
|
+
endpointUrl?: string;
|
|
51
|
+
accessKeyId?: string;
|
|
52
|
+
secretAccessKey?: string;
|
|
53
|
+
prefix?: string;
|
|
54
|
+
publicUrl?: string;
|
|
55
|
+
} | null;
|
|
42
56
|
/** When true, run a fast URL-only discovery crawl — skips screenshots, DB saves, and AI element extraction. Returns discoveredUrls in the result object. */
|
|
43
57
|
discoveryMode?: boolean;
|
|
44
58
|
maxScreens?: number;
|
package/dist/crawler.js
CHANGED
|
@@ -49,6 +49,13 @@ import { screensToSitemapUrls, writeSitemap } from "./sitemap-writer.js";
|
|
|
49
49
|
import { generateOpenApiSpec, specToYaml } from "./openapi-generator.js";
|
|
50
50
|
import { WebhookNotifier } from "./webhook-notifier.js";
|
|
51
51
|
import { generatePlaywrightTest } from "./playwright-recorder.js";
|
|
52
|
+
const _crawlLLMCache = /* @__PURE__ */ new Map();
|
|
53
|
+
async function getCachedCrawlLLM(tenantId) {
|
|
54
|
+
if (_crawlLLMCache.has(tenantId)) return _crawlLLMCache.get(tenantId);
|
|
55
|
+
const cfg = await getTaskAgentLLMConfig(tenantId, "crawl").catch(() => null);
|
|
56
|
+
_crawlLLMCache.set(tenantId, cfg);
|
|
57
|
+
return cfg;
|
|
58
|
+
}
|
|
52
59
|
function getGitInfo() {
|
|
53
60
|
try {
|
|
54
61
|
const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8", timeout: 3e3 }).trim();
|
|
@@ -325,39 +332,104 @@ async function crawlProject(opts, onProgress) {
|
|
|
325
332
|
}
|
|
326
333
|
};
|
|
327
334
|
const isDocker = !!process.env.PLAYWRIGHT_IN_DOCKER || process.env.NODE_ENV === "production";
|
|
328
|
-
|
|
329
|
-
let
|
|
330
|
-
let
|
|
335
|
+
let stagehandModel = config.llmModel.includes("/") ? config.llmModel : `openai/${config.llmModel}`;
|
|
336
|
+
let stagehandModelOpts = {};
|
|
337
|
+
let stagehandConfigured = false;
|
|
331
338
|
let configuredProvider = "";
|
|
332
339
|
try {
|
|
333
340
|
const crawlLLM = await getTaskAgentLLMConfig(opts.tenantId, "crawl");
|
|
334
341
|
if (crawlLLM) {
|
|
335
342
|
configuredProvider = crawlLLM.provider;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
const hasKey = crawlLLM.apiKey !== void 0 && crawlLLM.apiKey !== null;
|
|
344
|
+
const apiKey = hasKey ? crawlLLM.apiKey : void 0;
|
|
345
|
+
const baseUrl = crawlLLM.baseUrl || void 0;
|
|
346
|
+
const m = crawlLLM.model;
|
|
347
|
+
if (m && (hasKey || crawlLLM.provider === "ollama" || crawlLLM.provider === "bedrock" || crawlLLM.provider === "vertex")) {
|
|
348
|
+
stagehandConfigured = true;
|
|
349
|
+
switch (crawlLLM.provider) {
|
|
350
|
+
case "anthropic":
|
|
351
|
+
stagehandModel = m.includes("/") ? m : `anthropic/${m}`;
|
|
352
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
353
|
+
break;
|
|
354
|
+
case "openai":
|
|
355
|
+
stagehandModel = m.includes("/") ? m : `openai/${m}`;
|
|
356
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
357
|
+
break;
|
|
358
|
+
case "gemini":
|
|
359
|
+
stagehandModel = `google/${m}`;
|
|
360
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
361
|
+
break;
|
|
362
|
+
case "grok":
|
|
363
|
+
stagehandModel = `xai/${m}`;
|
|
364
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
365
|
+
break;
|
|
366
|
+
case "openrouter":
|
|
367
|
+
stagehandModel = `openai/${m}`;
|
|
368
|
+
stagehandModelOpts = { ...apiKey ? { apiKey } : {}, baseURL: baseUrl || "https://openrouter.ai/api/v1" };
|
|
369
|
+
break;
|
|
370
|
+
case "ollama":
|
|
371
|
+
stagehandModel = `ollama/${m}`;
|
|
372
|
+
stagehandModelOpts = { baseURL: baseUrl || "http://localhost:11434" };
|
|
373
|
+
break;
|
|
374
|
+
case "nvidia":
|
|
375
|
+
stagehandModel = `openai/${m.startsWith("openai/") ? m.slice(7) : m}`;
|
|
376
|
+
stagehandModelOpts = { ...apiKey ? { apiKey } : {}, baseURL: baseUrl || "https://integrate.api.nvidia.com/v1" };
|
|
377
|
+
break;
|
|
378
|
+
case "groq":
|
|
379
|
+
stagehandModel = `groq/${m}`;
|
|
380
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
381
|
+
break;
|
|
382
|
+
case "mistral":
|
|
383
|
+
stagehandModel = `mistral/${m}`;
|
|
384
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
385
|
+
break;
|
|
386
|
+
case "deepseek":
|
|
387
|
+
stagehandModel = `deepseek/${m}`;
|
|
388
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
389
|
+
break;
|
|
390
|
+
case "perplexity":
|
|
391
|
+
stagehandModel = `perplexity/${m}`;
|
|
392
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
393
|
+
break;
|
|
394
|
+
case "togetherai":
|
|
395
|
+
stagehandModel = `togetherai/${m}`;
|
|
396
|
+
stagehandModelOpts = apiKey ? { apiKey } : {};
|
|
397
|
+
break;
|
|
398
|
+
case "bedrock":
|
|
399
|
+
stagehandModel = `bedrock/${m}`;
|
|
400
|
+
stagehandModelOpts = {};
|
|
401
|
+
break;
|
|
402
|
+
case "vertex":
|
|
403
|
+
stagehandModel = `vertex/${m}`;
|
|
404
|
+
stagehandModelOpts = {};
|
|
405
|
+
break;
|
|
406
|
+
case "azure":
|
|
407
|
+
stagehandModel = `azure/${m}`;
|
|
408
|
+
stagehandModelOpts = { ...apiKey ? { apiKey } : {}, ...baseUrl ? { baseURL: baseUrl } : {} };
|
|
409
|
+
break;
|
|
410
|
+
default:
|
|
411
|
+
stagehandModel = `openai/${m}`;
|
|
412
|
+
stagehandModelOpts = { ...apiKey ? { apiKey } : {}, ...baseUrl ? { baseURL: baseUrl } : {} };
|
|
344
413
|
}
|
|
345
414
|
}
|
|
346
415
|
}
|
|
347
416
|
} catch {
|
|
348
417
|
}
|
|
349
|
-
if (!
|
|
418
|
+
if (!stagehandConfigured) {
|
|
350
419
|
const envModel = process.env.STAGEHAND_MODEL ?? "";
|
|
351
420
|
const preferAnthropic = envModel.includes("claude") || !envModel && !process.env.OPENAI_API_KEY;
|
|
352
421
|
if (preferAnthropic && process.env.ANTHROPIC_API_KEY) {
|
|
353
|
-
|
|
422
|
+
stagehandModelOpts = { apiKey: process.env.ANTHROPIC_API_KEY };
|
|
354
423
|
stagehandModel = envModel.includes("/") ? envModel : `anthropic/${envModel || "claude-sonnet-4-6"}`;
|
|
424
|
+
stagehandConfigured = true;
|
|
355
425
|
} else if (process.env.OPENAI_API_KEY) {
|
|
356
|
-
|
|
426
|
+
stagehandModelOpts = { apiKey: process.env.OPENAI_API_KEY };
|
|
357
427
|
stagehandModel = envModel.includes("/") ? envModel : `openai/${envModel || "gpt-4o"}`;
|
|
428
|
+
stagehandConfigured = true;
|
|
358
429
|
} else if (process.env.ANTHROPIC_API_KEY) {
|
|
359
|
-
|
|
430
|
+
stagehandModelOpts = { apiKey: process.env.ANTHROPIC_API_KEY };
|
|
360
431
|
stagehandModel = envModel.includes("/") ? envModel : `anthropic/${envModel || "claude-sonnet-4-6"}`;
|
|
432
|
+
stagehandConfigured = true;
|
|
361
433
|
}
|
|
362
434
|
}
|
|
363
435
|
const elementLLM = await AppBrain.getTaskLLMConfig(opts.tenantId, "elementExtraction").catch(() => null);
|
|
@@ -390,10 +462,9 @@ async function crawlProject(opts, onProgress) {
|
|
|
390
462
|
type: "CRAWL_PROFILE",
|
|
391
463
|
payload: { profile: domainProfile, routeDecision }
|
|
392
464
|
}).catch((err) => console.warn("[crawler] profile evidence save failed:", err?.message ?? err));
|
|
393
|
-
if (!
|
|
394
|
-
const unsupported = configuredProvider && !["anthropic", "openai", "openrouter"].includes(configuredProvider);
|
|
465
|
+
if (!stagehandConfigured) {
|
|
395
466
|
throw new Error(
|
|
396
|
-
|
|
467
|
+
configuredProvider ? `No API key configured for provider "${configuredProvider}". Go to Workspace \u2192 Settings \u2192 AI Provider.` : "No AI provider configured. Go to Workspace \u2192 Settings \u2192 AI Provider to set up an AI provider for browser crawling."
|
|
397
468
|
);
|
|
398
469
|
}
|
|
399
470
|
const stealthArgs = [
|
|
@@ -529,7 +600,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
529
600
|
};
|
|
530
601
|
const stagehand = new Stagehand({
|
|
531
602
|
env: "LOCAL",
|
|
532
|
-
model: { modelName: stagehandModel,
|
|
603
|
+
model: { modelName: stagehandModel, ...stagehandModelOpts },
|
|
533
604
|
serverCache: true,
|
|
534
605
|
verbose: 1,
|
|
535
606
|
logger: stagehandLogger,
|
|
@@ -1339,7 +1410,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1339
1410
|
console.warn(`[crawler] connection failed for ${url} \u2014 skipping (chrome-error page)`);
|
|
1340
1411
|
continue;
|
|
1341
1412
|
}
|
|
1342
|
-
await page.waitForLoadState("
|
|
1413
|
+
await page.waitForLoadState("load", { timeout: 8e3 }).catch(() => {
|
|
1343
1414
|
});
|
|
1344
1415
|
await waitForStableDOM(page);
|
|
1345
1416
|
if (!opts.discoveryMode) {
|
|
@@ -1798,10 +1869,11 @@ async function crawlProject(opts, onProgress) {
|
|
|
1798
1869
|
return "/";
|
|
1799
1870
|
}
|
|
1800
1871
|
})()}`);
|
|
1872
|
+
const pageDpr = await page.evaluate(() => window.devicePixelRatio ?? 1).catch(() => 1);
|
|
1801
1873
|
const [, elements, nextLinks, atElements] = await Promise.all([
|
|
1802
1874
|
page.screenshot({ path: shotPath, fullPage: true }).then(async () => {
|
|
1803
1875
|
const cloudUrl = await uploadToCloud(shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`);
|
|
1804
|
-
return AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP", cloudUrl);
|
|
1876
|
+
return AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP", cloudUrl, pageDpr);
|
|
1805
1877
|
}).catch((err) => console.error("[crawler] page.screenshot failed:", err)),
|
|
1806
1878
|
extractElements(stagehand, page, elementLLM ?? void 0).catch((err) => {
|
|
1807
1879
|
console.error("[crawler] extractElements failed:", err);
|
|
@@ -1883,7 +1955,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1883
1955
|
await fs.writeFile(mdPath, scrubbed, "utf8");
|
|
1884
1956
|
const summaryPromise = (async () => {
|
|
1885
1957
|
try {
|
|
1886
|
-
const llmCfg = await
|
|
1958
|
+
const llmCfg = await getCachedCrawlLLM(opts.tenantId);
|
|
1887
1959
|
const snippet = extracted.markdown.slice(0, 1200);
|
|
1888
1960
|
const { text, usage } = await askTracked(
|
|
1889
1961
|
"You are a QA analyst. Given page content, write ONE crisp sentence describing what this screen does or shows. Be specific. No preamble.",
|
|
@@ -2020,15 +2092,38 @@ ${snippet}`,
|
|
|
2020
2092
|
logger.warn({ screenId: screen.id, err: String(aiErr) }, "[crawler] AI visual compare failed, keeping pHash-only verdict");
|
|
2021
2093
|
}
|
|
2022
2094
|
}
|
|
2023
|
-
|
|
2095
|
+
let { changeType, recommendation, changes: allChanges, bugReportDraft, aiSummary } = verdict;
|
|
2096
|
+
if (changeType !== "NONE") {
|
|
2097
|
+
try {
|
|
2098
|
+
const project = await AppBrain.getProject(opts.tenantId, opts.projectId);
|
|
2099
|
+
const threshold = project?.visualDiffThreshold ?? null;
|
|
2100
|
+
if (threshold !== null && pHashDistance < threshold) {
|
|
2101
|
+
changeType = "NONE";
|
|
2102
|
+
}
|
|
2103
|
+
} catch {
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2024
2106
|
if (changeType !== "NONE") {
|
|
2025
2107
|
let newScreenshotCloudUrl;
|
|
2108
|
+
let diffImageCloudUrl;
|
|
2026
2109
|
try {
|
|
2027
2110
|
const { getTenantStorageConfig, uploadScreenshotToTenantStorage, buildArtifactKey } = await import("@detiq/app-brain");
|
|
2028
2111
|
const storageCfg = await getTenantStorageConfig(opts.tenantId).catch(() => null);
|
|
2029
2112
|
if (storageCfg) {
|
|
2030
2113
|
const key = buildArtifactKey(opts.tenantId, opts.projectId, "visual-diffs", `${screen.id}-v${screen.version}.png`, storageCfg.prefix);
|
|
2031
2114
|
newScreenshotCloudUrl = await uploadScreenshotToTenantStorage(storageCfg, shotPath, key);
|
|
2115
|
+
try {
|
|
2116
|
+
const { generatePixelmatchDiff } = await import("./phash.js");
|
|
2117
|
+
const diffBuffer = await generatePixelmatchDiff(baseline.storagePath, shotPath, baseline.ignoredRegions ?? []);
|
|
2118
|
+
const diffTmpPath = `${shotPath}.diff.png`;
|
|
2119
|
+
await fs.writeFile(diffTmpPath, diffBuffer);
|
|
2120
|
+
const diffKey = buildArtifactKey(opts.tenantId, opts.projectId, "visual-diffs", `${screen.id}-v${screen.version}-diff.png`, storageCfg.prefix);
|
|
2121
|
+
diffImageCloudUrl = await uploadScreenshotToTenantStorage(storageCfg, diffTmpPath, diffKey);
|
|
2122
|
+
fs.unlink(diffTmpPath).catch(() => {
|
|
2123
|
+
});
|
|
2124
|
+
} catch (diffErr) {
|
|
2125
|
+
logger.warn({ screenId: screen.id, err: String(diffErr) }, "[crawler] pixelmatch diff generation failed, skipping");
|
|
2126
|
+
}
|
|
2032
2127
|
}
|
|
2033
2128
|
} catch {
|
|
2034
2129
|
}
|
|
@@ -2036,6 +2131,7 @@ ${snippet}`,
|
|
|
2036
2131
|
baselineId: baseline.id,
|
|
2037
2132
|
newScreenshotPath: shotPath,
|
|
2038
2133
|
newScreenshotCloudUrl,
|
|
2134
|
+
diffImageCloudUrl,
|
|
2039
2135
|
diffScore: pHashDistance,
|
|
2040
2136
|
changeType,
|
|
2041
2137
|
changes: allChanges,
|
|
@@ -2056,17 +2152,20 @@ ${snippet}`,
|
|
|
2056
2152
|
})();
|
|
2057
2153
|
}
|
|
2058
2154
|
const linkMappings = await extractLinkMappings(page, origin);
|
|
2059
|
-
for (
|
|
2060
|
-
const
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2155
|
+
for (let _ei = 0; _ei < elements.length; _ei += 4) {
|
|
2156
|
+
const _batch = elements.slice(_ei, _ei + 4);
|
|
2157
|
+
await Promise.all(_batch.map(async (el) => {
|
|
2158
|
+
const saved = await AppBrain.saveElement(opts.tenantId, opts.projectId, screen.id, el.meaning, el.role, el.expectedData, el.notes, void 0, true, el.boundingRect, el.ariaState, el.parentLandmark);
|
|
2159
|
+
const label = el.meaning;
|
|
2160
|
+
const role = el.role ?? "unknown";
|
|
2161
|
+
const selectorHint = atByRole.get(role) ?? role;
|
|
2162
|
+
const selectors = [
|
|
2163
|
+
{ type: "role-label", value: `${role}:${label}`, confidence: 0.9 },
|
|
2164
|
+
{ type: "aria-label", value: label, confidence: 0.85 },
|
|
2165
|
+
{ type: "tag-hint", value: selectorHint, confidence: 0.6 }
|
|
2166
|
+
];
|
|
2167
|
+
AppBrain.saveElementFingerprint(opts.tenantId, opts.projectId, saved.id, selectors).catch(console.warn);
|
|
2168
|
+
}));
|
|
2070
2169
|
}
|
|
2071
2170
|
for (const lm of linkMappings) {
|
|
2072
2171
|
const safety = assessActionSafety(lm.label, domainProfile);
|
|
@@ -2366,7 +2465,7 @@ ${snippet}`,
|
|
|
2366
2465
|
}
|
|
2367
2466
|
try {
|
|
2368
2467
|
const { runUrlPatternCompletion } = await import("./url-pattern-completer.js");
|
|
2369
|
-
const llmCfgForPatterns = await
|
|
2468
|
+
const llmCfgForPatterns = await getCachedCrawlLLM(opts.tenantId);
|
|
2370
2469
|
const urlPatternCandidates = await runUrlPatternCompletion(
|
|
2371
2470
|
Array.from(visited.keys()),
|
|
2372
2471
|
opts.appUrl,
|
|
@@ -2427,7 +2526,7 @@ ${snippet}`,
|
|
|
2427
2526
|
if (visited.size >= 3) {
|
|
2428
2527
|
try {
|
|
2429
2528
|
await onProgress?.(80, "Running gap analysis\u2026");
|
|
2430
|
-
const llmCfgForGap = await
|
|
2529
|
+
const llmCfgForGap = await getCachedCrawlLLM(opts.tenantId);
|
|
2431
2530
|
const discoveredForGap = Array.from(visited.entries()).map(([url, screenId]) => {
|
|
2432
2531
|
const d = discovered.find((dd) => dd.screenId === screenId);
|
|
2433
2532
|
return { url, name: d ? new URL(url).pathname.split("/").filter(Boolean).pop() ?? "screen" : void 0, elements: d?.elements };
|
|
@@ -2876,7 +2975,41 @@ ${snippet}`,
|
|
|
2876
2975
|
const ELEMENT_NOISE = /^(cookie|consent|privacy|accept|decline|dismiss|close|copy|©|v\d|switch to (dark|light)|dark mode|light mode|toggle (dark|light|theme)|theme (toggle|switch)|share|tweet|follow|subscribe to newsletter|skip to|back to top|\s*$)/i;
|
|
2877
2976
|
async function extractElements(stagehand, page, llmCfg) {
|
|
2878
2977
|
const domElements = await domFallbackElements(page);
|
|
2879
|
-
|
|
2978
|
+
const iframeElements = [];
|
|
2979
|
+
try {
|
|
2980
|
+
const iframes = page.frames ? page.frames() : [];
|
|
2981
|
+
for (const frame of iframes) {
|
|
2982
|
+
if (frame === page.mainFrame?.()) continue;
|
|
2983
|
+
const iframeUrl = await frame.url().catch(() => "");
|
|
2984
|
+
if (!iframeUrl || iframeUrl === "about:blank" || iframeUrl.startsWith("javascript:")) continue;
|
|
2985
|
+
const frameEls = await domFallbackElements(frame).catch(() => []);
|
|
2986
|
+
for (const el of frameEls.slice(0, 10)) {
|
|
2987
|
+
iframeElements.push({ ...el, notes: el.notes ? `${el.notes} [in iframe]` : "[in iframe]" });
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
} catch {
|
|
2991
|
+
}
|
|
2992
|
+
let dynamicElements = [];
|
|
2993
|
+
if (domElements.length > 0) {
|
|
2994
|
+
try {
|
|
2995
|
+
dynamicElements = await page.evaluate(() => {
|
|
2996
|
+
const out = [];
|
|
2997
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2998
|
+
const triggers = document.querySelectorAll('[role="combobox"],[role="listbox"] [role="option"],[aria-haspopup]');
|
|
2999
|
+
triggers.forEach((el) => {
|
|
3000
|
+
const label = (el.getAttribute("aria-label") || el.textContent?.trim() || "").slice(0, 80);
|
|
3001
|
+
if (!label || seen.has(label)) return;
|
|
3002
|
+
const role = el.getAttribute("role") ?? el.tagName.toLowerCase();
|
|
3003
|
+
seen.add(label);
|
|
3004
|
+
out.push({ meaning: label, role, notes: "[dynamic/revealed]" });
|
|
3005
|
+
});
|
|
3006
|
+
return out.slice(0, 10);
|
|
3007
|
+
}).catch(() => []);
|
|
3008
|
+
} catch {
|
|
3009
|
+
dynamicElements = [];
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
if (domElements.length >= 2) return [...domElements, ...iframeElements, ...dynamicElements];
|
|
2880
3013
|
const contentElements = await contentFallbackElements(page);
|
|
2881
3014
|
if (contentElements.length > 0) return [...domElements, ...contentElements];
|
|
2882
3015
|
try {
|
|
@@ -3540,8 +3673,6 @@ async function waitForScreenshotReady(page) {
|
|
|
3540
3673
|
}).catch(() => {
|
|
3541
3674
|
}));
|
|
3542
3675
|
await withHardCap(waitForStableDOM(page, 2e3, 500));
|
|
3543
|
-
await withHardCap(page.waitForTimeout(250).catch(() => {
|
|
3544
|
-
}));
|
|
3545
3676
|
} catch {
|
|
3546
3677
|
}
|
|
3547
3678
|
}
|