@cutleryapp/agent 1.0.35 → 1.0.37
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 +108 -44
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -230,6 +230,21 @@ class TestExecutor {
|
|
|
230
230
|
await page.locator(`[role="option"]:has-text("${optionValue}"), [class*="option"]:has-text("${optionValue}")`).first().click({ timeout: 1500 });
|
|
231
231
|
selHandled = true;
|
|
232
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 });
|
|
246
|
+
selHandled = true;
|
|
247
|
+
}
|
|
233
248
|
catch { /* fall to AI */ }
|
|
234
249
|
}
|
|
235
250
|
if (selHandled)
|
|
@@ -748,54 +763,103 @@ async function tryAIClick(page, selector) {
|
|
|
748
763
|
}
|
|
749
764
|
return false;
|
|
750
765
|
}
|
|
751
|
-
/**
|
|
752
|
-
async function tryAutocomplete(page,
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
766
|
+
/** Select an option from a React-select / autocomplete / combobox / dropdown */
|
|
767
|
+
async function tryAutocomplete(page, fieldLabel, value) {
|
|
768
|
+
// Click the matching option in whatever dropdown is currently open — returns true only on success
|
|
769
|
+
async function clickOpenOption() {
|
|
770
|
+
const optionSelectors = [
|
|
771
|
+
`[role="option"]:has-text("${value}")`,
|
|
772
|
+
`[class*="option"]:has-text("${value}")`,
|
|
773
|
+
`[class*="suggestion"]:has-text("${value}")`,
|
|
774
|
+
`[class*="item"]:has-text("${value}")`,
|
|
775
|
+
`li:has-text("${value}")`,
|
|
776
|
+
];
|
|
777
|
+
for (const sel of optionSelectors) {
|
|
778
|
+
try {
|
|
779
|
+
const opt = page.locator(sel).first();
|
|
780
|
+
if (await opt.isVisible({ timeout: 600 })) {
|
|
781
|
+
await opt.click({ timeout: 1500 });
|
|
782
|
+
console.log(` ✓ Dropdown option clicked via "${sel}"`);
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
catch { /* try next */ }
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
// Strategy 1: React-select — find the control that BELONGS to this label via DOM traversal
|
|
791
|
+
// so we don't accidentally open the wrong dropdown on a page with multiple selects
|
|
792
|
+
try {
|
|
793
|
+
const opened = await page.evaluate((lbl) => {
|
|
794
|
+
const allEls = Array.from(document.querySelectorAll('label, legend, [class*="label"]'));
|
|
795
|
+
const labelEl = allEls.find(el => (el.textContent || '').trim().toLowerCase().includes(lbl.toLowerCase()));
|
|
796
|
+
if (!labelEl)
|
|
797
|
+
return false;
|
|
798
|
+
// Walk up ancestors looking for a container that holds a react-select control
|
|
799
|
+
let ancestor = labelEl.parentElement;
|
|
800
|
+
for (let i = 0; i < 6 && ancestor; i++) {
|
|
801
|
+
const ctrl = ancestor.querySelector('[class*="react-select__control"],[class*="select__control"],[class*="Select__control"]');
|
|
802
|
+
if (ctrl) {
|
|
803
|
+
ctrl.click();
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
ancestor = ancestor.parentElement;
|
|
807
|
+
}
|
|
808
|
+
// Try label[for] → wrapper containing a react-select
|
|
809
|
+
if (labelEl.tagName === 'LABEL') {
|
|
810
|
+
const forId = labelEl.htmlFor;
|
|
811
|
+
const wrapper = forId ? document.getElementById(forId) : null;
|
|
812
|
+
const ctrl = wrapper?.querySelector('[class*="control"]');
|
|
813
|
+
if (ctrl) {
|
|
814
|
+
ctrl.click();
|
|
815
|
+
return true;
|
|
790
816
|
}
|
|
791
|
-
catch { /* try next */ }
|
|
792
817
|
}
|
|
793
|
-
|
|
794
|
-
|
|
818
|
+
return false;
|
|
819
|
+
}, fieldLabel);
|
|
820
|
+
if (opened) {
|
|
795
821
|
await page.waitForTimeout(300);
|
|
796
|
-
|
|
822
|
+
// Type into the now-visible input inside the react-select
|
|
823
|
+
const innerInput = page.locator('[class*="react-select__input"] input,[class*="select__input"] input').first();
|
|
824
|
+
try {
|
|
825
|
+
await innerInput.waitFor({ state: 'visible', timeout: 800 });
|
|
826
|
+
await innerInput.type(value, { delay: 60 });
|
|
827
|
+
}
|
|
828
|
+
catch {
|
|
829
|
+
await page.keyboard.type(value, { delay: 60 });
|
|
830
|
+
}
|
|
831
|
+
await page.waitForTimeout(600);
|
|
832
|
+
if (await clickOpenOption())
|
|
833
|
+
return true;
|
|
834
|
+
// No confirmed click → don't claim success, fall through to next strategy
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
catch { /* DOM eval failed, try next */ }
|
|
838
|
+
// Strategy 2: combobox / text input by label or placeholder — type + pick option
|
|
839
|
+
const esc = (s) => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
840
|
+
const labelRe = new RegExp(esc(fieldLabel), 'i');
|
|
841
|
+
const inputLocators = [
|
|
842
|
+
page.getByRole('combobox', { name: labelRe }),
|
|
843
|
+
page.getByLabel(labelRe),
|
|
844
|
+
page.getByPlaceholder(labelRe),
|
|
845
|
+
];
|
|
846
|
+
for (const loc of inputLocators) {
|
|
847
|
+
try {
|
|
848
|
+
const input = loc.first();
|
|
849
|
+
// Skip wrapper divs — getByLabel can return the React-select container div
|
|
850
|
+
const tag = await input.evaluate((el) => el.tagName.toLowerCase()).catch(() => '');
|
|
851
|
+
if (tag && !['input', 'textarea'].includes(tag))
|
|
852
|
+
continue;
|
|
853
|
+
await input.waitFor({ state: 'visible', timeout: 1500 });
|
|
854
|
+
await input.click({ timeout: 1500 });
|
|
855
|
+
await input.fill('');
|
|
856
|
+
await input.type(value, { delay: 60 });
|
|
857
|
+
await page.waitForTimeout(500);
|
|
858
|
+
if (await clickOpenOption())
|
|
859
|
+
return true;
|
|
860
|
+
// Only count success when the option was actually clicked
|
|
797
861
|
}
|
|
798
|
-
catch { /* try next
|
|
862
|
+
catch { /* try next */ }
|
|
799
863
|
}
|
|
800
864
|
return false;
|
|
801
865
|
}
|