@detiq/crawler 0.1.6 → 0.1.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/dist/skyvern.js CHANGED
@@ -5,8 +5,6 @@ async function skyvernNavigate(startUrl, navigationGoals, tenantConfig, triggerR
5
5
  console.warn("[skyvern] skyvernNavigate: no API config \u2014 skipping post-login vision navigation");
6
6
  return { screenshotUrls: [], visitedUrls: [], status: "failed" };
7
7
  }
8
- const { assertResolvesToPublicAddress } = await import("@detiq/core");
9
- await assertResolvesToPublicAddress(apiUrl);
10
8
  const visitedUrls = [];
11
9
  const screenshotUrls = [];
12
10
  const skyvernSteps = [];
@@ -93,8 +91,6 @@ async function skyvernFallback(task, tenantConfig) {
93
91
  );
94
92
  return "failed";
95
93
  }
96
- const { assertResolvesToPublicAddress } = await import("@detiq/core");
97
- await assertResolvesToPublicAddress(apiUrl);
98
94
  try {
99
95
  const res = await fetch(`${apiUrl}/api/v1/tasks`, {
100
96
  method: "POST",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/skyvern.ts"],
4
- "sourcesContent": ["/**\n * Skyvern Vision Fallback\n *\n * Skyvern combines computer vision + LLM reasoning to navigate sites that\n * defeat DOM-based approaches: CAPTCHAs, legacy apps, visual-only login flows,\n * government portals. It's the \"break glass\" option when Stagehand fails.\n *\n * Production setup (Phase 4+):\n * 1. Self-host: https://github.com/Skyvern-AI/skyvern\n * 2. Or use cloud: https://app.skyvern.com\n * 3. Set SKYVERN_API_URL + SKYVERN_API_KEY in .env\n *\n * Pricing note: vision tasks cost more per step than DOM-based Stagehand.\n * Use only when the fast path fails.\n */\n\nexport interface SkyvernTask {\n taskDescription: string;\n url: string;\n credentials?: { username: string; password: string; oneTimeCode?: string };\n}\n\nexport interface SkyvernConfig {\n apiUrl: string;\n apiKey: string;\n llmKey?: string;\n llmApiKey?: string;\n}\n\nexport interface SkyvernStep {\n order: number;\n action_type: string;\n element_description?: string;\n reasoning?: string;\n success: boolean;\n screenshot_url?: string;\n duration_ms?: number;\n error?: string;\n}\n\nexport interface SkyvernNavigateResult {\n screenshotUrls: string[];\n visitedUrls: string[];\n status: 'success' | 'failed' | 'partial';\n steps?: SkyvernStep[];\n taskId?: string;\n durationMs?: number;\n triggerReason?: string;\n}\n\n/**\n * Gap 5: Post-login navigation via Skyvern vision for apps where DOM analysis\n * cannot discover screens (canvas, WebGL, legacy Flash-replacement stacks).\n * Skyvern drives the browser via screenshot + LLM, returning visited page list.\n */\nexport async function skyvernNavigate(\n startUrl: string,\n navigationGoals: string[],\n tenantConfig?: SkyvernConfig,\n triggerReason?: string,\n): Promise<SkyvernNavigateResult> {\n const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;\n const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;\n\n if (!apiUrl || !apiKey) {\n console.warn('[skyvern] skyvernNavigate: no API config \u2014 skipping post-login vision navigation');\n return { screenshotUrls: [], visitedUrls: [], status: 'failed' };\n }\n\n const { assertResolvesToPublicAddress } = await import('@detiq/core');\n await assertResolvesToPublicAddress(apiUrl);\n\n const visitedUrls: string[] = [];\n const screenshotUrls: string[] = [];\n const skyvernSteps: SkyvernStep[] = [];\n let lastTaskId: string | undefined;\n let totalDurationMs = 0;\n\n for (const goal of navigationGoals) {\n try {\n const taskStart = Date.now();\n const res = await fetch(`${apiUrl}/api/v1/tasks`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify({\n url: startUrl,\n navigation_goal: goal,\n ...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),\n ...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),\n }),\n });\n if (!res.ok) continue;\n const data: any = await res.json();\n const taskId: string = data.task_id;\n if (!taskId) continue;\n lastTaskId = taskId;\n\n // Poll for task completion (max 60s per goal)\n let done = false;\n for (let p = 0; p < 12 && !done; p++) {\n await new Promise((r) => setTimeout(r, 5000));\n const poll = await fetch(`${apiUrl}/api/v1/tasks/${taskId}`, {\n headers: { 'x-api-key': apiKey },\n }).catch(() => null);\n if (!poll?.ok) continue;\n const pollData: any = await poll.json();\n if (pollData.status === 'completed' || pollData.status === 'failed') {\n done = true;\n totalDurationMs += Date.now() - taskStart;\n if (pollData.status === 'completed') {\n if (pollData.output?.url) visitedUrls.push(pollData.output.url);\n if (pollData.output?.screenshot_url) screenshotUrls.push(pollData.output.screenshot_url);\n }\n\n // Fetch step-level detail from Skyvern\n try {\n const stepsRes = await fetch(`${apiUrl}/api/v1/tasks/${taskId}/steps`, {\n headers: { 'x-api-key': apiKey },\n }).catch(() => null);\n if (stepsRes?.ok) {\n const stepsData: any = await stepsRes.json();\n const stepsList = Array.isArray(stepsData) ? stepsData : (stepsData?.steps ?? []);\n for (const s of stepsList) {\n if (s.output?.screenshot_url) screenshotUrls.push(s.output.screenshot_url);\n skyvernSteps.push({\n order: s.step_order ?? s.order ?? 0,\n action_type: s.action_type ?? s.action ?? 'unknown',\n element_description: s.output?.element_description,\n reasoning: s.output?.llm_response?.action_description ?? s.output?.reasoning,\n success: s.status === 'completed',\n screenshot_url: s.output?.screenshot_url,\n error: s.status === 'failed' ? (s.output?.errors?.[0] ?? 'Step failed') : undefined,\n });\n }\n }\n } catch { /* non-fatal */ }\n }\n }\n } catch (err) {\n console.warn(`[skyvern] Navigation goal failed: ${goal}`, err);\n }\n }\n\n return {\n screenshotUrls,\n visitedUrls,\n status: visitedUrls.length > 0 ? 'success' : screenshotUrls.length > 0 ? 'partial' : 'failed',\n steps: skyvernSteps,\n taskId: lastTaskId,\n durationMs: totalDurationMs || undefined,\n triggerReason,\n };\n}\n\nexport async function skyvernFallback(task: SkyvernTask, tenantConfig?: SkyvernConfig): Promise<'success' | 'failed'> {\n const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;\n const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;\n\n if (!apiUrl || !apiKey) {\n console.warn(\n '[skyvern] Vision fallback disabled \u2014 set SKYVERN_API_URL + SKYVERN_API_KEY in .env to enable.',\n '\\n[skyvern] Self-host: https://github.com/Skyvern-AI/skyvern'\n );\n return 'failed';\n }\n\n const { assertResolvesToPublicAddress } = await import('@detiq/core');\n await assertResolvesToPublicAddress(apiUrl);\n\n try {\n const res = await fetch(`${apiUrl}/api/v1/tasks`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify({\n url: task.url,\n navigation_goal: task.taskDescription,\n ...(task.credentials && {\n navigation_payload: {\n username: task.credentials.username,\n password: task.credentials.password,\n ...(task.credentials.oneTimeCode && { otp: task.credentials.oneTimeCode }),\n },\n }),\n ...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),\n ...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),\n }),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n console.error(`[skyvern] Task creation failed: ${res.status}`, body);\n return 'failed';\n }\n\n const data: any = await res.json();\n console.log(`[skyvern] Task queued: ${data.task_id}. Poll GET /api/v1/tasks/${data.task_id} for completion.`);\n // Full implementation: poll until status === \"completed\" | \"failed\".\n // For Phase 1 the login already happened in the browser context Skyvern manages,\n // so returning 'success' here means we can proceed in that browser session.\n return 'success';\n } catch (err) {\n console.error('[skyvern] Error:', err);\n return 'failed';\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Skyvern Vision Fallback\n *\n * Skyvern combines computer vision + LLM reasoning to navigate sites that\n * defeat DOM-based approaches: CAPTCHAs, legacy apps, visual-only login flows,\n * government portals. It's the \"break glass\" option when Stagehand fails.\n *\n * Production setup (Phase 4+):\n * 1. Self-host: https://github.com/Skyvern-AI/skyvern\n * 2. Or use cloud: https://app.skyvern.com\n * 3. Set SKYVERN_API_URL + SKYVERN_API_KEY in .env\n *\n * Pricing note: vision tasks cost more per step than DOM-based Stagehand.\n * Use only when the fast path fails.\n */\n\nexport interface SkyvernTask {\n taskDescription: string;\n url: string;\n credentials?: { username: string; password: string; oneTimeCode?: string };\n}\n\nexport interface SkyvernConfig {\n apiUrl: string;\n apiKey: string;\n llmKey?: string;\n llmApiKey?: string;\n}\n\nexport interface SkyvernStep {\n order: number;\n action_type: string;\n element_description?: string;\n reasoning?: string;\n success: boolean;\n screenshot_url?: string;\n duration_ms?: number;\n error?: string;\n}\n\nexport interface SkyvernNavigateResult {\n screenshotUrls: string[];\n visitedUrls: string[];\n status: 'success' | 'failed' | 'partial';\n steps?: SkyvernStep[];\n taskId?: string;\n durationMs?: number;\n triggerReason?: string;\n}\n\n/**\n * Gap 5: Post-login navigation via Skyvern vision for apps where DOM analysis\n * cannot discover screens (canvas, WebGL, legacy Flash-replacement stacks).\n * Skyvern drives the browser via screenshot + LLM, returning visited page list.\n */\nexport async function skyvernNavigate(\n startUrl: string,\n navigationGoals: string[],\n tenantConfig?: SkyvernConfig,\n triggerReason?: string,\n): Promise<SkyvernNavigateResult> {\n const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;\n const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;\n\n if (!apiUrl || !apiKey) {\n console.warn('[skyvern] skyvernNavigate: no API config \u2014 skipping post-login vision navigation');\n return { screenshotUrls: [], visitedUrls: [], status: 'failed' };\n }\n\n const visitedUrls: string[] = [];\n const screenshotUrls: string[] = [];\n const skyvernSteps: SkyvernStep[] = [];\n let lastTaskId: string | undefined;\n let totalDurationMs = 0;\n\n for (const goal of navigationGoals) {\n try {\n const taskStart = Date.now();\n const res = await fetch(`${apiUrl}/api/v1/tasks`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify({\n url: startUrl,\n navigation_goal: goal,\n ...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),\n ...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),\n }),\n });\n if (!res.ok) continue;\n const data: any = await res.json();\n const taskId: string = data.task_id;\n if (!taskId) continue;\n lastTaskId = taskId;\n\n // Poll for task completion (max 60s per goal)\n let done = false;\n for (let p = 0; p < 12 && !done; p++) {\n await new Promise((r) => setTimeout(r, 5000));\n const poll = await fetch(`${apiUrl}/api/v1/tasks/${taskId}`, {\n headers: { 'x-api-key': apiKey },\n }).catch(() => null);\n if (!poll?.ok) continue;\n const pollData: any = await poll.json();\n if (pollData.status === 'completed' || pollData.status === 'failed') {\n done = true;\n totalDurationMs += Date.now() - taskStart;\n if (pollData.status === 'completed') {\n if (pollData.output?.url) visitedUrls.push(pollData.output.url);\n if (pollData.output?.screenshot_url) screenshotUrls.push(pollData.output.screenshot_url);\n }\n\n // Fetch step-level detail from Skyvern\n try {\n const stepsRes = await fetch(`${apiUrl}/api/v1/tasks/${taskId}/steps`, {\n headers: { 'x-api-key': apiKey },\n }).catch(() => null);\n if (stepsRes?.ok) {\n const stepsData: any = await stepsRes.json();\n const stepsList = Array.isArray(stepsData) ? stepsData : (stepsData?.steps ?? []);\n for (const s of stepsList) {\n if (s.output?.screenshot_url) screenshotUrls.push(s.output.screenshot_url);\n skyvernSteps.push({\n order: s.step_order ?? s.order ?? 0,\n action_type: s.action_type ?? s.action ?? 'unknown',\n element_description: s.output?.element_description,\n reasoning: s.output?.llm_response?.action_description ?? s.output?.reasoning,\n success: s.status === 'completed',\n screenshot_url: s.output?.screenshot_url,\n error: s.status === 'failed' ? (s.output?.errors?.[0] ?? 'Step failed') : undefined,\n });\n }\n }\n } catch { /* non-fatal */ }\n }\n }\n } catch (err) {\n console.warn(`[skyvern] Navigation goal failed: ${goal}`, err);\n }\n }\n\n return {\n screenshotUrls,\n visitedUrls,\n status: visitedUrls.length > 0 ? 'success' : screenshotUrls.length > 0 ? 'partial' : 'failed',\n steps: skyvernSteps,\n taskId: lastTaskId,\n durationMs: totalDurationMs || undefined,\n triggerReason,\n };\n}\n\nexport async function skyvernFallback(task: SkyvernTask, tenantConfig?: SkyvernConfig): Promise<'success' | 'failed'> {\n const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;\n const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;\n\n if (!apiUrl || !apiKey) {\n console.warn(\n '[skyvern] Vision fallback disabled \u2014 set SKYVERN_API_URL + SKYVERN_API_KEY in .env to enable.',\n '\\n[skyvern] Self-host: https://github.com/Skyvern-AI/skyvern'\n );\n return 'failed';\n }\n\n try {\n const res = await fetch(`${apiUrl}/api/v1/tasks`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },\n body: JSON.stringify({\n url: task.url,\n navigation_goal: task.taskDescription,\n ...(task.credentials && {\n navigation_payload: {\n username: task.credentials.username,\n password: task.credentials.password,\n ...(task.credentials.oneTimeCode && { otp: task.credentials.oneTimeCode }),\n },\n }),\n ...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),\n ...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),\n }),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n console.error(`[skyvern] Task creation failed: ${res.status}`, body);\n return 'failed';\n }\n\n const data: any = await res.json();\n console.log(`[skyvern] Task queued: ${data.task_id}. Poll GET /api/v1/tasks/${data.task_id} for completion.`);\n // Full implementation: poll until status === \"completed\" | \"failed\".\n // For Phase 1 the login already happened in the browser context Skyvern manages,\n // so returning 'success' here means we can proceed in that browser session.\n return 'success';\n } catch (err) {\n console.error('[skyvern] Error:', err);\n return 'failed';\n }\n}\n"],
5
+ "mappings": "AAuDA,eAAsB,gBACpB,UACA,iBACA,cACA,eACgC;AAChC,QAAM,SAAS,cAAc,UAAU,QAAQ,IAAI;AACnD,QAAM,SAAS,cAAc,UAAU,QAAQ,IAAI;AAEnD,MAAI,CAAC,UAAU,CAAC,QAAQ;AACtB,YAAQ,KAAK,uFAAkF;AAC/F,WAAO,EAAE,gBAAgB,CAAC,GAAG,aAAa,CAAC,GAAG,QAAQ,SAAS;AAAA,EACjE;AAEA,QAAM,cAAwB,CAAC;AAC/B,QAAM,iBAA2B,CAAC;AAClC,QAAM,eAA8B,CAAC;AACrC,MAAI;AACJ,MAAI,kBAAkB;AAEtB,aAAW,QAAQ,iBAAiB;AAClC,QAAI;AACF,YAAM,YAAY,KAAK,IAAI;AAC3B,YAAM,MAAM,MAAM,MAAM,GAAG,MAAM,iBAAiB;AAAA,QAChD,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,aAAa,OAAO;AAAA,QACnE,MAAM,KAAK,UAAU;AAAA,UACnB,KAAK;AAAA,UACL,iBAAiB;AAAA,UACjB,GAAI,cAAc,SAAS,EAAE,SAAS,aAAa,OAAO,IAAI,QAAQ,IAAI,kBAAkB,EAAE,SAAS,QAAQ,IAAI,gBAAgB,IAAI,CAAC;AAAA,UACxI,GAAI,cAAc,YAAY,EAAE,aAAa,aAAa,UAAU,IAAI,QAAQ,IAAI,iBAAiB,EAAE,aAAa,QAAQ,IAAI,eAAe,IAAI,CAAC;AAAA,QACtJ,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,GAAI;AACb,YAAM,OAAY,MAAM,IAAI,KAAK;AACjC,YAAM,SAAiB,KAAK;AAC5B,UAAI,CAAC,OAAQ;AACb,mBAAa;AAGb,UAAI,OAAO;AACX,eAAS,IAAI,GAAG,IAAI,MAAM,CAAC,MAAM,KAAK;AACpC,cAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAI,CAAC;AAC5C,cAAM,OAAO,MAAM,MAAM,GAAG,MAAM,iBAAiB,MAAM,IAAI;AAAA,UAC3D,SAAS,EAAE,aAAa,OAAO;AAAA,QACjC,CAAC,EAAE,MAAM,MAAM,IAAI;AACnB,YAAI,CAAC,MAAM,GAAI;AACf,cAAM,WAAgB,MAAM,KAAK,KAAK;AACtC,YAAI,SAAS,WAAW,eAAe,SAAS,WAAW,UAAU;AACnE,iBAAO;AACP,6BAAmB,KAAK,IAAI,IAAI;AAChC,cAAI,SAAS,WAAW,aAAa;AACnC,gBAAI,SAAS,QAAQ,IAAK,aAAY,KAAK,SAAS,OAAO,GAAG;AAC9D,gBAAI,SAAS,QAAQ,eAAgB,gBAAe,KAAK,SAAS,OAAO,cAAc;AAAA,UACzF;AAGA,cAAI;AACF,kBAAM,WAAW,MAAM,MAAM,GAAG,MAAM,iBAAiB,MAAM,UAAU;AAAA,cACrE,SAAS,EAAE,aAAa,OAAO;AAAA,YACjC,CAAC,EAAE,MAAM,MAAM,IAAI;AACnB,gBAAI,UAAU,IAAI;AAChB,oBAAM,YAAiB,MAAM,SAAS,KAAK;AAC3C,oBAAM,YAAY,MAAM,QAAQ,SAAS,IAAI,YAAa,WAAW,SAAS,CAAC;AAC/E,yBAAW,KAAK,WAAW;AACzB,oBAAI,EAAE,QAAQ,eAAgB,gBAAe,KAAK,EAAE,OAAO,cAAc;AACzE,6BAAa,KAAK;AAAA,kBAChB,OAAO,EAAE,cAAc,EAAE,SAAS;AAAA,kBAClC,aAAa,EAAE,eAAe,EAAE,UAAU;AAAA,kBAC1C,qBAAqB,EAAE,QAAQ;AAAA,kBAC/B,WAAW,EAAE,QAAQ,cAAc,sBAAsB,EAAE,QAAQ;AAAA,kBACnE,SAAS,EAAE,WAAW;AAAA,kBACtB,gBAAgB,EAAE,QAAQ;AAAA,kBAC1B,OAAO,EAAE,WAAW,WAAY,EAAE,QAAQ,SAAS,CAAC,KAAK,gBAAiB;AAAA,gBAC5E,CAAC;AAAA,cACH;AAAA,YACF;AAAA,UACF,QAAQ;AAAA,UAAkB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,KAAK,qCAAqC,IAAI,IAAI,GAAG;AAAA,IAC/D;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ,YAAY,SAAS,IAAI,YAAY,eAAe,SAAS,IAAI,YAAY;AAAA,IACrF,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,YAAY,mBAAmB;AAAA,IAC/B;AAAA,EACF;AACF;AAEA,eAAsB,gBAAgB,MAAmB,cAA6D;AACpH,QAAM,SAAS,cAAc,UAAU,QAAQ,IAAI;AACnD,QAAM,SAAS,cAAc,UAAU,QAAQ,IAAI;AAEnD,MAAI,CAAC,UAAU,CAAC,QAAQ;AACtB,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,GAAG,MAAM,iBAAiB;AAAA,MAChD,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oBAAoB,aAAa,OAAO;AAAA,MACnE,MAAM,KAAK,UAAU;AAAA,QACnB,KAAK,KAAK;AAAA,QACV,iBAAiB,KAAK;AAAA,QACtB,GAAI,KAAK,eAAe;AAAA,UACtB,oBAAoB;AAAA,YAClB,UAAU,KAAK,YAAY;AAAA,YAC3B,UAAU,KAAK,YAAY;AAAA,YAC3B,GAAI,KAAK,YAAY,eAAe,EAAE,KAAK,KAAK,YAAY,YAAY;AAAA,UAC1E;AAAA,QACF;AAAA,QACA,GAAI,cAAc,SAAS,EAAE,SAAS,aAAa,OAAO,IAAI,QAAQ,IAAI,kBAAkB,EAAE,SAAS,QAAQ,IAAI,gBAAgB,IAAI,CAAC;AAAA,QACxI,GAAI,cAAc,YAAY,EAAE,aAAa,aAAa,UAAU,IAAI,QAAQ,IAAI,iBAAiB,EAAE,aAAa,QAAQ,IAAI,eAAe,IAAI,CAAC;AAAA,MACtJ,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,cAAQ,MAAM,mCAAmC,IAAI,MAAM,IAAI,IAAI;AACnE,aAAO;AAAA,IACT;AAEA,UAAM,OAAY,MAAM,IAAI,KAAK;AACjC,YAAQ,IAAI,0BAA0B,KAAK,OAAO,4BAA4B,KAAK,OAAO,kBAAkB;AAI5G,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,YAAQ,MAAM,oBAAoB,GAAG;AACrC,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -36,12 +36,12 @@ async function resolveLocator(page, step, healCtx) {
36
36
  const { selector, selectorStrategy = "text" } = step;
37
37
  if (!selector) return null;
38
38
  const primary = buildLocator(page, selector, selectorStrategy);
39
- const attached = await primary.first().isVisible({ timeout: 1500 }).catch(() => false);
39
+ const attached = await primary.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? "1500", 10) }).catch(() => false);
40
40
  if (attached) return { loc: primary, healed: false };
