@detiq/crawler 1.1.15 → 1.1.16
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 +236 -82
- package/dist/crawler.js.map +2 -2
- package/dist/job-queue.js +11 -1
- package/dist/job-queue.js.map +2 -2
- package/package.json +1 -1
- package/src/crawler.ts +147 -17
- package/src/job-queue.ts +9 -0
package/dist/crawler.js
CHANGED
|
@@ -951,6 +951,13 @@ async function crawlProject(opts, onProgress) {
|
|
|
951
951
|
logger.info({ url: finalUrl }, "[crawler] pre-auth page has 0 elements \u2014 deferring to authenticated BFS");
|
|
952
952
|
continue;
|
|
953
953
|
}
|
|
954
|
+
const AUTH_WALL_LABEL_RE = /^(sign.?in|log.?in|login|platform access|back\s+to|checking\s+auth|get started|create account|register|continue with|join now)/i;
|
|
955
|
+
const allAnchorOnly = preAuthElements.every((el) => el.role === "a");
|
|
956
|
+
const looksLikeAuthWall = preAuthElements.length <= 3 && (allAnchorOnly || preAuthElements.every((el) => AUTH_WALL_LABEL_RE.test(el.label)));
|
|
957
|
+
if (looksLikeAuthWall) {
|
|
958
|
+
logger.info({ url: finalUrl, elements: preAuthElements.length, labels: preAuthElements.map((e) => e.label) }, "[crawler] pre-auth auth wall \u2014 deferring to authenticated BFS");
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
954
961
|
const pathParts = finalPath.split("/").filter(Boolean);
|
|
955
962
|
const screenName = pathParts.map((p) => p.charAt(0).toUpperCase() + p.slice(1).replace(/[-_]/g, " ")).join(" \u2014 ") || "Home";
|
|
956
963
|
const screen = await AppBrain.saveScreen(opts.tenantId, opts.projectId, screenName, finalUrl, opts.jobId, "LINK_FOLLOW").catch(() => null);
|
|
@@ -1644,7 +1651,23 @@ async function crawlProject(opts, onProgress) {
|
|
|
1644
1651
|
const s = window.getComputedStyle(el);
|
|
1645
1652
|
return s.display !== "none" && s.visibility !== "hidden" && s.opacity !== "0";
|
|
1646
1653
|
}
|
|
1647
|
-
|
|
1654
|
+
const NAV_SEL = [
|
|
1655
|
+
"nav a",
|
|
1656
|
+
'[role="navigation"] a',
|
|
1657
|
+
"header a",
|
|
1658
|
+
"footer nav a",
|
|
1659
|
+
'[aria-label*="navigation" i] a',
|
|
1660
|
+
'[aria-label*="sidebar" i] a',
|
|
1661
|
+
"aside a",
|
|
1662
|
+
'[role="complementary"] a',
|
|
1663
|
+
'[class*="sidebar" i] a',
|
|
1664
|
+
'[class*="side-nav" i] a',
|
|
1665
|
+
'[class*="sidenav" i] a',
|
|
1666
|
+
'[class*="nav-menu" i] a',
|
|
1667
|
+
'[class*="navmenu" i] a',
|
|
1668
|
+
"[data-nav] a"
|
|
1669
|
+
].join(", ");
|
|
1670
|
+
document.querySelectorAll(NAV_SEL).forEach((a) => {
|
|
1648
1671
|
if (!isV(a)) return;
|
|
1649
1672
|
const label = (a.getAttribute("aria-label") || a.textContent?.replace(/\s+/g, " ").trim() || "").slice(0, 60);
|
|
1650
1673
|
const href = a.href ?? "";
|
|
@@ -1654,6 +1677,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
1654
1677
|
while (cur) {
|
|
1655
1678
|
const t = cur.tagName?.toLowerCase();
|
|
1656
1679
|
const r = cur.getAttribute?.("role")?.toLowerCase();
|
|
1680
|
+
const cl = (cur.className ?? "").toLowerCase();
|
|
1657
1681
|
if (t === "header" || r === "banner") {
|
|
1658
1682
|
landmark = "Header";
|
|
1659
1683
|
break;
|
|
@@ -1662,6 +1686,10 @@ async function crawlProject(opts, onProgress) {
|
|
|
1662
1686
|
landmark = "Footer";
|
|
1663
1687
|
break;
|
|
1664
1688
|
}
|
|
1689
|
+
if (t === "aside" || r === "complementary" || cl.includes("sidebar") || cl.includes("sidenav") || cl.includes("side-nav")) {
|
|
1690
|
+
landmark = "Sidebar";
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1665
1693
|
if (t === "nav" || r === "navigation") {
|
|
1666
1694
|
landmark = cur.getAttribute("aria-label")?.trim() || "Site Navigation";
|
|
1667
1695
|
break;
|
|
@@ -1687,6 +1715,44 @@ async function crawlProject(opts, onProgress) {
|
|
|
1687
1715
|
fc++;
|
|
1688
1716
|
}
|
|
1689
1717
|
});
|
|
1718
|
+
{
|
|
1719
|
+
const navHrefsSeen = /* @__PURE__ */ new Set();
|
|
1720
|
+
for (const it of items) {
|
|
1721
|
+
try {
|
|
1722
|
+
const h = JSON.parse(it.notes ?? "{}").href;
|
|
1723
|
+
if (h) navHrefsSeen.add(h);
|
|
1724
|
+
} catch {
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const gpGroups = /* @__PURE__ */ new Map();
|
|
1728
|
+
document.querySelectorAll("a[href]").forEach((a) => {
|
|
1729
|
+
if (!isV(a)) return;
|
|
1730
|
+
const href = a.href ?? "";
|
|
1731
|
+
if (!href || /^(javascript:|#$|mailto:|tel:)/i.test(href) || navHrefsSeen.has(href)) return;
|
|
1732
|
+
const label = (a.getAttribute("aria-label") || a.textContent?.replace(/\s+/g, " ").trim() || "").slice(0, 60);
|
|
1733
|
+
if (!label || label.length > 40) return;
|
|
1734
|
+
const parent = a.parentElement;
|
|
1735
|
+
const gp = parent?.parentElement;
|
|
1736
|
+
if (!parent || !gp) return;
|
|
1737
|
+
if (parent.querySelectorAll("a").length !== 1) return;
|
|
1738
|
+
const gpTag = gp.tagName.toLowerCase();
|
|
1739
|
+
if (["body", "html", "form"].includes(gpTag)) return;
|
|
1740
|
+
const arr = gpGroups.get(gp) ?? [];
|
|
1741
|
+
arr.push({ label, href });
|
|
1742
|
+
gpGroups.set(gp, arr);
|
|
1743
|
+
});
|
|
1744
|
+
for (const [, links] of gpGroups.entries()) {
|
|
1745
|
+
if (links.length >= 4) {
|
|
1746
|
+
for (const { label, href } of links) {
|
|
1747
|
+
const k = `cluster|${label.toLowerCase()}|${href}`;
|
|
1748
|
+
if (!seen.has(k)) {
|
|
1749
|
+
seen.add(k);
|
|
1750
|
+
items.push({ meaning: label, role: "nav-link", notes: JSON.stringify({ href: href.slice(0, 150), landmark: "Sidebar" }) });
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1690
1756
|
return items;
|
|
1691
1757
|
}).catch(() => []);
|
|
1692
1758
|
const allElements = [
|
|
@@ -2329,7 +2395,7 @@ async function crawlProject(opts, onProgress) {
|
|
|
2329
2395
|
const lastSeg = allSegs[allSegs.length - 1];
|
|
2330
2396
|
pathName = lastSeg.slice(0, 8).toUpperCase();
|
|
2331
2397
|
}
|
|
2332
|
-
const SIG_PARAMS = ["product_cat", "category", "cat", "type", "tag", "brand", "filter", "tab", "section", "view"];
|
|
2398
|
+
const SIG_PARAMS = ["product_cat", "category", "cat", "type", "tag", "brand", "filter", "tab", "section", "view", "play", "mode", "id", "page"];
|
|
2333
2399
|
const paramSuffix = SIG_PARAMS.map((p) => {
|
|
2334
2400
|
const v = u.searchParams.get(p);
|
|
2335
2401
|
return v ? ` \xB7 ${v.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}` : "";
|
|
@@ -2349,7 +2415,8 @@ async function crawlProject(opts, onProgress) {
|
|
|
2349
2415
|
}
|
|
2350
2416
|
const candidate = parts[0].trim();
|
|
2351
2417
|
const isBrand = candidate.split(/\s+/).length === 1 && (candidate.toLowerCase().includes(brandName) || candidate.toLowerCase() === brandName);
|
|
2352
|
-
|
|
2418
|
+
const isSiteGenericTitle = homeFingerprintTitle !== "" && candidate === homeFingerprintTitle;
|
|
2419
|
+
if (!isBrand && !isSiteGenericTitle && candidate.split(/\s+/).length > 1 && !/^\d[\d\s]*$/.test(candidate)) {
|
|
2353
2420
|
return candidate;
|
|
2354
2421
|
}
|
|
2355
2422
|
}
|
|
@@ -2843,95 +2910,121 @@ ${snippet}`,
|
|
|
2843
2910
|
try {
|
|
2844
2911
|
const baseline = await AppBrain.getVisualBaselineForDiff(opts.tenantId, screen.id, "DESKTOP", gitInfo.branch);
|
|
2845
2912
|
if (baseline && baseline.storagePath !== shotPath) {
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
screenPurpose: "Application screen captured during automated crawl",
|
|
2871
|
-
tenantId: opts.tenantId,
|
|
2872
|
-
projectId: opts.projectId
|
|
2873
|
-
});
|
|
2874
|
-
verdict = mergeAiVisualVerdict(verdict, aiResult, screen.name);
|
|
2875
|
-
} catch (aiErr) {
|
|
2876
|
-
logger.warn({ screenId: screen.id, err: String(aiErr) }, "[crawler] AI visual compare failed, keeping pHash-only verdict");
|
|
2913
|
+
let baselinePath = baseline.storagePath;
|
|
2914
|
+
let tempBaselinePath = null;
|
|
2915
|
+
const localExists = await fs.access(baseline.storagePath).then(() => true, () => false);
|
|
2916
|
+
if (!localExists) {
|
|
2917
|
+
if (baseline.cloudUrl) {
|
|
2918
|
+
try {
|
|
2919
|
+
const resp = await fetch(baseline.cloudUrl);
|
|
2920
|
+
if (resp.ok) {
|
|
2921
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
2922
|
+
tempBaselinePath = `${shotPath}.baseline-tmp.png`;
|
|
2923
|
+
await fs.writeFile(tempBaselinePath, buf);
|
|
2924
|
+
baselinePath = tempBaselinePath;
|
|
2925
|
+
logger.info({ screenId: screen.id }, "[crawler] VisualDiff baseline downloaded from cloud storage");
|
|
2926
|
+
} else {
|
|
2927
|
+
logger.warn({ screenId: screen.id, status: resp.status }, "[crawler] VisualDiff skipped \u2014 baseline cloud fetch returned non-200");
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
} catch (dlErr) {
|
|
2931
|
+
logger.warn({ screenId: screen.id, err: String(dlErr) }, "[crawler] VisualDiff skipped \u2014 baseline cloud download failed");
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
} else {
|
|
2935
|
+
logger.warn({ screenId: screen.id }, "[crawler] VisualDiff skipped \u2014 baseline not on disk and no cloudUrl available");
|
|
2936
|
+
return;
|
|
2877
2937
|
}
|
|
2878
2938
|
}
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2939
|
+
try {
|
|
2940
|
+
const { screenshotDiffPercent, gridLayoutDiff } = await import("./phash.js");
|
|
2941
|
+
const [{ pHashDistance, changeLevel }, layoutChanges, elementShifts] = await Promise.all([
|
|
2942
|
+
screenshotDiffPercent(baselinePath, shotPath, baseline.ignoredRegions),
|
|
2943
|
+
gridLayoutDiff(baselinePath, shotPath, baseline.ignoredRegions),
|
|
2944
|
+
AppBrain.computeElementLayoutDiff(opts.tenantId, screen.id, "DESKTOP")
|
|
2945
|
+
]);
|
|
2946
|
+
let verdict = pHashVerdict(pHashDistance, changeLevel, [
|
|
2947
|
+
...layoutChanges,
|
|
2948
|
+
...elementShifts.map((s) => ({ location: "element", severity: s.severity, description: s.description }))
|
|
2949
|
+
]);
|
|
2950
|
+
if (verdict.changeType === "SIGNIFICANT" || verdict.changeType === "CRITICAL") {
|
|
2951
|
+
try {
|
|
2952
|
+
const regions = baseline.ignoredRegions ?? [];
|
|
2953
|
+
const [baselineBuffer, newBuffer] = regions.length > 0 ? await (async () => {
|
|
2954
|
+
const { applyMask } = await import("./phash.js");
|
|
2955
|
+
return Promise.all([applyMask(baselinePath, regions), applyMask(shotPath, regions)]);
|
|
2956
|
+
})() : await Promise.all([
|
|
2957
|
+
fs.readFile(baselinePath),
|
|
2958
|
+
fs.readFile(shotPath)
|
|
2959
|
+
]);
|
|
2960
|
+
const aiResult = await aiVisualCompare({
|
|
2961
|
+
baselineImageBase64: baselineBuffer.toString("base64"),
|
|
2962
|
+
newImageBase64: newBuffer.toString("base64"),
|
|
2963
|
+
screenName: screen.name,
|
|
2964
|
+
screenPurpose: "Application screen captured during automated crawl",
|
|
2965
|
+
tenantId: opts.tenantId,
|
|
2966
|
+
projectId: opts.projectId
|
|
2967
|
+
});
|
|
2968
|
+
verdict = mergeAiVisualVerdict(verdict, aiResult, screen.name);
|
|
2969
|
+
} catch (aiErr) {
|
|
2970
|
+
logger.warn({ screenId: screen.id, err: String(aiErr) }, "[crawler] AI visual compare failed, keeping pHash-only verdict");
|
|
2886
2971
|
}
|
|
2887
|
-
} catch {
|
|
2888
2972
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
if (storageCfg) {
|
|
2897
|
-
const key = buildArtifactKey(opts.tenantId, opts.projectId, "visual-diffs", `${screen.id}-v${screen.version}.png`, storageCfg.prefix);
|
|
2898
|
-
newScreenshotCloudUrl = await uploadScreenshotToTenantStorage(storageCfg, shotPath, key);
|
|
2899
|
-
try {
|
|
2900
|
-
const { generatePixelmatchDiff } = await import("./phash.js");
|
|
2901
|
-
const diffBuffer = await generatePixelmatchDiff(baseline.storagePath, shotPath, baseline.ignoredRegions ?? []);
|
|
2902
|
-
const diffTmpPath = `${shotPath}.diff.png`;
|
|
2903
|
-
await fs.writeFile(diffTmpPath, diffBuffer);
|
|
2904
|
-
const diffKey = buildArtifactKey(opts.tenantId, opts.projectId, "visual-diffs", `${screen.id}-v${screen.version}-diff.png`, storageCfg.prefix);
|
|
2905
|
-
diffImageCloudUrl = await uploadScreenshotToTenantStorage(storageCfg, diffTmpPath, diffKey);
|
|
2906
|
-
fs.unlink(diffTmpPath).catch(() => {
|
|
2907
|
-
});
|
|
2908
|
-
} catch (diffErr) {
|
|
2909
|
-
logger.warn({ screenId: screen.id, err: String(diffErr) }, "[crawler] pixelmatch diff generation failed, skipping");
|
|
2973
|
+
let { changeType, recommendation, changes: allChanges, bugReportDraft, aiSummary } = verdict;
|
|
2974
|
+
if (changeType !== "NONE") {
|
|
2975
|
+
try {
|
|
2976
|
+
const project = await AppBrain.getProject(opts.tenantId, opts.projectId);
|
|
2977
|
+
const threshold = project?.visualDiffThreshold ?? null;
|
|
2978
|
+
if (threshold !== null && pHashDistance < threshold) {
|
|
2979
|
+
changeType = "NONE";
|
|
2910
2980
|
}
|
|
2981
|
+
} catch {
|
|
2911
2982
|
}
|
|
2912
|
-
} catch {
|
|
2913
2983
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2984
|
+
if (changeType !== "NONE") {
|
|
2985
|
+
let newScreenshotCloudUrl;
|
|
2986
|
+
let diffImageCloudUrl;
|
|
2987
|
+
try {
|
|
2988
|
+
const { getTenantStorageConfig, uploadScreenshotToTenantStorage, buildArtifactKey } = await import("@detiq/app-brain");
|
|
2989
|
+
const storageCfg = await getTenantStorageConfig(opts.tenantId).catch(() => null);
|
|
2990
|
+
if (storageCfg) {
|
|
2991
|
+
const key = buildArtifactKey(opts.tenantId, opts.projectId, "visual-diffs", `${screen.id}-v${screen.version}.png`, storageCfg.prefix);
|
|
2992
|
+
newScreenshotCloudUrl = await uploadScreenshotToTenantStorage(storageCfg, shotPath, key);
|
|
2993
|
+
try {
|
|
2994
|
+
const { generatePixelmatchDiff } = await import("./phash.js");
|
|
2995
|
+
const diffBuffer = await generatePixelmatchDiff(baselinePath, shotPath, baseline.ignoredRegions ?? []);
|
|
2996
|
+
const diffTmpPath = `${shotPath}.diff.png`;
|
|
2997
|
+
await fs.writeFile(diffTmpPath, diffBuffer);
|
|
2998
|
+
const diffKey = buildArtifactKey(opts.tenantId, opts.projectId, "visual-diffs", `${screen.id}-v${screen.version}-diff.png`, storageCfg.prefix);
|
|
2999
|
+
diffImageCloudUrl = await uploadScreenshotToTenantStorage(storageCfg, diffTmpPath, diffKey);
|
|
3000
|
+
fs.unlink(diffTmpPath).catch(() => {
|
|
3001
|
+
});
|
|
3002
|
+
} catch (diffErr) {
|
|
3003
|
+
logger.warn({ screenId: screen.id, err: String(diffErr) }, "[crawler] pixelmatch diff generation failed, skipping");
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
} catch {
|
|
3007
|
+
}
|
|
3008
|
+
await AppBrain.saveVisualDiff(opts.tenantId, {
|
|
3009
|
+
baselineId: baseline.id,
|
|
3010
|
+
newScreenshotPath: shotPath,
|
|
3011
|
+
newScreenshotCloudUrl,
|
|
3012
|
+
diffImageCloudUrl,
|
|
3013
|
+
diffScore: pHashDistance,
|
|
3014
|
+
changeType,
|
|
3015
|
+
changes: allChanges,
|
|
3016
|
+
recommendation,
|
|
3017
|
+
bugReportDraft,
|
|
3018
|
+
aiSummary
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
} finally {
|
|
3022
|
+
if (tempBaselinePath) fs.unlink(tempBaselinePath).catch(() => {
|
|
2925
3023
|
});
|
|
2926
3024
|
}
|
|
2927
3025
|
}
|
|
2928
3026
|
} catch (err) {
|
|
2929
|
-
|
|
2930
|
-
if (missing) {
|
|
2931
|
-
logger.warn({ screenId: screen.id, version: screen.version, err: String(err?.message ?? err) }, "[crawler] VisualDiff skipped \u2014 baseline screenshot not on disk (stored in cloud only). Run a full recrawl to regenerate baselines.");
|
|
2932
|
-
} else {
|
|
2933
|
-
console.warn("[crawler] VisualDiff pipeline failed:", err);
|
|
2934
|
-
}
|
|
3027
|
+
console.warn("[crawler] VisualDiff pipeline failed:", err);
|
|
2935
3028
|
}
|
|
2936
3029
|
})();
|
|
2937
3030
|
}
|
|
@@ -4355,7 +4448,26 @@ async function contentFallbackElements(page) {
|
|
|
4355
4448
|
const s = window.getComputedStyle(el);
|
|
4356
4449
|
return s.display !== "none" && s.visibility !== "hidden" && s.opacity !== "0";
|
|
4357
4450
|
};
|
|
4358
|
-
|
|
4451
|
+
const navSel = [
|
|
4452
|
+
"nav a",
|
|
4453
|
+
'[role="navigation"] a',
|
|
4454
|
+
"header a",
|
|
4455
|
+
"header nav a",
|
|
4456
|
+
"footer nav a",
|
|
4457
|
+
'[aria-label*="navigation" i] a',
|
|
4458
|
+
'[aria-label*="menu" i] a',
|
|
4459
|
+
'[aria-label*="sidebar" i] a',
|
|
4460
|
+
"aside a",
|
|
4461
|
+
'[role="complementary"] a',
|
|
4462
|
+
'[class*="sidebar" i] a',
|
|
4463
|
+
'[class*="side-nav" i] a',
|
|
4464
|
+
'[class*="sidenav" i] a',
|
|
4465
|
+
'[class*="nav-menu" i] a',
|
|
4466
|
+
'[class*="navmenu" i] a',
|
|
4467
|
+
"[data-nav] a"
|
|
4468
|
+
].join(", ");
|
|
4469
|
+
const navLinksFoundCount = { n: 0 };
|
|
4470
|
+
document.querySelectorAll(navSel).forEach((a) => {
|
|
4359
4471
|
if (!isNavVisible(a)) return;
|
|
4360
4472
|
const label = (a.getAttribute("aria-label") || a.textContent?.replace(/\s+/g, " ").trim() || "").slice(0, 60);
|
|
4361
4473
|
const href = a.href;
|
|
@@ -4365,6 +4477,7 @@ async function contentFallbackElements(page) {
|
|
|
4365
4477
|
while (el) {
|
|
4366
4478
|
const tag = el.tagName?.toLowerCase();
|
|
4367
4479
|
const role = el.getAttribute?.("role")?.toLowerCase();
|
|
4480
|
+
const cl = (el.className ?? "").toLowerCase();
|
|
4368
4481
|
if (tag === "header" || role === "banner") {
|
|
4369
4482
|
landmark = "Header";
|
|
4370
4483
|
break;
|
|
@@ -4373,6 +4486,10 @@ async function contentFallbackElements(page) {
|
|
|
4373
4486
|
landmark = "Footer";
|
|
4374
4487
|
break;
|
|
4375
4488
|
}
|
|
4489
|
+
if (tag === "aside" || role === "complementary" || cl.includes("sidebar") || cl.includes("sidenav") || cl.includes("side-nav")) {
|
|
4490
|
+
landmark = "Sidebar";
|
|
4491
|
+
break;
|
|
4492
|
+
}
|
|
4376
4493
|
if (tag === "nav" || role === "navigation") {
|
|
4377
4494
|
const directLabel = el.getAttribute("aria-label")?.trim() ?? "";
|
|
4378
4495
|
const labelId = el.getAttribute("aria-labelledby");
|
|
@@ -4383,7 +4500,44 @@ async function contentFallbackElements(page) {
|
|
|
4383
4500
|
el = el.parentElement;
|
|
4384
4501
|
}
|
|
4385
4502
|
addUniq({ meaning: label, role: "nav-link", notes: JSON.stringify({ href: href.slice(0, 150), landmark }) });
|
|
4503
|
+
navLinksFoundCount.n++;
|
|
4386
4504
|
});
|
|
4505
|
+
{
|
|
4506
|
+
const navHrefsAlready = /* @__PURE__ */ new Set();
|
|
4507
|
+
for (const item of out) {
|
|
4508
|
+
if (item.role === "nav-link") {
|
|
4509
|
+
try {
|
|
4510
|
+
const h = JSON.parse(item.notes ?? "{}").href;
|
|
4511
|
+
if (h) navHrefsAlready.add(h);
|
|
4512
|
+
} catch {
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
const gpGroups2 = /* @__PURE__ */ new Map();
|
|
4517
|
+
document.querySelectorAll("a[href]").forEach((a) => {
|
|
4518
|
+
if (!isNavVisible(a)) return;
|
|
4519
|
+
const href = a.href ?? "";
|
|
4520
|
+
if (!href || /^(javascript:|#$|mailto:|tel:)/i.test(href) || navHrefsAlready.has(href)) return;
|
|
4521
|
+
const label = (a.getAttribute("aria-label") || a.textContent?.replace(/\s+/g, " ").trim() || "").slice(0, 60);
|
|
4522
|
+
if (!label || label.length > 40) return;
|
|
4523
|
+
const parent = a.parentElement;
|
|
4524
|
+
const gp = parent?.parentElement;
|
|
4525
|
+
if (!parent || !gp) return;
|
|
4526
|
+
if (parent.querySelectorAll("a").length !== 1) return;
|
|
4527
|
+
const gpTag = gp.tagName.toLowerCase();
|
|
4528
|
+
if (["body", "html", "form"].includes(gpTag)) return;
|
|
4529
|
+
const arr = gpGroups2.get(gp) ?? [];
|
|
4530
|
+
arr.push({ label, href });
|
|
4531
|
+
gpGroups2.set(gp, arr);
|
|
4532
|
+
});
|
|
4533
|
+
for (const [, links] of gpGroups2.entries()) {
|
|
4534
|
+
if (links.length >= 4) {
|
|
4535
|
+
for (const { label, href } of links) {
|
|
4536
|
+
addUniq({ meaning: label, role: "nav-link", notes: JSON.stringify({ href: href.slice(0, 150), landmark: "Sidebar" }) });
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4387
4541
|
document.querySelectorAll('[aria-label*="breadcrumb" i] a, [class*="breadcrumb" i] a, nav[aria-label*="breadcrumb" i] a').forEach((a) => {
|
|
4388
4542
|
const label = a.textContent?.replace(/\s+/g, " ").trim().slice(0, 60);
|
|
4389
4543
|
const href = a.href;
|