intellitester 0.2.72 → 0.3.1
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/README.md +73 -0
- package/dist/{chunk-7JQTLV5I.cjs → chunk-4DYHFFIN.cjs} +312 -13
- package/dist/chunk-4DYHFFIN.cjs.map +1 -0
- package/dist/{chunk-RRSF22DD.js → chunk-N27W7IMZ.js} +310 -11
- package/dist/chunk-N27W7IMZ.js.map +1 -0
- package/dist/{chunk-CGPHTHY4.cjs → chunk-N7TZR4A2.cjs} +23 -8
- package/dist/chunk-N7TZR4A2.cjs.map +1 -0
- package/dist/{chunk-55PK74QB.js → chunk-UHOYI7C3.js} +23 -8
- package/dist/chunk-UHOYI7C3.js.map +1 -0
- package/dist/cli/index.cjs +157 -46
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +116 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +35 -35
- package/dist/index.d.cts +288 -1
- package/dist/index.d.ts +288 -1
- package/dist/index.js +2 -2
- package/dist/{loader-425Z4YO5.cjs → loader-MPGLPS4F.cjs} +16 -16
- package/dist/{loader-425Z4YO5.cjs.map → loader-MPGLPS4F.cjs.map} +1 -1
- package/dist/{loader-IZFGTOTG.js → loader-VRXN2VTR.js} +3 -3
- package/dist/{loader-IZFGTOTG.js.map → loader-VRXN2VTR.js.map} +1 -1
- package/package.json +1 -1
- package/schemas/intellitester.config.schema.json +12 -3
- package/dist/chunk-55PK74QB.js.map +0 -1
- package/dist/chunk-7JQTLV5I.cjs.map +0 -1
- package/dist/chunk-CGPHTHY4.cjs.map +0 -1
- package/dist/chunk-RRSF22DD.js.map +0 -1
package/README.md
CHANGED
|
@@ -113,6 +113,79 @@ When execution pauses, you can:
|
|
|
113
113
|
- Examine selectors and elements
|
|
114
114
|
- Continue execution when ready
|
|
115
115
|
|
|
116
|
+
### Log Action
|
|
117
|
+
|
|
118
|
+
Use the `log` action to output debug information during test execution:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
steps:
|
|
122
|
+
# Log a static message
|
|
123
|
+
- type: log
|
|
124
|
+
message: "Starting checkout flow"
|
|
125
|
+
|
|
126
|
+
# Log JavaScript expression result
|
|
127
|
+
- type: log
|
|
128
|
+
eval: "document.title"
|
|
129
|
+
|
|
130
|
+
# Log element content
|
|
131
|
+
- type: log
|
|
132
|
+
target: { css: ".error-message" }
|
|
133
|
+
format: html # text (default), html, or json
|
|
134
|
+
|
|
135
|
+
# Log inside iframe
|
|
136
|
+
- type: log
|
|
137
|
+
target: { css: ".stripe-error" }
|
|
138
|
+
frame: { css: "iframe[name='stripe']" }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## AI-Assisted Test Healing
|
|
142
|
+
|
|
143
|
+
IntelliTester can automatically fix broken selectors using AI when tests fail.
|
|
144
|
+
|
|
145
|
+
### Configuration
|
|
146
|
+
|
|
147
|
+
Enable AI healing in `intellitester.config.yaml`:
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
ai:
|
|
151
|
+
provider: groq # anthropic, openai, ollama, groq, openrouter
|
|
152
|
+
model: llama-3.3-70b-versatile
|
|
153
|
+
apiKey: ${GROQ_API_KEY}
|
|
154
|
+
temperature: 0.2
|
|
155
|
+
maxTokens: 4096
|
|
156
|
+
|
|
157
|
+
healing:
|
|
158
|
+
enabled: true
|
|
159
|
+
maxAttempts: 3 # 1-10
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Supported Providers
|
|
163
|
+
|
|
164
|
+
| Provider | Env Variable | Example Model |
|
|
165
|
+
|----------|--------------|---------------|
|
|
166
|
+
| `anthropic` | `ANTHROPIC_API_KEY` | `claude-3-5-sonnet-20241022` |
|
|
167
|
+
| `openai` | `OPENAI_API_KEY` | `gpt-4o` |
|
|
168
|
+
| `groq` | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
|
169
|
+
| `openrouter` | `OPENROUTER_API_KEY` | `anthropic/claude-3.5-sonnet` |
|
|
170
|
+
| `ollama` | - | `llama3.2` |
|
|
171
|
+
|
|
172
|
+
### How It Works
|
|
173
|
+
|
|
174
|
+
When an action fails:
|
|
175
|
+
1. AI analyzes the page HTML and error message
|
|
176
|
+
2. Suggests a new selector (testId, text, role, or css)
|
|
177
|
+
3. Validates the suggestion finds an element
|
|
178
|
+
4. Retries the action with the fixed selector
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
[FAIL] tap - Element not found: testId="old-button-id"
|
|
182
|
+
|
|
183
|
+
🔧 Attempting AI-assisted healing (max 3 attempts)...
|
|
184
|
+
✅ AI found fix: {"text": "Submit Order"}
|
|
185
|
+
|
|
186
|
+
[OK] tap
|
|
187
|
+
```
|
|
188
|
+
|
|
116
189
|
## Iframe Targeting (frame)
|
|
117
190
|
|
|
118
191
|
Target elements inside iframes using the `frame` property. Essential for payment forms (Stripe, PayPal), embedded widgets, and third-party integrations.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkN7TZR4A2_cjs = require('./chunk-N7TZR4A2.cjs');
|
|
4
4
|
var chunkYNHXOSMZ_cjs = require('./chunk-YNHXOSMZ.cjs');
|
|
5
5
|
var chunk2ZSINOCK_cjs = require('./chunk-2ZSINOCK.cjs');
|
|
6
6
|
var uniqueNamesGenerator = require('unique-names-generator');
|
|
@@ -1039,6 +1039,56 @@ var OllamaProvider = class {
|
|
|
1039
1039
|
return typeof content === "string" ? content : JSON.stringify(content);
|
|
1040
1040
|
}
|
|
1041
1041
|
};
|
|
1042
|
+
var GroqProvider = class {
|
|
1043
|
+
constructor(config) {
|
|
1044
|
+
this.config = config;
|
|
1045
|
+
const apiKey = config.apiKey ? resolveEnvVars(config.apiKey) : process.env.GROQ_API_KEY;
|
|
1046
|
+
this.client = new openai.OpenAI({
|
|
1047
|
+
apiKey,
|
|
1048
|
+
model: this.config.model,
|
|
1049
|
+
temperature: this.config.temperature,
|
|
1050
|
+
baseURL: "https://api.groq.com/openai/v1"
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
async generateCompletion(prompt, systemPrompt) {
|
|
1054
|
+
const messages = [];
|
|
1055
|
+
if (systemPrompt) {
|
|
1056
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
1057
|
+
}
|
|
1058
|
+
messages.push({ role: "user", content: prompt });
|
|
1059
|
+
const response = await this.client.chat({ messages });
|
|
1060
|
+
const content = response.message.content;
|
|
1061
|
+
if (!content) {
|
|
1062
|
+
throw new Error("No content in GROQ response");
|
|
1063
|
+
}
|
|
1064
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
var OpenRouterProvider = class {
|
|
1068
|
+
constructor(config) {
|
|
1069
|
+
this.config = config;
|
|
1070
|
+
const apiKey = config.apiKey ? resolveEnvVars(config.apiKey) : process.env.OPENROUTER_API_KEY;
|
|
1071
|
+
this.client = new openai.OpenAI({
|
|
1072
|
+
apiKey,
|
|
1073
|
+
model: this.config.model,
|
|
1074
|
+
temperature: this.config.temperature,
|
|
1075
|
+
baseURL: "https://openrouter.ai/api/v1"
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
async generateCompletion(prompt, systemPrompt) {
|
|
1079
|
+
const messages = [];
|
|
1080
|
+
if (systemPrompt) {
|
|
1081
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
1082
|
+
}
|
|
1083
|
+
messages.push({ role: "user", content: prompt });
|
|
1084
|
+
const response = await this.client.chat({ messages });
|
|
1085
|
+
const content = response.message.content;
|
|
1086
|
+
if (!content) {
|
|
1087
|
+
throw new Error("No content in OpenRouter response");
|
|
1088
|
+
}
|
|
1089
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1042
1092
|
function createAIProvider(config) {
|
|
1043
1093
|
switch (config.provider) {
|
|
1044
1094
|
case "anthropic":
|
|
@@ -1047,6 +1097,10 @@ function createAIProvider(config) {
|
|
|
1047
1097
|
return new OpenAIProvider(config);
|
|
1048
1098
|
case "ollama":
|
|
1049
1099
|
return new OllamaProvider(config);
|
|
1100
|
+
case "groq":
|
|
1101
|
+
return new GroqProvider(config);
|
|
1102
|
+
case "openrouter":
|
|
1103
|
+
return new OpenRouterProvider(config);
|
|
1050
1104
|
}
|
|
1051
1105
|
}
|
|
1052
1106
|
|
|
@@ -1143,6 +1197,178 @@ Return ONLY valid JSON, no additional text.`;
|
|
|
1143
1197
|
};
|
|
1144
1198
|
}
|
|
1145
1199
|
}
|
|
1200
|
+
|
|
1201
|
+
// src/ai/healingAgent.ts
|
|
1202
|
+
async function checkSelector(page, selector) {
|
|
1203
|
+
try {
|
|
1204
|
+
const count = await page.locator(selector).count();
|
|
1205
|
+
if (count === 0) return { found: false, count: 0 };
|
|
1206
|
+
const texts = await page.locator(selector).allTextContents();
|
|
1207
|
+
return { found: true, count, texts: texts.slice(0, 5) };
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
return { found: false, count: 0, error: String(e) };
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async function checkByText(page, text) {
|
|
1213
|
+
try {
|
|
1214
|
+
const locator = page.getByText(text, { exact: false });
|
|
1215
|
+
const count = await locator.count();
|
|
1216
|
+
if (count === 0) return { found: false, count: 0 };
|
|
1217
|
+
return { found: true, count };
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
return { found: false, count: 0, error: String(e) };
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function checkByRole(page, role, name) {
|
|
1223
|
+
try {
|
|
1224
|
+
const locator = page.getByRole(role, name ? { name } : void 0);
|
|
1225
|
+
const count = await locator.count();
|
|
1226
|
+
if (count === 0) return { found: false, count: 0 };
|
|
1227
|
+
return { found: true, count };
|
|
1228
|
+
} catch (e) {
|
|
1229
|
+
return { found: false, count: 0, error: String(e) };
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
async function checkTestId(page, testId) {
|
|
1233
|
+
try {
|
|
1234
|
+
const selector = `[data-testid="${testId}"], #${CSS.escape(testId)}`;
|
|
1235
|
+
const count = await page.locator(selector).count();
|
|
1236
|
+
return { found: count > 0, count };
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
return { found: false, count: 0, error: String(e) };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function extractLocatorFromResponse(response) {
|
|
1242
|
+
const jsonMatch = response.match(/\{[\s\S]*?"(?:testId|text|css|role)"[\s\S]*?\}/);
|
|
1243
|
+
if (!jsonMatch) return null;
|
|
1244
|
+
try {
|
|
1245
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1246
|
+
if (parsed.testId || parsed.text || parsed.css || parsed.role) {
|
|
1247
|
+
return parsed;
|
|
1248
|
+
}
|
|
1249
|
+
} catch {
|
|
1250
|
+
const codeMatch = response.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
1251
|
+
if (codeMatch) {
|
|
1252
|
+
try {
|
|
1253
|
+
const parsed = JSON.parse(codeMatch[1]);
|
|
1254
|
+
if (parsed.testId || parsed.text || parsed.css || parsed.role) {
|
|
1255
|
+
return parsed;
|
|
1256
|
+
}
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
const cssMatch = response.match(/css['":\s]+['"]([^'"]+)['"]/i);
|
|
1262
|
+
if (cssMatch) return { css: cssMatch[1] };
|
|
1263
|
+
const textMatch = response.match(/text['":\s]+['"]([^'"]+)['"]/i);
|
|
1264
|
+
if (textMatch) return { text: textMatch[1] };
|
|
1265
|
+
const testIdMatch = response.match(/testId['":\s]+['"]([^'"]+)['"]/i);
|
|
1266
|
+
if (testIdMatch) return { testId: testIdMatch[1] };
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
async function runHealingAgent(context, aiConfig, maxAttempts = 3) {
|
|
1270
|
+
const provider = createAIProvider(aiConfig);
|
|
1271
|
+
const currentTarget = "target" in context.action ? context.action.target : null;
|
|
1272
|
+
const systemPrompt = `You are debugging a failing web test action. Your goal is to analyze the page and suggest a working selector.
|
|
1273
|
+
|
|
1274
|
+
When suggesting a selector, respond with a JSON object containing ONE of these fields:
|
|
1275
|
+
- testId: for data-testid attributes (most reliable)
|
|
1276
|
+
- text: for visible text content
|
|
1277
|
+
- css: for CSS selectors
|
|
1278
|
+
- role: for ARIA roles (with optional "name" field)
|
|
1279
|
+
|
|
1280
|
+
Example responses:
|
|
1281
|
+
{"testId": "submit-button"}
|
|
1282
|
+
{"text": "Sign In"}
|
|
1283
|
+
{"css": "button.primary"}
|
|
1284
|
+
{"role": "button", "name": "Submit"}
|
|
1285
|
+
|
|
1286
|
+
Prefer selectors in this order of reliability:
|
|
1287
|
+
1. testId - most stable, unlikely to change
|
|
1288
|
+
2. text - good for buttons, links
|
|
1289
|
+
3. role + name - good for accessible elements
|
|
1290
|
+
4. css - last resort, more brittle
|
|
1291
|
+
|
|
1292
|
+
Respond ONLY with the JSON selector object, no other text.`;
|
|
1293
|
+
let attempts = 0;
|
|
1294
|
+
let lastExplanation = "";
|
|
1295
|
+
while (attempts < maxAttempts) {
|
|
1296
|
+
attempts++;
|
|
1297
|
+
const validationResults = [];
|
|
1298
|
+
if (currentTarget) {
|
|
1299
|
+
if (currentTarget.testId) {
|
|
1300
|
+
const result = await checkTestId(context.page, currentTarget.testId);
|
|
1301
|
+
validationResults.push(`testId "${currentTarget.testId}": ${result.found ? `found ${result.count} elements` : "NOT FOUND"}`);
|
|
1302
|
+
}
|
|
1303
|
+
if (currentTarget.text) {
|
|
1304
|
+
const result = await checkByText(context.page, currentTarget.text);
|
|
1305
|
+
validationResults.push(`text "${currentTarget.text}": ${result.found ? `found ${result.count} elements` : "NOT FOUND"}`);
|
|
1306
|
+
}
|
|
1307
|
+
if (currentTarget.css) {
|
|
1308
|
+
const result = await checkSelector(context.page, currentTarget.css);
|
|
1309
|
+
validationResults.push(`css "${currentTarget.css}": ${result.found ? `found ${result.count} elements` : "NOT FOUND"}`);
|
|
1310
|
+
}
|
|
1311
|
+
if (currentTarget.role) {
|
|
1312
|
+
const result = await checkByRole(context.page, currentTarget.role, currentTarget.name);
|
|
1313
|
+
validationResults.push(`role "${currentTarget.role}"${currentTarget.name ? ` name="${currentTarget.name}"` : ""}: ${result.found ? `found ${result.count} elements` : "NOT FOUND"}`);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
const prompt = `Action type: ${context.action.type}
|
|
1317
|
+
Error: ${context.error}
|
|
1318
|
+
|
|
1319
|
+
Failed selector: ${JSON.stringify(currentTarget)}
|
|
1320
|
+
|
|
1321
|
+
Validation results:
|
|
1322
|
+
${validationResults.length > 0 ? validationResults.join("\n") : "No current selector to validate"}
|
|
1323
|
+
|
|
1324
|
+
Page HTML (first 6000 chars):
|
|
1325
|
+
${context.pageContent.slice(0, 6e3)}
|
|
1326
|
+
|
|
1327
|
+
Based on the page content, suggest a working selector for this action. Respond with a JSON object.`;
|
|
1328
|
+
try {
|
|
1329
|
+
const response = await provider.generateCompletion(prompt, systemPrompt);
|
|
1330
|
+
const suggestedLocator = extractLocatorFromResponse(response);
|
|
1331
|
+
if (!suggestedLocator) {
|
|
1332
|
+
lastExplanation = `Attempt ${attempts}: Could not parse selector from AI response`;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
let isValid = false;
|
|
1336
|
+
if (suggestedLocator.testId) {
|
|
1337
|
+
const result = await checkTestId(context.page, suggestedLocator.testId);
|
|
1338
|
+
isValid = result.found;
|
|
1339
|
+
} else if (suggestedLocator.text) {
|
|
1340
|
+
const result = await checkByText(context.page, suggestedLocator.text);
|
|
1341
|
+
isValid = result.found;
|
|
1342
|
+
} else if (suggestedLocator.css) {
|
|
1343
|
+
const result = await checkSelector(context.page, suggestedLocator.css);
|
|
1344
|
+
isValid = result.found;
|
|
1345
|
+
} else if (suggestedLocator.role) {
|
|
1346
|
+
const result = await checkByRole(context.page, suggestedLocator.role, suggestedLocator.name);
|
|
1347
|
+
isValid = result.found;
|
|
1348
|
+
}
|
|
1349
|
+
if (isValid) {
|
|
1350
|
+
const fixedAction = { ...context.action };
|
|
1351
|
+
if ("target" in fixedAction) {
|
|
1352
|
+
fixedAction.target = suggestedLocator;
|
|
1353
|
+
}
|
|
1354
|
+
return {
|
|
1355
|
+
success: true,
|
|
1356
|
+
fixedAction,
|
|
1357
|
+
attempts,
|
|
1358
|
+
explanation: `Found working selector: ${JSON.stringify(suggestedLocator)}`
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
lastExplanation = `Attempt ${attempts}: Suggested selector ${JSON.stringify(suggestedLocator)} did not find any elements`;
|
|
1362
|
+
} catch (e) {
|
|
1363
|
+
lastExplanation = `Attempt ${attempts}: AI error - ${e instanceof Error ? e.message : String(e)}`;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return {
|
|
1367
|
+
success: false,
|
|
1368
|
+
attempts,
|
|
1369
|
+
explanation: lastExplanation || "Could not find a working selector within the allowed attempts"
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1146
1372
|
var SERVER_MARKER_FILE = "server.json";
|
|
1147
1373
|
var INTELLITESTER_DIR = ".intellitester";
|
|
1148
1374
|
var getMarkerPath = (cwd) => path4__namespace.join(cwd, INTELLITESTER_DIR, SERVER_MARKER_FILE);
|
|
@@ -1818,7 +2044,8 @@ async function handleInteractiveError(page, action, error, screenshotDir, stepIn
|
|
|
1818
2044
|
return response.action || "abort";
|
|
1819
2045
|
}
|
|
1820
2046
|
async function executeActionWithRetry(page, action, index, options) {
|
|
1821
|
-
const { baseUrl, context, screenshotDir, debugMode, interactive, aiConfig, browserName } = options;
|
|
2047
|
+
const { baseUrl, context, screenshotDir, debugMode, interactive, aiConfig, browserName, healing } = options;
|
|
2048
|
+
const extras = {};
|
|
1822
2049
|
const buildTrackPayload = (stepExtras) => {
|
|
1823
2050
|
if (!("track" in action)) return null;
|
|
1824
2051
|
const track2 = action.track;
|
|
@@ -2231,6 +2458,50 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
2231
2458
|
}
|
|
2232
2459
|
break;
|
|
2233
2460
|
}
|
|
2461
|
+
case "log": {
|
|
2462
|
+
const logAction = action;
|
|
2463
|
+
const format = logAction.format ?? "text";
|
|
2464
|
+
let logOutput;
|
|
2465
|
+
if (logAction.message) {
|
|
2466
|
+
logOutput = interpolateVariables(logAction.message, context.variables);
|
|
2467
|
+
console.log(`[LOG] ${logOutput}`);
|
|
2468
|
+
} else if (logAction.eval) {
|
|
2469
|
+
const interpolated = interpolateVariables(logAction.eval, context.variables);
|
|
2470
|
+
try {
|
|
2471
|
+
const result = await page.evaluate(interpolated);
|
|
2472
|
+
logOutput = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
2473
|
+
console.log(`[LOG eval] ${logOutput}`);
|
|
2474
|
+
} catch (evalError) {
|
|
2475
|
+
logOutput = `[eval error] ${evalError instanceof Error ? evalError.message : String(evalError)}`;
|
|
2476
|
+
console.log(`[LOG] ${logOutput}`);
|
|
2477
|
+
}
|
|
2478
|
+
} else if (logAction.target) {
|
|
2479
|
+
const frameContext = getActionContext(page, logAction.frame);
|
|
2480
|
+
const handle = resolveLocatorInContext(frameContext, logAction.target);
|
|
2481
|
+
switch (format) {
|
|
2482
|
+
case "html":
|
|
2483
|
+
logOutput = await handle.innerHTML();
|
|
2484
|
+
break;
|
|
2485
|
+
case "json":
|
|
2486
|
+
const text = await handle.textContent() ?? "";
|
|
2487
|
+
try {
|
|
2488
|
+
logOutput = JSON.stringify(JSON.parse(text), null, 2);
|
|
2489
|
+
} catch {
|
|
2490
|
+
logOutput = text;
|
|
2491
|
+
}
|
|
2492
|
+
break;
|
|
2493
|
+
case "text":
|
|
2494
|
+
default:
|
|
2495
|
+
logOutput = await handle.textContent() ?? "";
|
|
2496
|
+
break;
|
|
2497
|
+
}
|
|
2498
|
+
console.log(`[LOG element] ${logOutput}`);
|
|
2499
|
+
} else {
|
|
2500
|
+
logOutput = "(no content)";
|
|
2501
|
+
}
|
|
2502
|
+
extras.logOutput = logOutput;
|
|
2503
|
+
break;
|
|
2504
|
+
}
|
|
2234
2505
|
case "fail": {
|
|
2235
2506
|
const failAction = action;
|
|
2236
2507
|
throw new Error(failAction.message);
|
|
@@ -2296,7 +2567,7 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
2296
2567
|
});
|
|
2297
2568
|
}
|
|
2298
2569
|
} else {
|
|
2299
|
-
const { loadWorkflowDefinition, loadTestDefinition: loadTestDefinition2 } = await import('./loader-
|
|
2570
|
+
const { loadWorkflowDefinition, loadTestDefinition: loadTestDefinition2 } = await import('./loader-MPGLPS4F.cjs');
|
|
2300
2571
|
const workflowPath = path4__namespace.default.resolve(process.cwd(), branchToExecute.workflow);
|
|
2301
2572
|
const workflowDir = path4__namespace.default.dirname(workflowPath);
|
|
2302
2573
|
if (debugMode) {
|
|
@@ -2343,9 +2614,36 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
2343
2614
|
if (trackedPayload) {
|
|
2344
2615
|
await chunkYNHXOSMZ_cjs.track(trackedPayload);
|
|
2345
2616
|
}
|
|
2346
|
-
return;
|
|
2617
|
+
return extras;
|
|
2347
2618
|
} catch (err) {
|
|
2348
2619
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
2620
|
+
if (healing?.enabled && aiConfig && hasTarget(action)) {
|
|
2621
|
+
const maxAttempts = healing.maxAttempts ?? 3;
|
|
2622
|
+
console.log(`
|
|
2623
|
+
\u{1F527} Attempting AI-assisted healing (max ${maxAttempts} attempts)...`);
|
|
2624
|
+
try {
|
|
2625
|
+
const pageContent = await page.content();
|
|
2626
|
+
const healingContext = {
|
|
2627
|
+
page,
|
|
2628
|
+
action,
|
|
2629
|
+
error: error.message,
|
|
2630
|
+
pageContent
|
|
2631
|
+
};
|
|
2632
|
+
const healingResult = await runHealingAgent(healingContext, aiConfig, maxAttempts);
|
|
2633
|
+
if (healingResult.success && healingResult.fixedAction) {
|
|
2634
|
+
console.log(`\u2705 AI found fix: ${healingResult.explanation}`);
|
|
2635
|
+
const fixedExtras = await executeActionWithRetry(page, healingResult.fixedAction, index, {
|
|
2636
|
+
...options,
|
|
2637
|
+
healing: { enabled: false }
|
|
2638
|
+
});
|
|
2639
|
+
return fixedExtras;
|
|
2640
|
+
} else {
|
|
2641
|
+
console.log(`\u274C AI healing failed: ${healingResult.explanation}`);
|
|
2642
|
+
}
|
|
2643
|
+
} catch (healingError) {
|
|
2644
|
+
console.log(`\u274C AI healing error: ${healingError instanceof Error ? healingError.message : String(healingError)}`);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2349
2647
|
if (interactive && aiConfig && hasTarget(action)) {
|
|
2350
2648
|
const choice = await handleInteractiveError(page, action, error, screenshotDir, index, aiConfig);
|
|
2351
2649
|
switch (choice) {
|
|
@@ -2354,7 +2652,7 @@ async function executeActionWithRetry(page, action, index, options) {
|
|
|
2354
2652
|
continue;
|
|
2355
2653
|
case "skip":
|
|
2356
2654
|
console.log("Skipping step...\n");
|
|
2357
|
-
return;
|
|
2655
|
+
return extras;
|
|
2358
2656
|
case "debug":
|
|
2359
2657
|
console.log("Opening Playwright Inspector...\n");
|
|
2360
2658
|
await page.pause();
|
|
@@ -2732,16 +3030,17 @@ var runWebTest = async (test, options = {}) => {
|
|
|
2732
3030
|
}
|
|
2733
3031
|
continue;
|
|
2734
3032
|
}
|
|
2735
|
-
await executeActionWithRetry(page, action, index, {
|
|
3033
|
+
const actionExtras = await executeActionWithRetry(page, action, index, {
|
|
2736
3034
|
baseUrl: options.baseUrl ?? test.config?.web?.baseUrl,
|
|
2737
3035
|
context: executionContext,
|
|
2738
3036
|
screenshotDir,
|
|
2739
3037
|
debugMode,
|
|
2740
3038
|
interactive,
|
|
2741
3039
|
aiConfig: options.aiConfig,
|
|
2742
|
-
browserName
|
|
3040
|
+
browserName,
|
|
3041
|
+
healing: options.healing
|
|
2743
3042
|
});
|
|
2744
|
-
sizeResults.push({ action, status: "passed" });
|
|
3043
|
+
sizeResults.push({ action, status: "passed", logOutput: actionExtras.logOutput });
|
|
2745
3044
|
} catch (error) {
|
|
2746
3045
|
const message = error instanceof Error ? error.message : String(error);
|
|
2747
3046
|
sizeResults.push({ action, status: "failed", error: message });
|
|
@@ -3350,7 +3649,7 @@ async function runTestInWorkflow(test, page, context, options, _workflowDir, wor
|
|
|
3350
3649
|
if (debugMode) {
|
|
3351
3650
|
console.log(` [DEBUG] waitForBranch: loading workflow from ${workflowPath}`);
|
|
3352
3651
|
}
|
|
3353
|
-
const { loadWorkflowDefinition } = await import('./loader-
|
|
3652
|
+
const { loadWorkflowDefinition } = await import('./loader-MPGLPS4F.cjs');
|
|
3354
3653
|
const nestedWorkflow = await loadWorkflowDefinition(workflowPath);
|
|
3355
3654
|
if (branch.variables) {
|
|
3356
3655
|
for (const [key, value] of Object.entries(branch.variables)) {
|
|
@@ -3360,7 +3659,7 @@ async function runTestInWorkflow(test, page, context, options, _workflowDir, wor
|
|
|
3360
3659
|
}
|
|
3361
3660
|
for (const testRef of nestedWorkflow.tests) {
|
|
3362
3661
|
const testFilePath = path4__namespace.default.resolve(path4__namespace.default.dirname(workflowPath), testRef.file);
|
|
3363
|
-
const nestedTest = await
|
|
3662
|
+
const nestedTest = await chunkN7TZR4A2_cjs.loadTestDefinition(testFilePath);
|
|
3364
3663
|
if (nestedTest.variables) {
|
|
3365
3664
|
for (const [key, value] of Object.entries(nestedTest.variables)) {
|
|
3366
3665
|
const interpolated = interpolateVariables(value, context.variables);
|
|
@@ -3622,7 +3921,7 @@ Starting workflow: ${workflow.name}`);
|
|
|
3622
3921
|
console.log(` Test ID: ${testRef.id}`);
|
|
3623
3922
|
}
|
|
3624
3923
|
try {
|
|
3625
|
-
const test = await
|
|
3924
|
+
const test = await chunkN7TZR4A2_cjs.loadTestDefinition(testFilePath);
|
|
3626
3925
|
if (testRef.variables) {
|
|
3627
3926
|
for (const [key, value] of Object.entries(testRef.variables)) {
|
|
3628
3927
|
const interpolated = interpolateWorkflowVariables(
|
|
@@ -4069,5 +4368,5 @@ exports.runWorkflowWithContext = runWorkflowWithContext;
|
|
|
4069
4368
|
exports.setupAppwriteTracking = setupAppwriteTracking;
|
|
4070
4369
|
exports.startTrackingServer = startTrackingServer;
|
|
4071
4370
|
exports.webServerManager = webServerManager;
|
|
4072
|
-
//# sourceMappingURL=chunk-
|
|
4073
|
-
//# sourceMappingURL=chunk-
|
|
4371
|
+
//# sourceMappingURL=chunk-4DYHFFIN.cjs.map
|
|
4372
|
+
//# sourceMappingURL=chunk-4DYHFFIN.cjs.map
|