@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.
@@ -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
- await page.selectOption(selMatch[2].trim(), { label: selMatch[1].trim() });
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
- () => page.locator(`label:has-text("${act.label || ''}")`).click({ timeout: 4000 }),
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
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": {