@cutleryapp/agent 1.0.19 → 1.0.20
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 +6 -98
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -60,9 +60,9 @@ class TestExecutor {
|
|
|
60
60
|
});
|
|
61
61
|
let stepError;
|
|
62
62
|
try {
|
|
63
|
+
// Navigate is handled directly — URL extraction doesn't need vision
|
|
63
64
|
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);
|
|
65
|
+
const urlMatch = raw.match(/(?:navigate\s+to|go\s+to)\s+(https?:\/\/\S+|\/\S*|\S+\.\S+)/i);
|
|
66
66
|
if (urlMatch) {
|
|
67
67
|
let url = urlMatch[1].trim();
|
|
68
68
|
if (url.startsWith("/") && this.options.baseUrl) {
|
|
@@ -71,110 +71,18 @@ class TestExecutor {
|
|
|
71
71
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
72
72
|
}
|
|
73
73
|
else if (this.options.baseUrl) {
|
|
74
|
-
// Step says "navigate to" but no URL found — go to baseUrl
|
|
75
74
|
await page.goto(this.options.baseUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
|
-
else if (lower.includes("click")) {
|
|
79
|
-
const labelMatch = raw.match(/click\s+(?:on\s+)?(?:the\s+)?"?([^"]+?)"?(?:\s+(?:button|link|tab))?$/i);
|
|
80
|
-
let label = labelMatch?.[1]?.trim();
|
|
81
|
-
if (label) {
|
|
82
|
-
// Split "Add to cart under Sauce Labs Bike Light product" into target + scope
|
|
83
|
-
const scopeMatch = label.match(/^(.+?)\s+(?:under|inside|within|in the|in)\s+(.+)$/i);
|
|
84
|
-
const target = scopeMatch ? scopeMatch[1].trim() : label;
|
|
85
|
-
const scope = scopeMatch ? scopeMatch[2].trim() : null;
|
|
86
|
-
const nameRe = new RegExp(escapeRegex(target), 'i');
|
|
87
|
-
const clicked = scope
|
|
88
|
-
? await tryClickScoped(page, nameRe, target, scope)
|
|
89
|
-
: await tryClick(page, nameRe, target);
|
|
90
|
-
if (!clicked)
|
|
91
|
-
throw new Error(`Could not find clickable element: "${label}"`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
else if (lower.includes("fill") || lower.includes("type") || lower.includes("enter")) {
|
|
95
|
-
// Support both quoted and unquoted formats:
|
|
96
|
-
// Fill "standard_user" in "Username"
|
|
97
|
-
// Fill standard_user in Username field
|
|
98
|
-
// Split on first " in " / " into " to separate value from field
|
|
99
|
-
const match = raw.match(/(?:enter|fill|type)\s+"([^"]+)"\s+(?:in|into)\s+(?:the\s+)?"?([^"]+?)"?\s*(?:field|input|box|area)?\s*$/i) ||
|
|
100
|
-
raw.match(/(?:enter|fill|type)\s+(\S+)\s+(?:in|into)\s+(?:the\s+)?(.+?)\s*(?:field|input|box|area)?\s*$/i);
|
|
101
|
-
if (match) {
|
|
102
|
-
const value = match[1].trim();
|
|
103
|
-
const fieldLabel = match[2].trim();
|
|
104
|
-
const looksLikeCss = (s) => /[#.\[\]:>]/.test(s);
|
|
105
|
-
if (looksLikeCss(fieldLabel)) {
|
|
106
|
-
await page.waitForSelector(fieldLabel, { state: "visible", timeout: 5000 });
|
|
107
|
-
await page.fill(fieldLabel, value);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
await tryFill(page, fieldLabel, value);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
else if (lower.includes("wait") && lower.includes("second")) {
|
|
115
|
-
const ms = raw.match(/wait\s+(\d+)\s*sec/i);
|
|
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
|
|
126
|
-
const isNegative = /not\s+(?:displayed|visible|present)/i.test(raw);
|
|
127
|
-
const textMatch = raw.match(/"([^"]+)"/) ||
|
|
128
|
-
raw.match(/(?:verify|check|assert)\s+(?:i\s+see\s+(?:text\s+)?|text\s+)?(.+?)(?:\s+is\s+(?:not\s+)?(?:displayed|visible|present))?$/i);
|
|
129
|
-
if (textMatch) {
|
|
130
|
-
const expected = textMatch[1].trim();
|
|
131
|
-
if (isNegative) {
|
|
132
|
-
const content = await page.textContent('body') || '';
|
|
133
|
-
if (content.includes(expected))
|
|
134
|
-
throw new Error(`Text "${expected}" should NOT be visible but was found`);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
try {
|
|
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
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
else if (lower.includes("select") || lower.includes("choose")) {
|
|
147
|
-
const selMatch = raw.match(/select\s+"?([^"]+?)"?\s+(?:from|in)\s+"?([^"]+?)"?\s*(?:dropdown|select|field)?$/i);
|
|
148
|
-
if (selMatch) {
|
|
149
|
-
try {
|
|
150
|
-
await page.selectOption(selMatch[2].trim(), { label: selMatch[1].trim() });
|
|
151
|
-
}
|
|
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);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
77
|
else {
|
|
163
|
-
//
|
|
78
|
+
// ALL other steps: AI reads the screen and performs the action
|
|
79
|
+
console.log(` 🤖 AI executing: "${raw}"`);
|
|
164
80
|
await aiStepFallback(page, raw);
|
|
165
81
|
}
|
|
166
82
|
}
|
|
167
83
|
catch (err) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
await aiStepFallback(page, raw);
|
|
172
|
-
stepError = undefined; // AI recovered it
|
|
173
|
-
}
|
|
174
|
-
catch (aiErr) {
|
|
175
|
-
stepError = err.message; // Report original error
|
|
176
|
-
result.success = false;
|
|
177
|
-
}
|
|
84
|
+
stepError = err.message;
|
|
85
|
+
result.success = false;
|
|
178
86
|
}
|
|
179
87
|
// Screenshot after each step
|
|
180
88
|
let screenshotB64 = "";
|