@detiq/crawler 1.0.7 → 1.0.9
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 +10 -3
- package/dist/crawler.js.map +2 -2
- package/dist/job-queue.js +34 -1
- package/dist/job-queue.js.map +2 -2
- package/package.json +1 -1
- package/src/crawler.ts +12 -2
- package/src/job-queue.ts +38 -1
package/package.json
CHANGED
package/src/crawler.ts
CHANGED
|
@@ -269,6 +269,7 @@ async function launchChromeCDP(
|
|
|
269
269
|
'--remote-allow-origins=*',
|
|
270
270
|
'--no-first-run',
|
|
271
271
|
'--no-default-browser-check',
|
|
272
|
+
'--window-size=1280,900',
|
|
272
273
|
],
|
|
273
274
|
});
|
|
274
275
|
|
|
@@ -2665,6 +2666,8 @@ export async function crawlProject(opts: {
|
|
|
2665
2666
|
}
|
|
2666
2667
|
|
|
2667
2668
|
const linkMappings = await extractLinkMappings(page, origin);
|
|
2669
|
+
// Clear existing elements before re-saving — prevents duplicates on re-crawl
|
|
2670
|
+
await AppBrain.clearScreenElements(opts.tenantId, opts.projectId, screen.id).catch(() => {});
|
|
2668
2671
|
// Batch element saves: 4 concurrent DB writes instead of serial round-trips
|
|
2669
2672
|
for (let _ei = 0; _ei < elements.length; _ei += 4) {
|
|
2670
2673
|
const _batch = elements.slice(_ei, _ei + 4);
|
|
@@ -4040,8 +4043,15 @@ async function contentFallbackElements(page: any): Promise<Array<{ meaning: stri
|
|
|
4040
4043
|
});
|
|
4041
4044
|
|
|
4042
4045
|
// 8. Navigation links — explicitly captured (excluded from interactive element selector)
|
|
4043
|
-
|
|
4044
|
-
|
|
4046
|
+
// Use a lighter visibility check: skip offsetParent because nav is often inside a responsive
|
|
4047
|
+
// container (e.g. hidden lg:grid) that may be display:none at the crawler's viewport,
|
|
4048
|
+
// causing offsetParent to be null even though the link exists in the page's nav structure.
|
|
4049
|
+
const isNavVisible = (el: Element) => {
|
|
4050
|
+
const s = window.getComputedStyle(el);
|
|
4051
|
+
return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';
|
|
4052
|
+
};
|
|
4053
|
+
document.querySelectorAll('nav a, [role="navigation"] a, header a, header nav a, footer nav a, [aria-label*="navigation" i] a, [aria-label*="menu" i] a').forEach((a: any) => {
|
|
4054
|
+
if (!isNavVisible(a)) return;
|
|
4045
4055
|
const label = (a.getAttribute('aria-label') || a.textContent?.replace(/\s+/g, ' ').trim() || '').slice(0, 60);
|
|
4046
4056
|
const href = a.href;
|
|
4047
4057
|
if (!label || !href || /^(javascript:|#$)/i.test(href)) return;
|
package/src/job-queue.ts
CHANGED
|
@@ -51,7 +51,7 @@ async function notifyAdmins(tenantId: string, data: {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
import { AppBrain, getProjectStorageConfig, buildArtifactKey, uploadArtifact, getTenantStorageConfig, uploadScreenshotToTenantStorage, uploadFileToTenantStorage, notifyRunCompletion, recordLLMCall } from '@detiq/app-brain';
|
|
54
|
-
import { generateTaxonomySkillFiles, groupScreensWithAI, generateExecutionPlan, refreshTestCasesSkillFile, refreshTestDataSkillFile, refreshTestScriptSkillFile, refreshRequirementsCoverageSkillFile } from '@detiq/agents';
|
|
54
|
+
import { generateTaxonomySkillFiles, groupScreensWithAI, generateExecutionPlan, refreshTestCasesSkillFile, refreshTestDataSkillFile, refreshTestScriptSkillFile, refreshRequirementsCoverageSkillFile, detectFieldsFromHtml, mergeIntoCatalog } from '@detiq/agents';
|
|
55
55
|
import { crawlProject } from './crawler.js';
|
|
56
56
|
import { httpCrawl } from './http-engine.js';
|
|
57
57
|
import { hashContent, normalizeText } from './incremental-diff.js';
|
|
@@ -751,6 +751,43 @@ export async function processJob(jobId: string, config: any, opts?: ProcessJobOp
|
|
|
751
751
|
}
|
|
752
752
|
await generateTaxonomySkillFiles(tenantId, projectId, projectMap.screens as any, result.apiEndpoints, result.performanceMetrics);
|
|
753
753
|
|
|
754
|
+
// Auto-detect AppField catalog from crawled DOM snapshots.
|
|
755
|
+
// Runs before test generation so getFieldsForCase() returns typed fields (FORM mode).
|
|
756
|
+
// Capped at 20 screens and only where domSnapshotPath exists to bound LLM cost.
|
|
757
|
+
try {
|
|
758
|
+
const { readFile } = await import('node:fs/promises');
|
|
759
|
+
const { existsSync } = await import('node:fs');
|
|
760
|
+
const screensWithDom = (projectMap.screens as any[]).filter((s: any) => s.domSnapshotPath);
|
|
761
|
+
const detectLimit = Math.min(screensWithDom.length, 20);
|
|
762
|
+
let fieldDetectAdded = 0;
|
|
763
|
+
for (let si = 0; si < detectLimit; si++) {
|
|
764
|
+
const screen = screensWithDom[si];
|
|
765
|
+
try {
|
|
766
|
+
let htmlContent: string | null = null;
|
|
767
|
+
const domPath: string = screen.domSnapshotPath;
|
|
768
|
+
if (domPath.startsWith('https://') || domPath.startsWith('http://')) {
|
|
769
|
+
const res = await fetch(domPath);
|
|
770
|
+
if (res.ok) htmlContent = await res.text();
|
|
771
|
+
} else if (existsSync(domPath)) {
|
|
772
|
+
htmlContent = await readFile(domPath, 'utf8');
|
|
773
|
+
}
|
|
774
|
+
if (!htmlContent) continue;
|
|
775
|
+
const detected = await detectFieldsFromHtml({ tenantId, projectId, html: htmlContent });
|
|
776
|
+
if (detected.length > 0) {
|
|
777
|
+
const merged = await mergeIntoCatalog({ tenantId, projectId, detected });
|
|
778
|
+
fieldDetectAdded += merged.added;
|
|
779
|
+
}
|
|
780
|
+
} catch (sErr) {
|
|
781
|
+
logger.warn({ screenId: screen.id, err: sErr }, '[field-detect] skipped screen');
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (fieldDetectAdded > 0 || detectLimit > 0) {
|
|
785
|
+
logger.info({ screens: detectLimit, added: fieldDetectAdded, projectId }, '[field-detect] catalog populated from crawl');
|
|
786
|
+
}
|
|
787
|
+
} catch (fdErr) {
|
|
788
|
+
logger.warn({ err: fdErr }, '[field-detect] post-crawl field detection failed');
|
|
789
|
+
}
|
|
790
|
+
|
|
754
791
|
// Cloud storage: upload screenshots to tenant S3/R2 if configured
|
|
755
792
|
const tenantStorageCfg = await getTenantStorageConfig(tenantId).catch(() => null);
|
|
756
793
|
if (tenantStorageCfg) {
|