@cutleryapp/agent 1.0.42 → 1.0.44
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/mcp-executor.js +84 -61
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -85,6 +85,9 @@ class TestExecutor {
|
|
|
85
85
|
const stepAttachment = (testCase.step_attachments || {})[String(i)] || null;
|
|
86
86
|
console.log(` 📎 Step ${i} attachment: ${stepAttachment ? `YES (${stepAttachment.length} chars)` : 'none'}`);
|
|
87
87
|
let stepError;
|
|
88
|
+
// Dismiss any open overlay (date picker, dropdown, modal) from the previous step
|
|
89
|
+
if (i > 0)
|
|
90
|
+
await page.keyboard.press('Escape').catch(() => { });
|
|
88
91
|
try {
|
|
89
92
|
// When a reference image is attached, skip MCP strategies entirely and go
|
|
90
93
|
// straight to the AI multi-field loop so it can scan the form and fill everything.
|
|
@@ -182,6 +185,20 @@ class TestExecutor {
|
|
|
182
185
|
}
|
|
183
186
|
else {
|
|
184
187
|
await tryFill(page, fieldLabel, value);
|
|
188
|
+
// After filling, check if an autocomplete dropdown appeared and click the option
|
|
189
|
+
try {
|
|
190
|
+
const optSel = [
|
|
191
|
+
`[role="option"]:has-text("${value}")`,
|
|
192
|
+
`[class*="option"]:has-text("${value}")`,
|
|
193
|
+
`[class*="suggestion"]:has-text("${value}")`,
|
|
194
|
+
`li:has-text("${value}")`,
|
|
195
|
+
].join(', ');
|
|
196
|
+
const opt = page.locator(optSel).first();
|
|
197
|
+
if (await opt.isVisible({ timeout: 400 })) {
|
|
198
|
+
await opt.click({ timeout: 1000 });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch { /* no autocomplete dropdown, that's fine */ }
|
|
185
202
|
}
|
|
186
203
|
}
|
|
187
204
|
handled = true;
|
|
@@ -448,10 +465,23 @@ class TestExecutor {
|
|
|
448
465
|
}
|
|
449
466
|
}
|
|
450
467
|
catch (err) {
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
468
|
+
// AI last-resort recovery — only when Playwright strategies all failed
|
|
469
|
+
if (process.env.OPENAI_API_KEY) {
|
|
470
|
+
console.log(` 🤖 Playwright failed, trying AI: ${err.message.split('\n')[0]}`);
|
|
471
|
+
try {
|
|
472
|
+
await aiSingleShot(page, raw);
|
|
473
|
+
}
|
|
474
|
+
catch (aiErr) {
|
|
475
|
+
console.log(` ⚠️ AI recovery also failed: ${aiErr.message.split('\n')[0]}`);
|
|
476
|
+
stepError = err.message;
|
|
477
|
+
result.success = false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
console.log(` ⚠️ Step failed: ${err.message.split('\n')[0]}`);
|
|
482
|
+
stepError = err.message;
|
|
483
|
+
result.success = false;
|
|
484
|
+
}
|
|
455
485
|
}
|
|
456
486
|
// Screenshot on failure, on the last step, or every 5 steps — not every step
|
|
457
487
|
let screenshotB64 = "";
|
|
@@ -1206,42 +1236,47 @@ function parseDate(value) {
|
|
|
1206
1236
|
async function tryFillDate(page, label, value) {
|
|
1207
1237
|
const labelRe = new RegExp(escapeRegex(label), 'i');
|
|
1208
1238
|
const parsed = parseDate(value);
|
|
1209
|
-
//
|
|
1239
|
+
// First close any already-open calendar/overlay before we start
|
|
1240
|
+
await page.keyboard.press('Escape').catch(() => { });
|
|
1241
|
+
await page.waitForTimeout(100);
|
|
1242
|
+
// Strategy 1: native input[type="date"] — fill with ISO format, no calendar involved
|
|
1210
1243
|
if (parsed) {
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
try {
|
|
1217
|
-
const loc = page.locator(sel).first();
|
|
1218
|
-
if (await loc.isVisible({ timeout: 400 })) {
|
|
1219
|
-
await loc.fill(parsed.iso, { timeout: 1000 });
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1244
|
+
try {
|
|
1245
|
+
const native = page.locator('input[type="date"], input[type="datetime-local"]').first();
|
|
1246
|
+
if (await native.isVisible({ timeout: 300 })) {
|
|
1247
|
+
await native.fill(parsed.iso, { timeout: 1000 });
|
|
1248
|
+
return;
|
|
1222
1249
|
}
|
|
1223
|
-
catch { /* try next */ }
|
|
1224
1250
|
}
|
|
1251
|
+
catch { /* not a native date input */ }
|
|
1225
1252
|
}
|
|
1226
|
-
// Strategy 2:
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
await
|
|
1235
|
-
|
|
1253
|
+
// Strategy 2: react-datepicker / custom date input — click, select-all, type, Tab to commit
|
|
1254
|
+
const dateLocators = [
|
|
1255
|
+
page.getByLabel(labelRe).first(),
|
|
1256
|
+
page.getByPlaceholder(labelRe).first(),
|
|
1257
|
+
page.getByRole('textbox', { name: labelRe }).first(),
|
|
1258
|
+
];
|
|
1259
|
+
for (const loc of dateLocators) {
|
|
1260
|
+
try {
|
|
1261
|
+
if (!await loc.isVisible({ timeout: 400 }))
|
|
1262
|
+
continue;
|
|
1263
|
+
// Click to focus (may open calendar)
|
|
1264
|
+
await loc.click({ timeout: 800 });
|
|
1265
|
+
await page.waitForTimeout(150);
|
|
1266
|
+
// Select all existing text and replace
|
|
1267
|
+
await page.keyboard.press('Control+a');
|
|
1268
|
+
await page.keyboard.press('Meta+a'); // Mac
|
|
1269
|
+
await page.keyboard.type(value, { delay: 40 });
|
|
1236
1270
|
await page.waitForTimeout(150);
|
|
1237
|
-
|
|
1238
|
-
await page.keyboard.
|
|
1239
|
-
await page.
|
|
1240
|
-
|
|
1271
|
+
// Tab out to commit — this also closes the calendar
|
|
1272
|
+
await page.keyboard.press('Tab');
|
|
1273
|
+
await page.waitForTimeout(200);
|
|
1274
|
+
// If calendar is still open, press Escape
|
|
1275
|
+
await page.keyboard.press('Escape').catch(() => { });
|
|
1241
1276
|
return;
|
|
1242
1277
|
}
|
|
1278
|
+
catch { /* try next */ }
|
|
1243
1279
|
}
|
|
1244
|
-
catch { /* fall through */ }
|
|
1245
1280
|
// Strategy 3: fallback to regular fill
|
|
1246
1281
|
await tryFill(page, label, value);
|
|
1247
1282
|
}
|
|
@@ -1249,39 +1284,27 @@ async function tryFill(page, label, value) {
|
|
|
1249
1284
|
const labelRe = new RegExp(escapeRegex(label), "i");
|
|
1250
1285
|
const variants = labelVariants(label);
|
|
1251
1286
|
const attrContains = (attr) => variants.map(v => `input[${attr}*="${cssEscape(v)}" i], textarea[${attr}*="${cssEscape(v)}" i], [contenteditable="true"][${attr}*="${cssEscape(v)}" i]`).join(", ");
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
() => page.
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
catch { /* all 3 failed, try phase 2 */ }
|
|
1267
|
-
// Phase 2: attribute-based selectors covering name/id/data-test variants (300ms each)
|
|
1268
|
-
const exactAttrs = variants.flatMap(v => [
|
|
1269
|
-
`input[name="${cssEscape(v)}" i]`, `input[id="${cssEscape(v)}" i]`,
|
|
1270
|
-
`textarea[name="${cssEscape(v)}" i]`, `textarea[id="${cssEscape(v)}" i]`,
|
|
1271
|
-
]).join(", ");
|
|
1272
|
-
const phase2 = [
|
|
1273
|
-
() => page.locator(exactAttrs).first().fill(value),
|
|
1274
|
-
() => page.locator(`${attrContains("name")}, ${attrContains("id")}`).first().fill(value),
|
|
1275
|
-
() => page.locator(attrContains("data-test")).first().fill(value),
|
|
1276
|
-
() => page.locator(attrContains("aria-label")).first().fill(value),
|
|
1277
|
-
() => page.locator(attrContains("placeholder")).first().fill(value),
|
|
1287
|
+
// Sequential strategies — do NOT run concurrently (concurrent fills can cross-contaminate fields)
|
|
1288
|
+
// Each uses a short timeout since if the element exists, Playwright resolves near-instantly.
|
|
1289
|
+
const strategies = [
|
|
1290
|
+
['getByLabel', () => page.getByLabel(labelRe).first().fill(value)],
|
|
1291
|
+
['getByPlaceholder', () => page.getByPlaceholder(labelRe).first().fill(value)],
|
|
1292
|
+
['getByRole', () => page.getByRole("textbox", { name: labelRe }).first().fill(value)],
|
|
1293
|
+
['id/name exact', () => page.locator(variants.flatMap(v => [
|
|
1294
|
+
`input[name="${cssEscape(v)}" i]`, `input[id="${cssEscape(v)}" i]`,
|
|
1295
|
+
`textarea[name="${cssEscape(v)}" i]`, `textarea[id="${cssEscape(v)}" i]`,
|
|
1296
|
+
]).join(", ")).first().fill(value)],
|
|
1297
|
+
['id/name contains', () => page.locator(`${attrContains("name")}, ${attrContains("id")}`).first().fill(value)],
|
|
1298
|
+
['data-test', () => page.locator(attrContains("data-test")).first().fill(value)],
|
|
1299
|
+
['aria-label', () => page.locator(attrContains("aria-label")).first().fill(value)],
|
|
1300
|
+
['placeholder attr', () => page.locator(attrContains("placeholder")).first().fill(value)],
|
|
1278
1301
|
];
|
|
1279
|
-
for (const fn of
|
|
1302
|
+
for (const [, fn] of strategies) {
|
|
1280
1303
|
try {
|
|
1281
|
-
await
|
|
1304
|
+
await Promise.race([fn(), new Promise((_, r) => setTimeout(() => r(new Error('t')), 400))]);
|
|
1282
1305
|
return;
|
|
1283
1306
|
}
|
|
1284
|
-
catch { /* next */ }
|
|
1307
|
+
catch { /* next strategy */ }
|
|
1285
1308
|
}
|
|
1286
1309
|
throw new Error(`Could not find input field: "${label}"`);
|
|
1287
1310
|
}
|