@detiq/crawler 1.1.9 → 1.1.11

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.
Files changed (83) hide show
  1. package/dist/accessibility-auditor.js +54 -74
  2. package/dist/ai-login.js +227 -249
  3. package/dist/auth-token-rotator.js +57 -67
  4. package/dist/auth.js +299 -336
  5. package/dist/browser-extension.js +104 -142
  6. package/dist/captcha-solver.js +98 -125
  7. package/dist/color-contrast.js +77 -89
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +54 -71
  10. package/dist/cookie-consent.js +68 -83
  11. package/dist/cookie-jar.js +53 -60
  12. package/dist/crawler.js +4494 -5353
  13. package/dist/custom-js-injector.js +45 -61
  14. package/dist/dark-mode-capture.js +67 -81
  15. package/dist/dead-link-detector.js +47 -55
  16. package/dist/element-hash.js +8 -12
  17. package/dist/email-otp.js +154 -195
  18. package/dist/engine-router.js +87 -98
  19. package/dist/export/bulk-export.js +70 -55
  20. package/dist/export/vector-export.js +77 -105
  21. package/dist/extractors/advanced.js +249 -266
  22. package/dist/extractors/artifact-envelope.js +59 -58
  23. package/dist/failure-analysis-agent.js +37 -33
  24. package/dist/feature-flag-detector.js +51 -66
  25. package/dist/form-detector.js +55 -75
  26. package/dist/frontier.js +76 -101
  27. package/dist/gap-pass.js +68 -89
  28. package/dist/graphql-detector.js +62 -72
  29. package/dist/http-engine.js +282 -330
  30. package/dist/incremental-diff.js +54 -87
  31. package/dist/index.js +67 -28
  32. package/dist/input-fuzzer.js +93 -97
  33. package/dist/interactive-probe.js +715 -827
  34. package/dist/job-queue.js +2151 -2169
  35. package/dist/js-coverage.js +52 -56
  36. package/dist/js-router-detector.js +86 -117
  37. package/dist/language-detector.js +51 -63
  38. package/dist/link-classifier.js +32 -50
  39. package/dist/live-view.js +91 -108
  40. package/dist/locator-extractor.js +151 -187
  41. package/dist/logger.js +9 -7
  42. package/dist/markdown-extract.js +48 -61
  43. package/dist/native-crawler.js +780 -946
  44. package/dist/network-observer.js +135 -156
  45. package/dist/network-throttle.js +29 -40
  46. package/dist/openapi-generator.js +77 -92
  47. package/dist/pdf-extractor.js +72 -90
  48. package/dist/performance-metrics.js +80 -102
  49. package/dist/phash.js +125 -144
  50. package/dist/pii-scrubber.js +65 -68
  51. package/dist/playwright-recorder.js +70 -58
  52. package/dist/policies/crawl-safety.js +31 -34
  53. package/dist/profiles/domain-profile.js +130 -125
  54. package/dist/proxy.js +146 -172
  55. package/dist/rate-limiter.js +58 -65
  56. package/dist/redirect-tracker.js +60 -64
  57. package/dist/redis-frontier.js +116 -136
  58. package/dist/redis-queue.js +157 -144
  59. package/dist/response-cache.js +127 -140
  60. package/dist/responsive-detector.js +42 -46
  61. package/dist/retry-policy.js +46 -50
  62. package/dist/robots-txt.js +69 -83
  63. package/dist/screen-reader.js +45 -70
  64. package/dist/screenshot-masker.js +59 -77
  65. package/dist/seo-checker.js +88 -107
  66. package/dist/session-capture.js +192 -212
  67. package/dist/session-export.js +107 -133
  68. package/dist/sitemap-writer.js +38 -41
  69. package/dist/sitemap.js +141 -202
  70. package/dist/skyvern.js +123 -148
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +74 -90
  73. package/dist/stealth/human-behavior.js +156 -173
  74. package/dist/telemetry.js +68 -82
  75. package/dist/test-executor.js +309 -335
  76. package/dist/tls-fingerprint.js +77 -88
  77. package/dist/url-pattern-completer.js +71 -90
  78. package/dist/visual-diff-verdict.js +23 -38
  79. package/dist/webhook-notifier.js +71 -69
  80. package/dist/websocket-detector.js +56 -68
  81. package/dist/worker.js +29 -38
  82. package/package.json +1 -1
  83. package/src/job-queue.ts +136 -0
