skopix 2.0.1 → 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.
@@ -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,9 +273,27 @@ 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: 0, type: 'replay', provider: 'replay',
276
+ duration: Date.now() - startedAt, type: 'replay', provider: 'replay',
275
277
  }, { spaces: 2 }).catch(() => {});
276
278
 
279
+ // Write report.html — two-column layout with inline screenshots
280
+ try {
281
+ const duration = ((Date.now() - startedAt) / 1000).toFixed(1);
282
+ const stepRows = allSteps.slice(0, stepNum).map((s, i) => {
283
+ const desc = (s.description || (s.action + ' ' + (s.stableSelector || s.selector || ''))).replace(/</g, '&lt;').replace(/>/g, '&gt;');
284
+ const screenshotFile = 'step-' + String(i + 1).padStart(3, '0') + '.png';
285
+ const isAssert = s.action === 'assert';
286
+ const isFailed = !passed && i === stepNum - 1;
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>';
290
+ }).join('\n');
291
+
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>';
293
+
294
+ await fs.writeFile(path.join(sessionDir, 'report.html'), html);
295
+ } catch (e) { /* ignore report html errors */ }
296
+
277
297
  send({ type: 'stdout', text: '' });
278
298
  send({ type: 'stdout', text: '━'.repeat(60) });
279
299
  send({ type: 'stdout', text: ' Status: ' + (passed ? 'PASSED ✓' : 'FAILED ✗') });
@@ -2117,14 +2117,13 @@ function testIdFromName(name) {
2117
2117
 
2118
2118
  // ─── SESSIONS ─────────────────────────────────────────────────────────────────
2119
2119
  async function listSessions(reportsDir) {
2120
- if (!await fs.pathExists(reportsDir)) { process.stderr.write('[sessions] reportsDir not found: ' + reportsDir + '\n'); return []; }
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/cli/index.js CHANGED
@@ -32,7 +32,7 @@ program
32
32
  .requiredOption('-u, --url <url>', 'Target URL to test')
33
33
  .requiredOption('-g, --goal <goal>', 'Testing goal (e.g. "complete the checkout flow")')
34
34
  .option('-c, --credentials <file>', 'Path to credentials YAML file')
35
- .option('-o, --output <dir>', 'Output directory for reports', './skopix-reports')
35
+ .option('-o, --output <dir>', 'Output directory for reports')
36
36
  .option('-m, --max-steps <number>', 'Maximum steps the agent will take', '20')
37
37
  .option('--headless', 'Run browser in headless mode', false)
38
38
  .option('--no-video', 'Disable video recording')
@@ -53,7 +53,7 @@ program
53
53
  program
54
54
  .command('report')
55
55
  .description('Open the latest report in your browser')
56
- .option('-d, --dir <dir>', 'Reports directory', './skopix-reports')
56
+ .option('-d, --dir <dir>', 'Reports directory')
57
57
  .action(reportCommand);
58
58
 
59
59
  program
@@ -68,7 +68,7 @@ program
68
68
  .command('dashboard')
69
69
  .description('Launch the web dashboard')
70
70
  .option('-p, --port <port>', 'Port to run the server on', '9000')
71
- .option('-d, --dir <dir>', 'Reports directory', './skopix-reports')
71
+ .option('-d, --dir <dir>', 'Reports directory')
72
72
  .option('-h, --host <host>', 'Host to bind to (default 127.0.0.1; use 0.0.0.0 for team mode)')
73
73
  .option('--team', 'Enable multi-user team mode (requires SQLite)')
74
74
  .option('--no-open', 'Do not auto-open the browser')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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": {