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.
- package/cli/commands/agent.js +16 -3
- package/cli/commands/dashboard.js +13 -4
- package/package.json +1 -1
- package/web/app/index.html +16 -3
package/cli/commands/agent.js
CHANGED
|
@@ -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
|
|
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) {
|
|
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) {
|
|
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:
|
|
3079
|
-
|
|
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
package/web/app/index.html
CHANGED
|
@@ -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; }
|