@cutleryapp/agent 1.0.13 → 1.0.15

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.
@@ -80,7 +80,7 @@ class TestExecutor {
80
80
  let label = labelMatch?.[1]?.trim();
81
81
  if (label) {
82
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)\s+(.+)$/i);
83
+ const scopeMatch = label.match(/^(.+?)\s+(?:under|inside|within|in the|in)\s+(.+)$/i);
84
84
  const target = scopeMatch ? scopeMatch[1].trim() : label;
85
85
  const scope = scopeMatch ? scopeMatch[2].trim() : null;
86
86
  const nameRe = new RegExp(escapeRegex(target), 'i');
@@ -221,21 +221,54 @@ async function tryClickScoped(page, nameRe, target, scope) {
221
221
  const FAST = 3000;
222
222
  // Strip trailing generic nouns that won't appear verbatim on the page
223
223
  const cleanScope = scope.replace(/\s+(?:product|item|section|card|row|container|element|button|link|area|panel|block)$/i, '').trim();
224
- const strategies = [
225
- () => page.locator(`:has-text("${cleanScope}")`).last().getByRole('button', { name: nameRe }).first().click({ timeout: FAST }),
226
- () => page.locator(`:has-text("${cleanScope}")`).last().getByRole('link', { name: nameRe }).first().click({ timeout: FAST }),
227
- () => page.locator(`:has-text("${cleanScope}")`).last().getByText(nameRe).first().click({ timeout: FAST }),
228
- // Fallback: ignore scope and click anywhere
229
- () => page.getByRole('button', { name: nameRe }).first().click({ timeout: FAST }),
230
- () => page.getByText(nameRe).first().click({ timeout: FAST }),
224
+ // Use card/item container selectors — these are tight enough to contain the button
225
+ // but not so deep that they exclude it. Filter by scope text, then click target within.
226
+ const containerSelectors = [
227
+ '[class*="item"]',
228
+ '[class*="card"]',
229
+ '[class*="product"]',
230
+ 'li',
231
+ 'article',
232
+ 'tr',
233
+ '[role="listitem"]',
234
+ '[role="row"]',
231
235
  ];
232
- for (const fn of strategies) {
236
+ for (const containerSel of containerSelectors) {
233
237
  try {
234
- await fn();
235
- return true;
238
+ const container = page.locator(containerSel).filter({ hasText: cleanScope });
239
+ const count = await container.count();
240
+ if (count === 0)
241
+ continue;
242
+ // Try button first, then any clickable element
243
+ try {
244
+ await container.first().getByRole('button', { name: nameRe }).first().click({ timeout: FAST });
245
+ return true;
246
+ }
247
+ catch { /* try next */ }
248
+ try {
249
+ await container.first().getByRole('link', { name: nameRe }).first().click({ timeout: FAST });
250
+ return true;
251
+ }
252
+ catch { /* try next */ }
253
+ try {
254
+ await container.first().getByText(nameRe).first().click({ timeout: FAST });
255
+ return true;
256
+ }
257
+ catch { /* try next */ }
236
258
  }
237
- catch { /* try next */ }
259
+ catch { /* try next container */ }
260
+ }
261
+ // Fallback: ignore scope
262
+ try {
263
+ await page.getByRole('button', { name: nameRe }).first().click({ timeout: FAST });
264
+ return true;
265
+ }
266
+ catch { }
267
+ try {
268
+ await page.getByText(nameRe).first().click({ timeout: FAST });
269
+ return true;
238
270
  }
271
+ catch { }
239
272
  return false;
240
273
  }
241
274
  async function tryFill(page, label, value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cutleryapp/agent",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
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": {