skopix 2.0.11 → 2.0.13

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.
@@ -247,8 +247,17 @@ export async function agentCommand(options) {
247
247
  send({ type: 'sessionId', sessionId: runId });
248
248
 
249
249
  const browser = await chromium.launch({ headless: test.headless || false, args: ['--no-sandbox'] });
250
- const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 }, recordVideo: { dir: sessionDir, size: { width: 1280, height: 800 } } });
250
+ const wantReport = test.generateReport !== false;
251
+ const browserZoom = test.browserZoom || 0.8;
252
+ const ctx = await browser.newContext({
253
+ viewport: { width: 1920, height: 1080 },
254
+ deviceScaleFactor: 1,
255
+ ...(wantReport ? { recordVideo: { dir: sessionDir, size: { width: 1920, height: 1080 } } } : {}),
256
+ });
251
257
  const page = await ctx.newPage();
258
+ if (browserZoom !== 1) {
259
+ await page.addInitScript((zoom) => { document.documentElement.style.zoom = zoom; }, browserZoom);
260
+ }
252
261
 
253
262
  const allSteps = [...(setupTest ? (setupTest.steps || []) : []), ...(test.steps || [])];
254
263
  let stepNum = 0, passed = true, failReason = '';
@@ -354,12 +363,16 @@ export async function agentCommand(options) {
354
363
  if (count > 1) {
355
364
  let bi = 0, bd = Infinity;
356
365
  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 {} }
366
+ await page.locator(s).nth(bi).scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
357
367
  await page.locator(s).nth(bi).click({ timeout: 5000 }); clicked = true;
358
- } else if (count === 1) { await page.locator(s).first().click({ timeout: 5000 }); clicked = true; }
368
+ } else if (count === 1) {
369
+ await page.locator(s).first().scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
370
+ await page.locator(s).first().click({ timeout: 5000 }); clicked = true;
371
+ }
359
372
  } catch {}
360
373
  }
361
374
  }
362
- if (!clicked) { for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().click({ timeout: 5000 }); clicked = true; } catch {} } }
375
+ if (!clicked) { for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {}); await page.locator(s).first().click({ timeout: 5000 }); clicked = true; } catch {} } }
363
376
  if (!clicked) { for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().click({ force: true, timeout: 5000 }); clicked = true; } catch {} } }
364
377
  if (!clicked) throw new Error('Could not click: ' + selectors.join(', '));
365
378
  await page.waitForTimeout(400);
@@ -2992,14 +2992,18 @@ function startReplay(test, setupTest, activeRuns, reportsDir, currentUser, env)
2992
2992
  if (count > 1) {
2993
2993
  let bestIdx = 0, bestDist = Infinity;
2994
2994
  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-targetX,2)+Math.pow(box.y+box.height/2-targetY,2)); if (d < bestDist) { bestDist = d; bestIdx = i; } } catch {} }
2995
+ await page.locator(s).nth(bestIdx).scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
2995
2996
  await page.locator(s).nth(bestIdx).click({ timeout: 5000 });
2996
2997
  if (count > 1) broadcast({ type: 'stdout', text: ' (matched element ' + (bestIdx+1) + ' of ' + count + ' by position)' });
2997
2998
  clicked = true;
2998
- } else if (count === 1) { await page.locator(s).first().waitFor({ state: 'visible', timeout: 3000 }); await page.locator(s).first().click({ timeout: 5000 }); clicked = true; }
2999
+ } else if (count === 1) {
3000
+ await page.locator(s).first().scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
3001
+ await page.locator(s).first().waitFor({ state: 'visible', timeout: 3000 }); await page.locator(s).first().click({ timeout: 5000 }); clicked = true;
3002
+ }
2999
3003
  } catch {}
3000
3004
  }
3001
3005
  }