41
41
  for (const strategy of LOCATOR_STRATEGIES) {
42
42
  if (strategy === selectorStrategy) continue;
43
43
  const loc = buildLocator(page, selector, strategy);
44
- const visible = await loc.first().isVisible({ timeout: 800 }).catch(() => false);
44
+ const visible = await loc.first().isVisible({ timeout: parseInt(process.env.TEST_SHORT_TIMEOUT_MS ?? "800", 10) }).catch(() => false);
45
45
  if (visible) return { loc, healed: true, healedVia: "mechanical" };
46
46
  }
47
47
  if (healCtx) {
@@ -56,7 +56,7 @@ async function resolveLocator(page, step, healCtx) {
56
56
  });
57
57
  if (result.healed && result.updatedTest) {
58
58
  const healedLoc = page.getByText(result.updatedTest.elementMeaning, { exact: false });
59
- const visible = await healedLoc.first().isVisible({ timeout: 1500 }).catch(() => false);
59
+ const visible = await healedLoc.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? "1500", 10) }).catch(() => false);
60
60
  if (visible) {
61
61
  return {
62
62
  loc: healedLoc,
@@ -91,7 +91,7 @@ async function runPlan(page, plan, options, consoleLogs, networkErrors) {
91
91
  duration: 0
92
92
  };
93
93
  try {
94
- const timeout = step.timeout ?? 8e3;
94
+ const timeout = step.timeout ?? parseInt(process.env.TEST_STEP_TIMEOUT_MS ?? "8000", 10);
95
95
  switch (step.action) {
96
96
  case "navigate":
97
97
  if (!step.url) throw new Error("Navigate step missing url");
@@ -101,6 +101,8 @@ async function runPlan(page, plan, options, consoleLogs, networkErrors) {
101
101
  case "click": {
102
102
  const r = await resolveLocator(page, step, options.healCtx);
103
103
  if (!r) throw new Error("Could not resolve locator");
104
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
105
+ result.elementSelector = step.selector;
104
106
  await r.loc.first().click({ timeout });
105
107
  applyHealInfo(result, r);
106
108
  break;
@@ -108,6 +110,8 @@ async function runPlan(page, plan, options, consoleLogs, networkErrors) {
108
110
  case "fill": {
109
111
  const r = await resolveLocator(page, step, options.healCtx);
110
112
  if (!r) throw new Error("Could not resolve locator");
113
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
114
+ result.elementSelector = step.selector;
111
115
  await r.loc.first().fill(step.value ?? "", { timeout });
112
116
  applyHealInfo(result, r);
113
117
  break;
@@ -115,6 +119,8 @@ async function runPlan(page, plan, options, consoleLogs, networkErrors) {
115
119
  case "select": {
116
120
  const r = await resolveLocator(page, step, options.healCtx);
117
121
  if (!r) throw new Error("Could not resolve locator");
122
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
123
+ result.elementSelector = step.selector;
118
124
  await r.loc.first().selectOption(step.value ?? "", { timeout });
119
125
  applyHealInfo(result, r);
120
126
  break;
@@ -122,6 +128,8 @@ async function runPlan(page, plan, options, consoleLogs, networkErrors) {
122
128
  case "hover": {
123
129
  const r = await resolveLocator(page, step, options.healCtx);
124
130
  if (!r) throw new Error("Could not resolve locator");
131
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
132
+ result.elementSelector = step.selector;
125
133
  await r.loc.first().hover({ timeout });
126
134
  applyHealInfo(result, r);
127
135
  break;
@@ -133,6 +141,8 @@ async function runPlan(page, plan, options, consoleLogs, networkErrors) {
133
141
  const r = await resolveLocator(page, step, options.healCtx);
134
142
  if (!r) throw new Error("Could not resolve locator");
135
143
  await r.loc.first().waitFor({ state: "visible", timeout });
144
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
145
+ result.elementSelector = step.selector;
136
146
  applyHealInfo(result, r);
137
147
  break;
138
148
  }
@@ -143,6 +153,8 @@ async function runPlan(page, plan, options, consoleLogs, networkErrors) {
143
153
  if (!text?.includes(step.value ?? "")) {
144
154
  throw new Error(`Expected text "${step.value}" not found. Got: "${text?.slice(0, 100)}"`);
145
155
  }
156
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
157
+ result.elementSelector = step.selector;
146
158
  applyHealInfo(result, r);
147
159
  break;
148
160
  }
@@ -154,7 +166,7 @@ async function runPlan(page, plan, options, consoleLogs, networkErrors) {
154
166
  break;
155
167
  }
156
168
  case "wait":
157
- await page.waitForTimeout(step.timeout ?? 2e3);
169
+ await page.waitForTimeout(step.timeout ?? parseInt(process.env.TEST_STEP_DELAY_MS ?? "2000", 10));
158
170
  break;
159
171
  case "screenshot": {
160
172
  const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;
@@ -172,7 +184,7 @@ async function runPlan(page, plan, options, consoleLogs, networkErrors) {
172
184
  }
173
185
  }
174
186
  try {
175
- await page.waitForLoadState("networkidle", { timeout: 1500 }).catch(() => {
187
+ await page.waitForLoadState("networkidle", { timeout: parseInt(process.env.TEST_NETWORK_IDLE_TIMEOUT_MS ?? "1500", 10) }).catch(() => {
176
188
  });
177
189
  if (!result.screenshotPath) {
178
190
  const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/test-executor.ts"],
4
- "sourcesContent": ["import { chromium, firefox, webkit, type Page, type Locator } from 'playwright';\nimport type { ExecutionStep } from '@detiq/agents';\nimport { assertCrawlUrlSafe } from '@detiq/core';\nimport { analyzeFailure } from './failure-analysis-agent.js';\n\nexport interface StepResult {\n stepNumber: number;\n description: string;\n status: 'PASSED' | 'FAILED' | 'SKIPPED';\n duration: number;\n error?: string;\n screenshotPath?: string;\n healed?: boolean;\n /** 'mechanical' = same selector text, different Playwright locator strategy.\n * 'ai' = AI Self-Healing Agent matched a semantically different element\n * (the app's selector text itself changed, e.g. \"Sign In\" -> \"Log In\"). */\n healedVia?: 'mechanical' | 'ai';\n healReasoning?: string;\n healConfidence?: number;\n}\n\nexport interface ExecutionResult {\n stepResults: StepResult[];\n status: 'PASSED' | 'FAILED' | 'ERROR' | 'FLAKY';\n errorStep?: number;\n totalDuration: number;\n finalScreenshotPath?: string;\n retryCount?: number;\n consoleLogs?: Array<{ type: string; text: string; timestamp: number }>;\n networkErrors?: Array<{ url: string; status: number; method: string; timestamp: number }>;\n rootCauseSummary?: string;\n networkTimings?: Array<{ url: string; method: string; status: number; duration: number }>;\n}\n\nconst LOCATOR_STRATEGIES: ExecutionStep['selectorStrategy'][] = ['text', 'label', 'role', 'css'];\n\nfunction buildLocator(page: Page, selector: string, strategy: ExecutionStep['selectorStrategy'] = 'text'): Locator {\n switch (strategy) {\n case 'css': return page.locator(selector);\n case 'text': return page.getByText(selector, { exact: false });\n case 'role': return page.getByRole(selector as any);\n case 'label': return page.getByLabel(selector);\n default: return page.locator(selector);\n }\n}\n\n/** Scans the live page for visible interactive elements \u2014 the \"current page elements\"\n * fed to the AI Self-Healing Agent when mechanical locator strategies fail. */\nasync function extractCurrentPageElements(page: Page): Promise<Array<{ meaning: string; role: string; ariaLabel?: string; textContent?: string }>> {\n return page.evaluate(() => {\n const els = Array.from(document.querySelectorAll<HTMLElement>(\n 'a, button, input, select, textarea, [role=\"button\"], [role=\"link\"], [role=\"tab\"], [role=\"menuitem\"], [aria-label]'\n ));\n return els\n .filter((el) => {\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0;\n })\n .slice(0, 200)\n .map((el) => {\n const ariaLabel = el.getAttribute('aria-label') ?? undefined;\n const textContent = (el.textContent ?? '').replace(/\\s+/g, ' ').trim().slice(0, 100) || undefined;\n const role = el.getAttribute('role') ?? el.tagName.toLowerCase();\n return { meaning: ariaLabel || textContent || role, role, ariaLabel, textContent };\n });\n });\n}\n\n/**\n * Resolves a step's target element in three tiers:\n * 1. Exact selector + strategy as generated.\n * 2. Mechanical: same selector text, other Playwright locator strategies\n * (handles a strategy mismatch, not an actual UI change).\n * 3. AI: when tenantId/projectId are supplied, asks the Self-Healing Agent to find\n * the current page's best semantic match for the failed step description \u2014 the\n * tier that actually survives the app's selector text changing.\n */\nasync function resolveLocator(\n page: Page,\n step: ExecutionStep,\n healCtx?: { tenantId: string; projectId: string },\n): Promise<{ loc: Locator; healed: boolean; healedVia?: 'mechanical' | 'ai'; healReasoning?: string; healConfidence?: number } | null> {\n const { selector, selectorStrategy = 'text' } = step;\n if (!selector) return null;\n\n const primary = buildLocator(page, selector, selectorStrategy);\n const attached = await primary.first().isVisible({ timeout: 1500 }).catch(() => false);\n if (attached) return { loc: primary, healed: false };\n\n // Tier 2 \u2014 mechanical: try other strategies before giving up\n for (const strategy of LOCATOR_STRATEGIES) {\n if (strategy === selectorStrategy) continue;\n const loc = buildLocator(page, selector, strategy);\n const visible = await loc.first().isVisible({ timeout: 800 }).catch(() => false);\n if (visible) return { loc, healed: true, healedVia: 'mechanical' };\n }\n\n // Tier 3 \u2014 AI: the selector text itself may no longer match anything on the page.\n if (healCtx) {\n try {\n const { healElement } = await import('@detiq/agents');\n const currentPageElements = await extractCurrentPageElements(page);\n const result = await healElement({\n tenantId: healCtx.tenantId,\n projectId: healCtx.projectId,\n failedElementMeaning: step.description || selector,\n currentPageElements,\n });\n if (result.healed && result.updatedTest) {\n const healedLoc = page.getByText(result.updatedTest.elementMeaning, { exact: false });\n const visible = await healedLoc.first().isVisible({ timeout: 1500 }).catch(() => false);\n if (visible) {\n return {\n loc: healedLoc,\n healed: true,\n healedVia: 'ai',\n healReasoning: result.reasoning,\n healConfidence: result.confidence,\n };\n }\n }\n } catch {\n // AI healing is a best-effort last resort \u2014 any failure here (LLM error, bad\n // JSON, etc.) just falls through to the caller's normal Playwright error below.\n }\n }\n\n // Return primary \u2014 caller will fail with a meaningful Playwright error\n return { loc: primary, healed: false };\n}\n\nfunction applyHealInfo(result: StepResult, r: { healed: boolean; healedVia?: 'mechanical' | 'ai'; healReasoning?: string; healConfidence?: number }): void {\n result.healed = r.healed;\n result.healedVia = r.healedVia;\n result.healReasoning = r.healReasoning;\n result.healConfidence = r.healConfidence;\n}\n\nasync function runPlan(\n page: Page,\n plan: ExecutionStep[],\n options: { screenshotDir: string; testRunId: string; onStepComplete?: (result: StepResult) => void; healCtx?: { tenantId: string; projectId: string } },\n consoleLogs: Array<{ type: string; text: string; timestamp: number }>,\n networkErrors: Array<{ url: string; status: number; method: string; timestamp: number }>,\n): Promise<{ stepResults: StepResult[]; errorStep?: number; finalScreenshotPath?: string; consoleLogs: typeof consoleLogs; networkErrors: typeof networkErrors }> {\n const stepResults: StepResult[] = [];\n let errorStep: number | undefined;\n let finalScreenshotPath: string | undefined;\n\n for (const step of plan) {\n const stepStart = Date.now();\n const result: StepResult = {\n stepNumber: step.stepNumber,\n description: step.description,\n status: 'FAILED',\n duration: 0,\n };\n\n try {\n const timeout = step.timeout ?? 8000;\n\n switch (step.action) {\n case 'navigate':\n if (!step.url) throw new Error('Navigate step missing url');\n assertCrawlUrlSafe(step.url);\n await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout });\n break;\n\n case 'click': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().click({ timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'fill': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().fill(step.value ?? '', { timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'select': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().selectOption(step.value ?? '', { timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'hover': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().hover({ timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'press_key':\n await page.keyboard.press(step.value ?? 'Enter');\n break;\n\n case 'assert_visible': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().waitFor({ state: 'visible', timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'assert_text': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n const text = await r.loc.first().textContent({ timeout });\n if (!text?.includes(step.value ?? '')) {\n throw new Error(`Expected text \"${step.value}\" not found. Got: \"${text?.slice(0, 100)}\"`);\n }\n applyHealInfo(result, r);\n break;\n }\n\n case 'assert_url': {\n const url = page.url();\n if (!url.includes(step.value ?? '')) {\n throw new Error(`Expected URL to contain \"${step.value}\". Got: \"${url}\"`);\n }\n break;\n }\n\n case 'wait':\n await page.waitForTimeout(step.timeout ?? 2000);\n break;\n\n case 'screenshot': {\n const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;\n await page.screenshot({ path: ssPath, fullPage: true });\n result.screenshotPath = ssPath;\n break;\n }\n }\n\n result.status = 'PASSED';\n } catch (err: any) {\n result.status = step.optional ? 'SKIPPED' : 'FAILED';\n result.error = err.message;\n if (!step.optional) {\n errorStep = step.stepNumber;\n }\n }\n\n try {\n await page.waitForLoadState('networkidle', { timeout: 1500 }).catch(() => {});\n if (!result.screenshotPath) {\n const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;\n await page.screenshot({ path: ssPath, fullPage: true });\n result.screenshotPath = ssPath;\n }\n } catch { /* Screenshot evidence is best-effort and should not fail the run. */ }\n\n result.duration = Date.now() - stepStart;\n stepResults.push(result);\n options.onStepComplete?.(result);\n\n if (result.status === 'FAILED') break;\n }\n\n try {\n const finalPath = `${options.screenshotDir}/${options.testRunId}-final.png`;\n await page.screenshot({ path: finalPath, fullPage: true });\n finalScreenshotPath = finalPath;\n } catch { /* non-fatal */ }\n\n return { stepResults, errorStep, finalScreenshotPath, consoleLogs, networkErrors };\n}\n\nfunction interpolate(template: string | undefined, data: Record<string, string>): string | undefined {\n if (!template) return template;\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => data[key] ?? `{{${key}}}`);\n}\n\nexport async function executeTestPlan(\n plan: ExecutionStep[],\n options: {\n screenshotDir: string;\n testRunId: string;\n viewport?: { width: number; height: number };\n onStepComplete?: (result: StepResult) => void;\n /** Run the test up to this many times to detect flakiness (default 1). */\n maxRuns?: number;\n browser?: 'chromium' | 'firefox' | 'webkit';\n /** Data row for parameterized execution: substitutes {{varname}} in step fields. */\n dataRow?: Record<string, string>;\n /** Enables the AI Self-Healing Agent tier when a step's selector can't be resolved\n * even after trying alternate locator strategies. Omit to keep mechanical-only\n * healing (no LLM calls during execution). */\n healCtx?: { tenantId: string; projectId: string };\n /** Decrypted Playwright storageState JSON \u2014 restores a logged-in session so\n * steps don't have to re-authenticate. Without this, any test case behind a\n * login wall fails at the first authenticated step. */\n storageState?: string;\n },\n): Promise<ExecutionResult> {\n if (options.dataRow && Object.keys(options.dataRow).length > 0) {\n const data = options.dataRow;\n plan = plan.map((step) => ({\n ...step,\n url: interpolate(step.url, data),\n selector: interpolate(step.selector, data),\n value: interpolate(step.value, data),\n description: interpolate(step.description, data) ?? step.description,\n }));\n }\n const maxRuns = options.maxRuns ?? 1;\n const browserEngine = { chromium, firefox, webkit }[options.browser ?? 'chromium'] ?? chromium;\n const start = Date.now();\n\n const outcomes: ('PASSED' | 'FAILED')[] = [];\n let lastResult: Awaited<ReturnType<typeof runPlan>> | null = null;\n let lastNetworkTimings: Array<{ url: string; method: string; status: number; duration: number }> = [];\n\n for (let run = 0; run < maxRuns; run++) {\n const browser = await browserEngine.launch({\n headless: true,\n // Optional override for environments with a non-standard/pre-provisioned\n // browser install (e.g. a custom base image with only system Chromium).\n ...(process.env.PLAYWRIGHT_EXECUTABLE_PATH ? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH } : {}),\n });\n let parsedStorageState: any;\n try {\n parsedStorageState = options.storageState ? JSON.parse(options.storageState) : undefined;\n } catch {\n parsedStorageState = undefined;\n }\n const context = await browser.newContext({\n viewport: options.viewport ?? { width: 1440, height: 900 },\n ...(parsedStorageState ? { storageState: parsedStorageState } : {}),\n });\n const page = await context.newPage();\n\n await page\n .addStyleTag({ content: '*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }' })\n .catch(() => {});\n\n const consoleLogs: Array<{ type: string; text: string; timestamp: number }> = [];\n const networkErrors: Array<{ url: string; status: number; method: string; timestamp: number }> = [];\n const networkTimings: Array<{ url: string; method: string; status: number; duration: number }> = [];\n const requestStartTimes = new Map<string, number>();\n\n page.on('console', msg => {\n if (consoleLogs.length < 100) {\n consoleLogs.push({ type: msg.type(), text: msg.text().slice(0, 500), timestamp: Date.now() });\n }\n });\n page.on('request', request => {\n const url = request.url();\n if (url.includes('/api/') || url.match(/\\/(graphql|rest|v\\d)\\//)) {\n requestStartTimes.set(request.url(), Date.now());\n }\n });\n page.on('response', response => {\n const url = response.url();\n const start = requestStartTimes.get(url);\n if (start && networkTimings.length < 200) {\n networkTimings.push({ url: url.slice(0, 300), method: response.request().method(), status: response.status(), duration: Date.now() - start });\n requestStartTimes.delete(url);\n }\n if (!response.ok() && !url.includes('favicon') && networkErrors.length < 50) {\n networkErrors.push({\n url: url.slice(0, 300),\n status: response.status(),\n method: response.request().method(),\n timestamp: Date.now(),\n });\n }\n });\n\n const TEST_EXECUTION_TIMEOUT_MS = 5 * 60_000;\n lastResult = await Promise.race([\n runPlan(page, plan, options, consoleLogs, networkErrors),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Test execution timed out')), TEST_EXECUTION_TIMEOUT_MS)\n ),\n ]);\n lastNetworkTimings = networkTimings;\n await browser.close();\n\n const passed = !lastResult.stepResults.some((r) => r.status === 'FAILED');\n outcomes.push(passed ? 'PASSED' : 'FAILED');\n\n // If all subsequent runs are consistent with first, no need to keep going\n if (run > 0 && outcomes.every((o) => o === outcomes[0])) break;\n }\n\n const passCount = outcomes.filter((o) => o === 'PASSED').length;\n const failCount = outcomes.filter((o) => o === 'FAILED').length;\n\n let status: ExecutionResult['status'];\n if (maxRuns > 1 && passCount > 0 && failCount > 0) {\n status = 'FLAKY';\n } else if (passCount === outcomes.length) {\n status = 'PASSED';\n } else {\n status = 'FAILED';\n }\n\n let rootCauseSummary: string | undefined;\n if (status === 'FAILED' || status === 'FLAKY') {\n const failedStep = lastResult!.stepResults.find(s => s.status === 'FAILED');\n rootCauseSummary = await analyzeFailure({\n tenantId: options.healCtx?.tenantId,\n error: failedStep?.error ?? 'Unknown error',\n stepDescription: failedStep?.description,\n consoleLogs: lastResult!.consoleLogs,\n networkErrors: lastResult!.networkErrors,\n screenshotPath: lastResult!.finalScreenshotPath,\n }).catch(() => undefined) ?? undefined;\n }\n\n return {\n stepResults: lastResult!.stepResults,\n status,\n errorStep: lastResult!.errorStep,\n totalDuration: Date.now() - start,\n finalScreenshotPath: lastResult!.finalScreenshotPath,\n retryCount: outcomes.length - 1,\n consoleLogs: lastResult!.consoleLogs,\n networkErrors: lastResult!.networkErrors,\n rootCauseSummary,\n networkTimings: lastNetworkTimings,\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { chromium, firefox, webkit, type Page, type Locator } from 'playwright';\nimport type { ExecutionStep } from '@detiq/agents';\nimport { assertCrawlUrlSafe } from '@detiq/core';\nimport { analyzeFailure } from './failure-analysis-agent.js';\n\nexport interface StepResult {\n stepNumber: number;\n description: string;\n status: 'PASSED' | 'FAILED' | 'SKIPPED';\n duration: number;\n error?: string;\n screenshotPath?: string;\n healed?: boolean;\n /** 'mechanical' = same selector text, different Playwright locator strategy.\n * 'ai' = AI Self-Healing Agent matched a semantically different element\n * (the app's selector text itself changed, e.g. \"Sign In\" -> \"Log In\"). */\n healedVia?: 'mechanical' | 'ai';\n healReasoning?: string;\n healConfidence?: number;\n elementBoundingRect?: { x: number; y: number; width: number; height: number } | null;\n elementSelector?: string;\n}\n\nexport interface ExecutionResult {\n stepResults: StepResult[];\n status: 'PASSED' | 'FAILED' | 'ERROR' | 'FLAKY';\n errorStep?: number;\n totalDuration: number;\n finalScreenshotPath?: string;\n retryCount?: number;\n consoleLogs?: Array<{ type: string; text: string; timestamp: number }>;\n networkErrors?: Array<{ url: string; status: number; method: string; timestamp: number }>;\n rootCauseSummary?: string;\n networkTimings?: Array<{ url: string; method: string; status: number; duration: number }>;\n}\n\nconst LOCATOR_STRATEGIES: ExecutionStep['selectorStrategy'][] = ['text', 'label', 'role', 'css'];\n\nfunction buildLocator(page: Page, selector: string, strategy: ExecutionStep['selectorStrategy'] = 'text'): Locator {\n switch (strategy) {\n case 'css': return page.locator(selector);\n case 'text': return page.getByText(selector, { exact: false });\n case 'role': return page.getByRole(selector as any);\n case 'label': return page.getByLabel(selector);\n default: return page.locator(selector);\n }\n}\n\n/** Scans the live page for visible interactive elements \u2014 the \"current page elements\"\n * fed to the AI Self-Healing Agent when mechanical locator strategies fail. */\nasync function extractCurrentPageElements(page: Page): Promise<Array<{ meaning: string; role: string; ariaLabel?: string; textContent?: string }>> {\n return page.evaluate(() => {\n const els = Array.from(document.querySelectorAll<HTMLElement>(\n 'a, button, input, select, textarea, [role=\"button\"], [role=\"link\"], [role=\"tab\"], [role=\"menuitem\"], [aria-label]'\n ));\n return els\n .filter((el) => {\n const rect = el.getBoundingClientRect();\n return rect.width > 0 && rect.height > 0;\n })\n .slice(0, 200)\n .map((el) => {\n const ariaLabel = el.getAttribute('aria-label') ?? undefined;\n const textContent = (el.textContent ?? '').replace(/\\s+/g, ' ').trim().slice(0, 100) || undefined;\n const role = el.getAttribute('role') ?? el.tagName.toLowerCase();\n return { meaning: ariaLabel || textContent || role, role, ariaLabel, textContent };\n });\n });\n}\n\n/**\n * Resolves a step's target element in three tiers:\n * 1. Exact selector + strategy as generated.\n * 2. Mechanical: same selector text, other Playwright locator strategies\n * (handles a strategy mismatch, not an actual UI change).\n * 3. AI: when tenantId/projectId are supplied, asks the Self-Healing Agent to find\n * the current page's best semantic match for the failed step description \u2014 the\n * tier that actually survives the app's selector text changing.\n */\nasync function resolveLocator(\n page: Page,\n step: ExecutionStep,\n healCtx?: { tenantId: string; projectId: string },\n): Promise<{ loc: Locator; healed: boolean; healedVia?: 'mechanical' | 'ai'; healReasoning?: string; healConfidence?: number } | null> {\n const { selector, selectorStrategy = 'text' } = step;\n if (!selector) return null;\n\n const primary = buildLocator(page, selector, selectorStrategy);\n const attached = await primary.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);\n if (attached) return { loc: primary, healed: false };\n\n // Tier 2 \u2014 mechanical: try other strategies before giving up\n for (const strategy of LOCATOR_STRATEGIES) {\n if (strategy === selectorStrategy) continue;\n const loc = buildLocator(page, selector, strategy);\n const visible = await loc.first().isVisible({ timeout: parseInt(process.env.TEST_SHORT_TIMEOUT_MS ?? '800', 10) }).catch(() => false);\n if (visible) return { loc, healed: true, healedVia: 'mechanical' };\n }\n\n // Tier 3 \u2014 AI: the selector text itself may no longer match anything on the page.\n if (healCtx) {\n try {\n const { healElement } = await import('@detiq/agents');\n const currentPageElements = await extractCurrentPageElements(page);\n const result = await healElement({\n tenantId: healCtx.tenantId,\n projectId: healCtx.projectId,\n failedElementMeaning: step.description || selector,\n currentPageElements,\n });\n if (result.healed && result.updatedTest) {\n const healedLoc = page.getByText(result.updatedTest.elementMeaning, { exact: false });\n const visible = await healedLoc.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);\n if (visible) {\n return {\n loc: healedLoc,\n healed: true,\n healedVia: 'ai',\n healReasoning: result.reasoning,\n healConfidence: result.confidence,\n };\n }\n }\n } catch {\n // AI healing is a best-effort last resort \u2014 any failure here (LLM error, bad\n // JSON, etc.) just falls through to the caller's normal Playwright error below.\n }\n }\n\n // Return primary \u2014 caller will fail with a meaningful Playwright error\n return { loc: primary, healed: false };\n}\n\nfunction applyHealInfo(result: StepResult, r: { healed: boolean; healedVia?: 'mechanical' | 'ai'; healReasoning?: string; healConfidence?: number }): void {\n result.healed = r.healed;\n result.healedVia = r.healedVia;\n result.healReasoning = r.healReasoning;\n result.healConfidence = r.healConfidence;\n}\n\nasync function runPlan(\n page: Page,\n plan: ExecutionStep[],\n options: { screenshotDir: string; testRunId: string; onStepComplete?: (result: StepResult) => void; healCtx?: { tenantId: string; projectId: string } },\n consoleLogs: Array<{ type: string; text: string; timestamp: number }>,\n networkErrors: Array<{ url: string; status: number; method: string; timestamp: number }>,\n): Promise<{ stepResults: StepResult[]; errorStep?: number; finalScreenshotPath?: string; consoleLogs: typeof consoleLogs; networkErrors: typeof networkErrors }> {\n const stepResults: StepResult[] = [];\n let errorStep: number | undefined;\n let finalScreenshotPath: string | undefined;\n\n for (const step of plan) {\n const stepStart = Date.now();\n const result: StepResult = {\n stepNumber: step.stepNumber,\n description: step.description,\n status: 'FAILED',\n duration: 0,\n };\n\n try {\n const timeout = step.timeout ?? parseInt(process.env.TEST_STEP_TIMEOUT_MS ?? '8000', 10);\n\n switch (step.action) {\n case 'navigate':\n if (!step.url) throw new Error('Navigate step missing url');\n assertCrawlUrlSafe(step.url);\n await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout });\n break;\n\n case 'click': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().click({ timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'fill': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().fill(step.value ?? '', { timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'select': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().selectOption(step.value ?? '', { timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'hover': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n await r.loc.first().hover({ timeout });\n applyHealInfo(result, r);\n break;\n }\n\n case 'press_key':\n await page.keyboard.press(step.value ?? 'Enter');\n break;\n\n case 'assert_visible': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n await r.loc.first().waitFor({ state: 'visible', timeout });\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result, r);\n break;\n }\n\n case 'assert_text': {\n const r = await resolveLocator(page, step, options.healCtx);\n if (!r) throw new Error('Could not resolve locator');\n const text = await r.loc.first().textContent({ timeout });\n if (!text?.includes(step.value ?? '')) {\n throw new Error(`Expected text \"${step.value}\" not found. Got: \"${text?.slice(0, 100)}\"`);\n }\n result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);\n result.elementSelector = step.selector;\n applyHealInfo(result, r);\n break;\n }\n\n case 'assert_url': {\n const url = page.url();\n if (!url.includes(step.value ?? '')) {\n throw new Error(`Expected URL to contain \"${step.value}\". Got: \"${url}\"`);\n }\n break;\n }\n\n case 'wait':\n await page.waitForTimeout(step.timeout ?? parseInt(process.env.TEST_STEP_DELAY_MS ?? '2000', 10));\n break;\n\n case 'screenshot': {\n const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;\n await page.screenshot({ path: ssPath, fullPage: true });\n result.screenshotPath = ssPath;\n break;\n }\n }\n\n result.status = 'PASSED';\n } catch (err: any) {\n result.status = step.optional ? 'SKIPPED' : 'FAILED';\n result.error = err.message;\n if (!step.optional) {\n errorStep = step.stepNumber;\n }\n }\n\n try {\n await page.waitForLoadState('networkidle', { timeout: parseInt(process.env.TEST_NETWORK_IDLE_TIMEOUT_MS ?? '1500', 10) }).catch(() => {});\n if (!result.screenshotPath) {\n const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;\n await page.screenshot({ path: ssPath, fullPage: true });\n result.screenshotPath = ssPath;\n }\n } catch { /* Screenshot evidence is best-effort and should not fail the run. */ }\n\n result.duration = Date.now() - stepStart;\n stepResults.push(result);\n options.onStepComplete?.(result);\n\n if (result.status === 'FAILED') break;\n }\n\n try {\n const finalPath = `${options.screenshotDir}/${options.testRunId}-final.png`;\n await page.screenshot({ path: finalPath, fullPage: true });\n finalScreenshotPath = finalPath;\n } catch { /* non-fatal */ }\n\n return { stepResults, errorStep, finalScreenshotPath, consoleLogs, networkErrors };\n}\n\nfunction interpolate(template: string | undefined, data: Record<string, string>): string | undefined {\n if (!template) return template;\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => data[key] ?? `{{${key}}}`);\n}\n\nexport async function executeTestPlan(\n plan: ExecutionStep[],\n options: {\n screenshotDir: string;\n testRunId: string;\n viewport?: { width: number; height: number };\n onStepComplete?: (result: StepResult) => void;\n /** Run the test up to this many times to detect flakiness (default 1). */\n maxRuns?: number;\n browser?: 'chromium' | 'firefox' | 'webkit';\n /** Data row for parameterized execution: substitutes {{varname}} in step fields. */\n dataRow?: Record<string, string>;\n /** Enables the AI Self-Healing Agent tier when a step's selector can't be resolved\n * even after trying alternate locator strategies. Omit to keep mechanical-only\n * healing (no LLM calls during execution). */\n healCtx?: { tenantId: string; projectId: string };\n /** Decrypted Playwright storageState JSON \u2014 restores a logged-in session so\n * steps don't have to re-authenticate. Without this, any test case behind a\n * login wall fails at the first authenticated step. */\n storageState?: string;\n },\n): Promise<ExecutionResult> {\n if (options.dataRow && Object.keys(options.dataRow).length > 0) {\n const data = options.dataRow;\n plan = plan.map((step) => ({\n ...step,\n url: interpolate(step.url, data),\n selector: interpolate(step.selector, data),\n value: interpolate(step.value, data),\n description: interpolate(step.description, data) ?? step.description,\n }));\n }\n const maxRuns = options.maxRuns ?? 1;\n const browserEngine = { chromium, firefox, webkit }[options.browser ?? 'chromium'] ?? chromium;\n const start = Date.now();\n\n const outcomes: ('PASSED' | 'FAILED')[] = [];\n let lastResult: Awaited<ReturnType<typeof runPlan>> | null = null;\n let lastNetworkTimings: Array<{ url: string; method: string; status: number; duration: number }> = [];\n\n for (let run = 0; run < maxRuns; run++) {\n const browser = await browserEngine.launch({\n headless: true,\n // Optional override for environments with a non-standard/pre-provisioned\n // browser install (e.g. a custom base image with only system Chromium).\n ...(process.env.PLAYWRIGHT_EXECUTABLE_PATH ? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH } : {}),\n });\n let parsedStorageState: any;\n try {\n parsedStorageState = options.storageState ? JSON.parse(options.storageState) : undefined;\n } catch {\n parsedStorageState = undefined;\n }\n const context = await browser.newContext({\n viewport: options.viewport ?? { width: 1440, height: 900 },\n ...(parsedStorageState ? { storageState: parsedStorageState } : {}),\n });\n const page = await context.newPage();\n\n await page\n .addStyleTag({ content: '*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }' })\n .catch(() => {});\n\n const consoleLogs: Array<{ type: string; text: string; timestamp: number }> = [];\n const networkErrors: Array<{ url: string; status: number; method: string; timestamp: number }> = [];\n const networkTimings: Array<{ url: string; method: string; status: number; duration: number }> = [];\n const requestStartTimes = new Map<string, number>();\n\n page.on('console', msg => {\n if (consoleLogs.length < 100) {\n consoleLogs.push({ type: msg.type(), text: msg.text().slice(0, 500), timestamp: Date.now() });\n }\n });\n page.on('request', request => {\n const url = request.url();\n if (url.includes('/api/') || url.match(/\\/(graphql|rest|v\\d)\\//)) {\n requestStartTimes.set(request.url(), Date.now());\n }\n });\n page.on('response', response => {\n const url = response.url();\n const start = requestStartTimes.get(url);\n if (start && networkTimings.length < 200) {\n networkTimings.push({ url: url.slice(0, 300), method: response.request().method(), status: response.status(), duration: Date.now() - start });\n requestStartTimes.delete(url);\n }\n if (!response.ok() && !url.includes('favicon') && networkErrors.length < 50) {\n networkErrors.push({\n url: url.slice(0, 300),\n status: response.status(),\n method: response.request().method(),\n timestamp: Date.now(),\n });\n }\n });\n\n const TEST_EXECUTION_TIMEOUT_MS = 5 * 60_000;\n lastResult = await Promise.race([\n runPlan(page, plan, options, consoleLogs, networkErrors),\n new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error('Test execution timed out')), TEST_EXECUTION_TIMEOUT_MS)\n ),\n ]);\n lastNetworkTimings = networkTimings;\n await browser.close();\n\n const passed = !lastResult.stepResults.some((r) => r.status === 'FAILED');\n outcomes.push(passed ? 'PASSED' : 'FAILED');\n\n // If all subsequent runs are consistent with first, no need to keep going\n if (run > 0 && outcomes.every((o) => o === outcomes[0])) break;\n }\n\n const passCount = outcomes.filter((o) => o === 'PASSED').length;\n const failCount = outcomes.filter((o) => o === 'FAILED').length;\n\n let status: ExecutionResult['status'];\n if (maxRuns > 1 && passCount > 0 && failCount > 0) {\n status = 'FLAKY';\n } else if (passCount === outcomes.length) {\n status = 'PASSED';\n } else {\n status = 'FAILED';\n }\n\n let rootCauseSummary: string | undefined;\n if (status === 'FAILED' || status === 'FLAKY') {\n const failedStep = lastResult!.stepResults.find(s => s.status === 'FAILED');\n rootCauseSummary = await analyzeFailure({\n tenantId: options.healCtx?.tenantId,\n error: failedStep?.error ?? 'Unknown error',\n stepDescription: failedStep?.description,\n consoleLogs: lastResult!.consoleLogs,\n networkErrors: lastResult!.networkErrors,\n screenshotPath: lastResult!.finalScreenshotPath,\n }).catch(() => undefined) ?? undefined;\n }\n\n return {\n stepResults: lastResult!.stepResults,\n status,\n errorStep: lastResult!.errorStep,\n totalDuration: Date.now() - start,\n finalScreenshotPath: lastResult!.finalScreenshotPath,\n retryCount: outcomes.length - 1,\n consoleLogs: lastResult!.consoleLogs,\n networkErrors: lastResult!.networkErrors,\n rootCauseSummary,\n networkTimings: lastNetworkTimings,\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["start"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@detiq/crawler",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
6
6
  "type": "module",
package/src/ai-login.ts CHANGED
@@ -141,7 +141,20 @@ export async function loginWithStagehand(
141
141
  await logStep('fill_password', async () => {
142
142
  const observations = await stagehand.observe('Find the password input field');
143
143
  const selector = observations[0]?.selector || 'input[type="password"]';
144
- await pg.locator(selector).fill(creds.password);
144
+ try {
145
+ await pg.locator(selector).fill(creds.password);
146
+ } catch {
147
+ // Stagehand locator failed (e.g. form re-rendered, proxy can't locate element).
148
+ // Fall back to direct DOM fill via evaluate so the password never touches the LLM.
149
+ await pg.evaluate((pwd: string) => {
150
+ const input = document.querySelector('input[type="password"]') as HTMLInputElement | null;
151
+ if (!input) throw new Error('No password field found on page');
152
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
153
+ setter?.call(input, pwd);
154
+ input.dispatchEvent(new Event('input', { bubbles: true }));
155
+ input.dispatchEvent(new Event('change', { bubbles: true }));
156
+ }, creds.password);
157
+ }
145
158
  });
146
159
  await logStep('click_submit', () => actWithMouseSim(stagehand, 'Click the sign in, log in, login, or continue button to submit credentials'));
147
160
  await pg.waitForLoadState('networkidle').catch(() => pg.waitForTimeout(2000));
package/src/crawler.ts CHANGED
@@ -397,6 +397,8 @@ export async function crawlProject(opts: {
397
397
  tenantId: string;
398
398
  projectId: string;
399
399
  appUrl: string;
400
+ /** The viewport used for this crawl pass */
401
+ viewport?: 'DESKTOP' | 'MOBILE_PORTRAIT' | 'TABLET';
400
402
  credentials?: Credentials;
401
403
  /** Playwright storageState JSON string — if present, session is restored and AI login is skipped. */
402
404
  storageState?: string;
@@ -1059,9 +1061,13 @@ export async function crawlProject(opts: {
1059
1061
  }, state.origins);
1060
1062
  }
1061
1063
 
1062
- // Reload to apply cookies + trigger localStorage restore initScript
1064
+ // Navigate to appUrl with cookies now set.
1065
+ // We MUST navigate to appUrl (not reload current page) because the initial
1066
+ // goto ran without cookies and was redirected to /login. Reloading /login stays
1067
+ // at /login even with a valid cookie (public path, no auth redirect). Navigating
1068
+ // to appUrl lets server middleware see the cookie and route to the authenticated app.
1063
1069
  await onProgress?.(12, 'Restoring session');
1064
- await rawPage.reload({ waitUntil: 'domcontentloaded' });
1070
+ await rawPage.goto(opts.appUrl, { waitUntil: 'domcontentloaded' }).catch(() => { });
1065
1071
  await waitForStableDOM(page);
1066
1072
  // SPA root pages redirect client-side (useEffect → router.replace). Wait for it.
1067
1073
  if (page.url() === opts.appUrl || page.url() === opts.appUrl.replace(/\/$/, '')) {
@@ -2157,8 +2163,12 @@ export async function crawlProject(opts: {
2157
2163
  // Parallelize: screenshot + element extraction + link discovery + AT capture (all work on current page state)
2158
2164
  const [, elements, nextLinks, atElements] = await Promise.all([
2159
2165
  page.screenshot({ path: shotPath, fullPage: true })
2160
- .then(() => AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, 'DESKTOP')),
2161
- extractElements(stagehand, page, elementLLM ?? undefined).catch(() => [] as Awaited<ReturnType<typeof extractElements>>),
2166
+ .then(() => AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, 'DESKTOP'))
2167
+ .catch((err: any) => console.error('[crawler] page.screenshot failed:', err)),
2168
+ extractElements(stagehand, page, elementLLM ?? undefined).catch((err: any) => {
2169
+ console.error('[crawler] extractElements failed:', err);
2170
+ return [] as Awaited<ReturnType<typeof extractElements>>;
2171
+ }),
2162
2172
  // Always run AI link discovery (even when cache exists) — cache is merged in, not used as a replacement.
2163
2173
  // Skipping AI when cache exists was a bug: stale cache from broken crawls permanently blocked discovery.
2164
2174
  discoverLinks(stagehand, origin, page, linkLLM ?? undefined, aiSkipCount > 0)
@@ -2635,7 +2645,7 @@ export async function crawlProject(opts: {
2635
2645
  fcp: rawPerf?.fcp,
2636
2646
  cls: rawPerf?.cls,
2637
2647
  ttfb: rawPerf?.ttfb,
2638
- viewport: 'DESKTOP',
2648
+ viewport: opts.viewport ?? 'DESKTOP',
2639
2649
  ...(lhScores ? {
2640
2650
  lighthousePerf: lhScores.perf,
2641
2651
  lighthouseA11y: lhScores.a11y,
@@ -3062,8 +3072,7 @@ export async function crawlProject(opts: {
3062
3072
  logger.info({ url: sessionCheckUrl }, '[P5] Session alive — proceeding with viewport pass');
3063
3073
  }
3064
3074
  } catch (err) {
3065
- logger.warn({ err: String(err) }, '[P5] Session health check failed — skipping viewport pass to avoid bad screenshots');
3066
- sessionAliveForViewports = false;
3075
+ logger.warn({ err: String(err) }, '[P5] Session health check navigation failed — proceeding with viewport pass anyway');
3067
3076
  }
3068
3077
 
3069
3078
  // Step 5: Multi-viewport pass — screenshot + DOM element extraction at each device profile.
@@ -0,0 +1,12 @@
1
+ @@ -2160,8 +2160,9 @@
2
+
3
+ const [, elements, nextLinks, atElements] = await Promise.all([
4
+ page.screenshot({ path: shotPath, fullPage: true })
5
+ - .then(() => AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, 'DESKTOP')),
6
+ - extractElements(stagehand, page, elementLLM ?? undefined).catch(() => [] as Awaited<ReturnType<typeof extractElements>>),
7
+ + .then(() => AppBrain.saveScreenshot(opts.tenantId, opts.projectId, screen.id, shotPath, screen.version, 'DESKTOP'))
8
+ + .catch((err) => console.error('[crawler] page.screenshot failed:', err)),
9
+ + extractElements(stagehand, page, elementLLM ?? undefined).catch((err) => { console.error('[crawler] extractElements failed:', err); return [] as Awaited<ReturnType<typeof extractElements>> }),
10
+ extractNavigationLinks(page).catch(() => [] as string[]),
11
+ stagehand.page.extractElements({
12
+
@@ -290,7 +290,8 @@ export async function httpCrawl(opts: HttpCrawlOpts): Promise<HttpCrawlResult> {
290
290
  const maxScreens = opts.maxScreens ?? 100;
291
291
  const cacheDir = opts.cacheDir ?? process.env.CRAWLER_CACHE_DIR ?? '/tmp/zeta-cache';
292
292
  const timeoutMs = opts.timeoutMs ?? 15_000;
293
- const userAgent = opts.userAgent ?? 'ZeTa-Crawler/1.0 (+https://zetaai.dpdns.org)';
293
+ const CRAWLER_USER_AGENT = process.env.CRAWLER_USER_AGENT ?? 'ZeTa-Crawler/1.0 (+https://zetaai.io/crawler)';
294
+ const userAgent = opts.userAgent ?? CRAWLER_USER_AGENT;
294
295
  const cache = new ResponseCache(cacheDir);
295
296
  const robots = opts.respectRobotsTxt !== false ? new RobotsCache() : null;
296
297
  const urlFilterRe = opts.urlFilter ? (() => { try { return new RegExp(opts.urlFilter!, 'i'); } catch { return null; } })() : null;
package/src/job-queue.ts CHANGED
@@ -1325,7 +1325,8 @@ async function processRunTestSuiteJob(jobId: string, config: any) {
1325
1325
  }
1326
1326
 
1327
1327
  const aiHealingEnabled = await isFeatureEnabled(tenantId, 'AI_SELF_HEALING');
1328
- const screenshotDir = config.screenshotDir || platformConfig.screenshotDir || '/tmp/zeta-screenshots';
1328
+ const SCREENSHOT_DIR = process.env.SCREENSHOT_DIR ?? '/tmp/zeta-screenshots';
1329
+ const screenshotDir = config.screenshotDir || platformConfig.screenshotDir || SCREENSHOT_DIR;
1329
1330
  const VALID_BROWSERS = ['chromium', 'firefox', 'webkit'];
1330
1331
  const validBrowser = VALID_BROWSERS.includes(browser) ? browser : 'chromium';
1331
1332
 
@@ -1824,7 +1825,7 @@ async function processMonitorJob(jobId: string, config: any, opts?: ProcessJobOp
1824
1825
  opts?.onProgress?.(pct);
1825
1826
 
1826
1827
  try {
1827
- const crawlResult = await httpCrawl({ appUrl: url, maxScreens: 1, respectRobotsTxt: false, timeoutMs: 20_000 });
1828
+ const crawlResult = await httpCrawl({ appUrl: url, maxScreens: 1, respectRobotsTxt: false, timeoutMs: parseInt(process.env.CRAWL_TIMEOUT_MS ?? '20000', 10) });
1828
1829
  const screen = crawlResult.screens[0];
1829
1830
  if (!screen) {
1830
1831
  results.push({ url, changed: false, reason: 'fetch_empty', currentHash: '', previousHash: null });
package/src/skyvern.ts CHANGED
@@ -67,9 +67,6 @@ export async function skyvernNavigate(
67
67
  return { screenshotUrls: [], visitedUrls: [], status: 'failed' };
68
68
  }
69
69
 
70
- const { assertResolvesToPublicAddress } = await import('@detiq/core');
71
- await assertResolvesToPublicAddress(apiUrl);
72
-
73
70
  const visitedUrls: string[] = [];
74
71
  const screenshotUrls: string[] = [];
75
72
  const skyvernSteps: SkyvernStep[] = [];
@@ -164,9 +161,6 @@ export async function skyvernFallback(task: SkyvernTask, tenantConfig?: SkyvernC
164
161
  return 'failed';
165
162
  }
166
163
 
167
- const { assertResolvesToPublicAddress } = await import('@detiq/core');
168
- await assertResolvesToPublicAddress(apiUrl);
169
-
170
164
  try {
171
165
  const res = await fetch(`${apiUrl}/api/v1/tasks`, {
172
166
  method: 'POST',
@@ -17,6 +17,8 @@ export interface StepResult {
17
17
  healedVia?: 'mechanical' | 'ai';
18
18
  healReasoning?: string;
19
19
  healConfidence?: number;
20
+ elementBoundingRect?: { x: number; y: number; width: number; height: number } | null;
21
+ elementSelector?: string;
20
22
  }
21
23
 
22
24
  export interface ExecutionResult {
@@ -84,14 +86,14 @@ async function resolveLocator(
84
86
  if (!selector) return null;
85
87
 
86
88
  const primary = buildLocator(page, selector, selectorStrategy);
87
- const attached = await primary.first().isVisible({ timeout: 1500 }).catch(() => false);
89
+ const attached = await primary.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);
88
90
  if (attached) return { loc: primary, healed: false };
89
91
 
90
92
  // Tier 2 — mechanical: try other strategies before giving up
91
93
  for (const strategy of LOCATOR_STRATEGIES) {
92
94
  if (strategy === selectorStrategy) continue;
93
95
  const loc = buildLocator(page, selector, strategy);
94
- const visible = await loc.first().isVisible({ timeout: 800 }).catch(() => false);
96
+ const visible = await loc.first().isVisible({ timeout: parseInt(process.env.TEST_SHORT_TIMEOUT_MS ?? '800', 10) }).catch(() => false);
95
97
  if (visible) return { loc, healed: true, healedVia: 'mechanical' };
96
98
  }
97
99
 
@@ -108,7 +110,7 @@ async function resolveLocator(
108
110
  });
109
111
  if (result.healed && result.updatedTest) {
110
112
  const healedLoc = page.getByText(result.updatedTest.elementMeaning, { exact: false });
111
- const visible = await healedLoc.first().isVisible({ timeout: 1500 }).catch(() => false);
113
+ const visible = await healedLoc.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);
112
114
  if (visible) {
113
115
  return {
114
116
  loc: healedLoc,
@@ -157,7 +159,7 @@ async function runPlan(
157
159
  };
158
160
 
159
161
  try {
160
- const timeout = step.timeout ?? 8000;
162
+ const timeout = step.timeout ?? parseInt(process.env.TEST_STEP_TIMEOUT_MS ?? '8000', 10);
161
163
 
162
164
  switch (step.action) {
163
165
  case 'navigate':
@@ -169,6 +171,8 @@ async function runPlan(
169
171
  case 'click': {
170
172
  const r = await resolveLocator(page, step, options.healCtx);
171
173
  if (!r) throw new Error('Could not resolve locator');
174
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
175
+ result.elementSelector = step.selector;
172
176
  await r.loc.first().click({ timeout });
173
177
  applyHealInfo(result, r);
174
178
  break;
@@ -177,6 +181,8 @@ async function runPlan(
177
181
  case 'fill': {
178
182
  const r = await resolveLocator(page, step, options.healCtx);
179
183
  if (!r) throw new Error('Could not resolve locator');
184
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
185
+ result.elementSelector = step.selector;
180
186
  await r.loc.first().fill(step.value ?? '', { timeout });
181
187
  applyHealInfo(result, r);
182
188
  break;
@@ -185,6 +191,8 @@ async function runPlan(
185
191
  case 'select': {
186
192
  const r = await resolveLocator(page, step, options.healCtx);
187
193
  if (!r) throw new Error('Could not resolve locator');
194
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
195
+ result.elementSelector = step.selector;
188
196
  await r.loc.first().selectOption(step.value ?? '', { timeout });
189
197
  applyHealInfo(result, r);
190
198
  break;
@@ -193,6 +201,8 @@ async function runPlan(
193
201
  case 'hover': {
194
202
  const r = await resolveLocator(page, step, options.healCtx);
195
203
  if (!r) throw new Error('Could not resolve locator');
204
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
205
+ result.elementSelector = step.selector;
196
206
  await r.loc.first().hover({ timeout });
197
207
  applyHealInfo(result, r);
198
208
  break;
@@ -206,6 +216,8 @@ async function runPlan(
206
216
  const r = await resolveLocator(page, step, options.healCtx);
207
217
  if (!r) throw new Error('Could not resolve locator');
208
218
  await r.loc.first().waitFor({ state: 'visible', timeout });
219
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
220
+ result.elementSelector = step.selector;
209
221
  applyHealInfo(result, r);
210
222
  break;
211
223
  }
@@ -217,6 +229,8 @@ async function runPlan(
217
229
  if (!text?.includes(step.value ?? '')) {
218
230
  throw new Error(`Expected text "${step.value}" not found. Got: "${text?.slice(0, 100)}"`);
219
231
  }
232
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
233
+ result.elementSelector = step.selector;
220
234
  applyHealInfo(result, r);
221
235
  break;
222
236
  }
@@ -230,7 +244,7 @@ async function runPlan(
230
244
  }
231
245
 
232
246
  case 'wait':
233
- await page.waitForTimeout(step.timeout ?? 2000);
247
+ await page.waitForTimeout(step.timeout ?? parseInt(process.env.TEST_STEP_DELAY_MS ?? '2000', 10));
234
248
  break;
235
249
 
236
250
  case 'screenshot': {
@@ -251,7 +265,7 @@ async function runPlan(
251
265
  }
252
266
 
253
267
  try {
254
- await page.waitForLoadState('networkidle', { timeout: 1500 }).catch(() => {});
268
+ await page.waitForLoadState('networkidle', { timeout: parseInt(process.env.TEST_NETWORK_IDLE_TIMEOUT_MS ?? '1500', 10) }).catch(() => {});
255
269
  if (!result.screenshotPath) {
256
270
  const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;
257
271
  await page.screenshot({ path: ssPath, fullPage: true });
@@ -35,6 +35,8 @@ 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;
@@ -13,6 +13,13 @@ export interface StepResult {
13
13
  healedVia?: 'mechanical' | 'ai';
14
14
  healReasoning?: string;
15
15
  healConfidence?: number;
16
+ elementBoundingRect?: {
17
+ x: number;
18
+ y: number;
19
+ width: number;
20
+ height: number;
21
+ } | null;
22
+ elementSelector?: string;
16
23
  }
17
24
  export interface ExecutionResult {
18
25
  stepResults: StepResult[];