@cutleryapp/agent 1.0.31 → 1.0.33
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 +104 -6
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -206,15 +206,53 @@ class TestExecutor {
|
|
|
206
206
|
handled = true;
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
-
// 6. Select — dropdown
|
|
209
|
+
// 6. Select — native dropdown, then React-select/autocomplete fallback
|
|
210
210
|
if (!handled && (lower.includes("select") || lower.includes("choose"))) {
|
|
211
|
-
const selMatch = raw.match(/select\s+"?([^"]+?)"?\s+(?:from|in)\s+"?([^"]+?)"?\s*(?:dropdown|select|field)?$/i);
|
|
211
|
+
const selMatch = raw.match(/(?:select|choose)\s+"?([^"]+?)"?\s+(?:from|in)\s+"?([^"]+?)"?\s*(?:dropdown|select|field)?$/i);
|
|
212
212
|
if (selMatch) {
|
|
213
|
+
const optionValue = selMatch[1].trim();
|
|
214
|
+
const fieldLabel = selMatch[2].trim();
|
|
215
|
+
let selHandled = false;
|
|
216
|
+
// Try native <select>
|
|
213
217
|
try {
|
|
214
|
-
|
|
218
|
+
const fieldLoc = page.getByLabel(new RegExp(fieldLabel, 'i')).first();
|
|
219
|
+
await fieldLoc.selectOption({ label: optionValue });
|
|
220
|
+
selHandled = true;
|
|
221
|
+
}
|
|
222
|
+
catch { /* not a native select */ }
|
|
223
|
+
// Try React-select / autocomplete typeahead
|
|
224
|
+
if (!selHandled) {
|
|
225
|
+
selHandled = await tryAutocomplete(page, fieldLabel, optionValue);
|
|
226
|
+
}
|
|
227
|
+
// Try clicking a visible option in an already-open dropdown
|
|
228
|
+
if (!selHandled) {
|
|
229
|
+
try {
|
|
230
|
+
await page.locator(`[role="option"]:has-text("${optionValue}"), [class*="option"]:has-text("${optionValue}")`).first().click({ timeout: 3000 });
|
|
231
|
+
selHandled = true;
|
|
232
|
+
}
|
|
233
|
+
catch { /* fall to AI */ }
|
|
234
|
+
}
|
|
235
|
+
if (selHandled)
|
|
215
236
|
handled = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// 6b. Check/uncheck — checkbox or radio by label
|
|
240
|
+
if (!handled && (lower.startsWith("check ") || lower.startsWith("tick ") || lower.startsWith("select ")) && !handled) {
|
|
241
|
+
const cbMatch = raw.match(/(?:check|tick|select)\s+"?([^"]+?)"?\s*(?:checkbox|option|hobby|hobbies)?$/i);
|
|
242
|
+
if (cbMatch) {
|
|
243
|
+
const labelText = cbMatch[1].trim();
|
|
244
|
+
try {
|
|
245
|
+
// Try label click (works for hidden checkboxes with styled labels)
|
|
246
|
+
await page.locator(`label:has-text("${labelText}")`).first().click({ timeout: 3000 });
|
|
247
|
+
handled = true;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
try {
|
|
251
|
+
await page.getByLabel(new RegExp(labelText, 'i')).first().check({ timeout: 3000 });
|
|
252
|
+
handled = true;
|
|
253
|
+
}
|
|
254
|
+
catch { /* fall to AI */ }
|
|
216
255
|
}
|
|
217
|
-
catch { /* fall to AI */ }
|
|
218
256
|
}
|
|
219
257
|
}
|
|
220
258
|
// 7. AI — single-shot for deterministic steps, full loop for intent steps
|
|
@@ -623,9 +661,14 @@ ${domElements}` + (stepAttachment ? `\n\nThe REFERENCE IMAGE (second image) show
|
|
|
623
661
|
const checked = await el.isChecked().catch(() => false);
|
|
624
662
|
if (!checked) {
|
|
625
663
|
let done = false;
|
|
664
|
+
// Derive a label text from selector for label-based fallbacks
|
|
665
|
+
const labelHint = act.label || act.selector.replace(/[#.\[\]"'=*^$]/g, ' ').trim();
|
|
626
666
|
for (const fn of [
|
|
627
|
-
() => el.click({ timeout: 4000 }),
|
|
628
|
-
() =>
|
|
667
|
+
() => el.click({ force: true, timeout: 4000 }), // force bypasses visibility
|
|
668
|
+
() => el.check({ force: true, timeout: 4000 }),
|
|
669
|
+
() => page.locator(`label[for="${act.selector.replace('#', '')}"]`).click({ timeout: 4000 }),
|
|
670
|
+
() => page.locator(`label:has-text("${labelHint}")`).first().click({ timeout: 4000 }),
|
|
671
|
+
() => page.locator(`label:has(${act.selector})`).click({ timeout: 4000 }),
|
|
629
672
|
]) {
|
|
630
673
|
try {
|
|
631
674
|
await fn();
|
|
@@ -705,6 +748,57 @@ async function tryAIClick(page, selector) {
|
|
|
705
748
|
}
|
|
706
749
|
return false;
|
|
707
750
|
}
|
|
751
|
+
/** Fill an autocomplete/typeahead input: type value, wait for dropdown, click first matching option */
|
|
752
|
+
async function tryAutocomplete(page, labelOrSelector, value) {
|
|
753
|
+
const TIMEOUT = 4000;
|
|
754
|
+
const locators = [
|
|
755
|
+
page.getByLabel(new RegExp(labelOrSelector, 'i')),
|
|
756
|
+
page.getByPlaceholder(new RegExp(labelOrSelector, 'i')),
|
|
757
|
+
page.locator(labelOrSelector),
|
|
758
|
+
];
|
|
759
|
+
for (const loc of locators) {
|
|
760
|
+
try {
|
|
761
|
+
const input = loc.first();
|
|
762
|
+
await input.waitFor({ state: 'visible', timeout: TIMEOUT });
|
|
763
|
+
await input.click({ timeout: TIMEOUT });
|
|
764
|
+
await input.fill('', { timeout: TIMEOUT });
|
|
765
|
+
// Type slowly so the autocomplete can react
|
|
766
|
+
await input.type(value, { delay: 80 });
|
|
767
|
+
// Wait for dropdown options to appear
|
|
768
|
+
await page.waitForTimeout(600);
|
|
769
|
+
// Try common dropdown option selectors
|
|
770
|
+
const optionSelectors = [
|
|
771
|
+
`[class*="option"]:has-text("${value}")`,
|
|
772
|
+
`[class*="suggestion"]:has-text("${value}")`,
|
|
773
|
+
`[class*="item"]:has-text("${value}")`,
|
|
774
|
+
`[role="option"]:has-text("${value}")`,
|
|
775
|
+
`[role="listbox"] [class*="option"]`,
|
|
776
|
+
`[class*="menu"] [class*="option"]`,
|
|
777
|
+
`[class*="dropdown"] li`,
|
|
778
|
+
`ul[class*="auto"] li`,
|
|
779
|
+
`.react-select__option`,
|
|
780
|
+
`.autocomplete-item`,
|
|
781
|
+
];
|
|
782
|
+
for (const optSel of optionSelectors) {
|
|
783
|
+
try {
|
|
784
|
+
const opt = page.locator(optSel).first();
|
|
785
|
+
if (await opt.isVisible({ timeout: 800 })) {
|
|
786
|
+
await opt.click({ timeout: TIMEOUT });
|
|
787
|
+
console.log(` ✓ Autocomplete: typed "${value}", clicked option via "${optSel}"`);
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch { /* try next */ }
|
|
792
|
+
}
|
|
793
|
+
// Fallback: press Enter if a highlighted option exists
|
|
794
|
+
await page.keyboard.press('Enter');
|
|
795
|
+
await page.waitForTimeout(300);
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
catch { /* try next locator */ }
|
|
799
|
+
}
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
708
802
|
/** Try filling with multiple selector strategies */
|
|
709
803
|
async function tryAIFill(page, selector, value) {
|
|
710
804
|
const TIMEOUT = 5000;
|
|
@@ -904,6 +998,10 @@ async function tryFill(page, label, value) {
|
|
|
904
998
|
errors.push(e?.message?.split("\n")[0] || String(e));
|
|
905
999
|
}
|
|
906
1000
|
}
|
|
1001
|
+
// Autocomplete fallback — type + wait for dropdown + click option
|
|
1002
|
+
const acSuccess = await tryAutocomplete(page, label, value);
|
|
1003
|
+
if (acSuccess)
|
|
1004
|
+
return;
|
|
907
1005
|
// AI vision fallback
|
|
908
1006
|
const aiSuccess = await aiFillFallback(page, label, value);
|
|
909
1007
|
if (aiSuccess)
|