skopix 2.0.38 → 2.0.39

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.
@@ -1811,6 +1811,25 @@ export async function dashboardCommand(options) {
1811
1811
  sendJSON(res, 200, result);
1812
1812
  return;
1813
1813
  }
1814
+ if (pathname.match(/^\/api\/step-tester\/[^/]+\/run-sequence$/) && method === 'POST') {
1815
+ const testerId = pathname.split('/')[3];
1816
+ const { steps } = JSON.parse(await readBody(req));
1817
+ const session = stepTesterSessions.get(testerId);
1818
+ if (!session) { sendJSON(res, 404, { error: 'Session not found' }); return; }
1819
+ // Run steps one at a time, streaming results via SSE
1820
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' });
1821
+ for (let i = 0; i < steps.length; i++) {
1822
+ const step = steps[i];
1823
+ res.write(`data: ${JSON.stringify({ type: 'step-start', index: i })}\n\n`);
1824
+ const result = await executeStepTesterAction(session.page, step);
1825
+ res.write(`data: ${JSON.stringify({ type: 'step-result', index: i, ...result })}\n\n`);
1826
+ if (!result.passed) break; // stop on first failure
1827
+ await new Promise(r => setTimeout(r, 300)); // small gap between steps
1828
+ }
1829
+ res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
1830
+ res.end();
1831
+ return;
1832
+ }
1814
1833
  if (pathname.match(/^\/api\/step-tester\/[^/]+\/stop$/) && method === 'POST') {
1815
1834
  const testerId = pathname.split('/')[3];
1816
1835
  await stopStepTester(testerId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.38",
3
+ "version": "2.0.39",
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": {
@@ -1681,6 +1681,10 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1681
1681
  </div>
1682
1682
  <div style="display:flex;gap:8px;align-items:center">
1683
1683
  <input class="form-input" id="builder-test-name" type="text" placeholder="Test name..." style="width:220px">
1684
+ <button class="btn btn-ghost" id="btn-preview-test" onclick="startBuilderPreview()" style="color:#f59e0b;border-color:rgba(245,158,11,0.3)">
1685
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
1686
+ Preview
1687
+ </button>
1684
1688
  <button class="btn btn-primary" onclick="saveBuiltTest()">Save test</button>
1685
1689
  </div>
1686
1690
  </div>
@@ -1704,7 +1708,7 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1704
1708
  </div>
1705
1709
 
1706
1710
  <!-- RIGHT: Test steps being built -->
1707
- <div style="display:flex;flex-direction:column;overflow:hidden">
1711
+ <div style="display:flex;flex-direction:column;overflow:hidden;position:relative">
1708
1712
  <div style="padding:14px 20px;border-bottom:1px solid var(--border);flex-shrink:0;display:flex;align-items:center;justify-content:space-between">
1709
1713
  <div>
1710
1714
  <div style="font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:0.1em">TEST STEPS</div>
@@ -1720,6 +1724,36 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1720
1724
  ← Click steps from the library to add them here
1721
1725
  </div>
1722
1726
  </div>
1727
+
1728
+ <!-- PREVIEW OVERLAY -->
1729
+ <div id="builder-preview-overlay" style="display:none;position:absolute;inset:0;background:var(--bg);flex-direction:column;z-index:10">
1730
+ <div style="padding:14px 20px;border-bottom:1px solid var(--border);flex-shrink:0;display:flex;align-items:center;justify-content:space-between;background:rgba(245,158,11,0.06)">
1731
+ <div style="display:flex;align-items:center;gap:10px">
1732
+ <span style="color:#f59e0b;font-size:14px" id="preview-status-dot">⏸</span>
1733
+ <div>
1734
+ <div style="font-family:var(--mono);font-size:10px;color:#f59e0b;letter-spacing:0.1em">PREVIEW MODE</div>
1735
+ <div style="font-family:var(--mono);font-size:11px;color:var(--muted);margin-top:2px" id="preview-status-text">Browser open — ready to run</div>
1736
+ </div>
1737
+ </div>
1738
+ <div style="display:flex;gap:8px">
1739
+ <button class="btn btn-ghost" id="btn-preview-run-all" onclick="previewRunAll()" style="font-size:11px;padding:4px 12px;color:#f59e0b;border-color:rgba(245,158,11,0.3)">
1740
+ ▶▶ Run all
1741
+ </button>
1742
+ <button class="btn btn-ghost" id="btn-preview-run-next" onclick="previewRunNext()" style="font-size:11px;padding:4px 12px;color:#f59e0b;border-color:rgba(245,158,11,0.3)">
1743
+ ▶ Run next
1744
+ </button>
1745
+ <button class="btn btn-ghost" onclick="stopBuilderPreview()" style="font-size:11px;padding:4px 10px">
1746
+ ✕ Stop preview
1747
+ </button>
1748
+ </div>
1749
+ </div>
1750
+ <div id="preview-steps-list" style="overflow-y:auto;flex:1;padding:8px 0"></div>
1751
+ <div style="padding:12px 20px;border-top:1px solid var(--border);flex-shrink:0;display:flex;align-items:center;gap:10px" id="preview-summary" style="display:none">
1752
+ <span id="preview-passed" style="font-family:var(--mono);font-size:12px;color:#34d399"></span>
1753
+ <span id="preview-failed" style="font-family:var(--mono);font-size:12px;color:#ef4444"></span>
1754
+ </div>
1755
+ </div>
1756
+
1723
1757
  </div>
1724
1758
 
1725
1759
  </div>
@@ -6083,9 +6117,14 @@ function openTestBuilder() {
6083
6117
  }
6084
6118
 
6085
6119
  function closeTestBuilder() {
6120
+ // Stop preview if active
6121
+ if (previewState.testerId) {
6122
+ fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/stop', { method: 'POST' }).catch(() => {});
6123
+ previewState.testerId = null;
6124
+ }
6125
+ document.getElementById('builder-preview-overlay').style.display = 'none';
6086
6126
  const builder = document.getElementById('view-test-builder');
6087
6127
  if (builder) builder.style.display = 'none';
6088
- // Restore all views to their default display state before switching
6089
6128
  document.querySelectorAll('.view').forEach(v => {
6090
6129
  if (v.id !== 'view-test-builder') v.style.display = '';
6091
6130
  });
@@ -6382,6 +6421,204 @@ function harvestAnother() {
6382
6421
  document.getElementById('harvester-status').textContent = 'Navigate to the next element, then click Capture.';
6383
6422
  }
6384
6423
 
6424
+ // ── TEST BUILDER PREVIEW ─────────────────────────────────────────────────────
6425
+ let previewState = { testerId: null, currentStep: 0, results: [], running: false };
6426
+
6427
+ function renderPreviewSteps() {
6428
+ const container = document.getElementById('preview-steps-list');
6429
+ if (!container) return;
6430
+ const actionColors = { click:'#22d3ee', type:'#a78bfa', check:'#34d399', assert:'#fb923c', select:'#f472b6', scroll:'#6b7280' };
6431
+
6432
+ container.innerHTML = builderSteps.map((s, i) => {
6433
+ const result = previewState.results[i];
6434
+ const isCurrent = i === previewState.currentStep && !result;
6435
+ const status = result ? (result.passed ? '✓' : '✗') : (isCurrent ? '▶' : '○');
6436
+ const statusColor = result ? (result.passed ? '#34d399' : '#ef4444') : (isCurrent ? '#f59e0b' : 'var(--muted2)');
6437
+ const bg = isCurrent ? 'rgba(245,158,11,0.06)' : result && !result.passed ? 'rgba(239,68,68,0.04)' : '';
6438
+ return `<div style="display:flex;align-items:flex-start;gap:12px;padding:10px 20px;border-bottom:1px solid var(--border);background:${bg}">
6439
+ <span style="font-family:var(--mono);font-size:13px;color:${statusColor};min-width:20px;margin-top:1px">${status}</span>
6440
+ <div style="flex:1;min-width:0">
6441
+ <div style="display:flex;align-items:center;gap:8px">
6442
+ <span style="font-family:var(--mono);font-size:10px;color:${actionColors[s.action]||'var(--muted)'}">${s.action}</span>
6443
+ <span style="font-size:12px;color:var(--text)">${escapeHtml(s.description||'')}</span>
6444
+ </div>
6445
+ <code style="font-size:10px;color:var(--muted2)">${escapeHtml(s.stableSelector||s.selector||'')}</code>
6446
+ ${result && !result.passed ? `<div style="font-family:var(--mono);font-size:11px;color:#ef4444;margin-top:4px">✗ ${escapeHtml(result.error||'Failed')}</div>` : ''}
6447
+ ${result && result.screenshot ? `<img src="data:image/jpeg;base64,${result.screenshot}" style="width:100%;max-width:300px;border-radius:6px;margin-top:8px;border:1px solid var(--border)" loading="lazy">` : ''}
6448
+ </div>
6449
+ </div>`;
6450
+ }).join('');
6451
+
6452
+ // Scroll current step into view
6453
+ const items = container.querySelectorAll('div[style*="border-bottom"]');
6454
+ if (items[previewState.currentStep]) items[previewState.currentStep].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
6455
+ }
6456
+
6457
+ function updatePreviewSummary() {
6458
+ const passed = previewState.results.filter(r => r && r.passed).length;
6459
+ const failed = previewState.results.filter(r => r && !r.passed).length;
6460
+ const total = previewState.results.filter(r => r).length;
6461
+ if (total === 0) return;
6462
+ document.getElementById('preview-passed').textContent = `✓ ${passed} passed`;
6463
+ document.getElementById('preview-failed').textContent = failed > 0 ? `✗ ${failed} failed` : '';
6464
+ document.getElementById('preview-summary').style.display = 'flex';
6465
+ }
6466
+
6467
+ async function startBuilderPreview() {
6468
+ const url = document.getElementById('builder-url')?.value?.trim();
6469
+ if (builderSteps.length === 0) { showToast('Add steps first'); return; }
6470
+ if (!url) { showToast('Set a start URL first'); document.getElementById('builder-url').focus(); return; }
6471
+
6472
+ const btn = document.getElementById('btn-preview-test');
6473
+ btn.disabled = true;
6474
+ btn.textContent = 'Opening...';
6475
+
6476
+ try {
6477
+ const res = await fetch(API_BASE + '/api/step-tester/start', {
6478
+ method: 'POST',
6479
+ headers: { 'Content-Type': 'application/json' },
6480
+ body: JSON.stringify({ url, selector: builderSteps[0]?.stableSelector || '' }),
6481
+ });
6482
+ const data = await res.json();
6483
+ if (!res.ok) throw new Error(data.error || 'Failed');
6484
+
6485
+ previewState = { testerId: data.testerId, currentStep: 0, results: [], running: false };
6486
+
6487
+ // Show overlay
6488
+ document.getElementById('builder-preview-overlay').style.display = 'flex';
6489
+ document.getElementById('preview-status-text').textContent = 'Browser open — navigate to start, then click Run next or Run all';
6490
+ document.getElementById('preview-summary').style.display = 'none';
6491
+ renderPreviewSteps();
6492
+ btn.textContent = '⏸ Previewing';
6493
+ } catch (err) {
6494
+ showToast('Error: ' + err.message);
6495
+ btn.disabled = false;
6496
+ btn.textContent = 'Preview';
6497
+ }
6498
+ }
6499
+
6500
+ async function stopBuilderPreview() {
6501
+ if (previewState.testerId) {
6502
+ await fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/stop', { method: 'POST' }).catch(() => {});
6503
+ previewState.testerId = null;
6504
+ }
6505
+ document.getElementById('builder-preview-overlay').style.display = 'none';
6506
+ const btn = document.getElementById('btn-preview-test');
6507
+ btn.disabled = false;
6508
+ btn.textContent = 'Preview';
6509
+ }
6510
+
6511
+ async function previewRunNext() {
6512
+ if (!previewState.testerId || previewState.running) return;
6513
+ if (previewState.currentStep >= builderSteps.length) return;
6514
+
6515
+ previewState.running = true;
6516
+ const step = builderSteps[previewState.currentStep];
6517
+ document.getElementById('preview-status-text').textContent = `Running step ${previewState.currentStep + 1}/${builderSteps.length}...`;
6518
+ document.getElementById('btn-preview-run-next').disabled = true;
6519
+ document.getElementById('btn-preview-run-all').disabled = true;
6520
+ renderPreviewSteps();
6521
+
6522
+ try {
6523
+ const res = await fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/run', {
6524
+ method: 'POST',
6525
+ headers: { 'Content-Type': 'application/json' },
6526
+ body: JSON.stringify({ selector: step.stableSelector || step.selector, action: step.action, value: step.value || '', assertType: step.assertType }),
6527
+ });
6528
+ const result = await res.json();
6529
+ previewState.results[previewState.currentStep] = result;
6530
+
6531
+ if (result.passed) {
6532
+ previewState.currentStep++;
6533
+ document.getElementById('preview-status-text').textContent =
6534
+ previewState.currentStep >= builderSteps.length
6535
+ ? `✓ All ${builderSteps.length} steps passed!`
6536
+ : `✓ Step ${previewState.currentStep} passed — ready for next`;
6537
+ document.getElementById('preview-status-dot').textContent = previewState.currentStep >= builderSteps.length ? '✓' : '⏸';
6538
+ document.getElementById('preview-status-dot').style.color = previewState.currentStep >= builderSteps.length ? '#34d399' : '#f59e0b';
6539
+ } else {
6540
+ document.getElementById('preview-status-text').textContent = `✗ Step ${previewState.currentStep + 1} failed — fix the step and try again`;
6541
+ document.getElementById('preview-status-dot').textContent = '✗';
6542
+ document.getElementById('preview-status-dot').style.color = '#ef4444';
6543
+ }
6544
+ } catch (err) {
6545
+ previewState.results[previewState.currentStep] = { passed: false, error: err.message };
6546
+ }
6547
+
6548
+ previewState.running = false;
6549
+ document.getElementById('btn-preview-run-next').disabled = previewState.currentStep >= builderSteps.length;
6550
+ document.getElementById('btn-preview-run-all').disabled = false;
6551
+ renderPreviewSteps();
6552
+ updatePreviewSummary();
6553
+ }
6554
+
6555
+ async function previewRunAll() {
6556
+ if (!previewState.testerId || previewState.running) return;
6557
+ previewState.running = true;
6558
+ previewState.results = [];
6559
+ previewState.currentStep = 0;
6560
+ document.getElementById('btn-preview-run-all').disabled = true;
6561
+ document.getElementById('btn-preview-run-next').disabled = true;
6562
+ document.getElementById('preview-summary').style.display = 'none';
6563
+
6564
+ const steps = builderSteps.map(s => ({
6565
+ selector: s.stableSelector || s.selector,
6566
+ action: s.action,
6567
+ value: s.value || '',
6568
+ assertType: s.assertType,
6569
+ }));
6570
+
6571
+ try {
6572
+ const es = new EventSource(API_BASE + '/api/step-tester/' + previewState.testerId + '/run-sequence?' + new URLSearchParams({ steps: JSON.stringify(steps) }));
6573
+
6574
+ // Use POST with EventSource workaround — send steps via a separate POST then listen
6575
+ const res = await fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/run-sequence', {
6576
+ method: 'POST',
6577
+ headers: { 'Content-Type': 'application/json' },
6578
+ body: JSON.stringify({ steps }),
6579
+ });
6580
+
6581
+ const reader = res.body.getReader();
6582
+ const decoder = new TextDecoder();
6583
+ let buffer = '';
6584
+
6585
+ while (true) {
6586
+ const { done, value } = await reader.read();
6587
+ if (done) break;
6588
+ buffer += decoder.decode(value, { stream: true });
6589
+ const lines = buffer.split('\n');
6590
+ buffer = lines.pop();
6591
+ for (const line of lines) {
6592
+ if (!line.startsWith('data: ')) continue;
6593
+ try {
6594
+ const msg = JSON.parse(line.slice(6));
6595
+ if (msg.type === 'step-start') {
6596
+ previewState.currentStep = msg.index;
6597
+ document.getElementById('preview-status-text').textContent = `Running step ${msg.index + 1}/${builderSteps.length}...`;
6598
+ renderPreviewSteps();
6599
+ } else if (msg.type === 'step-result') {
6600
+ previewState.results[msg.index] = { passed: msg.passed, error: msg.error, screenshot: msg.screenshot };
6601
+ previewState.currentStep = msg.passed ? msg.index + 1 : msg.index;
6602
+ renderPreviewSteps();
6603
+ updatePreviewSummary();
6604
+ } else if (msg.type === 'done') {
6605
+ const allPassed = previewState.results.every(r => r && r.passed);
6606
+ document.getElementById('preview-status-text').textContent = allPassed ? `✓ All ${builderSteps.length} steps passed!` : `✗ Preview complete — some steps failed`;
6607
+ document.getElementById('preview-status-dot').textContent = allPassed ? '✓' : '✗';
6608
+ document.getElementById('preview-status-dot').style.color = allPassed ? '#34d399' : '#ef4444';
6609
+ }
6610
+ } catch {}
6611
+ }
6612
+ }
6613
+ } catch (err) {
6614
+ document.getElementById('preview-status-text').textContent = 'Error: ' + err.message;
6615
+ }
6616
+
6617
+ previewState.running = false;
6618
+ document.getElementById('btn-preview-run-all').disabled = false;
6619
+ document.getElementById('btn-preview-run-next').disabled = previewState.currentStep >= builderSteps.length;
6620
+ }
6621
+
6385
6622
  // ── SUITE TEST PICKER ────────────────────────────────────────────────────────
6386
6623
  function openSuiteTestPicker() {
6387
6624
  const list = document.getElementById('stp-test-list');