@cutleryapp/agent 1.0.39 → 1.0.41

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.
@@ -334,36 +334,67 @@ class TestExecutor {
334
334
  const optionValue = selMatch[1].trim();
335
335
  const fieldLabel = selMatch[2].trim();
336
336
  let selHandled = false;
337
- // Try native <select>
338
- try {
339
- const fieldLoc = page.getByLabel(new RegExp(fieldLabel, 'i')).first();
340
- await fieldLoc.selectOption({ label: optionValue }, { timeout: 800 });
341
- selHandled = true;
337
+ // 1. Native <select> — try by label, name, id
338
+ if (!selHandled) {
339
+ const fieldRe = new RegExp(fieldLabel.replace(/[\s_-]+/g, '[\\s_-]*'), 'i');
340
+ const selectLocators = [
341
+ page.getByLabel(fieldRe),
342
+ page.locator(`select[name*="${fieldLabel}" i]`),
343
+ page.locator(`select[id*="${fieldLabel}" i]`),
344
+ page.locator(`select[data-test*="${fieldLabel}" i]`),
345
+ ];
346
+ for (const loc of selectLocators) {
347
+ try {
348
+ const el = loc.first();
349
+ const tag = await el.evaluate((n) => n.tagName.toLowerCase()).catch(() => '');
350
+ if (tag !== 'select')
351
+ continue; // only drive actual <select> elements
352
+ // Try matching by label text, then by value
353
+ const matched = await Promise.any([
354
+ el.selectOption({ label: optionValue }, { timeout: 1000 }),
355
+ el.selectOption({ value: optionValue }, { timeout: 1000 }),
356
+ el.selectOption(optionValue, { timeout: 1000 }),
357
+ ]).then(() => true).catch(() => false);
358
+ if (matched) {
359
+ selHandled = true;
360
+ break;
361
+ }
362
+ }
363
+ catch { /* next */ }
364
+ }
342
365
  }
343
- catch { /* not a native select */ }
344
- // Try React-select / autocomplete typeahead
366
+ // 2. Radio / checkbox label click before autocomplete so radio buttons
367
+ // are handled in <100ms instead of waiting through autocomplete timeouts
345
368
  if (!selHandled) {
346
- selHandled = await tryAutocomplete(page, fieldLabel, optionValue);
369
+ try {
370
+ await page.locator(`label:has-text("${optionValue}")`).first().click({ timeout: 800 });
371
+ selHandled = true;
372
+ }
373
+ catch { /* not a labelled radio/checkbox */ }
347
374
  }
348
- // Try clicking a visible option in an already-open dropdown
349
375
  if (!selHandled) {
350
376
  try {
351
- await page.locator(`[role="option"]:has-text("${optionValue}"), [class*="option"]:has-text("${optionValue}")`).first().click({ timeout: 1500 });
377
+ await page.locator(`input[type="radio"][value="${optionValue}" i], input[type="checkbox"][value="${optionValue}" i]`).first().click({ force: true, timeout: 800 });
352
378
  selHandled = true;
353
379
  }
354
- catch { /* try checkbox */ }
380
+ catch { /* not an input with matching value */ }
355
381
  }
356
- // Checkbox / radio fallback — label click or direct input click
382
+ // 3. React-select / autocomplete typeahead
383
+ if (!selHandled) {
384
+ selHandled = await tryAutocomplete(page, fieldLabel, optionValue);
385
+ }
386
+ // 4. Already-open dropdown option
357
387
  if (!selHandled) {
358
388
  try {
359
- await page.locator(`label:has-text("${optionValue}")`).first().click({ timeout: 2000 });
389
+ await page.locator(`[role="option"]:has-text("${optionValue}"), [class*="option"]:has-text("${optionValue}")`).first().click({ timeout: 1000 });
360
390
  selHandled = true;
361
391
  }
362
- catch { /* try input by value */ }
392
+ catch { /* try text input fallback */ }
363
393
  }
394
+ // 5. Text input / textarea fallback — handles "Select X in Y" where Y is a textarea
364
395
  if (!selHandled) {
365
396
  try {
366
- await page.locator(`input[type="radio"][value="${optionValue}" i], input[type="checkbox"][value="${optionValue}" i]`).first().click({ force: true, timeout: 2000 });
397
+ await tryFill(page, fieldLabel, optionValue);
367
398
  selHandled = true;
368
399
  }
369
400
  catch { /* fall to AI */ }
@@ -978,12 +1009,17 @@ async function tryAutocomplete(page, fieldLabel, value) {
978
1009
  for (const loc of inputLocators) {
979
1010
  try {
980
1011
  const input = loc.first();
981
- // Skip wrapper divs getByLabel can return the React-select container div
1012
+ // Skip wrapper divs and non-text inputs (radio/checkbox handled by label click above)
982
1013
  const tag = await input.evaluate((el) => el.tagName.toLowerCase()).catch(() => '');
983
1014
  if (tag && !['input', 'textarea'].includes(tag))
984
1015
  continue;
985
- await input.waitFor({ state: 'visible', timeout: 1500 });
986
- await input.click({ timeout: 1500 });
1016
+ if (tag === 'input') {
1017
+ const inputType = await input.evaluate((el) => el.type || '').catch(() => '');
1018
+ if (['radio', 'checkbox', 'submit', 'button', 'file', 'hidden'].includes(inputType))
1019
+ continue;
1020
+ }
1021
+ await input.waitFor({ state: 'visible', timeout: 500 });
1022
+ await input.click({ timeout: 500 });
987
1023
  await input.fill('');
988
1024
  await input.type(value, { delay: 30 });
989
1025
  await page.waitForTimeout(350);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
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": {