@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.
Files changed (2) hide show
  1. package/dist/mcp-executor.js +107 -58
  2. package/package.json +1 -1
@@ -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: 3000 });
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: 3000 });
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: 3000 });
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 = 1500;
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 ? 10 : 6;
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: 4000 }),
646
- () => page.locator(act.selector).first().selectOption({ value: act.value }, { timeout: 4000 }),
647
- async () => { await page.locator(act.selector).first().click({ timeout: 3000 }); await page.getByText(act.value, { exact: false }).first().click({ timeout: 3000 }); },
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: 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 }),
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 = 5000;
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
- /** 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),
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 loc of locators) {
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.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
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(300);
844
+ await page.waitForTimeout(200);
796
845
  return true;
797
846
  }
798
- catch { /* try next locator */ }
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 = 1500;
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
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": {