3002
- 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 {} } }
3006
+ if (!clicked) { for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {}); await page.locator(s).first().waitFor({ state: 'visible', timeout: 3000 }); await page.locator(s).first().click({ timeout: 5000 }); clicked = true; } catch {} } }
3003
3007
  if (!clicked) { for (const s of selectors) { if (clicked) break; try { await page.locator(s).first().click({ force: true, timeout: 5000 }); clicked = true; } catch {} } }
3004
3008
  if (!clicked && step.element) { const tag = (step.element.tag||'').toLowerCase(); if (['i','svg','path','span','img'].includes(tag)) { 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 {} } } }
3005
3009
  if (!clicked && step.element && step.element.text && step.element.text.length > 1) { try { await page.locator('*:has-text("' + step.element.text.replace(/"/g, '\\"') + '")').first().click({ timeout: 5000 }); clicked = true; } catch {} }
@@ -3073,12 +3077,17 @@ function startReplay(test, setupTest, activeRuns, reportsDir, currentUser, env)
3073
3077
  });
3074
3078
 
3075
3079
  const wantReport = test.generateReport !== false;
3080
+ const browserZoom = test.browserZoom || 0.8;
3076
3081
 
3077
3082
  const ctx = await chromiumBrowser.newContext({
3078
- viewport: { width: 1280, height: 800 },
3079
- ...(wantReport ? { recordVideo: { dir: sessionDir, size: { width: 1280, height: 800 } } } : {}),
3083
+ viewport: { width: 1920, height: 1080 },
3084
+ deviceScaleFactor: 1,
3085
+ ...(wantReport ? { recordVideo: { dir: sessionDir, size: { width: 1920, height: 1080 } } } : {}),
3080
3086
  });
3081
3087
  const page = await ctx.newPage();
3088
+ if (browserZoom !== 1) {
3089
+ await page.addInitScript((zoom) => { document.documentElement.style.zoom = zoom; }, browserZoom);
3090
+ }
3082
3091
 
3083
3092
  // Navigate to start URL
3084
3093
  if (test.url) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.11",
3
+ "version": "2.0.13",
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": {
@@ -4,6 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Skopix Dashboard</title>
7
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%230d1117'/%3E%3Cpolygon points='16,6 26,11 16,16 6,11' fill='%2300d4ff'/%3E%3Cpolygon points='6,11 16,16 26,11 26,14 16,19 6,14' fill='%2300a8cc'/%3E%3Cpolygon points='6,14 16,19 26,14 26,17 16,22 6,17' fill='%23007a99'/%3E%3C/svg%3E">
7
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
10
  <link href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet">
@@ -2236,6 +2237,16 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
2236
2237
  <input type="checkbox" id="re-generate-report" checked style="width:14px;height:14px;accent-color:var(--cyan)">
2237
2238
  <span style="color:var(--muted)">Generate <span style="color:var(--cyan)">report</span> (video + screenshots)</span>
2238
2239
  </label>
2240
+ <div style="display:flex;align-items:center;gap:8px">
2241
+ <label style="font-family:var(--mono);font-size:12px;color:var(--muted);white-space:nowrap">Browser zoom:</label>
2242
+ <select class="form-select" id="re-browser-zoom" style="padding:5px 8px;font-size:12px;width:90px">
2243
+ <option value="1">100%</option>
2244
+ <option value="0.9">90%</option>
2245
+ <option value="0.8" selected>80%</option>
2246
+ <option value="0.75">75%</option>
2247
+ <option value="0.67">67%</option>
2248
+ </select>
2249
+ </div>
2239
2250
  <div style="display:flex;align-items:center;gap:8px;flex:1">
2240
2251
  <label style="font-family:var(--mono);font-size:12px;color:var(--muted);white-space:nowrap">Run setup:</label>
2241
2252
  <select class="form-select" id="re-setup" style="flex:1;padding:5px 8px;font-size:12px">
@@ -5456,7 +5467,7 @@ async function confirmAddToSuite() {
5456
5467
  }
5457
5468
 
5458
5469
  // ── RECORDED TEST EDITOR ────────────────────────────────────────────────────
5459
- let reEditorState = { scope: null, testId: null, steps: [], testName: '', url: '', reusable: false, setup: '', credentials: '', tags: [], generateReport: true };
5470
+ let reEditorState = { scope: null, testId: null, steps: [], testName: '', url: '', reusable: false, setup: '', credentials: '', tags: [], generateReport: true, browserZoom: 0.8 };
5460
5471
  let reAssertInsertAfter = -1;
5461
5472
 
5462
5473
  async function openRecordedTestEditor(scope, testId) {
@@ -5464,11 +5475,12 @@ async function openRecordedTestEditor(scope, testId) {
5464
5475
  const res = await fetch(API_BASE + '/api/test/' + encodeURIComponent(scope) + '/' + encodeURIComponent(testId));
5465
5476
  const test = await res.json();
5466
5477
  if (!res.ok) { showToast(test.error || 'Failed to load test'); return; }
5467
- reEditorState = { scope, testId, steps: JSON.parse(JSON.stringify(test.steps || [])), testName: test.name || '', url: test.url || '', reusable: !!test.reusable, setup: test.setup || '', credentials: test.credentials || '', tags: test.tags || [], generateReport: test.generateReport !== false };
5478
+ reEditorState = { scope, testId, steps: JSON.parse(JSON.stringify(test.steps || [])), testName: test.name || '', url: test.url || '', reusable: !!test.reusable, setup: test.setup || '', credentials: test.credentials || '', tags: test.tags || [], generateReport: test.generateReport !== false, browserZoom: test.browserZoom || 0.8 };
5468
5479
  document.getElementById('re-name').value = reEditorState.testName;
5469
5480
  document.getElementById('re-url').value = reEditorState.url;
5470
5481
  document.getElementById('re-reusable').checked = reEditorState.reusable;
5471
5482
  document.getElementById('re-generate-report').checked = reEditorState.generateReport;
5483
+ document.getElementById('re-browser-zoom').value = String(reEditorState.browserZoom);
5472
5484
  document.getElementById('re-tags').value = (reEditorState.tags || []).join(', ');
5473
5485
  // Populate credentials dropdown
5474
5486
  const credSel = document.getElementById('re-credentials');
@@ -5725,6 +5737,7 @@ async function saveRecordedEdits() {
5725
5737
  const url = document.getElementById('re-url').value.trim();
5726
5738
  const reusable = document.getElementById('re-reusable').checked;
5727
5739
  const generateReport = document.getElementById('re-generate-report')?.checked !== false;
5740
+ const browserZoom = parseFloat(document.getElementById('re-browser-zoom')?.value || '0.8');
5728
5741
  const setup = document.getElementById('re-setup').value || null;
5729
5742
  const credentials = document.getElementById('re-credentials')?.value || '';
5730
5743
  const tags = (document.getElementById('re-tags')?.value || '').split(',').map(t => t.trim()).filter(Boolean);
@@ -5732,7 +5745,7 @@ async function saveRecordedEdits() {
5732
5745
  try {
5733
5746
  const res = await fetch(API_BASE + '/api/test/' + encodeURIComponent(reEditorState.scope) + '/' + encodeURIComponent(reEditorState.testId), {
5734
5747
  method: 'PUT', headers: { 'Content-Type': 'application/json' },
5735
- body: JSON.stringify({ name, url, type: 'recorded', steps: reEditorState.steps, reusable, setup, credentials, tags, generateReport }),
5748
+ body: JSON.stringify({ name, url, type: 'recorded', steps: reEditorState.steps, reusable, setup, credentials, tags, generateReport, browserZoom }),
5736
5749
  });
5737
5750
  const data = await res.json();
5738
5751
  if (!res.ok) { showToast(data.error || 'Failed to save'); return; }