@detiq/crawler 1.0.7 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detiq/crawler",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
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
- document.querySelectorAll('nav a, [role="navigation"] a, header nav a, footer nav a, [aria-label*="navigation" i] a, [aria-label*="menu" i] a').forEach((a: any) => {
4044
- if (!isVisible(a)) return;
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) {