@cutleryapp/agent 1.0.19 → 1.0.21
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 +37 -49
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -60,26 +60,27 @@ class TestExecutor {
|
|
|
60
60
|
});
|
|
61
61
|
let stepError;
|
|
62
62
|
try {
|
|
63
|
+
let handled = false;
|
|
64
|
+
// 1. Navigate — direct URL goto, no selector needed
|
|
63
65
|
if (lower.includes("navigate to") || lower.includes("go to")) {
|
|
64
|
-
|
|
65
|
-
const urlMatch = raw.match(/(?:navigate to|go to)\s+(https?:\/\/\S+|\/\S*|\S+\.\S+)/i);
|
|
66
|
+
const urlMatch = raw.match(/(?:navigate\s+to|go\s+to)\s+(https?:\/\/\S+|\/\S*|\S+\.\S+)/i);
|
|
66
67
|
if (urlMatch) {
|
|
67
68
|
let url = urlMatch[1].trim();
|
|
68
|
-
if (url.startsWith("/") && this.options.baseUrl)
|
|
69
|
+
if (url.startsWith("/") && this.options.baseUrl)
|
|
69
70
|
url = this.options.baseUrl.replace(/\/$/, "") + url;
|
|
70
|
-
}
|
|
71
71
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
72
|
+
handled = true;
|
|
72
73
|
}
|
|
73
74
|
else if (this.options.baseUrl) {
|
|
74
|
-
// Step says "navigate to" but no URL found — go to baseUrl
|
|
75
75
|
await page.goto(this.options.baseUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
76
|
+
handled = true;
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
+
// 2. Click — smart selector strategies via MCP/Playwright
|
|
80
|
+
if (!handled && lower.includes("click")) {
|
|
79
81
|
const labelMatch = raw.match(/click\s+(?:on\s+)?(?:the\s+)?"?([^"]+?)"?(?:\s+(?:button|link|tab))?$/i);
|
|
80
|
-
|
|
82
|
+
const label = labelMatch?.[1]?.trim();
|
|
81
83
|
if (label) {
|
|
82
|
-
// Split "Add to cart under Sauce Labs Bike Light product" into target + scope
|
|
83
84
|
const scopeMatch = label.match(/^(.+?)\s+(?:under|inside|within|in the|in)\s+(.+)$/i);
|
|
84
85
|
const target = scopeMatch ? scopeMatch[1].trim() : label;
|
|
85
86
|
const scope = scopeMatch ? scopeMatch[2].trim() : null;
|
|
@@ -87,15 +88,12 @@ class TestExecutor {
|
|
|
87
88
|
const clicked = scope
|
|
88
89
|
? await tryClickScoped(page, nameRe, target, scope)
|
|
89
90
|
: await tryClick(page, nameRe, target);
|
|
90
|
-
if (
|
|
91
|
-
|
|
91
|
+
if (clicked)
|
|
92
|
+
handled = true;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Fill "standard_user" in "Username"
|
|
97
|
-
// Fill standard_user in Username field
|
|
98
|
-
// Split on first " in " / " into " to separate value from field
|
|
95
|
+
// 3. Fill — smart selector strategies via MCP/Playwright
|
|
96
|
+
if (!handled && (lower.includes("fill") || lower.includes("type") || lower.includes("enter"))) {
|
|
99
97
|
const match = raw.match(/(?:enter|fill|type)\s+"([^"]+)"\s+(?:in|into)\s+(?:the\s+)?"?([^"]+?)"?\s*(?:field|input|box|area)?\s*$/i) ||
|
|
100
98
|
raw.match(/(?:enter|fill|type)\s+(\S+)\s+(?:in|into)\s+(?:the\s+)?(.+?)\s*(?:field|input|box|area)?\s*$/i);
|
|
101
99
|
if (match) {
|
|
@@ -109,20 +107,11 @@ class TestExecutor {
|
|
|
109
107
|
else {
|
|
110
108
|
await tryFill(page, fieldLabel, value);
|
|
111
109
|
}
|
|
110
|
+
handled = true;
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (ms)
|
|
117
|
-
await page.waitForTimeout(parseInt(ms[1]) * 1000);
|
|
118
|
-
}
|
|
119
|
-
else if (lower.includes("wait for") && !lower.includes("second")) {
|
|
120
|
-
const sel = extractSelector(raw, /wait for\s+"?([^"]+)"?\s+to be/i);
|
|
121
|
-
if (sel)
|
|
122
|
-
await page.waitForSelector(sel, { state: "visible", timeout: 15000 });
|
|
123
|
-
}
|
|
124
|
-
else if (lower.includes("verify") || lower.includes("check") || lower.includes("assert") || lower.includes("should")) {
|
|
125
|
-
// Support: Verify "text", Verify I see text Foo, Verify text Foo is not displayed
|
|
113
|
+
// 4. Verify — check page text
|
|
114
|
+
if (!handled && (lower.includes("verify") || lower.includes("assert") || lower.includes("check") || lower.includes("should"))) {
|
|
126
115
|
const isNegative = /not\s+(?:displayed|visible|present)/i.test(raw);
|
|
127
116
|
const textMatch = raw.match(/"([^"]+)"/) ||
|
|
128
117
|
raw.match(/(?:verify|check|assert)\s+(?:i\s+see\s+(?:text\s+)?|text\s+)?(.+?)(?:\s+is\s+(?:not\s+)?(?:displayed|visible|present))?$/i);
|
|
@@ -131,48 +120,47 @@ class TestExecutor {
|
|
|
131
120
|
if (isNegative) {
|
|
132
121
|
const content = await page.textContent('body') || '';
|
|
133
122
|
if (content.includes(expected))
|
|
134
|
-
throw new Error(`Text "${expected}" should NOT be visible
|
|
123
|
+
throw new Error(`Text "${expected}" should NOT be visible`);
|
|
135
124
|
}
|
|
136
125
|
else {
|
|
137
|
-
|
|
138
|
-
await page.waitForFunction((text) => document.body.innerText.includes(text), expected, { timeout: 10000 });
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
throw new Error(`Expected text not found: "${expected}"`);
|
|
142
|
-
}
|
|
126
|
+
await page.waitForFunction((t) => document.body.innerText.includes(t), expected, { timeout: 10000 });
|
|
143
127
|
}
|
|
128
|
+
handled = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// 5. Wait — simple timeout
|
|
132
|
+
if (!handled && lower.includes("wait")) {
|
|
133
|
+
const ms = raw.match(/wait\s+(\d+)\s*(?:second|ms|millisecond)/i);
|
|
134
|
+
if (ms) {
|
|
135
|
+
await page.waitForTimeout(parseInt(ms[1]) * (raw.toLowerCase().includes('ms') ? 1 : 1000));
|
|
136
|
+
handled = true;
|
|
144
137
|
}
|
|
145
138
|
}
|
|
146
|
-
|
|
139
|
+
// 6. Select — dropdown
|
|
140
|
+
if (!handled && (lower.includes("select") || lower.includes("choose"))) {
|
|
147
141
|
const selMatch = raw.match(/select\s+"?([^"]+?)"?\s+(?:from|in)\s+"?([^"]+?)"?\s*(?:dropdown|select|field)?$/i);
|
|
148
142
|
if (selMatch) {
|
|
149
143
|
try {
|
|
150
144
|
await page.selectOption(selMatch[2].trim(), { label: selMatch[1].trim() });
|
|
145
|
+
handled = true;
|
|
151
146
|
}
|
|
152
|
-
catch {
|
|
153
|
-
// fallback: click the dropdown then click the option
|
|
154
|
-
await tryClick(page, new RegExp(escapeRegex(selMatch[2].trim()), 'i'), selMatch[2].trim());
|
|
155
|
-
await tryClick(page, new RegExp(escapeRegex(selMatch[1].trim()), 'i'), selMatch[1].trim());
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
await aiStepFallback(page, raw);
|
|
147
|
+
catch { /* fall to AI */ }
|
|
160
148
|
}
|
|
161
149
|
}
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
// 7. AI fallback — for anything not handled or ambiguous
|
|
151
|
+
if (!handled) {
|
|
152
|
+
console.log(` 🤖 MCP could not handle step, using AI: "${raw}"`);
|
|
164
153
|
await aiStepFallback(page, raw);
|
|
165
154
|
}
|
|
166
155
|
}
|
|
167
156
|
catch (err) {
|
|
168
|
-
//
|
|
169
|
-
console.log(` ⚠️
|
|
157
|
+
// MCP execution failed — let AI try to recover
|
|
158
|
+
console.log(` ⚠️ MCP step failed (${err.message}), trying AI...`);
|
|
170
159
|
try {
|
|
171
160
|
await aiStepFallback(page, raw);
|
|
172
|
-
stepError = undefined; // AI recovered it
|
|
173
161
|
}
|
|
174
162
|
catch (aiErr) {
|
|
175
|
-
stepError = err.message;
|
|
163
|
+
stepError = err.message;
|
|
176
164
|
result.success = false;
|
|
177
165
|
}
|
|
178
166
|
}
|