@cutleryapp/agent 1.0.44 → 1.0.46

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.
@@ -85,9 +85,14 @@ class TestExecutor {
85
85
  const stepAttachment = (testCase.step_attachments || {})[String(i)] || null;
86
86
  console.log(` 📎 Step ${i} attachment: ${stepAttachment ? `YES (${stepAttachment.length} chars)` : 'none'}`);
87
87
  let stepError;
88
- // Dismiss any open overlay (date picker, dropdown, modal) from the previous step
89
- if (i > 0)
88
+ // Dismiss any open overlay (calendar, dropdown, modal) left from the previous step
89
+ if (i > 0) {
90
90
  await page.keyboard.press('Escape').catch(() => { });
91
+ await page.waitForTimeout(80);
92
+ // Click a neutral spot (top-left corner) to blur active element and close popups
93
+ await page.mouse.click(10, 10).catch(() => { });
94
+ await page.waitForTimeout(80);
95
+ }
91
96
  try {
92
97
  // When a reference image is attached, skip MCP strategies entirely and go
93
98
  // straight to the AI multi-field loop so it can scan the form and fill everything.
@@ -387,21 +392,19 @@ class TestExecutor {
387
392
  catch { /* next */ }
388
393
  }
389
394
  }
390
- // 2. Radio / checkbox label click before autocomplete so radio buttons
391
- // are handled in <100ms instead of waiting through autocomplete timeouts
392
- if (!selHandled) {
393
- try {
394
- await page.locator(`label:has-text("${optionValue}")`).first().click({ timeout: 800 });
395
- selHandled = true;
396
- }
397
- catch { /* not a labelled radio/checkbox */ }
398
- }
395
+ // 2. Radio / checkbox race all selectors simultaneously; first click wins
399
396
  if (!selHandled) {
400
397
  try {
401
- await page.locator(`input[type="radio"][value="${optionValue}" i], input[type="checkbox"][value="${optionValue}" i]`).first().click({ force: true, timeout: 800 });
398
+ await Promise.any([
399
+ page.locator(`label:text-is("${optionValue}")`).first().click({ timeout: 300 }),
400
+ page.locator(`label:has-text("${optionValue}")`).first().click({ timeout: 300 }),
401
+ page.locator(`[role="radio"]:has-text("${optionValue}")`).first().click({ timeout: 300 }),
402
+ page.locator(`input[type="radio"][value="${optionValue}" i]`).first().click({ force: true, timeout: 300 }),
403
+ page.locator(`input[type="checkbox"][value="${optionValue}" i]`).first().click({ force: true, timeout: 300 }),
404
+ ]);
402
405
  selHandled = true;
403
406
  }
404
- catch { /* not an input with matching value */ }
407
+ catch { /* not a radio/checkbox, fall through */ }
405
408
  }
406
409
  // 3. React-select / autocomplete typeahead
407
410
  if (!selHandled) {
@@ -433,17 +436,14 @@ class TestExecutor {
433
436
  if (cbMatch) {
434
437
  const labelText = cbMatch[1].trim();
435
438
  try {
436
- // Try label click (works for hidden checkboxes with styled labels)
437
- await page.locator(`label:has-text("${labelText}")`).first().click({ timeout: 2000 });
439
+ await Promise.any([
440
+ page.locator(`label:text-is("${labelText}")`).first().click({ timeout: 300 }),
441
+ page.locator(`label:has-text("${labelText}")`).first().click({ timeout: 300 }),
442
+ page.getByLabel(new RegExp(labelText, 'i')).first().check({ force: true, timeout: 300 }),
443
+ ]);
438
444
  handled = true;
439
445
  }
440
- catch {
441
- try {
442
- await page.getByLabel(new RegExp(labelText, 'i')).first().check({ timeout: 2000 });
443
- handled = true;
444
- }
445
- catch { /* fall to AI */ }
446
- }
446
+ catch { /* fall to AI */ }
447
447
  }
448
448
  }
449
449
  // 7. Generic click fallback — try any element containing the step text before AI
@@ -967,25 +967,20 @@ async function tryAIClick(page, selector) {
967
967
  async function tryAutocomplete(page, fieldLabel, value) {
968
968
  // Click the matching option in whatever dropdown is currently open — returns true only on success
969
969
  async function clickOpenOption() {
970
- const optionSelectors = [
971
- `[role="option"]:has-text("${value}")`,
972
- `[class*="option"]:has-text("${value}")`,
973
- `[class*="suggestion"]:has-text("${value}")`,
974
- `[class*="item"]:has-text("${value}")`,
975
- `li:has-text("${value}")`,
976
- ];
977
- for (const sel of optionSelectors) {
978
- try {
979
- const opt = page.locator(sel).first();
980
- if (await opt.isVisible({ timeout: 600 })) {
981
- await opt.click({ timeout: 1500 });
982
- console.log(` ✓ Dropdown option clicked via "${sel}"`);
983
- return true;
984
- }
985
- }
986
- catch { /* try next */ }
970
+ // Race all option selectors simultaneously — first visible one wins
971
+ try {
972
+ await Promise.any([
973
+ `[role="option"]:has-text("${value}")`,
974
+ `[class*="option"]:has-text("${value}")`,
975
+ `[class*="suggestion"]:has-text("${value}")`,
976
+ `[class*="item"]:has-text("${value}")`,
977
+ `li:has-text("${value}")`,
978
+ ].map(sel => page.locator(sel).first().click({ timeout: 300 })));
979
+ return true;
980
+ }
981
+ catch {
982
+ return false;
987
983
  }
988
- return false;
989
984
  }
990
985
  // Strategy 1: React-select — find the control that BELONGS to this label via DOM traversal
991
986
  // so we don't accidentally open the wrong dropdown on a page with multiple selects
@@ -1022,13 +1017,13 @@ async function tryAutocomplete(page, fieldLabel, value) {
1022
1017
  // Type into the now-visible input inside the react-select
1023
1018
  const innerInput = page.locator('[class*="react-select__input"] input,[class*="select__input"] input').first();
1024
1019
  try {
1025
- await innerInput.waitFor({ state: 'visible', timeout: 800 });
1026
- await innerInput.type(value, { delay: 30 });
1020
+ await innerInput.waitFor({ state: 'visible', timeout: 400 });
1021
+ await innerInput.type(value, { delay: 20 });
1027
1022
  }
1028
1023
  catch {
1029
- await page.keyboard.type(value, { delay: 30 });
1024
+ await page.keyboard.type(value, { delay: 20 });
1030
1025
  }
1031
- await page.waitForTimeout(400);
1026
+ await page.waitForTimeout(250);
1032
1027
  if (await clickOpenOption())
1033
1028
  return true;
1034
1029
  // No confirmed click → don't claim success, fall through to next strategy
@@ -1055,11 +1050,11 @@ async function tryAutocomplete(page, fieldLabel, value) {
1055
1050
  if (['radio', 'checkbox', 'submit', 'button', 'file', 'hidden'].includes(inputType))
1056
1051
  continue;
1057
1052
  }
1058
- await input.waitFor({ state: 'visible', timeout: 500 });
1059
- await input.click({ timeout: 500 });
1053
+ await input.waitFor({ state: 'visible', timeout: 300 });
1054
+ await input.click({ timeout: 300 });
1060
1055
  await input.fill('');
1061
- await input.type(value, { delay: 30 });
1062
- await page.waitForTimeout(350);
1056
+ await input.type(value, { delay: 20 });
1057
+ await page.waitForTimeout(250);
1063
1058
  if (await clickOpenOption())
1064
1059
  return true;
1065
1060
  }
@@ -1268,11 +1263,15 @@ async function tryFillDate(page, label, value) {
1268
1263
  await page.keyboard.press('Meta+a'); // Mac
1269
1264
  await page.keyboard.type(value, { delay: 40 });
1270
1265
  await page.waitForTimeout(150);
1271
- // Tab out to commit this also closes the calendar
1266
+ // Tab out to commit and close the calendar
1272
1267
  await page.keyboard.press('Tab');
1273
1268
  await page.waitForTimeout(200);
1274
- // If calendar is still open, press Escape
1269
+ // Aggressively close any calendar still open
1275
1270
  await page.keyboard.press('Escape').catch(() => { });
1271
+ await page.waitForTimeout(100);
1272
+ // Click neutral area to ensure calendar/overlay is gone
1273
+ await page.mouse.click(10, 10).catch(() => { });
1274
+ await page.waitForTimeout(150);
1276
1275
  return;
1277
1276
  }
1278
1277
  catch { /* try next */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.44",
3
+ "version": "1.0.46",
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": {