@@ -1,358 +1,332 @@
1
- import { chromium, firefox, webkit } from 'playwright';
2
- import { assertCrawlUrlSafe } from '@detiq/core';
3
- import { analyzeFailure } from './failure-analysis-agent.js';
4
- const LOCATOR_STRATEGIES = ['text', 'label', 'role', 'css'];
5
- function buildLocator(page, selector, strategy = 'text') {
6
- switch (strategy) {
7
- case 'css': return page.locator(selector);
8
- case 'text': return page.getByText(selector, { exact: false });
9
- case 'role': return page.getByRole(selector);
10
- case 'label': return page.getByLabel(selector);
11
- default: return page.locator(selector);
12
- }
1
+ import { chromium, firefox, webkit } from "playwright";
2
+ import { assertCrawlUrlSafe } from "@detiq/core";
3
+ import { analyzeFailure } from "./failure-analysis-agent.js";
4
+ const LOCATOR_STRATEGIES = ["text", "label", "role", "css"];
5
+ function buildLocator(page, selector, strategy = "text") {
6
+ switch (strategy) {
7
+ case "css":
8
+ return page.locator(selector);
9
+ case "text":
10
+ return page.getByText(selector, { exact: false });
11
+ case "role":
12
+ return page.getByRole(selector);
13
+ case "label":
14
+ return page.getByLabel(selector);
15
+ default:
16
+ return page.locator(selector);
17
+ }
13
18
  }
14
- /** Scans the live page for visible interactive elements — the "current page elements"
15
- * fed to the AI Self-Healing Agent when mechanical locator strategies fail. */
16
19
  async function extractCurrentPageElements(page) {
17
- return page.evaluate(() => {
18
- const els = Array.from(document.querySelectorAll('a, button, input, select, textarea, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [aria-label]'));
19
- return els
20
- .filter((el) => {
21
- const rect = el.getBoundingClientRect();
22
- return rect.width > 0 && rect.height > 0;
23
- })
24
- .slice(0, 200)
25
- .map((el) => {
26
- const ariaLabel = el.getAttribute('aria-label') ?? undefined;
27
- const textContent = (el.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 100) || undefined;
28
- const role = el.getAttribute('role') ?? el.tagName.toLowerCase();
29
- return { meaning: ariaLabel || textContent || role, role, ariaLabel, textContent };
30
- });
20
+ return page.evaluate(() => {
21
+ const els = Array.from(document.querySelectorAll(
22
+ 'a, button, input, select, textarea, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [aria-label]'
23
+ ));
24
+ return els.filter((el) => {
25
+ const rect = el.getBoundingClientRect();
26
+ return rect.width > 0 && rect.height > 0;
27
+ }).slice(0, 200).map((el) => {
28
+ const ariaLabel = el.getAttribute("aria-label") ?? void 0;
29
+ const textContent = (el.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 100) || void 0;
30
+ const role = el.getAttribute("role") ?? el.tagName.toLowerCase();
31
+ return { meaning: ariaLabel || textContent || role, role, ariaLabel, textContent };
31
32
  });
33
+ });
32
34
  }
33
- /**
34
- * Resolves a step's target element in three tiers:
35
- * 1. Exact selector + strategy as generated.
36
- * 2. Mechanical: same selector text, other Playwright locator strategies
37
- * (handles a strategy mismatch, not an actual UI change).
38
- * 3. AI: when tenantId/projectId are supplied, asks the Self-Healing Agent to find
39
- * the current page's best semantic match for the failed step description — the
40
- * tier that actually survives the app's selector text changing.
41
- */
42
35
  async function resolveLocator(page, step, healCtx) {
43
- const { selector, selectorStrategy = 'text' } = step;
44
- if (!selector)
45
- return null;
46
- const primary = buildLocator(page, selector, selectorStrategy);
47
- const attached = await primary.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);
48
- if (attached)
49
- return { loc: primary, healed: false };
50
- // Tier 2 — mechanical: try other strategies before giving up
51
- for (const strategy of LOCATOR_STRATEGIES) {
52
- if (strategy === selectorStrategy)
53
- continue;
54
- const loc = buildLocator(page, selector, strategy);
55
- const visible = await loc.first().isVisible({ timeout: parseInt(process.env.TEST_SHORT_TIMEOUT_MS ?? '800', 10) }).catch(() => false);
56
- if (visible)
57
- return { loc, healed: true, healedVia: 'mechanical' };
58
- }
59
- // Tier 3 — AI: the selector text itself may no longer match anything on the page.
60
- if (healCtx) {
61
- try {
62
- const { healElement } = await import('@detiq/agents');
63
- const currentPageElements = await extractCurrentPageElements(page);
64
- const result = await healElement({
65
- tenantId: healCtx.tenantId,
66
- projectId: healCtx.projectId,
67
- failedElementMeaning: step.description || selector,
68
- currentPageElements,
69
- });
70
- if (result.healed && result.updatedTest) {
71
- const healedLoc = page.getByText(result.updatedTest.elementMeaning, { exact: false });
72
- const visible = await healedLoc.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? '1500', 10) }).catch(() => false);
73
- if (visible) {
74
- return {
75
- loc: healedLoc,
76
- healed: true,
77
- healedVia: 'ai',
78
- healReasoning: result.reasoning,
79
- healConfidence: result.confidence,
80
- };
81
- }
82
- }
83
- }
84
- catch {
85
- // AI healing is a best-effort last resort — any failure here (LLM error, bad
86
- // JSON, etc.) just falls through to the caller's normal Playwright error below.
36
+ const { selector, selectorStrategy = "text" } = step;
37
+ if (!selector) return null;
38
+ const primary = buildLocator(page, selector, selectorStrategy);
39
+ const attached = await primary.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? "1500", 10) }).catch(() => false);
40
+ if (attached) return { loc: primary, healed: false };
41
+ for (const strategy of LOCATOR_STRATEGIES) {
42
+ if (strategy === selectorStrategy) continue;
43
+ const loc = buildLocator(page, selector, strategy);
44
+ const visible = await loc.first().isVisible({ timeout: parseInt(process.env.TEST_SHORT_TIMEOUT_MS ?? "800", 10) }).catch(() => false);
45
+ if (visible) return { loc, healed: true, healedVia: "mechanical" };
46
+ }
47
+ if (healCtx) {
48
+ try {
49
+ const { healElement } = await import("@detiq/agents");
50
+ const currentPageElements = await extractCurrentPageElements(page);
51
+ const result = await healElement({
52
+ tenantId: healCtx.tenantId,
53
+ projectId: healCtx.projectId,
54
+ failedElementMeaning: step.description || selector,
55
+ currentPageElements
56
+ });
57
+ if (result.healed && result.updatedTest) {
58
+ const healedLoc = page.getByText(result.updatedTest.elementMeaning, { exact: false });
59
+ const visible = await healedLoc.first().isVisible({ timeout: parseInt(process.env.TEST_WAIT_TIMEOUT_MS ?? "1500", 10) }).catch(() => false);
60
+ if (visible) {
61
+ return {
62
+ loc: healedLoc,
63
+ healed: true,
64
+ healedVia: "ai",
65
+ healReasoning: result.reasoning,
66
+ healConfidence: result.confidence
67
+ };
87
68
  }
69
+ }
70
+ } catch {
88
71
  }
89
- // Return primary — caller will fail with a meaningful Playwright error
90
- return { loc: primary, healed: false };
72
+ }
73
+ return { loc: primary, healed: false };
91
74
  }
