@detiq/crawler 1.1.6 → 1.1.7
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.js +160 -16
- package/dist/crawler.js.map +2 -2
- package/dist/failure-analysis-agent.js +3 -2
- package/dist/failure-analysis-agent.js.map +2 -2
- package/dist/gap-pass.js +1 -0
- package/dist/gap-pass.js.map +2 -2
- package/dist/job-queue.js +8 -4
- package/dist/job-queue.js.map +2 -2
- package/dist/sitemap.js +5 -3
- package/dist/sitemap.js.map +2 -2
- package/dist/url-pattern-completer.js +1 -0
- package/dist/url-pattern-completer.js.map +2 -2
- package/package.json +1 -1
package/dist/crawler.js
CHANGED
|
@@ -10,7 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import fs from "node:fs/promises";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { AppBrain, recordLLMCall } from "@detiq/app-brain";
|
|
13
|
-
import { aiVisualCompare, askTracked, estimateCost, getTaskAgentLLMConfig } from "@detiq/agents";
|
|
13
|
+
import { aiVisualCompare, askTracked, deterministicSeed, estimateCost, getTaskAgentLLMConfig } from "@detiq/agents";
|
|
14
14
|
import { config } from "@detiq/core";
|
|
15
15
|
import { pHashVerdict, mergeAiVisualVerdict } from "./visual-diff-verdict.js";
|
|
16
16
|
import { skyvernFallback, skyvernNavigate } from "./skyvern.js";
|
|
@@ -795,6 +795,10 @@ async function crawlProject(opts, onProgress) {
|
|
|
795
795
|
await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch(() => {
|
|
796
796
|
});
|
|
797
797
|
await waitForStableDOM(page);
|
|
798
|
+
const homeFingerprintTitle = await page.title().catch(() => "");
|
|
799
|
+
const homeFingerprintElemCount = await page.evaluate(
|
|
800
|
+
() => document.querySelectorAll('button,a,input,select,textarea,[role="button"]').length
|
|
801
|
+
).catch(() => -1);
|
|
798
802
|
if (!opts.discoveryMode) {
|
|
799
803
|
try {
|
|
800
804
|
await runAuthDiscoveryPhase(page, opts, new URL(opts.appUrl).origin, onProgress);
|
|
@@ -1275,6 +1279,93 @@ async function crawlProject(opts, onProgress) {
|
|
|
1275
1279
|
let totalAttemptsThisSegment = 0;
|
|
1276
1280
|
const maxAttempts = Math.max(segmentSize * 4, 200);
|
|
1277
1281
|
let pendingSessionRestore = null;
|
|
1282
|
+
const CRAWL_PAGE_PARALLELISM = Math.max(1, parseInt(process.env.CRAWL_PAGE_PARALLELISM ?? "5", 10));
|
|
1283
|
+
async function runLiteWorker(workerPage) {
|
|
1284
|
+
while (queue.length > 0 && visited.size < maxScreens && visited.size < segmentSize) {
|
|
1285
|
+
if (opts.abortSignal?.aborted) break;
|
|
1286
|
+
if (++totalAttemptsThisSegment > maxAttempts) break;
|
|
1287
|
+
const url = queue.shift();
|
|
1288
|
+
if (!url) break;
|
|
1289
|
+
const canonUrl = canonicalize(url);
|
|
1290
|
+
if (visited.has(canonUrl)) continue;
|
|
1291
|
+
try {
|
|
1292
|
+
const { pathname } = new URL(url);
|
|
1293
|
+
if (SKIP_PATH.test(pathname) || isGeneratedDynamicPath(pathname)) continue;
|
|
1294
|
+
} catch {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
visited.set(canonUrl, "_pending_");
|
|
1298
|
+
try {
|
|
1299
|
+
const navErr = await workerPage.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 }).then(() => null, (e) => e);
|
|
1300
|
+
if (navErr && /timeout/i.test(String(navErr?.message ?? navErr))) {
|
|
1301
|
+
await workerPage.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
await workerPage.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => {
|
|
1305
|
+
});
|
|
1306
|
+
if (homeFingerprintElemCount >= 0 && url !== startUrl) {
|
|
1307
|
+
const wTitle = await workerPage.title().catch(() => "");
|
|
1308
|
+
if (wTitle === homeFingerprintTitle && wTitle !== "") {
|
|
1309
|
+
const wElemCount = await workerPage.evaluate(
|
|
1310
|
+
() => document.querySelectorAll('button,a,input,select,textarea,[role="button"]').length
|
|
1311
|
+
).catch(() => -1);
|
|
1312
|
+
if (wElemCount >= 0 && wElemCount === homeFingerprintElemCount) {
|
|
1313
|
+
console.log(`[lite-worker] skip ${url} \u2014 soft-404 (homepage fingerprint match)`);
|
|
1314
|
+
visited.set(canonUrl, "_soft404_");
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const pathParts = new URL(url).pathname.split("/").filter(Boolean);
|
|
1320
|
+
const name = pathParts.map((p) => p.charAt(0).toUpperCase() + p.slice(1).replace(/[-_]/g, " ")).join(" \u2014 ") || "Home";
|
|
1321
|
+
const screen = await AppBrain.saveScreen(opts.tenantId, opts.projectId, name, url, opts.jobId, discoveryMethodFor(url));
|
|
1322
|
+
visited.set(canonUrl, screen.id);
|
|
1323
|
+
const shotPath = path.join(opts.screenshotDir ?? "/tmp", `${screen.id}-desktop-v${screen.version}.png`);
|
|
1324
|
+
const [elements] = await Promise.all([
|
|
1325
|
+
domFallbackElements(workerPage).catch(() => []),
|
|
1326
|
+
workerPage.screenshot({ path: shotPath, fullPage: true }).then(async () => {
|
|
1327
|
+
const cloudUrl = await uploadToCloud(shotPath, `${opts.tenantId}/${opts.projectId}/screenshots/${path.basename(shotPath)}`);
|
|
1328
|
+
return AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, "DESKTOP", cloudUrl);
|
|
1329
|
+
}).catch(() => {
|
|
1330
|
+
})
|
|
1331
|
+
]);
|
|
1332
|
+
const nextLinks = await workerPage.evaluate((originStr) => {
|
|
1333
|
+
return Array.from(document.querySelectorAll("a[href]")).map((a) => {
|
|
1334
|
+
try {
|
|
1335
|
+
return new URL(a.getAttribute("href"), window.location.href).href;
|
|
1336
|
+
} catch {
|
|
1337
|
+
return "";
|
|
1338
|
+
}
|
|
1339
|
+
}).filter((h) => h && new URL(h).origin === originStr && !h.includes("#")).slice(0, 200);
|
|
1340
|
+
}, origin).catch(() => []);
|
|
1341
|
+
for (const link of nextLinks) {
|
|
1342
|
+
const cLink = canonicalize(link);
|
|
1343
|
+
if (!visited.has(cLink)) {
|
|
1344
|
+
queue.push(link);
|
|
1345
|
+
edges.push({ fromUrl: url, toUrl: link });
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
discovered.push({ url, screenId: screen.id, elements: elements.length, elementFacts: elements, screenshotPath: shotPath, version: screen.version });
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
logger.warn({ url, err: err?.message ?? String(err) }, "[lite-worker] URL processing failed");
|
|
1351
|
+
discovered.push({ url, error: err?.message ?? String(err) });
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const liteWorkerPromises = [];
|
|
1356
|
+
if (CRAWL_PAGE_PARALLELISM > 1 && playwrightBrowser) {
|
|
1357
|
+
const pwCtx = playwrightBrowser.contexts()[0];
|
|
1358
|
+
for (let i = 1; i < CRAWL_PAGE_PARALLELISM; i++) {
|
|
1359
|
+
const wp = await pwCtx.newPage();
|
|
1360
|
+
await wp.addInitScript(() => {
|
|
1361
|
+
Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
|
|
1362
|
+
}).catch(() => {
|
|
1363
|
+
});
|
|
1364
|
+
wp.setDefaultTimeout(45e3);
|
|
1365
|
+
liteWorkerPromises.push(runLiteWorker(wp).finally(() => wp.close().catch(() => {
|
|
1366
|
+
})));
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1278
1369
|
while (queue.length > 0 && visited.size < maxScreens && visited.size < segmentSize) {
|
|
1279
1370
|
if (pendingSessionRestore) {
|
|
1280
1371
|
await pendingSessionRestore();
|
|
@@ -1390,9 +1481,16 @@ async function crawlProject(opts, onProgress) {
|
|
|
1390
1481
|
logger.info({ url }, "[crawler] temporarily cleared session for public auth route");
|
|
1391
1482
|
}
|
|
1392
1483
|
}
|
|
1393
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout:
|
|
1394
|
-
|
|
1395
|
-
|
|
1484
|
+
const navErr15 = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 }).then(() => null, (e) => e);
|
|
1485
|
+
if (navErr15) {
|
|
1486
|
+
if (/timeout/i.test(String(navErr15?.message ?? navErr15))) {
|
|
1487
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch((navErr) => {
|
|
1488
|
+
logger.warn({ url, err: navErr?.message ?? String(navErr) }, "[crawler] navigation failed \u2014 skipping");
|
|
1489
|
+
});
|
|
1490
|
+
} else {
|
|
1491
|
+
logger.warn({ url, err: navErr15?.message ?? String(navErr15) }, "[crawler] navigation failed \u2014 skipping");
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1396
1494
|
rawPage.off("response", onRedirectResponse);
|
|
1397
1495
|
if (redirectHops.length > 0) {
|
|
1398
1496
|
redirectRegistry.register({
|
|
@@ -1661,6 +1759,16 @@ async function crawlProject(opts, onProgress) {
|
|
|
1661
1759
|
seenDomHashes.add(preDomHash);
|
|
1662
1760
|
continue;
|
|
1663
1761
|
}
|
|
1762
|
+
if (homeFingerprintElemCount >= 0 && url !== startUrl && pageTitle === homeFingerprintTitle && pageTitle !== "") {
|
|
1763
|
+
const pElemCount = await page.evaluate(
|
|
1764
|
+
() => document.querySelectorAll('button,a,input,select,textarea,[role="button"]').length
|
|
1765
|
+
).catch(() => -1);
|
|
1766
|
+
if (pElemCount >= 0 && pElemCount === homeFingerprintElemCount) {
|
|
1767
|
+
console.log(`[crawler] skip ${url} \u2014 soft-404 (homepage fingerprint match, title="${pageTitle}" elems=${pElemCount})`);
|
|
1768
|
+
seenDomHashes.add(preDomHash);
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1664
1772
|
const rawPerf = await page.evaluate(() => {
|
|
1665
1773
|
const nav = performance.getEntriesByType("navigation")[0];
|
|
1666
1774
|
const paints = performance.getEntriesByType("paint");
|
|
@@ -1897,7 +2005,8 @@ async function crawlProject(opts, onProgress) {
|
|
|
1897
2005
|
}),
|
|
1898
2006
|
// Always run AI link discovery (even when cache exists) — cache is merged in, not used as a replacement.
|
|
1899
2007
|
// Skipping AI when cache exists was a bug: stale cache from broken crawls permanently blocked discovery.
|
|
1900
|
-
|
|
2008
|
+
// Skip AI on simple pages (< 5 interactive elements) — DOM-only is sufficient and saves ~12s per page.
|
|
2009
|
+
discoverLinks(stagehand, origin, page, linkLLM ?? void 0, aiSkipCount > 0 || preDomCount < 5).then((freshLinks) => {
|
|
1901
2010
|
if (aiSkipCount > 0) aiSkipCount--;
|
|
1902
2011
|
aiErrorStreak = 0;
|
|
1903
2012
|
const merged = (cachedLinks ? Array.from(/* @__PURE__ */ new Set([...cachedLinks, ...freshLinks])) : freshLinks).filter((href) => isUsefulNavigationUrl(href, origin));
|
|
@@ -1987,6 +2096,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1987
2096
|
const llmCfg = await getCachedCrawlLLM(opts.tenantId);
|
|
1988
2097
|
const snippet = extracted.markdown.slice(0, 1200);
|
|
1989
2098
|
if (snippet.trim().length < 80) return void 0;
|
|
2099
|
+
const summarySeed = deterministicSeed(`${opts.tenantId}:${opts.projectId ?? ""}:page-summary:${url}`);
|
|
1990
2100
|
const { text, usage } = await askTracked(
|
|
1991
2101
|
"You are a QA analyst. Output ONLY a single complete sentence (no preamble, no quotes) describing what this screen does or shows. Be specific. Stop after the first sentence.",
|
|
1992
2102
|
`Screen: "${name}"
|
|
@@ -1995,7 +2105,9 @@ URL: ${url}
|
|
|
1995
2105
|
Content:
|
|
1996
2106
|
${snippet}`,
|
|
1997
2107
|
llmCfg,
|
|
1998
|
-
200
|
|
2108
|
+
200,
|
|
2109
|
+
void 0,
|
|
2110
|
+
{ temperature: 0, seed: summarySeed }
|
|
1999
2111
|
);
|
|
2000
2112
|
if (usage) {
|
|
2001
2113
|
recordLLMCall({
|
|
@@ -2394,6 +2506,7 @@ ${snippet}`,
|
|
|
2394
2506
|
discovered.push({ url, error: err?.message ?? String(err) });
|
|
2395
2507
|
}
|
|
2396
2508
|
}
|
|
2509
|
+
if (liteWorkerPromises.length > 0) await Promise.all(liteWorkerPromises);
|
|
2397
2510
|
if (opts.discoveryMode) {
|
|
2398
2511
|
return {
|
|
2399
2512
|
discoveredUrls: Array.from(visited.values()),
|
|
@@ -3320,11 +3433,20 @@ async function contentFallbackElements(page) {
|
|
|
3320
3433
|
return page.evaluate(() => {
|
|
3321
3434
|
const out = [];
|
|
3322
3435
|
const seen = /* @__PURE__ */ new Set();
|
|
3323
|
-
const
|
|
3436
|
+
const captureRect = (el) => {
|
|
3437
|
+
try {
|
|
3438
|
+
const r = el.getBoundingClientRect();
|
|
3439
|
+
if (r.width === 0 && r.height === 0) return void 0;
|
|
3440
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height, top: r.top, right: r.right, bottom: r.bottom, left: r.left };
|
|
3441
|
+
} catch {
|
|
3442
|
+
return void 0;
|
|
3443
|
+
}
|
|
3444
|
+
};
|
|
3445
|
+
const addUniq = (item, el) => {
|
|
3324
3446
|
const key = `${item.role}:${item.meaning.toLowerCase().slice(0, 60)}`;
|
|
3325
3447
|
if (!seen.has(key) && item.meaning.trim().length > 1) {
|
|
3326
3448
|
seen.add(key);
|
|
3327
|
-
out.push(item);
|
|
3449
|
+
out.push(el ? { ...item, boundingRect: captureRect(el) } : item);
|
|
3328
3450
|
}
|
|
3329
3451
|
};
|
|
3330
3452
|
const isVisible = (el) => {
|
|
@@ -3342,7 +3464,7 @@ async function contentFallbackElements(page) {
|
|
|
3342
3464
|
document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
|
|
3343
3465
|
if (!isVisible(h)) return;
|
|
3344
3466
|
const text = h.textContent?.replace(/\s+/g, " ").trim().slice(0, 100);
|
|
3345
|
-
if (text) addUniq({ meaning: text, role: h.tagName.toLowerCase(), notes: JSON.stringify({ level: h.tagName }) });
|
|
3467
|
+
if (text) addUniq({ meaning: text, role: h.tagName.toLowerCase(), notes: JSON.stringify({ level: h.tagName }) }, h);
|
|
3346
3468
|
});
|
|
3347
3469
|
document.querySelectorAll('details, [data-state="closed"], [data-state="open"]').forEach((el) => {
|
|
3348
3470
|
if (!isVisible(el)) return;
|
|
@@ -3373,7 +3495,7 @@ async function contentFallbackElements(page) {
|
|
|
3373
3495
|
document.querySelectorAll('[class*="card" i]:not(nav):not(header):not(footer), [class*="chip" i], [class*="badge" i], [class*="tag" i], [class*="pill" i]').forEach((el) => {
|
|
3374
3496
|
if (!isVisible(el)) return;
|
|
3375
3497
|
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 150);
|
|
3376
|
-
if (text && text.length > 2 && text.length < 200) addUniq({ meaning: text, role: "content-card" });
|
|
3498
|
+
if (text && text.length > 2 && text.length < 200) addUniq({ meaning: text, role: "content-card" }, el);
|
|
3377
3499
|
});
|
|
3378
3500
|
const mainContent = document.querySelector('main, [role="main"], article') ?? document.body;
|
|
3379
3501
|
let listItemCount = 0;
|
|
@@ -3382,14 +3504,14 @@ async function contentFallbackElements(page) {
|
|
|
3382
3504
|
if (!isVisible(li)) return;
|
|
3383
3505
|
const text = li.textContent?.replace(/\s+/g, " ").trim().slice(0, 150);
|
|
3384
3506
|
if (text && text.length > 3 && text.length < 200) {
|
|
3385
|
-
addUniq({ meaning: text, role: "list-item" });
|
|
3507
|
+
addUniq({ meaning: text, role: "list-item" }, li);
|
|
3386
3508
|
listItemCount++;
|
|
3387
3509
|
}
|
|
3388
3510
|
});
|
|
3389
3511
|
mainContent.querySelectorAll('section > p:first-of-type, article > p:first-of-type, [class*="section" i] > p:first-of-type').forEach((p) => {
|
|
3390
3512
|
if (!isVisible(p)) return;
|
|
3391
3513
|
const text = p.textContent?.replace(/\s+/g, " ").trim().slice(0, 250);
|
|
3392
|
-
if (text && text.length > 10) addUniq({ meaning: text, role: "section-paragraph" });
|
|
3514
|
+
if (text && text.length > 10) addUniq({ meaning: text, role: "section-paragraph" }, p);
|
|
3393
3515
|
});
|
|
3394
3516
|
document.querySelectorAll('[class*="cta" i] p, [class*="call-to-action" i] p, section:not(form) > p').forEach((p) => {
|
|
3395
3517
|
if (!isVisible(p)) return;
|
|
@@ -3774,7 +3896,7 @@ async function contentFallbackElements(page) {
|
|
|
3774
3896
|
document.querySelectorAll('[role="alert"], [role="status"], [role="banner"]:not(header), .alert, .notice, .notification, .warning-banner, .error-banner, .success-banner, [class*="alert-" i]:not(button), [class*="toast" i]:not(button), [class*="snackbar" i]').forEach((el) => {
|
|
3775
3897
|
if (!isVisible(el)) return;
|
|
3776
3898
|
const text = el.textContent?.replace(/\s+/g, " ").trim().slice(0, 150);
|
|
3777
|
-
if (text && text.length > 5) addUniq({ meaning: text, role: "alert-banner", notes: JSON.stringify({ ariaRole: el.getAttribute("role") }) });
|
|
3899
|
+
if (text && text.length > 5) addUniq({ meaning: text, role: "alert-banner", notes: JSON.stringify({ ariaRole: el.getAttribute("role") }) }, el);
|
|
3778
3900
|
});
|
|
3779
3901
|
let pricingCount = 0;
|
|
3780
3902
|
document.querySelectorAll('[class*="pricing" i], [class*="plan" i]:not(main):not(article), [class*="tier" i]:not(nav), [class*="package" i]:not(script)').forEach((el) => {
|
|
@@ -4334,7 +4456,7 @@ async function hasMeaningfulAuthOrPublicState(page, requestedUrl) {
|
|
|
4334
4456
|
return false;
|
|
4335
4457
|
}
|
|
4336
4458
|
})();
|
|
4337
|
-
const
|
|
4459
|
+
const domState = await page.evaluate(() => {
|
|
4338
4460
|
const text = (document.body?.innerText ?? "").toLowerCase();
|
|
4339
4461
|
const inputs = Array.from(document.querySelectorAll("input"));
|
|
4340
4462
|
const hasPassword = inputs.some((i) => i.type === "password");
|
|
@@ -4344,8 +4466,11 @@ async function hasMeaningfulAuthOrPublicState(page, requestedUrl) {
|
|
|
4344
4466
|
});
|
|
4345
4467
|
const hasAuthCopy = /(sign in|log in|login|sign up|create account|register|forgot password|reset password|verify email|one-time|otp|continue with email)/i.test(text);
|
|
4346
4468
|
const hasPublicNav = /(pricing|contact|features|documentation|docs|about|privacy|terms)/i.test(text);
|
|
4347
|
-
|
|
4348
|
-
|
|
4469
|
+
const isSoft404 = !hasPassword && !hasIdentityInput && /(page not found|404|this page (could not|doesn'?t) (be found|exist)|we couldn'?t find|no longer exists|been moved|doesn'?t exist)/i.test(text);
|
|
4470
|
+
return { hasPassword, hasIdentityInput, hasAuthCopy, hasPublicNav, isSoft404 };
|
|
4471
|
+
}).catch(() => ({ hasPassword: false, hasIdentityInput: false, hasAuthCopy: false, hasPublicNav: false, isSoft404: false }));
|
|
4472
|
+
if (domState.isSoft404) return false;
|
|
4473
|
+
const domLooksRelevant = domState.hasPassword || domState.hasIdentityInput || domState.hasAuthCopy || domState.hasPublicNav;
|
|
4349
4474
|
return pathLooksRelevant || domLooksRelevant;
|
|
4350
4475
|
}
|
|
4351
4476
|
async function discoverAuthCandidates(page, origin) {
|
|
@@ -4490,6 +4615,18 @@ async function runAuthDiscoveryPhase(page, opts, origin, onProgress) {
|
|
|
4490
4615
|
} catch {
|
|
4491
4616
|
}
|
|
4492
4617
|
}
|
|
4618
|
+
let homeTitle = "";
|
|
4619
|
+
let homeElemCount = -1;
|
|
4620
|
+
try {
|
|
4621
|
+
await page.goto(opts.appUrl, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => null);
|
|
4622
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
4623
|
+
});
|
|
4624
|
+
homeTitle = await page.title().catch(() => "");
|
|
4625
|
+
homeElemCount = await page.evaluate(
|
|
4626
|
+
() => document.querySelectorAll('button,a,input,select,textarea,[role="button"]').length
|
|
4627
|
+
).catch(() => -1);
|
|
4628
|
+
} catch {
|
|
4629
|
+
}
|
|
4493
4630
|
let captured = 0;
|
|
4494
4631
|
let ordinal = 0;
|
|
4495
4632
|
await onProgress?.(10, "Discovering public and auth flows");
|
|
@@ -4503,6 +4640,13 @@ async function runAuthDiscoveryPhase(page, opts, origin, onProgress) {
|
|
|
4503
4640
|
await waitForStableDOM(page);
|
|
4504
4641
|
await acceptCookieConsent(page, { waitMs: 400 }).catch(() => {
|
|
4505
4642
|
});
|
|
4643
|
+
if (current.source === "seed_path" && homeElemCount >= 0) {
|
|
4644
|
+
const pageTitle = await page.title().catch(() => "");
|
|
4645
|
+
const elemCount = await page.evaluate(
|
|
4646
|
+
() => document.querySelectorAll('button,a,input,select,textarea,[role="button"]').length
|
|
4647
|
+
).catch(() => -1);
|
|
4648
|
+
if (pageTitle === homeTitle && elemCount === homeElemCount) continue;
|
|
4649
|
+
}
|
|
4506
4650
|
const finalUrl = page.url?.() ?? current.url;
|
|
4507
4651
|
const captureKey = normalizeAuthUrl(finalUrl);
|
|
4508
4652
|
if (!capturedKeys.has(captureKey)) {
|