@cutleryapp/agent 1.0.38 → 1.0.39

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.
@@ -337,7 +337,7 @@ class TestExecutor {
337
337
  // Try native <select>
338
338
  try {
339
339
  const fieldLoc = page.getByLabel(new RegExp(fieldLabel, 'i')).first();
340
- await fieldLoc.selectOption({ label: optionValue }, { timeout: 3000 });
340
+ await fieldLoc.selectOption({ label: optionValue }, { timeout: 800 });
341
341
  selHandled = true;
342
342
  }
343
343
  catch { /* not a native select */ }
@@ -415,14 +415,17 @@ class TestExecutor {
415
415
  stepError = err.message;
416
416
  result.success = false;
417
417
  }
418
- // Screenshot after each step
418
+ // Screenshot on failure, on the last step, or every 5 steps — not every step
419
419
  let screenshotB64 = "";
420
- try {
421
- const buf = await page.screenshot({ fullPage: false });
422
- screenshotB64 = buf.toString("base64");
423
- result.screenshots.push(screenshotB64);
420
+ const isLastStep = i === steps.length - 1;
421
+ if (stepError || isLastStep || i % 5 === 0) {
422
+ try {
423
+ const buf = await page.screenshot({ fullPage: false });
424
+ screenshotB64 = buf.toString("base64");
425
+ result.screenshots.push(screenshotB64);
426
+ }
427
+ catch { /* ignore screenshot errors */ }
424
428
  }
425
- catch { /* ignore screenshot errors */ }
426
429
  result.steps.push({
427
430
  step: raw,
428
431
  action: raw,
@@ -468,24 +471,32 @@ function extractSelector(step, pattern) {
468
471
  }
469
472
  // Fast probe: try each locator strategy with a short timeout so fallbacks don't stall
470
473
  async function tryClick(page, nameRe, label) {
471
- const FAST = 800;
472
- const strategies = [
473
- () => page.getByRole('button', { name: nameRe }).first().click({ timeout: FAST }),
474
- () => page.getByRole('link', { name: nameRe }).first().click({ timeout: FAST }),
475
- () => page.getByText(nameRe).first().click({ timeout: FAST }),
476
- () => page.locator(`[value="${label}"], [aria-label="${label}"], [title="${label}"]`).first().click({ timeout: FAST }),
477
- // data-* attributes (common in test automation)
478
- () => page.locator(`[data-test*="${label}" i], [data-testid*="${label}" i], [id*="${label}" i]`).first().click({ timeout: FAST }),
474
+ // Race all common role/text strategies simultaneously — first one wins
475
+ const T = 1000;
476
+ const raceStrategies = [
477
+ page.getByRole('button', { name: nameRe }).first().click({ timeout: T }),
478
+ page.getByRole('link', { name: nameRe }).first().click({ timeout: T }),
479
+ page.getByRole('tab', { name: nameRe }).first().click({ timeout: T }),
480
+ page.getByText(nameRe, { exact: false }).first().click({ timeout: T }),
481
+ page.locator(`[aria-label="${label}" i],[title="${label}" i],[data-test*="${label}" i],[data-testid*="${label}" i]`).first().click({ timeout: T }),
479
482
  ];
480
- for (const fn of strategies) {
483
+ try {
484
+ await Promise.any(raceStrategies);
485
+ return true;
486
+ }
487
+ catch { /* all failed, try sequential fallbacks */ }
488
+ // Sequential fallbacks for id/value/role selectors
489
+ for (const fn of [
490
+ () => page.locator(`#${label.replace(/\s+/g, '-')}, #${label.replace(/\s+/g, '_')}, [id*="${label}" i]`).first().click({ timeout: 500 }),
491
+ () => page.locator(`input[value="${label}" i], button[value="${label}" i]`).first().click({ timeout: 500 }),
492
+ ]) {
481
493
  try {
482
494
  await fn();
483
495
  return true;
484
496
  }
485
- catch { /* try next */ }
497
+ catch { /* next */ }
486
498
  }
487
- // AI vision fallback
488
- return await aiClickFallback(page, label);
499
+ return false;
489
500
  }
490
501
  function buildAgentPrompt(stepText, round, hasAttachment = false) {
491
502
  const attachmentSection = hasAttachment ? `
@@ -944,12 +955,12 @@ async function tryAutocomplete(page, fieldLabel, value) {
944
955
  const innerInput = page.locator('[class*="react-select__input"] input,[class*="select__input"] input').first();
945
956
  try {
946
957
  await innerInput.waitFor({ state: 'visible', timeout: 800 });
947
- await innerInput.type(value, { delay: 60 });
958
+ await innerInput.type(value, { delay: 30 });
948
959
  }
949
960
  catch {
950
- await page.keyboard.type(value, { delay: 60 });
961
+ await page.keyboard.type(value, { delay: 30 });
951
962
  }
952
- await page.waitForTimeout(600);
963
+ await page.waitForTimeout(400);
953
964
  if (await clickOpenOption())
954
965
  return true;
955
966
  // No confirmed click → don't claim success, fall through to next strategy
@@ -974,11 +985,10 @@ async function tryAutocomplete(page, fieldLabel, value) {
974
985
  await input.waitFor({ state: 'visible', timeout: 1500 });
975
986
  await input.click({ timeout: 1500 });
976
987
  await input.fill('');
977
- await input.type(value, { delay: 60 });
978
- await page.waitForTimeout(500);
988
+ await input.type(value, { delay: 30 });
989
+ await page.waitForTimeout(350);
979
990
  if (await clickOpenOption())
980
991
  return true;
981
- // Only count success when the option was actually clicked
982
992
  }
983
993
  catch { /* try next */ }
984
994
  }
@@ -1077,7 +1087,7 @@ async function aiFillFallback(page, label, value) {
1077
1087
  }
1078
1088
  }
1079
1089
  async function tryClickScoped(page, nameRe, target, scope) {
1080
- const FAST = 3000;
1090
+ const FAST = 1000;
1081
1091
  // Strip trailing generic nouns that won't appear verbatim on the page
1082
1092
  const cleanScope = scope.replace(/\s+(?:product|item|section|card|row|container|element|button|link|area|panel|block)$/i, '').trim();
1083
1093
  // Use card/item container selectors — these are tight enough to contain the button
@@ -1131,59 +1141,44 @@ async function tryClickScoped(page, nameRe, target, scope) {
1131
1141
  return false;
1132
1142
  }
1133
1143
  async function tryFill(page, label, value) {
1134
- const FAST = 500;
1135
1144
  const labelRe = new RegExp(escapeRegex(label), "i");
1136
1145
  const variants = labelVariants(label);
1137
- const attrContains = (attr) => variants
1138
- .map((v) => `input[${attr}*="${cssEscape(v)}" i], textarea[${attr}*="${cssEscape(v)}" i], [contenteditable="true"][${attr}*="${cssEscape(v)}" i]`)
1139
- .join(", ");
1140
- // Ordered most-likely-to-resolve first so the success path matches the old
1141
- // 4-strategy implementation in latency. The extra strategies only run when
1142
- // the cheaper ones miss (i.e. the field genuinely needs fuzzier matching).
1143
- const strategies = [
1144
- // 1–2. Same as before — covers labelled inputs and placeholder-only inputs.
1146
+ const attrContains = (attr) => variants.map(v => `input[${attr}*="${cssEscape(v)}" i], textarea[${attr}*="${cssEscape(v)}" i], [contenteditable="true"][${attr}*="${cssEscape(v)}" i]`).join(", ");
1147
+ function withTimeout(fn, ms) {
1148
+ return Promise.race([fn(), new Promise((_, r) => setTimeout(() => r(new Error('timeout')), ms))]);
1149
+ }
1150
+ // Phase 1: race the 3 highest-coverage strategies simultaneously (500ms cap)
1151
+ // These cover labelled inputs, placeholder inputs, and role-textbox 95%+ of forms
1152
+ const phase1 = [
1145
1153
  () => page.getByLabel(labelRe).first().fill(value),
1146
1154
  () => page.getByPlaceholder(labelRe).first().fill(value),
1147
- // 3. Accessible role match.
1148
1155
  () => page.getByRole("textbox", { name: labelRe }).first().fill(value),
1149
- // 4. Common automation hooks.
1156
+ ];
1157
+ try {
1158
+ await Promise.any(phase1.map(fn => withTimeout(fn, 500)));
1159
+ return;
1160
+ }
1161
+ catch { /* all 3 failed, try phase 2 */ }
1162
+ // Phase 2: attribute-based selectors covering name/id/data-test variants (300ms each)
1163
+ const exactAttrs = variants.flatMap(v => [
1164
+ `input[name="${cssEscape(v)}" i]`, `input[id="${cssEscape(v)}" i]`,
1165
+ `textarea[name="${cssEscape(v)}" i]`, `textarea[id="${cssEscape(v)}" i]`,
1166
+ ]).join(", ");
1167
+ const phase2 = [
1168
+ () => page.locator(exactAttrs).first().fill(value),
1169
+ () => page.locator(`${attrContains("name")}, ${attrContains("id")}`).first().fill(value),
1150
1170
  () => page.locator(attrContains("data-test")).first().fill(value),
1151
- () => page.locator(attrContains("data-testid")).first().fill(value),
1152
- // 5. Native attributes — exact across all variants (kebab/snake/camel/etc.).
1153
- () => page
1154
- .locator(variants
1155
- .flatMap((v) => [
1156
- `input[name="${cssEscape(v)}" i]`,
1157
- `input[id="${cssEscape(v)}" i]`,
1158
- `textarea[name="${cssEscape(v)}" i]`,
1159
- `textarea[id="${cssEscape(v)}" i]`,
1160
- ])
1161
- .join(", "))
1162
- .first()
1163
- .fill(value),
1164
- // 6. Native attributes — contains across all variants.
1165
- () => page
1166
- .locator(`${attrContains("name")}, ${attrContains("id")}`)
1167
- .first()
1168
- .fill(value),
1169
- // 7. ARIA / placeholder fallbacks.
1170
1171
  () => page.locator(attrContains("aria-label")).first().fill(value),
1171
1172
  () => page.locator(attrContains("placeholder")).first().fill(value),
1172
1173
  ];
1173
- const errors = [];
1174
- for (const fn of strategies) {
1174
+ for (const fn of phase2) {
1175
1175
  try {
1176
- await Promise.race([
1177
- fn(),
1178
- new Promise((_, r) => setTimeout(() => r(new Error("timeout")), FAST)),
1179
- ]);
1176
+ await withTimeout(fn, 300);
1180
1177
  return;
1181
1178
  }
1182
- catch (e) {
1183
- errors.push(e?.message?.split("\n")[0] || String(e));
1184
- }
1179
+ catch { /* next */ }
1185
1180
  }
1186
- throw new Error(`Could not find input field: "${label}". Tried ${strategies.length} strategies.`);
1181
+ throw new Error(`Could not find input field: "${label}"`);
1187
1182
  }
1188
1183
  /** Token-aware variant generation matching executor.ts/labelVariants. */
1189
1184
  function labelVariants(label) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.38",
3
+ "version": "1.0.39",
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": {