@cutleryapp/agent 1.0.34 → 1.0.36
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 +107 -58
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -216,7 +216,7 @@ class TestExecutor {
|
|
|
216
216
|
// Try native <select>
|
|
217
217
|
try {
|
|
218
218
|
const fieldLoc = page.getByLabel(new RegExp(fieldLabel, 'i')).first();
|
|
219
|
-
await fieldLoc.selectOption({ label: optionValue });
|
|
219
|
+
await fieldLoc.selectOption({ label: optionValue }, { timeout: 3000 });
|
|
220
220
|
selHandled = true;
|
|
221
221
|
}
|
|
222
222
|
catch { /* not a native select */ }
|
|
@@ -227,7 +227,22 @@ class TestExecutor {
|
|
|
227
227
|
// Try clicking a visible option in an already-open dropdown
|
|
228
228
|
if (!selHandled) {
|
|
229
229
|
try {
|
|
230
|
-
await page.locator(`[role="option"]:has-text("${optionValue}"), [class*="option"]:has-text("${optionValue}")`).first().click({ timeout:
|
|
230
|
+
await page.locator(`[role="option"]:has-text("${optionValue}"), [class*="option"]:has-text("${optionValue}")`).first().click({ timeout: 1500 });
|
|
231
|
+
selHandled = true;
|
|
232
|
+
}
|
|
233
|
+
catch { /* try checkbox */ }
|
|
234
|
+
}
|
|
235
|
+
// Checkbox / radio fallback — label click or direct input click
|
|
236
|
+
if (!selHandled) {
|
|
237
|
+
try {
|
|
238
|
+
await page.locator(`label:has-text("${optionValue}")`).first().click({ timeout: 2000 });
|
|
239
|
+
selHandled = true;
|
|
240
|
+
}
|
|
241
|
+
catch { /* try input by value */ }
|
|
242
|
+
}
|
|
243
|
+
if (!selHandled) {
|
|
244
|
+
try {
|
|
245
|
+
await page.locator(`input[type="radio"][value="${optionValue}" i], input[type="checkbox"][value="${optionValue}" i]`).first().click({ force: true, timeout: 2000 });
|
|
231
246
|
selHandled = true;
|
|
232
247
|
}
|
|
233
248
|
catch { /* fall to AI */ }
|
|
@@ -243,12 +258,12 @@ class TestExecutor {
|
|
|
243
258
|
const labelText = cbMatch[1].trim();
|
|
244
259
|
try {
|
|
245
260
|
// Try label click (works for hidden checkboxes with styled labels)
|
|
246
|
-
await page.locator(`label:has-text("${labelText}")`).first().click({ timeout:
|
|
261
|
+
await page.locator(`label:has-text("${labelText}")`).first().click({ timeout: 2000 });
|
|
247
262
|
handled = true;
|
|
248
263
|
}
|
|
249
264
|
catch {
|
|
250
265
|
try {
|
|
251
|
-
await page.getByLabel(new RegExp(labelText, 'i')).first().check({ timeout:
|
|
266
|
+
await page.getByLabel(new RegExp(labelText, 'i')).first().check({ timeout: 2000 });
|
|
252
267
|
handled = true;
|
|
253
268
|
}
|
|
254
269
|
catch { /* fall to AI */ }
|
|
@@ -332,7 +347,7 @@ function extractSelector(step, pattern) {
|
|
|
332
347
|
}
|
|
333
348
|
// Fast probe: try each locator strategy with a short timeout so fallbacks don't stall
|
|
334
349
|
async function tryClick(page, nameRe, label) {
|
|
335
|
-
const FAST =
|
|
350
|
+
const FAST = 800;
|
|
336
351
|
const strategies = [
|
|
337
352
|
() => page.getByRole('button', { name: nameRe }).first().click({ timeout: FAST }),
|
|
338
353
|
() => page.getByRole('link', { name: nameRe }).first().click({ timeout: FAST }),
|
|
@@ -584,7 +599,7 @@ async function aiStepFallback(page, stepText, stepAttachment = null) {
|
|
|
584
599
|
console.log(` 🤖 aiStepFallback called. hasAttachment=${!!stepAttachment}`);
|
|
585
600
|
const { default: OpenAI } = await import('openai');
|
|
586
601
|
const openai = new OpenAI({ apiKey: openaiKey });
|
|
587
|
-
const MAX_ROUNDS = stepAttachment ?
|
|
602
|
+
const MAX_ROUNDS = stepAttachment ? 6 : 3;
|
|
588
603
|
let consecutiveFailures = 0;
|
|
589
604
|
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
590
605
|
const domElements = await extractDomElements(page);
|
|
@@ -642,9 +657,9 @@ ${domElements}` + (stepAttachment ? `\n\nThe REFERENCE IMAGE (second image) show
|
|
|
642
657
|
else if (act.action === 'select') {
|
|
643
658
|
let done = false;
|
|
644
659
|
for (const fn of [
|
|
645
|
-
() => page.locator(act.selector).first().selectOption({ label: act.value }, { timeout:
|
|
646
|
-
() => page.locator(act.selector).first().selectOption({ value: act.value }, { timeout:
|
|
647
|
-
async () => { await page.locator(act.selector).first().click({ timeout:
|
|
660
|
+
() => page.locator(act.selector).first().selectOption({ label: act.value }, { timeout: 2000 }),
|
|
661
|
+
() => page.locator(act.selector).first().selectOption({ value: act.value }, { timeout: 2000 }),
|
|
662
|
+
async () => { await page.locator(act.selector).first().click({ timeout: 2000 }); await page.getByText(act.value, { exact: false }).first().click({ timeout: 2000 }); },
|
|
648
663
|
]) {
|
|
649
664
|
try {
|
|
650
665
|
await fn();
|
|
@@ -664,11 +679,11 @@ ${domElements}` + (stepAttachment ? `\n\nThe REFERENCE IMAGE (second image) show
|
|
|
664
679
|
// Derive a label text from selector for label-based fallbacks
|
|
665
680
|
const labelHint = act.label || act.selector.replace(/[#.\[\]"'=*^$]/g, ' ').trim();
|
|
666
681
|
for (const fn of [
|
|
667
|
-
() => el.click({ force: true, timeout:
|
|
668
|
-
() => el.check({ force: true, timeout:
|
|
669
|
-
() => page.locator(`label[for="${act.selector.replace('#', '')}"]`).click({ timeout:
|
|
670
|
-
() => page.locator(`label:has-text("${labelHint}")`).first().click({ timeout:
|
|
671
|
-
() => page.locator(`label:has(${act.selector})`).click({ timeout:
|
|
682
|
+
() => el.click({ force: true, timeout: 2000 }), // force bypasses visibility
|
|
683
|
+
() => el.check({ force: true, timeout: 2000 }),
|
|
684
|
+
() => page.locator(`label[for="${act.selector.replace('#', '')}"]`).click({ timeout: 2000 }),
|
|
685
|
+
() => page.locator(`label:has-text("${labelHint}")`).first().click({ timeout: 2000 }),
|
|
686
|
+
() => page.locator(`label:has(${act.selector})`).click({ timeout: 2000 }),
|
|
672
687
|
]) {
|
|
673
688
|
try {
|
|
674
689
|
await fn();
|
|
@@ -726,7 +741,7 @@ ${domElements}` + (stepAttachment ? `\n\nThe REFERENCE IMAGE (second image) show
|
|
|
726
741
|
}
|
|
727
742
|
/** Try clicking with multiple selector strategies derived from AI suggestion */
|
|
728
743
|
async function tryAIClick(page, selector) {
|
|
729
|
-
const TIMEOUT =
|
|
744
|
+
const TIMEOUT = 3000;
|
|
730
745
|
// Build fallback variants: the AI selector + text-based alternatives
|
|
731
746
|
const textMatch = selector.match(/:has-text\("([^"]+)"\)|:text\("([^"]+)"\)/);
|
|
732
747
|
const text = textMatch ? (textMatch[1] || textMatch[2]) : null;
|
|
@@ -748,54 +763,88 @@ async function tryAIClick(page, selector) {
|
|
|
748
763
|
}
|
|
749
764
|
return false;
|
|
750
765
|
}
|
|
751
|
-
/**
|
|
752
|
-
async function tryAutocomplete(page,
|
|
753
|
-
const
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
766
|
+
/** Select an option from a React-select / autocomplete / combobox / dropdown */
|
|
767
|
+
async function tryAutocomplete(page, fieldLabel, value) {
|
|
768
|
+
const esc = (s) => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
769
|
+
const labelRe = new RegExp(esc(fieldLabel), 'i');
|
|
770
|
+
// Click an option from whatever dropdown is currently open on the page
|
|
771
|
+
async function clickOpenOption() {
|
|
772
|
+
const optionSelectors = [
|
|
773
|
+
`[role="option"]:has-text("${value}")`,
|
|
774
|
+
`[class*="option"]:has-text("${value}")`,
|
|
775
|
+
`[class*="suggestion"]:has-text("${value}")`,
|
|
776
|
+
`[class*="item"]:has-text("${value}")`,
|
|
777
|
+
`li:has-text("${value}")`,
|
|
778
|
+
];
|
|
779
|
+
for (const sel of optionSelectors) {
|
|
780
|
+
try {
|
|
781
|
+
const opt = page.locator(sel).first();
|
|
782
|
+
if (await opt.isVisible({ timeout: 600 })) {
|
|
783
|
+
await opt.click({ timeout: 1500 });
|
|
784
|
+
console.log(` ✓ Dropdown option clicked via "${sel}"`);
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
catch { /* try next */ }
|
|
789
|
+
}
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
// Strategy 1: React-select — click the control div, type into its hidden input
|
|
793
|
+
const reactControlSelectors = [
|
|
794
|
+
`[class*="react-select__control"]`,
|
|
795
|
+
`[class*="select__control"]`,
|
|
796
|
+
`[class*="Select__control"]`,
|
|
758
797
|
];
|
|
759
|
-
for (const
|
|
798
|
+
for (const ctrlSel of reactControlSelectors) {
|
|
799
|
+
try {
|
|
800
|
+
// Find a React-select control near the label text
|
|
801
|
+
// Find label element, then look for the control near it via DOM traversal
|
|
802
|
+
const labelEl = page.getByText(labelRe, { exact: false }).first();
|
|
803
|
+
if (!await labelEl.isVisible({ timeout: 500 }).catch(() => false))
|
|
804
|
+
continue;
|
|
805
|
+
// Try sibling/parent-scoped control
|
|
806
|
+
const ctrl = page.locator(ctrlSel);
|
|
807
|
+
if (await ctrl.count() === 0)
|
|
808
|
+
continue;
|
|
809
|
+
await ctrl.first().click({ timeout: 2000 });
|
|
810
|
+
// The hidden input inside the control is now active
|
|
811
|
+
const innerInput = page.locator(`[class*="react-select__input"] input, [class*="select__input"] input`).first();
|
|
812
|
+
await innerInput.type(value, { delay: 60 });
|
|
813
|
+
await page.waitForTimeout(500);
|
|
814
|
+
if (await clickOpenOption())
|
|
815
|
+
return true;
|
|
816
|
+
// If specific option not found, press Enter
|
|
817
|
+
await page.keyboard.press('Enter');
|
|
818
|
+
await page.waitForTimeout(200);
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
catch { /* try next */ }
|
|
822
|
+
}
|
|
823
|
+
// Strategy 2: combobox / input by label or placeholder — type and pick option
|
|
824
|
+
const inputLocators = [
|
|
825
|
+
page.getByRole('combobox', { name: labelRe }),
|
|
826
|
+
page.getByLabel(labelRe),
|
|
827
|
+
page.getByPlaceholder(labelRe),
|
|
828
|
+
];
|
|
829
|
+
for (const loc of inputLocators) {
|
|
760
830
|
try {
|
|
761
831
|
const input = loc.first();
|
|
762
|
-
await input.
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
await input.
|
|
767
|
-
|
|
768
|
-
await
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
|
832
|
+
const tag = await input.evaluate((el) => el.tagName.toLowerCase()).catch(() => '');
|
|
833
|
+
// Skip non-input elements (e.g. wrapper divs returned by getByLabel for React-select)
|
|
834
|
+
if (tag && !['input', 'textarea'].includes(tag))
|
|
835
|
+
continue;
|
|
836
|
+
await input.waitFor({ state: 'visible', timeout: 1500 });
|
|
837
|
+
await input.click({ timeout: 1500 });
|
|
838
|
+
await input.fill('');
|
|
839
|
+
await input.type(value, { delay: 60 });
|
|
840
|
+
await page.waitForTimeout(500);
|
|
841
|
+
if (await clickOpenOption())
|
|
842
|
+
return true;
|
|
794
843
|
await page.keyboard.press('Enter');
|
|
795
|
-
await page.waitForTimeout(
|
|
844
|
+
await page.waitForTimeout(200);
|
|
796
845
|
return true;
|
|
797
846
|
}
|
|
798
|
-
catch { /* try next
|
|
847
|
+
catch { /* try next */ }
|
|
799
848
|
}
|
|
800
849
|
return false;
|
|
801
850
|
}
|
|
@@ -946,7 +995,7 @@ async function tryClickScoped(page, nameRe, target, scope) {
|
|
|
946
995
|
return false;
|
|
947
996
|
}
|
|
948
997
|
async function tryFill(page, label, value) {
|
|
949
|
-
const FAST =
|
|
998
|
+
const FAST = 800;
|
|
950
999
|
const labelRe = new RegExp(escapeRegex(label), "i");
|
|
951
1000
|
const variants = labelVariants(label);
|
|
952
1001
|
const attrContains = (attr) => variants
|