@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.
@@ -60,29 +60,109 @@ class TestExecutor {
60
60
  });
61
61
  let stepError;
62
62
  try {
63
- // Navigate is handled directly — URL extraction doesn't need vision
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
- else {
78
- // ALL other steps: AI reads the screen and performs the action
79
- console.log(` 🤖 AI executing: "${raw}"`);
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
- stepError = err.message;
85
- result.success = false;
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 = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.20",
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": {