@cutleryapp/agent 1.0.43 → 1.0.45
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 +80 -41
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -85,6 +85,14 @@ 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 (calendar, dropdown, modal) left from the previous step
|
|
89
|
+
if (i > 0) {
|
|
90
|
+
await page.keyboard.press('Escape').catch(() => { });
|
|
91
|
+
await page.waitForTimeout(80);
|
|
92
|
+
// Click a neutral spot (top-left corner) to blur active element and close popups
|
|
93
|
+
await page.mouse.click(10, 10).catch(() => { });
|
|
94
|
+
await page.waitForTimeout(80);
|
|
95
|
+
}
|
|
88
96
|
try {
|
|
89
97
|
// When a reference image is attached, skip MCP strategies entirely and go
|
|
90
98
|
// straight to the AI multi-field loop so it can scan the form and fill everything.
|
|
@@ -182,6 +190,20 @@ class TestExecutor {
|
|
|
182
190
|
}
|
|
183
191
|
else {
|
|
184
192
|
await tryFill(page, fieldLabel, value);
|
|
193
|
+
// After filling, check if an autocomplete dropdown appeared and click the option
|
|
194
|
+
try {
|
|
195
|
+
const optSel = [
|
|
196
|
+
`[role="option"]:has-text("${value}")`,
|
|
197
|
+
`[class*="option"]:has-text("${value}")`,
|
|
198
|
+
`[class*="suggestion"]:has-text("${value}")`,
|
|
199
|
+
`li:has-text("${value}")`,
|
|
200
|
+
].join(', ');
|
|
201
|
+
const opt = page.locator(optSel).first();
|
|
202
|
+
if (await opt.isVisible({ timeout: 400 })) {
|
|
203
|
+
await opt.click({ timeout: 1000 });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch { /* no autocomplete dropdown, that's fine */ }
|
|
185
207
|
}
|
|
186
208
|
}
|
|
187
209
|
handled = true;
|
|
@@ -370,21 +392,29 @@ class TestExecutor {
|
|
|
370
392
|
catch { /* next */ }
|
|
371
393
|
}
|
|
372
394
|
}
|
|
373
|
-
// 2. Radio / checkbox
|
|
374
|
-
// are handled in <100ms instead of waiting through autocomplete timeouts
|
|
375
|
-
if (!selHandled) {
|
|
376
|
-
try {
|
|
377
|
-
await page.locator(`label:has-text("${optionValue}")`).first().click({ timeout: 800 });
|
|
378
|
-
selHandled = true;
|
|
379
|
-
}
|
|
380
|
-
catch { /* not a labelled radio/checkbox */ }
|
|
381
|
-
}
|
|
395
|
+
// 2. Radio / checkbox — try before autocomplete so radios resolve in <100ms
|
|
382
396
|
if (!selHandled) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
397
|
+
for (const sel of [
|
|
398
|
+
`label:text-is("${optionValue}")`, // exact case-insensitive
|
|
399
|
+
`label:has-text("${optionValue}")`, // substring match
|
|
400
|
+
`[role="radio"]:has-text("${optionValue}")`,
|
|
401
|
+
`input[type="radio"][value="${optionValue}" i]`,
|
|
402
|
+
`input[type="checkbox"][value="${optionValue}" i]`,
|
|
403
|
+
]) {
|
|
404
|
+
try {
|
|
405
|
+
const loc = page.locator(sel).first();
|
|
406
|
+
const tag = await loc.evaluate((el) => el.tagName.toLowerCase()).catch(() => 'label');
|
|
407
|
+
if (tag === 'input') {
|
|
408
|
+
await loc.click({ force: true, timeout: 800 });
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
await loc.click({ timeout: 800 });
|
|
412
|
+
}
|
|
413
|
+
selHandled = true;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
catch { /* try next */ }
|
|
386
417
|
}
|
|
387
|
-
catch { /* not an input with matching value */ }
|
|
388
418
|
}
|
|
389
419
|
// 3. React-select / autocomplete typeahead
|
|
390
420
|
if (!selHandled) {
|
|
@@ -1219,42 +1249,51 @@ function parseDate(value) {
|
|
|
1219
1249
|
async function tryFillDate(page, label, value) {
|
|
1220
1250
|
const labelRe = new RegExp(escapeRegex(label), 'i');
|
|
1221
1251
|
const parsed = parseDate(value);
|
|
1222
|
-
//
|
|
1252
|
+
// First close any already-open calendar/overlay before we start
|
|
1253
|
+
await page.keyboard.press('Escape').catch(() => { });
|
|
1254
|
+
await page.waitForTimeout(100);
|
|
1255
|
+
// Strategy 1: native input[type="date"] — fill with ISO format, no calendar involved
|
|
1223
1256
|
if (parsed) {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
try {
|
|
1230
|
-
const loc = page.locator(sel).first();
|
|
1231
|
-
if (await loc.isVisible({ timeout: 400 })) {
|
|
1232
|
-
await loc.fill(parsed.iso, { timeout: 1000 });
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1257
|
+
try {
|
|
1258
|
+
const native = page.locator('input[type="date"], input[type="datetime-local"]').first();
|
|
1259
|
+
if (await native.isVisible({ timeout: 300 })) {
|
|
1260
|
+
await native.fill(parsed.iso, { timeout: 1000 });
|
|
1261
|
+
return;
|
|
1235
1262
|
}
|
|
1236
|
-
catch { /* try next */ }
|
|
1237
1263
|
}
|
|
1264
|
+
catch { /* not a native date input */ }
|
|
1238
1265
|
}
|
|
1239
|
-
// Strategy 2:
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
await
|
|
1248
|
-
|
|
1266
|
+
// Strategy 2: react-datepicker / custom date input — click, select-all, type, Tab to commit
|
|
1267
|
+
const dateLocators = [
|
|
1268
|
+
page.getByLabel(labelRe).first(),
|
|
1269
|
+
page.getByPlaceholder(labelRe).first(),
|
|
1270
|
+
page.getByRole('textbox', { name: labelRe }).first(),
|
|
1271
|
+
];
|
|
1272
|
+
for (const loc of dateLocators) {
|
|
1273
|
+
try {
|
|
1274
|
+
if (!await loc.isVisible({ timeout: 400 }))
|
|
1275
|
+
continue;
|
|
1276
|
+
// Click to focus (may open calendar)
|
|
1277
|
+
await loc.click({ timeout: 800 });
|
|
1278
|
+
await page.waitForTimeout(150);
|
|
1279
|
+
// Select all existing text and replace
|
|
1280
|
+
await page.keyboard.press('Control+a');
|
|
1281
|
+
await page.keyboard.press('Meta+a'); // Mac
|
|
1282
|
+
await page.keyboard.type(value, { delay: 40 });
|
|
1283
|
+
await page.waitForTimeout(150);
|
|
1284
|
+
// Tab out to commit and close the calendar
|
|
1285
|
+
await page.keyboard.press('Tab');
|
|
1286
|
+
await page.waitForTimeout(200);
|
|
1287
|
+
// Aggressively close any calendar still open
|
|
1288
|
+
await page.keyboard.press('Escape').catch(() => { });
|
|
1289
|
+
await page.waitForTimeout(100);
|
|
1290
|
+
// Click neutral area to ensure calendar/overlay is gone
|
|
1291
|
+
await page.mouse.click(10, 10).catch(() => { });
|
|
1249
1292
|
await page.waitForTimeout(150);
|
|
1250
|
-
await input.click({ clickCount: 3 }); // select all existing text
|
|
1251
|
-
await page.keyboard.type(value, { delay: 30 });
|
|
1252
|
-
await page.keyboard.press('Escape'); // close calendar again after typing
|
|
1253
|
-
await page.keyboard.press('Tab'); // commit the value
|
|
1254
1293
|
return;
|
|
1255
1294
|
}
|
|
1295
|
+
catch { /* try next */ }
|
|
1256
1296
|
}
|
|
1257
|
-
catch { /* fall through */ }
|
|
1258
1297
|
// Strategy 3: fallback to regular fill
|
|
1259
1298
|
await tryFill(page, label, value);
|
|
1260
1299
|
}
|