skopix 2.0.2 → 2.0.3
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
CHANGED
|
@@ -218,6 +218,8 @@ export async function agentCommand(options) {
|
|
|
218
218
|
|
|
219
219
|
if (env) Object.assign(process.env, env);
|
|
220
220
|
|
|
221
|
+
const startedAt = Date.now();
|
|
222
|
+
|
|
221
223
|
try {
|
|
222
224
|
const { chromium } = await import('playwright');
|
|
223
225
|
const sessionDir = path.join(os.homedir(), '.skopix', runId);
|
|
@@ -271,24 +273,23 @@ export async function agentCommand(options) {
|
|
|
271
273
|
await fs.writeJson(path.join(sessionDir, 'report.json'), {
|
|
272
274
|
sessionId: runId, goalAchieved: passed, url: test.url || '',
|
|
273
275
|
goal: test.name + ' (recorded replay)', steps: allSteps.slice(0, stepNum),
|
|
274
|
-
duration:
|
|
276
|
+
duration: Date.now() - startedAt, type: 'replay', provider: 'replay',
|
|
275
277
|
}, { spaces: 2 }).catch(() => {});
|
|
276
278
|
|
|
277
|
-
// Write report.html — two-column layout
|
|
279
|
+
// Write report.html — two-column layout with inline screenshots
|
|
278
280
|
try {
|
|
279
|
-
const duration =
|
|
281
|
+
const duration = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
280
282
|
const stepRows = allSteps.slice(0, stepNum).map((s, i) => {
|
|
281
283
|
const desc = (s.description || (s.action + ' ' + (s.stableSelector || s.selector || ''))).replace(/</g, '<').replace(/>/g, '>');
|
|
282
284
|
const screenshotFile = 'step-' + String(i + 1).padStart(3, '0') + '.png';
|
|
283
285
|
const isAssert = s.action === 'assert';
|
|
284
286
|
const isFailed = !passed && i === stepNum - 1;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return '<div class="step ' + (isFailed ? 'failed-step' : '') + '" onclick="showStep(' + i + ')"><div class="step-num">' + String(i + 1).padStart(2, '0') + '</div><div class="step-body"><div class="step-action">' + s.action.toUpperCase() + '</div><div class="step-desc">' + desc + '</div></div></div>';
|
|
287
|
+
const actionLabel = isAssert ? ('✓ ' + (s.assertType || 'assert')) : s.action.toUpperCase();
|
|
288
|
+
const actionClass = isAssert ? 'assert-action' : (isFailed ? 'failed-action' : '');
|
|
289
|
+
return '<div class="step ' + (isFailed ? 'failed-step' : '') + '" onclick="showStep(' + i + ')"><div class="step-header"><div class="step-num">' + String(i + 1).padStart(2, '0') + '</div><div class="step-body"><div class="step-action ' + actionClass + '">' + actionLabel + '</div><div class="step-desc">' + desc + '</div></div></div><img class="step-shot" src="' + screenshotFile + '" loading="lazy" onerror="this.style.display=\'none\'"></div>';
|
|
289
290
|
}).join('\n');
|
|
290
291
|
|
|
291
|
-
const html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Skopix Report — ' + test.name + '</title><style>body{margin:0;font-family:-apple-system,sans-serif;background:#0d0d1a;color:#e8eaf0}.container{display:grid;grid-template-columns:1fr 1fr;height:100vh}.left{padding:24px;background:#080810;border-right:1px solid rgba(255,255,255,0.08);display:flex;flex-direction:column}.right{padding:
|
|
292
|
+
const html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Skopix Report — ' + test.name + '</title><style>body{margin:0;font-family:-apple-system,sans-serif;background:#0d0d1a;color:#e8eaf0}.container{display:grid;grid-template-columns:1fr 1fr;height:100vh}.left{padding:24px;background:#080810;border-right:1px solid rgba(255,255,255,0.08);display:flex;flex-direction:column;position:sticky;top:0}.right{padding:0;overflow-y:auto}h1{margin:0 0 8px 0;font-size:24px}.meta{color:#5a6180;font-size:13px;margin-bottom:24px}.status{display:inline-block;padding:4px 12px;border-radius:4px;font-size:12px;font-weight:600;margin-left:8px}.status.passed{background:rgba(34,197,94,0.15);color:#22c55e}.status.failed{background:rgba(239,68,68,0.15);color:#ef4444}#media video,#media img{width:100%;border-radius:8px;background:#000}.step{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,0.06);cursor:pointer;transition:background 0.15s}.step:hover{background:rgba(0,212,255,0.06)}.step.active{background:rgba(0,212,255,0.1)}.step-header{display:flex;gap:14px;margin-bottom:12px}.step-num{font-family:monospace;color:#5a6180;font-size:12px;min-width:24px}.step-action{font-family:monospace;font-size:11px;color:#00d4ff;letter-spacing:0.05em;margin-bottom:4px}.assert-action{color:#22c55e}.failed-action{color:#ef4444}.step-desc{font-size:13px}.failed-step .step-action{color:#ef4444}.step-shot{width:100%;border-radius:6px;border:1px solid rgba(255,255,255,0.08);max-height:300px;object-fit:contain;background:#000}</style></head><body><div class="container"><div class="left"><h1>' + test.name + '<span class="status ' + (passed ? 'passed' : 'failed') + '">' + (passed ? 'PASSED' : 'FAILED') + '</span></h1><div class="meta">' + allSteps.length + ' steps · ' + duration + 's · agent: ' + os.hostname() + '</div><div id="media"><video src="replay.webm" controls autoplay muted loop></video></div></div><div class="right">' + stepRows + '</div></div><script>function showStep(i){document.querySelectorAll(".step").forEach(s=>s.classList.remove("active"));document.querySelectorAll(".step")[i].classList.add("active");const img=document.createElement("img");img.src="step-"+String(i+1).padStart(3,"0")+".png";document.getElementById("media").innerHTML="";document.getElementById("media").appendChild(img);}</script></body></html>';
|
|
292
293
|
|
|
293
294
|
await fs.writeFile(path.join(sessionDir, 'report.html'), html);
|
|
294
295
|
} catch (e) { /* ignore report html errors */ }
|
|
@@ -2117,14 +2117,13 @@ function testIdFromName(name) {
|
|
|
2117
2117
|
|
|
2118
2118
|
// ─── SESSIONS ─────────────────────────────────────────────────────────────────
|
|
2119
2119
|
async function listSessions(reportsDir) {
|
|
2120
|
-
if (!await fs.pathExists(reportsDir))
|
|
2120
|
+
if (!await fs.pathExists(reportsDir)) return [];
|
|
2121
2121
|
const sessions = [];
|
|
2122
2122
|
// Check both reportsDir directly and reportsDir/sessions subfolder
|
|
2123
2123
|
const dirsToCheck = [reportsDir, path.join(reportsDir, 'sessions')];
|
|
2124
2124
|
for (const dir of dirsToCheck) {
|
|
2125
2125
|
if (!await fs.pathExists(dir)) continue;
|
|
2126
2126
|
const entries = await fs.readdir(dir);
|
|
2127
|
-
process.stderr.write('[sessions] scanning ' + dir + ' entries: ' + entries.join(',') + '\n');
|
|
2128
2127
|
for (const entry of entries) {
|
|
2129
2128
|
if (entry.startsWith('.')) continue;
|
|
2130
2129
|
const sessionPath = path.join(dir, entry);
|
package/package.json
CHANGED