skopix 2.0.19 → 2.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.
@@ -361,9 +361,10 @@ export async function agentCommand(options) {
361
361
  await page.waitForTimeout(800);
362
362
 
363
363
  } else if (step.action === 'click') {
364
- await page.waitForTimeout(100);
364
+ await page.waitForTimeout(300);
365
365
  let clicked = false;
366
366
  const selectors = [step.stableSelector, step.selector].filter(Boolean).map(sanitiseSelector);
367
+ // Strategy 1: coordinate-based (best for duplicate elements)
367
368
  if (!clicked && (step.elementX || step.clickX)) {
368
369
  const tx = step.elementX || step.clickX, ty = step.elementY || step.clickY;
369
370
  for (const s of selectors) {
@@ -372,56 +373,28 @@ export async function agentCommand(options) {
372
373
  const count = await page.locator(s).count();
373
374
  if (count > 1) {
374
375
  let bi = 0, bd = Infinity;
375
- for (let i = 0; i < count; i++) { try { const box = await page.locator(s).nth(i).boundingBox({ timeout: 2000 }); if (!box) continue; const d = Math.sqrt(Math.pow(box.x + box.width / 2 - tx, 2) + Math.pow(box.y + box.height / 2 - ty, 2)); if (d < bd) { bd = d; bi = i; } } catch {} }
376
+ for (let i = 0; i < count; i++) { try { const box = await page.locator(s).nth(i).boundingBox({ timeout: 2000 }); if (!box) continue; const d = Math.sqrt(Math.pow(box.x+box.width/2-tx,2)+Math.pow(box.y+box.height/2-ty,2)); if (d < bd) { bd=d; bi=i; } } catch {} }
376
377
  await page.locator(s).nth(bi).click({ timeout: 5000 }); clicked = true;
377
378
  } else if (count === 1) {
379
+ await page.locator(s).first().waitFor({ state: 'visible', timeout: 3000 });
378
380
  await page.locator(s).first().click({ timeout: 5000 }); clicked = true;
379
381
  }
380
382
  } catch {}
381
383
  }
382
384
  }
383
- if (!clicked) { for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().click({ timeout: 5000 }); clicked = true; } catch {} } }
384
- if (!clicked) { for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().scrollIntoViewIfNeeded({ timeout: 2000 }).catch(() => {}); await page.locator(s).first().click({ timeout: 5000 }); clicked = true; } catch {} } }
385
+ // Strategy 2: normal click
386
+ if (!clicked) { for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().waitFor({ state: 'visible', timeout: 3000 }); await page.locator(s).first().click({ timeout: 5000 }); clicked = true; } catch {} } }
387
+ // Strategy 3: force click
385
388
  if (!clicked) { for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().click({ force: true, timeout: 5000 }); clicked = true; } catch {} } }
386
- if (!clicked) {
387
- // Last resort JS dispatch for framework-managed elements
388
- for (const s of selectors) {
389
- if (clicked) break;
390
- try {
391
- await page.locator(s).first().evaluate(el => {
392
- el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
393
- el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
394
- el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
395
- });
396
- clicked = true;
397
- } catch {}
389
+ // Strategy 4: click clickable ancestor (for icons/SVGs)
390
+ if (!clicked && step.element) {
391
+ const tag = (step.element.tag || '').toLowerCase();
392
+ if (['i', 'svg', 'path', 'span', 'img'].includes(tag)) {
393
+ for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().locator('xpath=ancestor-or-self::*[self::a or self::button or @role="button"][1]').first().click({ timeout: 5000 }); clicked = true; } catch {} }
398
394
  }
399
395
  }
400
396
  if (!clicked) throw new Error('Could not click: ' + selectors.join(', '));
401
-
402
- // Special handling for Bootstrap dropdowns — wait for menu to appear
403
- try {
404
- const el = page.locator(selectors[0]).first();
405
- const isDropdown = await el.evaluate(e => e.hasAttribute('data-toggle') && e.getAttribute('data-toggle') === 'dropdown').catch(() => false);
406
- if (isDropdown) {
407
- // Try jQuery dropdown toggle first (Bootstrap 3)
408
- const toggled = await page.evaluate((sel) => {
409
- try {
410
- const el = document.querySelector(sel);
411
- if (el && window.$) { window.$(el).dropdown('toggle'); return true; }
412
- } catch {}
413
- return false;
414
- }, selectors[0]).catch(() => false);
415
- if (!toggled) {
416
- // Fallback: dispatch click on the element
417
- await el.evaluate(e => e.click());
418
- }
419
- await page.waitForSelector('.dropdown-menu', { state: 'visible', timeout: 3000 }).catch(() => {});
420
- await page.waitForTimeout(400);
421
- }
422
- } catch {}
423
-
424
- await page.waitForTimeout(400);
397
+ await page.waitForTimeout(500);
425
398
 
426
399
  } else if (step.action === 'type') {
427
400
  await page.locator(sel).first().click({ timeout: 5000 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.19",
3
+ "version": "2.0.21",
4
4
  "description": "Browser-based QA tool — record tests by using your app, replay them deterministically, generate Playwright code automatically",
5
5
  "main": "cli/index.js",
6
6
  "bin": {