@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.
- package/dist/mcp-executor.js +61 -66
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -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:
|
|
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
|
|
418
|
+
// Screenshot on failure, on the last step, or every 5 steps — not every step
|
|
419
419
|
let screenshotB64 = "";
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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 { /*
|
|
497
|
+
catch { /* next */ }
|
|
486
498
|
}
|
|
487
|
-
|
|
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:
|
|
958
|
+
await innerInput.type(value, { delay: 30 });
|
|
948
959
|
}
|
|
949
960
|
catch {
|
|
950
|
-
await page.keyboard.type(value, { delay:
|
|
961
|
+
await page.keyboard.type(value, { delay: 30 });
|
|
951
962
|
}
|
|
952
|
-
await page.waitForTimeout(
|
|
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:
|
|
978
|
-
await page.waitForTimeout(
|
|
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 =
|
|
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
|
-
|
|
1139
|
-
.
|
|
1140
|
-
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
const
|
|
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
|
-
|
|
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
|
|
1174
|
-
for (const fn of strategies) {
|
|
1174
|
+
for (const fn of phase2) {
|
|
1175
1175
|
try {
|
|
1176
|
-
await
|
|
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
|
|
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}"
|
|
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) {
|