@cutleryapp/agent 1.0.36 → 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.
@@ -765,9 +765,7 @@ async function tryAIClick(page, selector) {
765
765
  }
766
766
  /** Select an option from a React-select / autocomplete / combobox / dropdown */
767
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
768
+ // Click the matching option in whatever dropdown is currently open — returns true only on success
771
769
  async function clickOpenOption() {
772
770
  const optionSelectors = [
773
771
  `[role="option"]:has-text("${value}")`,
@@ -789,38 +787,57 @@ async function tryAutocomplete(page, fieldLabel, value) {
789
787
  }
790
788
  return false;
791
789
  }
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"]`,
797
- ];
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);
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;
816
+ }
817
+ }
818
+ return false;
819
+ }, fieldLabel);
820
+ if (opened) {
821
+ await page.waitForTimeout(300);
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);
814
832
  if (await clickOpenOption())
815
833
  return true;
816
- // If specific option not found, press Enter
817
- await page.keyboard.press('Enter');
818
- await page.waitForTimeout(200);
819
- return true;
834
+ // No confirmed click don't claim success, fall through to next strategy
820
835
  }
821
- catch { /* try next */ }
822
836
  }
823
- // Strategy 2: combobox / input by label or placeholder — type and pick option
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');
824
841
  const inputLocators = [
825
842
  page.getByRole('combobox', { name: labelRe }),
826
843
  page.getByLabel(labelRe),
@@ -829,8 +846,8 @@ async function tryAutocomplete(page, fieldLabel, value) {
829
846
  for (const loc of inputLocators) {
830
847
  try {
831
848
  const input = loc.first();
849
+ // Skip wrapper divs — getByLabel can return the React-select container div
832
850
  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
851
  if (tag && !['input', 'textarea'].includes(tag))
835
852
  continue;
836
853
  await input.waitFor({ state: 'visible', timeout: 1500 });
@@ -840,9 +857,7 @@ async function tryAutocomplete(page, fieldLabel, value) {
840
857
  await page.waitForTimeout(500);
841
858
  if (await clickOpenOption())
842
859
  return true;
843
- await page.keyboard.press('Enter');
844
- await page.waitForTimeout(200);
845
- return true;
860
+ // Only count success when the option was actually clicked
846
861
  }
847
862
  catch { /* try next */ }
848
863
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.36",
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": {