@cutleryapp/agent 1.0.39 → 1.0.41
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 +54 -18
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -334,36 +334,67 @@ class TestExecutor {
|
|
|
334
334
|
const optionValue = selMatch[1].trim();
|
|
335
335
|
const fieldLabel = selMatch[2].trim();
|
|
336
336
|
let selHandled = false;
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
337
|
+
// 1. Native <select> — try by label, name, id
|
|
338
|
+
if (!selHandled) {
|
|
339
|
+
const fieldRe = new RegExp(fieldLabel.replace(/[\s_-]+/g, '[\\s_-]*'), 'i');
|
|
340
|
+
const selectLocators = [
|
|
341
|
+
page.getByLabel(fieldRe),
|
|
342
|
+
page.locator(`select[name*="${fieldLabel}" i]`),
|
|
343
|
+
page.locator(`select[id*="${fieldLabel}" i]`),
|
|
344
|
+
page.locator(`select[data-test*="${fieldLabel}" i]`),
|
|
345
|
+
];
|
|
346
|
+
for (const loc of selectLocators) {
|
|
347
|
+
try {
|
|
348
|
+
const el = loc.first();
|
|
349
|
+
const tag = await el.evaluate((n) => n.tagName.toLowerCase()).catch(() => '');
|
|
350
|
+
if (tag !== 'select')
|
|
351
|
+
continue; // only drive actual <select> elements
|
|
352
|
+
// Try matching by label text, then by value
|
|
353
|
+
const matched = await Promise.any([
|
|
354
|
+
el.selectOption({ label: optionValue }, { timeout: 1000 }),
|
|
355
|
+
el.selectOption({ value: optionValue }, { timeout: 1000 }),
|
|
356
|
+
el.selectOption(optionValue, { timeout: 1000 }),
|
|
357
|
+
]).then(() => true).catch(() => false);
|
|
358
|
+
if (matched) {
|
|
359
|
+
selHandled = true;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch { /* next */ }
|
|
364
|
+
}
|
|
342
365
|
}
|
|
343
|
-
|
|
344
|
-
//
|
|
366
|
+
// 2. Radio / checkbox label click — before autocomplete so radio buttons
|
|
367
|
+
// are handled in <100ms instead of waiting through autocomplete timeouts
|
|
345
368
|
if (!selHandled) {
|
|
346
|
-
|
|
369
|
+
try {
|
|
370
|
+
await page.locator(`label:has-text("${optionValue}")`).first().click({ timeout: 800 });
|
|
371
|
+
selHandled = true;
|
|
372
|
+
}
|
|
373
|
+
catch { /* not a labelled radio/checkbox */ }
|
|
347
374
|
}
|
|
348
|
-
// Try clicking a visible option in an already-open dropdown
|
|
349
375
|
if (!selHandled) {
|
|
350
376
|
try {
|
|
351
|
-
await page.locator(`[
|
|
377
|
+
await page.locator(`input[type="radio"][value="${optionValue}" i], input[type="checkbox"][value="${optionValue}" i]`).first().click({ force: true, timeout: 800 });
|
|
352
378
|
selHandled = true;
|
|
353
379
|
}
|
|
354
|
-
catch { /*
|
|
380
|
+
catch { /* not an input with matching value */ }
|
|
355
381
|
}
|
|
356
|
-
//
|
|
382
|
+
// 3. React-select / autocomplete typeahead
|
|
383
|
+
if (!selHandled) {
|
|
384
|
+
selHandled = await tryAutocomplete(page, fieldLabel, optionValue);
|
|
385
|
+
}
|
|
386
|
+
// 4. Already-open dropdown option
|
|
357
387
|
if (!selHandled) {
|
|
358
388
|
try {
|
|
359
|
-
await page.locator(`
|
|
389
|
+
await page.locator(`[role="option"]:has-text("${optionValue}"), [class*="option"]:has-text("${optionValue}")`).first().click({ timeout: 1000 });
|
|
360
390
|
selHandled = true;
|
|
361
391
|
}
|
|
362
|
-
catch { /* try input
|
|
392
|
+
catch { /* try text input fallback */ }
|
|
363
393
|
}
|
|
394
|
+
// 5. Text input / textarea fallback — handles "Select X in Y" where Y is a textarea
|
|
364
395
|
if (!selHandled) {
|
|
365
396
|
try {
|
|
366
|
-
await page
|
|
397
|
+
await tryFill(page, fieldLabel, optionValue);
|
|
367
398
|
selHandled = true;
|
|
368
399
|
}
|
|
369
400
|
catch { /* fall to AI */ }
|
|
@@ -978,12 +1009,17 @@ async function tryAutocomplete(page, fieldLabel, value) {
|
|
|
978
1009
|
for (const loc of inputLocators) {
|
|
979
1010
|
try {
|
|
980
1011
|
const input = loc.first();
|
|
981
|
-
// Skip wrapper divs
|
|
1012
|
+
// Skip wrapper divs and non-text inputs (radio/checkbox handled by label click above)
|
|
982
1013
|
const tag = await input.evaluate((el) => el.tagName.toLowerCase()).catch(() => '');
|
|
983
1014
|
if (tag && !['input', 'textarea'].includes(tag))
|
|
984
1015
|
continue;
|
|
985
|
-
|
|
986
|
-
|
|
1016
|
+
if (tag === 'input') {
|
|
1017
|
+
const inputType = await input.evaluate((el) => el.type || '').catch(() => '');
|
|
1018
|
+
if (['radio', 'checkbox', 'submit', 'button', 'file', 'hidden'].includes(inputType))
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
await input.waitFor({ state: 'visible', timeout: 500 });
|
|
1022
|
+
await input.click({ timeout: 500 });
|
|
987
1023
|
await input.fill('');
|
|
988
1024
|
await input.type(value, { delay: 30 });
|
|
989
1025
|
await page.waitForTimeout(350);
|