92
75
  function applyHealInfo(result, r) {
93
- result.healed = r.healed;
94
- result.healedVia = r.healedVia;
95
- result.healReasoning = r.healReasoning;
96
- result.healConfidence = r.healConfidence;
76
+ result.healed = r.healed;
77
+ result.healedVia = r.healedVia;
78
+ result.healReasoning = r.healReasoning;
79
+ result.healConfidence = r.healConfidence;
97
80
  }
98
81
  async function runPlan(page, plan, options, consoleLogs, networkErrors) {
99
- const stepResults = [];
100
- let errorStep;
101
- let finalScreenshotPath;
102
- for (const step of plan) {
103
- const stepStart = Date.now();
104
- const result = {
105
- stepNumber: step.stepNumber,
106
- description: step.description,
107
- status: 'FAILED',
108
- duration: 0,
109
- };
110
- try {
111
- const timeout = step.timeout ?? parseInt(process.env.TEST_STEP_TIMEOUT_MS ?? '8000', 10);
112
- switch (step.action) {
113
- case 'navigate':
114
- if (!step.url)
115
- throw new Error('Navigate step missing url');
116
- assertCrawlUrlSafe(step.url);
117
- await page.goto(step.url, { waitUntil: 'domcontentloaded', timeout });
118
- break;
119
- case 'click': {
120
- const r = await resolveLocator(page, step, options.healCtx);
121
- if (!r)
122
- throw new Error('Could not resolve locator');
123
- result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
124
- result.elementSelector = step.selector;
125
- await r.loc.first().click({ timeout });
126
- applyHealInfo(result, r);
127
- break;
128
- }
129
- case 'fill': {
130
- const r = await resolveLocator(page, step, options.healCtx);
131
- if (!r)
132
- throw new Error('Could not resolve locator');
133
- result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
134
- result.elementSelector = step.selector;
135
- await r.loc.first().fill(step.value ?? '', { timeout });
136
- applyHealInfo(result, r);
137
- break;
138
- }
139
- case 'select': {
140
- const r = await resolveLocator(page, step, options.healCtx);
141
- if (!r)
142
- throw new Error('Could not resolve locator');
143
- result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
144
- result.elementSelector = step.selector;
145
- await r.loc.first().selectOption(step.value ?? '', { timeout });
146
- applyHealInfo(result, r);
147
- break;
148
- }
149
- case 'hover': {
150
- const r = await resolveLocator(page, step, options.healCtx);
151
- if (!r)
152
- throw new Error('Could not resolve locator');
153
- result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
154
- result.elementSelector = step.selector;
155
- await r.loc.first().hover({ timeout });
156
- applyHealInfo(result, r);
157
- break;
158
- }
159
- case 'press_key':
160
- await page.keyboard.press(step.value ?? 'Enter');
161
- break;
162
- case 'assert_visible': {
163
- const r = await resolveLocator(page, step, options.healCtx);
164
- if (!r)
165
- throw new Error('Could not resolve locator');
166
- await r.loc.first().waitFor({ state: 'visible', timeout });
167
- result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
168
- result.elementSelector = step.selector;
169
- applyHealInfo(result, r);
170
- break;
171
- }
172
- case 'assert_text': {
173
- const r = await resolveLocator(page, step, options.healCtx);
174
- if (!r)
175
- throw new Error('Could not resolve locator');
176
- const text = await r.loc.first().textContent({ timeout });
177
- if (!text?.includes(step.value ?? '')) {
178
- throw new Error(`Expected text "${step.value}" not found. Got: "${text?.slice(0, 100)}"`);
179
- }
180
- result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
181
- result.elementSelector = step.selector;
182
- applyHealInfo(result, r);
183
- break;
184
- }
185
- case 'assert_url': {
186
- const url = page.url();
187
- if (!url.includes(step.value ?? '')) {
188
- throw new Error(`Expected URL to contain "${step.value}". Got: "${url}"`);
189
- }
190
- break;
191
- }
192
- case 'wait':
193
- await page.waitForTimeout(step.timeout ?? parseInt(process.env.TEST_STEP_DELAY_MS ?? '2000', 10));
194
- break;
195
- case 'screenshot': {
196
- const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;
197
- await page.screenshot({ path: ssPath, fullPage: true });
198
- result.screenshotPath = ssPath;
199
- break;
200
- }
201
- }
202
- result.status = 'PASSED';
82
+ const stepResults = [];
83
+ let errorStep;
84
+ let finalScreenshotPath;
85
+ for (const step of plan) {
86
+ const stepStart = Date.now();
87
+ const result = {
88
+ stepNumber: step.stepNumber,
89
+ description: step.description,
90
+ status: "FAILED",
91
+ duration: 0
92
+ };
93
+ try {
94
+ const timeout = step.timeout ?? parseInt(process.env.TEST_STEP_TIMEOUT_MS ?? "8000", 10);
95
+ switch (step.action) {
96
+ case "navigate":
97
+ if (!step.url) throw new Error("Navigate step missing url");
98
+ assertCrawlUrlSafe(step.url);
99
+ await page.goto(step.url, { waitUntil: "domcontentloaded", timeout });
100
+ break;
101
+ case "click": {
102
+ const r = await resolveLocator(page, step, options.healCtx);
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;
106
+ await r.loc.first().click({ timeout });
107
+ applyHealInfo(result, r);
108
+ break;
109
+ }
110
+ case "fill": {
111
+ const r = await resolveLocator(page, step, options.healCtx);
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;
115
+ await r.loc.first().fill(step.value ?? "", { timeout });
116
+ applyHealInfo(result, r);
117
+ break;
203
118
  }
204
- catch (err) {
205
- result.status = step.optional ? 'SKIPPED' : 'FAILED';
206
- result.error = err.message;
207
- if (!step.optional) {
208
- errorStep = step.stepNumber;
209
- }
119
+ case "select": {
120
+ const r = await resolveLocator(page, step, options.healCtx);
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;
124
+ await r.loc.first().selectOption(step.value ?? "", { timeout });
125
+ applyHealInfo(result, r);
126
+ break;
210
127
  }
211
- try {
212
- await page.waitForLoadState('networkidle', { timeout: parseInt(process.env.TEST_NETWORK_IDLE_TIMEOUT_MS ?? '1500', 10) }).catch(() => { });
213
- if (!result.screenshotPath) {
214
- const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;
215
- await page.screenshot({ path: ssPath, fullPage: true });
216
- result.screenshotPath = ssPath;
217
- }
128
+ case "hover": {
129
+ const r = await resolveLocator(page, step, options.healCtx);
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;
133
+ await r.loc.first().hover({ timeout });
134
+ applyHealInfo(result, r);
135
+ break;
218
136
  }
219
- catch { /* Screenshot evidence is best-effort and should not fail the run. */ }
220
- result.duration = Date.now() - stepStart;
221
- stepResults.push(result);
222
- options.onStepComplete?.(result);
223
- if (result.status === 'FAILED')
224
- break;
137
+ case "press_key":
138
+ await page.keyboard.press(step.value ?? "Enter");
139
+ break;
140
+ case "assert_visible": {
141
+ const r = await resolveLocator(page, step, options.healCtx);
142
+ if (!r) throw new Error("Could not resolve locator");
143
+ await r.loc.first().waitFor({ state: "visible", timeout });
144
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
145
+ result.elementSelector = step.selector;
146
+ applyHealInfo(result, r);
147
+ break;
148
+ }
149
+ case "assert_text": {
150
+ const r = await resolveLocator(page, step, options.healCtx);
151
+ if (!r) throw new Error("Could not resolve locator");
152
+ const text = await r.loc.first().textContent({ timeout });
153
+ if (!text?.includes(step.value ?? "")) {
154
+ throw new Error(`Expected text "${step.value}" not found. Got: "${text?.slice(0, 100)}"`);
155
+ }
156
+ result.elementBoundingRect = await r.loc.first().boundingBox().catch(() => null);
157
+ result.elementSelector = step.selector;
158
+ applyHealInfo(result, r);
159
+ break;
160
+ }
161
+ case "assert_url": {
162
+ const url = page.url();
163
+ if (!url.includes(step.value ?? "")) {
164
+ throw new Error(`Expected URL to contain "${step.value}". Got: "${url}"`);
165
+ }
166
+ break;
167
+ }
168
+ case "wait":
169
+ await page.waitForTimeout(step.timeout ?? parseInt(process.env.TEST_STEP_DELAY_MS ?? "2000", 10));
170
+ break;
171
+ case "screenshot": {
172
+ const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;
173
+ await page.screenshot({ path: ssPath, fullPage: true });
174
+ result.screenshotPath = ssPath;
175
+ break;
176
+ }
177
+ }
178
+ result.status = "PASSED";
179
+ } catch (err) {
180
+ result.status = step.optional ? "SKIPPED" : "FAILED";
181
+ result.error = err.message;
182
+ if (!step.optional) {
183
+ errorStep = step.stepNumber;
184
+ }
225
185
  }
226
186
  try {
227
- const finalPath = `${options.screenshotDir}/${options.testRunId}-final.png`;
228
- await page.screenshot({ path: finalPath, fullPage: true });
229
- finalScreenshotPath = finalPath;
187
+ await page.waitForLoadState("networkidle", { timeout: parseInt(process.env.TEST_NETWORK_IDLE_TIMEOUT_MS ?? "1500", 10) }).catch(() => {
188
+ });
189
+ if (!result.screenshotPath) {
190
+ const ssPath = `${options.screenshotDir}/${options.testRunId}-step-${step.stepNumber}.png`;
191
+ await page.screenshot({ path: ssPath, fullPage: true });
192
+ result.screenshotPath = ssPath;
193
+ }
194
+ } catch {
230
195
  }
231
- catch { /* non-fatal */ }
232
- return { stepResults, errorStep, finalScreenshotPath, consoleLogs, networkErrors };
196
+ result.duration = Date.now() - stepStart;
197
+ stepResults.push(result);
198
+ options.onStepComplete?.(result);
199
+ if (result.status === "FAILED") break;
200
+ }
201
+ try {
202
+ const finalPath = `${options.screenshotDir}/${options.testRunId}-final.png`;
203
+ await page.screenshot({ path: finalPath, fullPage: true });
204
+ finalScreenshotPath = finalPath;
205
+ } catch {
206
+ }
207
+ return { stepResults, errorStep, finalScreenshotPath, consoleLogs, networkErrors };
233
208
  }
234
209
  function interpolate(template, data) {
235
- if (!template)
236
- return template;
237
- return template.replace(/\{\{(\w+)\}\}/g, (_, key) => data[key] ?? `{{${key}}}`);
210
+ if (!template) return template;
211
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => data[key] ?? `{{${key}}}`);
238
212
  }
239
- export async function executeTestPlan(plan, options) {
240
- if (options.dataRow && Object.keys(options.dataRow).length > 0) {
241
- const data = options.dataRow;
242
- plan = plan.map((step) => ({
243
- ...step,
244
- url: interpolate(step.url, data),
245
- selector: interpolate(step.selector, data),
246
- value: interpolate(step.value, data),
247
- description: interpolate(step.description, data) ?? step.description,
248
- }));
213
+ async function executeTestPlan(plan, options) {
214
+ if (options.dataRow && Object.keys(options.dataRow).length > 0) {
215
+ const data = options.dataRow;
216
+ plan = plan.map((step) => ({
217
+ ...step,
218
+ url: interpolate(step.url, data),
219
+ selector: interpolate(step.selector, data),
220
+ value: interpolate(step.value, data),
221
+ description: interpolate(step.description, data) ?? step.description
222
+ }));
223
+ }
224
+ const maxRuns = options.maxRuns ?? 1;
225
+ const browserEngine = { chromium, firefox, webkit }[options.browser ?? "chromium"] ?? chromium;
226
+ const start = Date.now();
227
+ const outcomes = [];
228
+ let lastResult = null;
229
+ let lastNetworkTimings = [];
230
+ for (let run = 0; run < maxRuns; run++) {
231
+ const browser = await browserEngine.launch({
232
+ headless: true,
233
+ // Optional override for environments with a non-standard/pre-provisioned
234
+ // browser install (e.g. a custom base image with only system Chromium).
235
+ ...process.env.PLAYWRIGHT_EXECUTABLE_PATH ? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH } : {}
236
+ });
237
+ let parsedStorageState;
238
+ try {
239
+ parsedStorageState = options.storageState ? JSON.parse(options.storageState) : void 0;
240
+ } catch {
241
+ parsedStorageState = void 0;
249
242
  }
250
- const maxRuns = options.maxRuns ?? 1;
251
- const browserEngine = { chromium, firefox, webkit }[options.browser ?? 'chromium'] ?? chromium;
252
- const start = Date.now();
253
- const outcomes = [];
254
- let lastResult = null;
255
- let lastNetworkTimings = [];
256
- for (let run = 0; run < maxRuns; run++) {
257
- const browser = await browserEngine.launch({
258
- headless: true,
259
- // Optional override for environments with a non-standard/pre-provisioned
260
- // browser install (e.g. a custom base image with only system Chromium).
261
- ...(process.env.PLAYWRIGHT_EXECUTABLE_PATH ? { executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH } : {}),
262
- });
263
- let parsedStorageState;
264
- try {
265
- parsedStorageState = options.storageState ? JSON.parse(options.storageState) : undefined;
266
- }
267
- catch {
268
- parsedStorageState = undefined;
269
- }
270
- const context = await browser.newContext({
271
- viewport: options.viewport ?? { width: 1440, height: 900 },
272
- ...(parsedStorageState ? { storageState: parsedStorageState } : {}),
273
- });
274
- const page = await context.newPage();
275
- await page
276
- .addStyleTag({ content: '*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }' })
277
- .catch(() => { });
278
- const consoleLogs = [];
279
- const networkErrors = [];
280
- const networkTimings = [];
281
- const requestStartTimes = new Map();
282
- page.on('console', msg => {
283
- if (consoleLogs.length < 100) {
284
- consoleLogs.push({ type: msg.type(), text: msg.text().slice(0, 500), timestamp: Date.now() });
285
- }
286
- });
287
- page.on('request', request => {
288
- const url = request.url();
289
- if (url.includes('/api/') || url.match(/\/(graphql|rest|v\d)\//)) {
290
- requestStartTimes.set(request.url(), Date.now());
291
- }
292
- });
293
- page.on('response', response => {
294
- const url = response.url();
295
- const start = requestStartTimes.get(url);
296
- if (start && networkTimings.length < 200) {
297
- networkTimings.push({ url: url.slice(0, 300), method: response.request().method(), status: response.status(), duration: Date.now() - start });
298
- requestStartTimes.delete(url);
299
- }
300
- if (!response.ok() && !url.includes('favicon') && networkErrors.length < 50) {
301
- networkErrors.push({
302
- url: url.slice(0, 300),
303
- status: response.status(),
304
- method: response.request().method(),
305
- timestamp: Date.now(),
306
- });
307
- }
243
+ const context = await browser.newContext({
244
+ viewport: options.viewport ?? { width: 1440, height: 900 },
245
+ ...parsedStorageState ? { storageState: parsedStorageState } : {}
246
+ });
247
+ const page = await context.newPage();
248
+ await page.addStyleTag({ content: "*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }" }).catch(() => {
249
+ });
250
+ const consoleLogs = [];
251
+ const networkErrors = [];
252
+ const networkTimings = [];
253
+ const requestStartTimes = /* @__PURE__ */ new Map();
254
+ page.on("console", (msg) => {
255
+ if (consoleLogs.length < 100) {
256
+ consoleLogs.push({ type: msg.type(), text: msg.text().slice(0, 500), timestamp: Date.now() });
257
+ }
258
+ });
259
+ page.on("request", (request) => {
260
+ const url = request.url();
261
+ if (url.includes("/api/") || url.match(/\/(graphql|rest|v\d)\//)) {
262
+ requestStartTimes.set(request.url(), Date.now());
263
+ }
264
+ });
265
+ page.on("response", (response) => {
266
+ const url = response.url();
267
+ const start2 = requestStartTimes.get(url);
268
+ if (start2 && networkTimings.length < 200) {
269
+ networkTimings.push({ url: url.slice(0, 300), method: response.request().method(), status: response.status(), duration: Date.now() - start2 });
270
+ requestStartTimes.delete(url);
271
+ }
272
+ if (!response.ok() && !url.includes("favicon") && networkErrors.length < 50) {
273
+ networkErrors.push({
274
+ url: url.slice(0, 300),
275
+ status: response.status(),
276
+ method: response.request().method(),
277
+ timestamp: Date.now()
308
278
  });
309
- const TEST_EXECUTION_TIMEOUT_MS = 5 * 60_000;
310
- lastResult = await Promise.race([
311
- runPlan(page, plan, options, consoleLogs, networkErrors),
312
- new Promise((_, reject) => setTimeout(() => reject(new Error('Test execution timed out')), TEST_EXECUTION_TIMEOUT_MS)),
313
- ]);
314
- lastNetworkTimings = networkTimings;
315
- await browser.close();
316
- const passed = !lastResult.stepResults.some((r) => r.status === 'FAILED');
317
- outcomes.push(passed ? 'PASSED' : 'FAILED');
318
- // If all subsequent runs are consistent with first, no need to keep going
319
- if (run > 0 && outcomes.every((o) => o === outcomes[0]))
320
- break;
321
- }
322
- const passCount = outcomes.filter((o) => o === 'PASSED').length;
323
- const failCount = outcomes.filter((o) => o === 'FAILED').length;
324
- let status;
325
- if (maxRuns > 1 && passCount > 0 && failCount > 0) {
326
- status = 'FLAKY';
327
- }
328
- else if (passCount === outcomes.length) {
329
- status = 'PASSED';
330
- }
331
- else {
332
- status = 'FAILED';
333
- }
334
- let rootCauseSummary;
335
- if (status === 'FAILED' || status === 'FLAKY') {
336
- const failedStep = lastResult.stepResults.find(s => s.status === 'FAILED');
337
- rootCauseSummary = await analyzeFailure({
338
- tenantId: options.healCtx?.tenantId,
339
- error: failedStep?.error ?? 'Unknown error',
340
- stepDescription: failedStep?.description,
341
- consoleLogs: lastResult.consoleLogs,
342
- networkErrors: lastResult.networkErrors,
343
- screenshotPath: lastResult.finalScreenshotPath,
344
- }).catch(() => undefined) ?? undefined;
345
- }
346
- return {
347
- stepResults: lastResult.stepResults,
348
- status,
349
- errorStep: lastResult.errorStep,
350
- totalDuration: Date.now() - start,
351
- finalScreenshotPath: lastResult.finalScreenshotPath,
352
- retryCount: outcomes.length - 1,
353
- consoleLogs: lastResult.consoleLogs,
354
- networkErrors: lastResult.networkErrors,
355
- rootCauseSummary,
356
- networkTimings: lastNetworkTimings,
357
- };
279
+ }
280
+ });
281
+ const TEST_EXECUTION_TIMEOUT_MS = 5 * 6e4;
282
+ lastResult = await Promise.race([
283
+ runPlan(page, plan, options, consoleLogs, networkErrors),
284
+ new Promise(
285
+ (_, reject) => setTimeout(() => reject(new Error("Test execution timed out")), TEST_EXECUTION_TIMEOUT_MS)
286
+ )
287
+ ]);
288
+ lastNetworkTimings = networkTimings;
289
+ await browser.close();
290
+ const passed = !lastResult.stepResults.some((r) => r.status === "FAILED");
291
+ outcomes.push(passed ? "PASSED" : "FAILED");
292
+ if (run > 0 && outcomes.every((o) => o === outcomes[0])) break;
293
+ }
294
+ const passCount = outcomes.filter((o) => o === "PASSED").length;
295
+ const failCount = outcomes.filter((o) => o === "FAILED").length;
296
+ let status;
297
+ if (maxRuns > 1 && passCount > 0 && failCount > 0) {
298
+ status = "FLAKY";
299
+ } else if (passCount === outcomes.length) {
300
+ status = "PASSED";
301
+ } else {
302
+ status = "FAILED";
303
+ }
304
+ let rootCauseSummary;
305
+ if (status === "FAILED" || status === "FLAKY") {
306
+ const failedStep = lastResult.stepResults.find((s) => s.status === "FAILED");
307
+ rootCauseSummary = await analyzeFailure({
308
+ tenantId: options.healCtx?.tenantId,
309
+ error: failedStep?.error ?? "Unknown error",
310
+ stepDescription: failedStep?.description,
311
+ consoleLogs: lastResult.consoleLogs,
312
+ networkErrors: lastResult.networkErrors,
313
+ screenshotPath: lastResult.finalScreenshotPath
314
+ }).catch(() => void 0) ?? void 0;
315
+ }
316
+ return {
317
+ stepResults: lastResult.stepResults,
318
+ status,
319
+ errorStep: lastResult.errorStep,
320
+ totalDuration: Date.now() - start,
321
+ finalScreenshotPath: lastResult.finalScreenshotPath,
322
+ retryCount: outcomes.length - 1,
323
+ consoleLogs: lastResult.consoleLogs,
324
+ networkErrors: lastResult.networkErrors,
325
+ rootCauseSummary,
326
+ networkTimings: lastNetworkTimings
327
+ };
358
328
  }
329
+ export {
330
+ executeTestPlan
331
+ };
332
+ //# sourceMappingURL=test-executor.js.map