@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.
@@ -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
- // Extract URL wherever it appears in the step (handles emoji/symbol prefixes)
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
- else if (lower.includes("click")) {
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
- let label = labelMatch?.[1]?.trim();
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 (!clicked)
91
- throw new Error(`Could not find clickable element: "${label}"`);
91
+ if (clicked)
92
+ handled = true;
92
93
  }
93
94
  }
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
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
- 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
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 but was found`);
123
+ throw new Error(`Text "${expected}" should NOT be visible`);
135
124
  }
136
125
  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
- }
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
- else if (lower.includes("select") || lower.includes("choose")) {
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
- else {
163
- // Unknown step — let AI interpret and execute it
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
- // If a recognised handler threw, try AI fallback before marking as failed
169
- console.log(` ⚠️ Step failed (${err.message}), trying AI fallback...`);
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; // Report original error
163
+ stepError = err.message;
176
164
  result.success = false;
177
165
  }
178
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Local agent that connects your machine to the Cutlery QA platform and runs UI tests via Playwright",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {