@cutleryapp/agent 1.0.44 → 1.0.46
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 +49 -50
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -85,9 +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 (
|
|
89
|
-
if (i > 0)
|
|
88
|
+
// Dismiss any open overlay (calendar, dropdown, modal) left from the previous step
|
|
89
|
+
if (i > 0) {
|
|
90
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
|
+
}
|
|
91
96
|
try {
|
|
92
97
|
// When a reference image is attached, skip MCP strategies entirely and go
|
|
93
98
|
// straight to the AI multi-field loop so it can scan the form and fill everything.
|
|
@@ -387,21 +392,19 @@ class TestExecutor {
|
|
|
387
392
|
catch { /* next */ }
|
|
388
393
|
}
|
|
389
394
|
}
|
|
390
|
-
// 2. Radio / checkbox
|
|
391
|
-
// are handled in <100ms instead of waiting through autocomplete timeouts
|
|
392
|
-
if (!selHandled) {
|
|
393
|
-
try {
|
|
394
|
-
await page.locator(`label:has-text("${optionValue}")`).first().click({ timeout: 800 });
|
|
395
|
-
selHandled = true;
|
|
396
|
-
}
|
|
397
|
-
catch { /* not a labelled radio/checkbox */ }
|
|
398
|
-
}
|
|
395
|
+
// 2. Radio / checkbox — race all selectors simultaneously; first click wins
|
|
399
396
|
if (!selHandled) {
|
|
400
397
|
try {
|
|
401
|
-
await
|
|
398
|
+
await Promise.any([
|
|
399
|
+
page.locator(`label:text-is("${optionValue}")`).first().click({ timeout: 300 }),
|
|
400
|
+
page.locator(`label:has-text("${optionValue}")`).first().click({ timeout: 300 }),
|
|
401
|
+
page.locator(`[role="radio"]:has-text("${optionValue}")`).first().click({ timeout: 300 }),
|
|
402
|
+
page.locator(`input[type="radio"][value="${optionValue}" i]`).first().click({ force: true, timeout: 300 }),
|
|
403
|
+
page.locator(`input[type="checkbox"][value="${optionValue}" i]`).first().click({ force: true, timeout: 300 }),
|
|
404
|
+
]);
|
|
402
405
|
selHandled = true;
|
|
403
406
|
}
|
|
404
|
-
catch { /* not
|
|
407
|
+
catch { /* not a radio/checkbox, fall through */ }
|
|
405
408
|
}
|
|
406
409
|
// 3. React-select / autocomplete typeahead
|
|
407
410
|
if (!selHandled) {
|
|
@@ -433,17 +436,14 @@ class TestExecutor {
|
|
|
433
436
|
if (cbMatch) {
|
|
434
437
|
const labelText = cbMatch[1].trim();
|
|
435
438
|
try {
|
|
436
|
-
|
|
437
|
-
|
|
439
|
+
await Promise.any([
|
|
440
|
+
page.locator(`label:text-is("${labelText}")`).first().click({ timeout: 300 }),
|
|
441
|
+
page.locator(`label:has-text("${labelText}")`).first().click({ timeout: 300 }),
|
|
442
|
+
page.getByLabel(new RegExp(labelText, 'i')).first().check({ force: true, timeout: 300 }),
|
|
443
|
+
]);
|
|
438
444
|
handled = true;
|
|
439
445
|
}
|
|
440
|
-
catch {
|
|
441
|
-
try {
|
|
442
|
-
await page.getByLabel(new RegExp(labelText, 'i')).first().check({ timeout: 2000 });
|
|
443
|
-
handled = true;
|
|
444
|
-
}
|
|
445
|
-
catch { /* fall to AI */ }
|
|
446
|
-
}
|
|
446
|
+
catch { /* fall to AI */ }
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
// 7. Generic click fallback — try any element containing the step text before AI
|
|
@@ -967,25 +967,20 @@ async function tryAIClick(page, selector) {
|
|
|
967
967
|
async function tryAutocomplete(page, fieldLabel, value) {
|
|
968
968
|
// Click the matching option in whatever dropdown is currently open — returns true only on success
|
|
969
969
|
async function clickOpenOption() {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
return true;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
catch { /* try next */ }
|
|
970
|
+
// Race all option selectors simultaneously — first visible one wins
|
|
971
|
+
try {
|
|
972
|
+
await Promise.any([
|
|
973
|
+
`[role="option"]:has-text("${value}")`,
|
|
974
|
+
`[class*="option"]:has-text("${value}")`,
|
|
975
|
+
`[class*="suggestion"]:has-text("${value}")`,
|
|
976
|
+
`[class*="item"]:has-text("${value}")`,
|
|
977
|
+
`li:has-text("${value}")`,
|
|
978
|
+
].map(sel => page.locator(sel).first().click({ timeout: 300 })));
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
return false;
|
|
987
983
|
}
|
|
988
|
-
return false;
|
|
989
984
|
}
|
|
990
985
|
// Strategy 1: React-select — find the control that BELONGS to this label via DOM traversal
|
|
991
986
|
// so we don't accidentally open the wrong dropdown on a page with multiple selects
|
|
@@ -1022,13 +1017,13 @@ async function tryAutocomplete(page, fieldLabel, value) {
|
|
|
1022
1017
|
// Type into the now-visible input inside the react-select
|
|
1023
1018
|
const innerInput = page.locator('[class*="react-select__input"] input,[class*="select__input"] input').first();
|
|
1024
1019
|
try {
|
|
1025
|
-
await innerInput.waitFor({ state: 'visible', timeout:
|
|
1026
|
-
await innerInput.type(value, { delay:
|
|
1020
|
+
await innerInput.waitFor({ state: 'visible', timeout: 400 });
|
|
1021
|
+
await innerInput.type(value, { delay: 20 });
|
|
1027
1022
|
}
|
|
1028
1023
|
catch {
|
|
1029
|
-
await page.keyboard.type(value, { delay:
|
|
1024
|
+
await page.keyboard.type(value, { delay: 20 });
|
|
1030
1025
|
}
|
|
1031
|
-
await page.waitForTimeout(
|
|
1026
|
+
await page.waitForTimeout(250);
|
|
1032
1027
|
if (await clickOpenOption())
|
|
1033
1028
|
return true;
|
|
1034
1029
|
// No confirmed click → don't claim success, fall through to next strategy
|
|
@@ -1055,11 +1050,11 @@ async function tryAutocomplete(page, fieldLabel, value) {
|
|
|
1055
1050
|
if (['radio', 'checkbox', 'submit', 'button', 'file', 'hidden'].includes(inputType))
|
|
1056
1051
|
continue;
|
|
1057
1052
|
}
|
|
1058
|
-
await input.waitFor({ state: 'visible', timeout:
|
|
1059
|
-
await input.click({ timeout:
|
|
1053
|
+
await input.waitFor({ state: 'visible', timeout: 300 });
|
|
1054
|
+
await input.click({ timeout: 300 });
|
|
1060
1055
|
await input.fill('');
|
|
1061
|
-
await input.type(value, { delay:
|
|
1062
|
-
await page.waitForTimeout(
|
|
1056
|
+
await input.type(value, { delay: 20 });
|
|
1057
|
+
await page.waitForTimeout(250);
|
|
1063
1058
|
if (await clickOpenOption())
|
|
1064
1059
|
return true;
|
|
1065
1060
|
}
|
|
@@ -1268,11 +1263,15 @@ async function tryFillDate(page, label, value) {
|
|
|
1268
1263
|
await page.keyboard.press('Meta+a'); // Mac
|
|
1269
1264
|
await page.keyboard.type(value, { delay: 40 });
|
|
1270
1265
|
await page.waitForTimeout(150);
|
|
1271
|
-
// Tab out to commit
|
|
1266
|
+
// Tab out to commit and close the calendar
|
|
1272
1267
|
await page.keyboard.press('Tab');
|
|
1273
1268
|
await page.waitForTimeout(200);
|
|
1274
|
-
//
|
|
1269
|
+
// Aggressively close any calendar still open
|
|
1275
1270
|
await page.keyboard.press('Escape').catch(() => { });
|
|
1271
|
+
await page.waitForTimeout(100);
|
|
1272
|
+
// Click neutral area to ensure calendar/overlay is gone
|
|
1273
|
+
await page.mouse.click(10, 10).catch(() => { });
|
|
1274
|
+
await page.waitForTimeout(150);
|
|
1276
1275
|
return;
|
|
1277
1276
|
}
|
|
1278
1277
|
catch { /* try next */ }
|