@cutleryapp/agent 1.0.20 → 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 +88 -8
- package/package.json +1 -1
package/dist/mcp-executor.js
CHANGED
|
@@ -60,29 +60,109 @@ class TestExecutor {
|
|
|
60
60
|
});
|
|
61
61
|
let stepError;
|
|
62
62
|
try {
|
|
63
|
-
|
|
63
|
+
let handled = false;
|
|
64
|
+
// 1. Navigate — direct URL goto, no selector needed
|
|
64
65
|
if (lower.includes("navigate to") || lower.includes("go to")) {
|
|
65
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
75
|
await page.goto(this.options.baseUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
76
|
+
handled = true;
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// 2. Click — smart selector strategies via MCP/Playwright
|
|
80
|
+
if (!handled && lower.includes("click")) {
|
|
81
|
+
const labelMatch = raw.match(/click\s+(?:on\s+)?(?:the\s+)?"?([^"]+?)"?(?:\s+(?:button|link|tab))?$/i);
|
|
82
|
+
const label = labelMatch?.[1]?.trim();
|
|
83
|
+
if (label) {
|
|
84
|
+
const scopeMatch = label.match(/^(.+?)\s+(?:under|inside|within|in the|in)\s+(.+)$/i);
|
|
85
|
+
const target = scopeMatch ? scopeMatch[1].trim() : label;
|
|
86
|
+
const scope = scopeMatch ? scopeMatch[2].trim() : null;
|
|
87
|
+
const nameRe = new RegExp(escapeRegex(target), 'i');
|
|
88
|
+
const clicked = scope
|
|
89
|
+
? await tryClickScoped(page, nameRe, target, scope)
|
|
90
|
+
: await tryClick(page, nameRe, target);
|
|
91
|
+
if (clicked)
|
|
92
|
+
handled = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 3. Fill — smart selector strategies via MCP/Playwright
|
|
96
|
+
if (!handled && (lower.includes("fill") || lower.includes("type") || lower.includes("enter"))) {
|
|
97
|
+
const match = raw.match(/(?:enter|fill|type)\s+"([^"]+)"\s+(?:in|into)\s+(?:the\s+)?"?([^"]+?)"?\s*(?:field|input|box|area)?\s*$/i) ||
|
|
98
|
+
raw.match(/(?:enter|fill|type)\s+(\S+)\s+(?:in|into)\s+(?:the\s+)?(.+?)\s*(?:field|input|box|area)?\s*$/i);
|
|
99
|
+
if (match) {
|
|
100
|
+
const value = match[1].trim();
|
|
101
|
+
const fieldLabel = match[2].trim();
|
|
102
|
+
const looksLikeCss = (s) => /[#.\[\]:>]/.test(s);
|
|
103
|
+
if (looksLikeCss(fieldLabel)) {
|
|
104
|
+
await page.waitForSelector(fieldLabel, { state: "visible", timeout: 5000 });
|
|
105
|
+
await page.fill(fieldLabel, value);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
await tryFill(page, fieldLabel, value);
|
|
109
|
+
}
|
|
110
|
+
handled = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// 4. Verify — check page text
|
|
114
|
+
if (!handled && (lower.includes("verify") || lower.includes("assert") || lower.includes("check") || lower.includes("should"))) {
|
|
115
|
+
const isNegative = /not\s+(?:displayed|visible|present)/i.test(raw);
|
|
116
|
+
const textMatch = raw.match(/"([^"]+)"/) ||
|
|
117
|
+
raw.match(/(?:verify|check|assert)\s+(?:i\s+see\s+(?:text\s+)?|text\s+)?(.+?)(?:\s+is\s+(?:not\s+)?(?:displayed|visible|present))?$/i);
|
|
118
|
+
if (textMatch) {
|
|
119
|
+
const expected = textMatch[1].trim();
|
|
120
|
+
if (isNegative) {
|
|
121
|
+
const content = await page.textContent('body') || '';
|
|
122
|
+
if (content.includes(expected))
|
|
123
|
+
throw new Error(`Text "${expected}" should NOT be visible`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
await page.waitForFunction((t) => document.body.innerText.includes(t), expected, { timeout: 10000 });
|
|
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;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// 6. Select — dropdown
|
|
140
|
+
if (!handled && (lower.includes("select") || lower.includes("choose"))) {
|
|
141
|
+
const selMatch = raw.match(/select\s+"?([^"]+?)"?\s+(?:from|in)\s+"?([^"]+?)"?\s*(?:dropdown|select|field)?$/i);
|
|
142
|
+
if (selMatch) {
|
|
143
|
+
try {
|
|
144
|
+
await page.selectOption(selMatch[2].trim(), { label: selMatch[1].trim() });
|
|
145
|
+
handled = true;
|
|
146
|
+
}
|
|
147
|
+
catch { /* fall to AI */ }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// 7. AI fallback — for anything not handled or ambiguous
|
|
151
|
+
if (!handled) {
|
|
152
|
+
console.log(` 🤖 MCP could not handle step, using AI: "${raw}"`);
|
|
80
153
|
await aiStepFallback(page, raw);
|
|
81
154
|
}
|
|
82
155
|
}
|
|
83
156
|
catch (err) {
|
|
84
|
-
|
|
85
|
-
|
|
157
|
+
// MCP execution failed — let AI try to recover
|
|
158
|
+
console.log(` ⚠️ MCP step failed (${err.message}), trying AI...`);
|
|
159
|
+
try {
|
|
160
|
+
await aiStepFallback(page, raw);
|
|
161
|
+
}
|
|
162
|
+
catch (aiErr) {
|
|
163
|
+
stepError = err.message;
|
|
164
|
+
result.success = false;
|
|
165
|
+
}
|
|
86
166
|
}
|
|
87
167
|
// Screenshot after each step
|
|
88
168
|
let screenshotB64 = "";
|