skopix 2.0.3 → 2.0.5

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.
@@ -383,7 +383,15 @@ export async function agentCommand(options) {
383
383
  } else if (step.action === 'assert') {
384
384
  const assertSel = sanitiseSelector(step.stableSelector || step.selector);
385
385
  switch (step.assertType) {
386
- case 'visible': await page.locator(assertSel).first().waitFor({ state: 'visible', timeout: 10000 }); break;
386
+ case 'visible': {
387
+ try {
388
+ await page.locator(assertSel).first().waitFor({ state: 'visible', timeout: 10000 });
389
+ } catch {
390
+ // Fall back to checking element exists in DOM (some elements are technically hidden but visually present via opacity/transform)
391
+ await page.locator(assertSel).first().waitFor({ state: 'attached', timeout: 5000 });
392
+ }
393
+ break;
394
+ }
387
395
  case 'text_contains': { const txt = await page.locator(assertSel).first().textContent({ timeout: 10000 }); if (!txt || !txt.includes(step.value || '')) throw new Error('Expected to contain "' + step.value + '"'); break; }
388
396
  case 'text_equals': { const txt = await page.locator(assertSel).first().textContent({ timeout: 10000 }); if ((txt || '').trim() !== (step.value || '').trim()) throw new Error('Expected "' + step.value + '"'); break; }
389
397
  case 'url_contains': if (!page.url().includes(step.value || '')) throw new Error('URL does not contain "' + step.value + '"'); break;
@@ -2898,7 +2898,11 @@ function startReplay(test, setupTest, activeRuns, reportsDir, currentUser, env)
2898
2898
  const assertSel = sanitiseSelector(step.stableSelector || step.selector);
2899
2899
  switch (step.assertType) {
2900
2900
  case 'visible':
2901
- await page.locator(assertSel).first().waitFor({ state: 'visible', timeout: 10000 });
2901
+ try {
2902
+ await page.locator(assertSel).first().waitFor({ state: 'visible', timeout: 10000 });
2903
+ } catch {
2904
+ await page.locator(assertSel).first().waitFor({ state: 'attached', timeout: 5000 });
2905
+ }
2902
2906
  broadcast({ type: 'stdout', text: ' \u2713 Visible: ' + assertSel });
2903
2907
  break;
2904
2908
  case 'text_contains': {
package/core/llm.js CHANGED
@@ -581,7 +581,7 @@ export async function processRecording({ steps, testName, url, provider, apiKey,
581
581
  + ' - For scroll actions: if selector is window use await page.evaluate(() => window.scrollTo(x, y)), otherwise use document.querySelector(sel).scrollTo(x, y)\n'
582
582
  + ' - Add await page.waitForLoadState("networkidle") after navigation actions\n'
583
583
  + ' - For assert steps: generate correct Playwright expect() calls:\n'
584
- + ' visible -> await expect(page.locator(selector)).toBeVisible();\n'
584
+ + ' visible -> await expect(page.locator(selector)).toBeAttached(); // use toBeAttached not toBeVisible — elements may be in DOM but hidden via CSS\n'
585
585
  + ' text_contains -> await expect(page.locator(selector)).toContainText("value");\n'
586
586
  + ' text_equals -> await expect(page.locator(selector)).toHaveText("value");\n'
587
587
  + ' url_contains -> await expect(page).toHaveURL(/value/);\n'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
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": {
@@ -5535,9 +5535,10 @@ function renderReSteps() {
5535
5535
  + '<button class="btn-icon" style="width:22px;height:22px;font-size:11px;color:#ef4444" title="Delete step" onclick="reStepDelete(' + i + ')">✕</button>'
5536
5536
  + '</div></div>'
5537
5537
  + (isAssert
5538
- ? '<div style="display:grid;grid-template-columns:1fr' + (s.assertType !== 'visible' ? ' 1fr' : '') + ';gap:6px">'
5538
+ ? '<div style="display:grid;grid-template-columns:1fr' + (s.assertType !== 'visible' ? ' 1fr' : '') + (s.assertType === 'attribute_contains' ? ' 1fr' : '') + ';gap:6px">'
5539
5539
  + '<div><div style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-bottom:3px">SELECTOR (assertType: ' + escapeHtml(s.assertType||'') + ')</div>'
5540
5540
  + '<input class="form-input" type="text" value="' + sel + '" placeholder="CSS selector..." style="padding:4px 8px;font-size:11px;height:auto;font-family:var(--mono)" onchange="reEditorState.steps[' + i + '].stableSelector=this.value;reEditorState.steps[' + i + '].selector=this.value"></div>'
5541
+ + (s.assertType === 'attribute_contains' ? '<div><div style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-bottom:3px">ATTRIBUTE</div><input class="form-input" type="text" value="' + escapeHtml(s.attribute||'title') + '" placeholder="e.g. fill, style, aria-label" style="padding:4px 8px;font-size:11px;height:auto;font-family:var(--mono)" onchange="reEditorState.steps[' + i + '].attribute=this.value"></div>' : '')
5541
5542
  + (s.assertType !== 'visible' ? '<div><div style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-bottom:3px">EXPECTED</div><input class="form-input" type="text" value="' + val + '" style="padding:4px 8px;font-size:11px;height:auto" onchange="reEditorState.steps[' + i + '].value=this.value"></div>' : '')
5542
5543
  + '</div>'
5543
5544
  : '<div style="display:grid;grid-template-columns:1fr' + (s.action === 'type' || s.action === 'select' ? ' 1fr' : '') + ';gap:6px">'