@cutleryapp/agent 1.0.32 → 1.0.34
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 +50 -7
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -61,7 +61,7 @@ class TestExecutor {
|
|
|
61
61
|
}
|
|
62
62
|
console.log('[executor] varMap:', JSON.stringify(varMap));
|
|
63
63
|
const resolveVars = (s) => s.replace(/\{\{([^}]+)\}\}/g, (match, key) => varMap[key.trim()] ?? match).trim();
|
|
64
|
-
const steps = testCase.automated_steps.map(resolveVars);
|
|
64
|
+
const steps = testCase.automated_steps.filter((s) => !s.startsWith('[DISABLED] ')).map(resolveVars);
|
|
65
65
|
console.log('[executor] resolved steps:', JSON.stringify(steps));
|
|
66
66
|
const browserLaunchers = { chromium: playwright_1.chromium, firefox: playwright_1.firefox, webkit: playwright_1.webkit };
|
|
67
67
|
const launchFn = browserLaunchers[this.options.browserType] ?? playwright_1.chromium;
|
|
@@ -206,15 +206,53 @@ class TestExecutor {
|
|
|
206
206
|
handled = true;
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
-
// 6. Select — dropdown
|
|
209
|
+
// 6. Select — native dropdown, then React-select/autocomplete fallback
|
|
210
210
|
if (!handled && (lower.includes("select") || lower.includes("choose"))) {
|
|
211
|
-
const selMatch = raw.match(/select\s+"?([^"]+?)"?\s+(?:from|in)\s+"?([^"]+?)"?\s*(?:dropdown|select|field)?$/i);
|
|
211
|
+
const selMatch = raw.match(/(?:select|choose)\s+"?([^"]+?)"?\s+(?:from|in)\s+"?([^"]+?)"?\s*(?:dropdown|select|field)?$/i);
|
|
212
212
|
if (selMatch) {
|
|
213
|
+
const optionValue = selMatch[1].trim();
|
|
214
|
+
const fieldLabel = selMatch[2].trim();
|
|
215
|
+
let selHandled = false;
|
|
216
|
+
// Try native <select>
|
|
213
217
|
try {
|
|
214
|
-
|
|
218
|
+
const fieldLoc = page.getByLabel(new RegExp(fieldLabel, 'i')).first();
|
|
219
|
+
await fieldLoc.selectOption({ label: optionValue });
|
|
220
|
+
selHandled = true;
|
|
221
|
+
}
|
|
222
|
+
catch { /* not a native select */ }
|
|
223
|
+
// Try React-select / autocomplete typeahead
|
|
224
|
+
if (!selHandled) {
|
|
225
|
+
selHandled = await tryAutocomplete(page, fieldLabel, optionValue);
|
|
226
|
+
}
|
|
227
|
+
// Try clicking a visible option in an already-open dropdown
|
|
228
|
+
if (!selHandled) {
|
|
229
|
+
try {
|
|
230
|
+
await page.locator(`[role="option"]:has-text("${optionValue}"), [class*="option"]:has-text("${optionValue}")`).first().click({ timeout: 3000 });
|
|
231
|
+
selHandled = true;
|
|
232
|
+
}
|
|
233
|
+
catch { /* fall to AI */ }
|
|
234
|
+
}
|
|
235
|
+
if (selHandled)
|
|
236
|
+
handled = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// 6b. Check/uncheck — checkbox or radio by label
|
|
240
|
+
if (!handled && (lower.startsWith("check ") || lower.startsWith("tick ") || lower.startsWith("select ")) && !handled) {
|
|
241
|
+
const cbMatch = raw.match(/(?:check|tick|select)\s+"?([^"]+?)"?\s*(?:checkbox|option|hobby|hobbies)?$/i);
|
|
242
|
+
if (cbMatch) {
|
|
243
|
+
const labelText = cbMatch[1].trim();
|
|
244
|
+
try {
|
|
245
|
+
// Try label click (works for hidden checkboxes with styled labels)
|
|
246
|
+
await page.locator(`label:has-text("${labelText}")`).first().click({ timeout: 3000 });
|
|
215
247
|
handled = true;
|
|
216
248
|
}
|
|
217
|
-
catch {
|
|
249
|
+
catch {
|
|
250
|
+
try {
|
|
251
|
+
await page.getByLabel(new RegExp(labelText, 'i')).first().check({ timeout: 3000 });
|
|
252
|
+
handled = true;
|
|
253
|
+
}
|
|
254
|
+
catch { /* fall to AI */ }
|
|
255
|
+
}
|
|
218
256
|
}
|
|
219
257
|
}
|
|
220
258
|
// 7. AI — single-shot for deterministic steps, full loop for intent steps
|
|
@@ -623,9 +661,14 @@ ${domElements}` + (stepAttachment ? `\n\nThe REFERENCE IMAGE (second image) show
|
|
|
623
661
|
const checked = await el.isChecked().catch(() => false);
|
|
624
662
|
if (!checked) {
|
|
625
663
|
let done = false;
|
|
664
|
+
// Derive a label text from selector for label-based fallbacks
|
|
665
|
+
const labelHint = act.label || act.selector.replace(/[#.\[\]"'=*^$]/g, ' ').trim();
|
|
626
666
|
for (const fn of [
|
|
627
|
-
() => el.click({ timeout: 4000 }),
|
|
628
|
-
() =>
|
|
667
|
+
() => el.click({ force: true, timeout: 4000 }), // force bypasses visibility
|
|
668
|
+
() => el.check({ force: true, timeout: 4000 }),
|
|
669
|
+
() => page.locator(`label[for="${act.selector.replace('#', '')}"]`).click({ timeout: 4000 }),
|
|
670
|
+
() => page.locator(`label:has-text("${labelHint}")`).first().click({ timeout: 4000 }),
|
|
671
|
+
() => page.locator(`label:has(${act.selector})`).click({ timeout: 4000 }),
|
|
629
672
|
]) {
|
|
630
673
|
try {
|
|
631
674
|
await fn();
|