@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.
Files changed (2) hide show
  1. package/dist/mcp-executor.js +108 -44
  2. package/package.json +1 -1
@@ -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
- /** Fill an autocomplete/typeahead input: type value, wait for dropdown, click first matching option */
752
- async function tryAutocomplete(page, labelOrSelector, value) {
753
- const TIMEOUT = 2000;
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: 500 })) {
786
- await opt.click({ timeout: TIMEOUT });
787
- console.log(` ✓ Autocomplete: typed "${value}", clicked option via "${optSel}"`);
788
- return true;
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
- // Fallback: press Enter if a highlighted option exists
794
- await page.keyboard.press('Enter');
818
+ return false;
819
+ }, fieldLabel);
820
+ if (opened) {
795
821
  await page.waitForTimeout(300);
796
- return true;
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 locator */ }
862
+ catch { /* try next */ }
799
863
  }
800
864
  return false;
801
865
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "Local agent that connects your machine to the Cutlery QA platform and runs UI tests via Playwright",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {