skopix 2.0.40 → 2.0.42

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.
@@ -1796,10 +1796,10 @@ export async function dashboardCommand(options) {
1796
1796
 
1797
1797
  // ─── STEP TESTER ───────────────────────────────────────────────────
1798
1798
  if (pathname === '/api/step-tester/start' && method === 'POST') {
1799
- const { url, selector } = JSON.parse(await readBody(req));
1799
+ const { url, selector, mode, steps } = JSON.parse(await readBody(req));
1800
1800
  try {
1801
1801
  const testerId = Math.random().toString(36).slice(2, 10);
1802
- await startStepTester(testerId, url, selector);
1802
+ await startStepTester(testerId, url, selector, mode || 'test', steps);
1803
1803
  sendJSON(res, 200, { testerId });
1804
1804
  } catch (err) { sendJSON(res, 500, { error: err.message }); }
1805
1805
  return;
@@ -3945,91 +3945,208 @@ async function syncTestsToLibrary(suitesDir) {
3945
3945
 
3946
3946
  const stepTesterSessions = new Map(); // testerId -> { browser, ctx, page }
3947
3947
 
3948
- async function startStepTester(testerId, url, selector) {
3948
+ async function startStepTester(testerId, url, selector, mode, steps) {
3949
3949
  const { chromium } = await import('playwright');
3950
3950
  const browser = await chromium.launch({ headless: false, args: ['--no-sandbox'] });
3951
3951
  const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
3952
3952
  const page = await ctx.newPage();
3953
3953
 
3954
- // Inject the tester toolbar on every page load
3955
- await ctx.addInitScript((sel) => {
3956
- window.__skopixTesterSelector = sel;
3957
- document.addEventListener('DOMContentLoaded', () => {
3958
- if (document.getElementById('__skopix_tester')) return;
3959
- const tb = document.createElement('div');
3960
- tb.id = '__skopix_tester';
3961
- tb.style.cssText = [
3962
- 'position:fixed', 'bottom:20px', 'left:50%', 'transform:translateX(-50%)',
3963
- 'z-index:2147483647', 'background:#0f1117', 'border:1px solid #f59e0b',
3964
- 'border-radius:12px', 'padding:12px 16px', 'display:flex', 'align-items:center',
3965
- 'gap:10px', 'font-family:monospace', 'font-size:12px', 'color:#e5e7eb',
3966
- 'box-shadow:0 4px 24px rgba(0,0,0,0.7)', 'min-width:400px',
3967
- ].join(';');
3968
- tb.innerHTML = `
3969
- <span style="color:#f59e0b;font-size:14px">⚡</span>
3970
- <span style="color:#9ca3af;font-size:11px">STEP TESTER</span>
3971
- <input id="__skopix_sel" value="${(sel||'').replace(/"/g,'&quot;')}" placeholder="selector..."
3972
- style="flex:1;background:#1a1d2e;border:1px solid #374151;border-radius:6px;padding:4px 8px;
3973
- color:#e5e7eb;font-family:monospace;font-size:11px;min-width:160px">
3974
- <select id="__skopix_action" style="background:#1a1d2e;border:1px solid #374151;border-radius:6px;
3975
- padding:4px 8px;color:#e5e7eb;font-family:monospace;font-size:11px">
3976
- <option value="click">click</option>
3977
- <option value="type">type</option>
3978
- <option value="check">check</option>
3979
- <option value="assert">assert (visible)</option>
3980
- <option value="assert_text">assert (text)</option>
3981
- </select>
3982
- <input id="__skopix_value" placeholder="value..." style="width:100px;background:#1a1d2e;
3983
- border:1px solid #374151;border-radius:6px;padding:4px 8px;color:#e5e7eb;
3984
- font-family:monospace;font-size:11px;display:none">
3985
- <button id="__skopix_run" style="background:#f59e0b;border:none;color:#000;
3986
- border-radius:6px;padding:5px 14px;cursor:pointer;font-size:11px;font-weight:700;
3987
- font-family:monospace">▶ Run</button>
3988
- <span id="__skopix_result" style="font-size:13px;min-width:20px"></span>
3989
- `;
3990
- document.body.appendChild(tb);
3991
-
3992
- // Show/hide value input based on action
3993
- document.getElementById('__skopix_action').addEventListener('change', function() {
3994
- const needsValue = ['type','assert_text'].includes(this.value);
3995
- document.getElementById('__skopix_value').style.display = needsValue ? '' : 'none';
3954
+ if (mode === 'preview' && steps && steps.length > 0) {
3955
+ // PREVIEW MODE — inject steps list toolbar
3956
+ await ctx.addInitScript((stepsData) => {
3957
+ window.__skopixPreviewSteps = stepsData;
3958
+ window.__skopixPreviewResults = {};
3959
+ document.addEventListener('DOMContentLoaded', () => {
3960
+ if (document.getElementById('__skopix_preview')) return;
3961
+ const tb = document.createElement('div');
3962
+ tb.id = '__skopix_preview';
3963
+ tb.style.cssText = [
3964
+ 'position:fixed', 'top:0', 'right:0', 'bottom:0', 'width:320px',
3965
+ 'z-index:2147483647', 'background:#0d0d1a', 'border-left:1px solid rgba(245,158,11,0.4)',
3966
+ 'display:flex', 'flex-direction:column', 'font-family:monospace', 'font-size:12px',
3967
+ 'box-shadow:-4px 0 24px rgba(0,0,0,0.5)',
3968
+ ].join(';');
3969
+ tb.innerHTML = `
3970
+ <div style="padding:12px 16px;border-bottom:1px solid rgba(245,158,11,0.2);background:rgba(245,158,11,0.06);flex-shrink:0">
3971
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
3972
+ <div style="display:flex;align-items:center;gap:8px">
3973
+ <span style="color:#f59e0b">⚡</span>
3974
+ <span style="color:#f59e0b;font-size:10px;letter-spacing:0.1em">PREVIEW</span>
3975
+ </div>
3976
+ <div style="display:flex;gap:6px">
3977
+ <button id="__skopix_run_next" style="background:transparent;border:1px solid rgba(245,158,11,0.4);color:#f59e0b;
3978
+ border-radius:6px;padding:4px 10px;cursor:pointer;font-size:11px;font-family:monospace">▶ Next</button>
3979
+ <button id="__skopix_run_all" style="background:#f59e0b;border:none;color:#000;
3980
+ border-radius:6px;padding:4px 10px;cursor:pointer;font-size:11px;font-family:monospace;font-weight:700">▶▶ All</button>
3981
+ </div>
3982
+ </div>
3983
+ <div id="__skopix_preview_status" style="color:#9ca3af;font-size:11px">Navigate to start, then click Run</div>
3984
+ </div>
3985
+ <div id="__skopix_steps" style="overflow-y:auto;flex:1;padding:0"></div>
3986
+ <div id="__skopix_preview_summary" style="display:none;padding:12px 16px;border-top:1px solid rgba(255,255,255,0.08);font-size:12px"></div>
3987
+ `;
3988
+ document.body.appendChild(tb);
3989
+
3990
+ function renderSteps(currentIdx, results) {
3991
+ const actionColors = { click:'#22d3ee', type:'#a78bfa', check:'#34d399', assert:'#fb923c', select:'#f472b6', scroll:'#6b7280' };
3992
+ document.getElementById('__skopix_steps').innerHTML = stepsData.map((s, i) => {
3993
+ const r = results[i];
3994
+ const isCurrent = i === currentIdx && !r;
3995
+ const status = r ? (r.passed ? '' : '✗') : (isCurrent ? '' : '');
3996
+ const statusColor = r ? (r.passed ? '#34d399' : '#ef4444') : (isCurrent ? '#f59e0b' : '#374151');
3997
+ const bg = isCurrent ? 'rgba(245,158,11,0.08)' : r && !r.passed ? 'rgba(239,68,68,0.06)' : '';
3998
+ return `<div style="padding:8px 14px;border-bottom:1px solid rgba(255,255,255,0.05);display:flex;gap:10px;align-items:flex-start;background:${bg}">
3999
+ <span style="color:${statusColor};font-size:13px;margin-top:1px;flex-shrink:0">${status}</span>
4000
+ <div style="min-width:0">
4001
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:2px">
4002
+ <span style="color:${actionColors[s.action]||'#6b7280'};font-size:10px">${s.action}</span>
4003
+ <span style="color:#e5e7eb;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px">${s.description||''}</span>
4004
+ </div>
4005
+ <code style="color:#4b5563;font-size:10px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${s.selector||''}</code>
4006
+ ${r && !r.passed ? `<div style="color:#ef4444;font-size:10px;margin-top:3px">${(r.error||'Failed').slice(0,80)}</div>` : ''}
4007
+ </div>
4008
+ </div>`;
4009
+ }).join('');
4010
+ }
4011
+
4012
+ window.__skopixPreviewCurrentStep = 0;
4013
+ window.__skopixPreviewResults = {};
4014
+ renderSteps(0, {});
4015
+
4016
+ document.getElementById('__skopix_run_next').addEventListener('click', (e) => {
4017
+ e.stopPropagation();
4018
+ if (window.__skopixPreviewRun) window.__skopixPreviewRun({ index: window.__skopixPreviewCurrentStep });
4019
+ });
4020
+
4021
+ document.getElementById('__skopix_run_all').addEventListener('click', (e) => {
4022
+ e.stopPropagation();
4023
+ if (window.__skopixPreviewRunAll) window.__skopixPreviewRunAll({ fromIndex: window.__skopixPreviewCurrentStep });
4024
+ });
4025
+
4026
+ window.__skopixUpdatePreview = (index, result, status) => {
4027
+ window.__skopixPreviewResults[index] = result;
4028
+ if (result && result.passed) window.__skopixPreviewCurrentStep = index + 1;
4029
+ document.getElementById('__skopix_preview_status').textContent = status || '';
4030
+ renderSteps(window.__skopixPreviewCurrentStep, window.__skopixPreviewResults);
4031
+ // Highlight element
4032
+ if (result) {
4033
+ try {
4034
+ const target = document.querySelector(stepsData[index]?.selector || '');
4035
+ if (target) {
4036
+ const orig = target.style.outline;
4037
+ target.style.outline = result.passed ? '3px solid #34d399' : '3px solid #ef4444';
4038
+ setTimeout(() => { target.style.outline = orig; }, 1500);
4039
+ }
4040
+ } catch {}
4041
+ }
4042
+ // Update summary
4043
+ const total = Object.keys(window.__skopixPreviewResults).length;
4044
+ const passed = Object.values(window.__skopixPreviewResults).filter(r=>r&&r.passed).length;
4045
+ const failed = total - passed;
4046
+ if (total > 0) {
4047
+ const summary = document.getElementById('__skopix_preview_summary');
4048
+ summary.style.display = '';
4049
+ summary.innerHTML = `<span style="color:#34d399">✓ ${passed}</span><span style="color:#4b5563"> / </span><span style="color:${failed>0?'#ef4444':'#4b5563'}">✗ ${failed}</span><span style="color:#4b5563"> of ${stepsData.length}</span>`;
4050
+ }
4051
+ };
3996
4052
  });
4053
+ }, steps.map(s => ({ action: s.action, selector: s.stableSelector||s.selector||'', description: s.description||s.action, value: s.value||'' })));
4054
+
4055
+ // Expose run functions
4056
+ await ctx.exposeFunction('__skopixPreviewRun', async ({ index }) => {
4057
+ const s = steps[index];
4058
+ if (!s) return;
4059
+ await page.evaluate((i, status) => { if(window.__skopixUpdatePreview) window.__skopixUpdatePreview(i, null, status); }, index, `Running step ${index+1}/${steps.length}...`);
4060
+ const result = await executeStepTesterAction(page, { selector: s.stableSelector||s.selector, action: s.action, value: s.value||'', assertType: s.assertType });
4061
+ await page.evaluate((i, r, total) => {
4062
+ if(window.__skopixUpdatePreview) window.__skopixUpdatePreview(i, r, r.passed ? (i+1 >= total ? `✓ All ${total} steps passed!` : `✓ Step ${i+1} passed`) : `✗ Step ${i+1} failed`);
4063
+ }, index, result, steps.length);
4064
+ });
3997
4065
 
3998
- // Run button posts to parent via window.__skopixTesterRun
3999
- document.getElementById('__skopix_run').addEventListener('click', function(e) {
4000
- e.stopPropagation();
4001
- const sel = document.getElementById('__skopix_sel').value.trim();
4002
- const action = document.getElementById('__skopix_action').value;
4003
- const value = document.getElementById('__skopix_value').value.trim();
4004
- const resultEl = document.getElementById('__skopix_result');
4005
- resultEl.textContent = '⏳';
4006
- if (window.__skopixTesterRun) {
4007
- window.__skopixTesterRun({ sel, action, value });
4008
- }
4066
+ await ctx.exposeFunction('__skopixPreviewRunAll', async ({ fromIndex }) => {
4067
+ for (let i = fromIndex; i < steps.length; i++) {
4068
+ const s = steps[i];
4069
+ await page.evaluate((i, total) => { if(window.__skopixUpdatePreview) window.__skopixUpdatePreview(i, null, `Running step ${i+1}/${total}...`); }, i, steps.length);
4070
+ const result = await executeStepTesterAction(page, { selector: s.stableSelector||s.selector, action: s.action, value: s.value||'', assertType: s.assertType });
4071
+ await page.evaluate((i, r, total) => {
4072
+ if(window.__skopixUpdatePreview) window.__skopixUpdatePreview(i, r, r.passed ? (i+1 >= total ? `✓ All ${total} steps passed!` : `Running...`) : `✗ Step ${i+1} failed — fix and retry`);
4073
+ }, i, result, steps.length);
4074
+ if (!result.passed) break;
4075
+ await new Promise(r => setTimeout(r, 400));
4076
+ }
4077
+ });
4078
+
4079
+ } else {
4080
+ // STEP TESTER MODE — single step toolbar
4081
+ await ctx.addInitScript((sel) => {
4082
+ window.__skopixTesterSelector = sel;
4083
+ document.addEventListener('DOMContentLoaded', () => {
4084
+ if (document.getElementById('__skopix_tester')) return;
4085
+ const tb = document.createElement('div');
4086
+ tb.id = '__skopix_tester';
4087
+ tb.style.cssText = [
4088
+ 'position:fixed', 'bottom:20px', 'left:50%', 'transform:translateX(-50%)',
4089
+ 'z-index:2147483647', 'background:#0f1117', 'border:1px solid #f59e0b',
4090
+ 'border-radius:12px', 'padding:12px 16px', 'display:flex', 'align-items:center',
4091
+ 'gap:10px', 'font-family:monospace', 'font-size:12px', 'color:#e5e7eb',
4092
+ 'box-shadow:0 4px 24px rgba(0,0,0,0.7)', 'min-width:400px',
4093
+ ].join(';');
4094
+ tb.innerHTML = `
4095
+ <span style="color:#f59e0b;font-size:14px">⚡</span>
4096
+ <span style="color:#9ca3af;font-size:11px">STEP TESTER</span>
4097
+ <input id="__skopix_sel" value="${(sel||'').replace(/"/g,'&quot;')}" placeholder="selector..."
4098
+ style="flex:1;background:#1a1d2e;border:1px solid #374151;border-radius:6px;padding:4px 8px;
4099
+ color:#e5e7eb;font-family:monospace;font-size:11px;min-width:160px">
4100
+ <select id="__skopix_action" style="background:#1a1d2e;border:1px solid #374151;border-radius:6px;
4101
+ padding:4px 8px;color:#e5e7eb;font-family:monospace;font-size:11px">
4102
+ <option value="click">click</option>
4103
+ <option value="type">type</option>
4104
+ <option value="check">check</option>
4105
+ <option value="assert">assert (visible)</option>
4106
+ <option value="assert_text">assert (text)</option>
4107
+ </select>
4108
+ <input id="__skopix_value" placeholder="value..." style="width:100px;background:#1a1d2e;
4109
+ border:1px solid #374151;border-radius:6px;padding:4px 8px;color:#e5e7eb;
4110
+ font-family:monospace;font-size:11px;display:none">
4111
+ <button id="__skopix_run" style="background:#f59e0b;border:none;color:#000;
4112
+ border-radius:6px;padding:5px 14px;cursor:pointer;font-size:11px;font-weight:700;
4113
+ font-family:monospace">▶ Run</button>
4114
+ <span id="__skopix_result" style="font-size:13px;min-width:20px"></span>
4115
+ `;
4116
+ document.body.appendChild(tb);
4117
+ document.getElementById('__skopix_action').addEventListener('change', function() {
4118
+ const needsValue = ['type','assert_text'].includes(this.value);
4119
+ document.getElementById('__skopix_value').style.display = needsValue ? '' : 'none';
4120
+ });
4121
+ document.getElementById('__skopix_run').addEventListener('click', function(e) {
4122
+ e.stopPropagation();
4123
+ const sel = document.getElementById('__skopix_sel').value.trim();
4124
+ const action = document.getElementById('__skopix_action').value;
4125
+ const value = document.getElementById('__skopix_value').value.trim();
4126
+ document.getElementById('__skopix_result').textContent = '⏳';
4127
+ if (window.__skopixTesterRun) window.__skopixTesterRun({ sel, action, value });
4128
+ });
4009
4129
  });
4130
+ }, selector || '');
4131
+
4132
+ await ctx.exposeFunction('__skopixTesterRun', async ({ sel, action, value }) => {
4133
+ const result = await executeStepTesterAction(page, { selector: sel, action, value });
4134
+ await page.evaluate((r, sel) => {
4135
+ const el = document.getElementById('__skopix_result');
4136
+ if (el) el.textContent = r.passed ? '✓' : '✗';
4137
+ try {
4138
+ const target = document.querySelector(sel);
4139
+ if (target) {
4140
+ const orig = target.style.outline;
4141
+ target.style.outline = r.passed ? '3px solid #34d399' : '3px solid #ef4444';
4142
+ setTimeout(() => { target.style.outline = orig; }, 1500);
4143
+ }
4144
+ } catch {}
4145
+ }, result, sel);
4010
4146
  });
4011
- }, selector || '');
4012
-
4013
- // Expose run function back to page
4014
- await ctx.exposeFunction('__skopixTesterRun', async ({ sel, action, value }) => {
4015
- const result = await executeStepTesterAction(page, { selector: sel, action, value });
4016
- await page.evaluate((r) => {
4017
- const el = document.getElementById('__skopix_result');
4018
- if (el) el.textContent = r.passed ? '✓' : '✗';
4019
- // Highlight element briefly
4020
- try {
4021
- const target = document.querySelector(r.selector);
4022
- if (target) {
4023
- const orig = target.style.outline;
4024
- target.style.outline = r.passed ? '3px solid #34d399' : '3px solid #ef4444';
4025
- setTimeout(() => { target.style.outline = orig; }, 1500);
4026
- }
4027
- } catch {}
4028
- }, { ...result, selector: sel });
4029
- });
4147
+ }
4030
4148
 
4031
4149
  if (url) await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {});
4032
-
4033
4150
  stepTesterSessions.set(testerId, { browser, ctx, page });
4034
4151
  }
4035
4152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.40",
3
+ "version": "2.0.42",
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": {
@@ -1708,7 +1708,7 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1708
1708
  </div>
1709
1709
 
1710
1710
  <!-- RIGHT: Test steps being built -->
1711
- <div style="display:flex;flex-direction:column;overflow:hidden;position:relative">
1711
+ <div style="display:flex;flex-direction:column;overflow:hidden">
1712
1712
  <div style="padding:14px 20px;border-bottom:1px solid var(--border);flex-shrink:0;display:flex;align-items:center;justify-content:space-between">
1713
1713
  <div>
1714
1714
  <div style="font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:0.1em">TEST STEPS</div>
@@ -1725,35 +1725,6 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1725
1725
  </div>
1726
1726
  </div>
1727
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
-
1757
1728
  </div>
1758
1729
 
1759
1730
  </div>
@@ -6122,7 +6093,6 @@ function closeTestBuilder() {
6122
6093
  fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/stop', { method: 'POST' }).catch(() => {});
6123
6094
  previewState.testerId = null;
6124
6095
  }
6125
- document.getElementById('builder-preview-overlay').style.display = 'none';
6126
6096
  const builder = document.getElementById('view-test-builder');
6127
6097
  if (builder) builder.style.display = 'none';
6128
6098
  document.querySelectorAll('.view').forEach(v => {
@@ -6422,47 +6392,7 @@ function harvestAnother() {
6422
6392
  }
6423
6393
 
6424
6394
  // ── 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
- }
6395
+ let previewState = { testerId: null };
6466
6396
 
6467
6397
  async function startBuilderPreview() {
6468
6398
  const url = document.getElementById('builder-url')?.value?.trim();
@@ -6477,19 +6407,25 @@ async function startBuilderPreview() {
6477
6407
  const res = await fetch(API_BASE + '/api/step-tester/start', {
6478
6408
  method: 'POST',
6479
6409
  headers: { 'Content-Type': 'application/json' },
6480
- body: JSON.stringify({ url, selector: builderSteps[0]?.stableSelector || '' }),
6410
+ body: JSON.stringify({
6411
+ url,
6412
+ mode: 'preview',
6413
+ steps: builderSteps.map(s => ({
6414
+ action: s.action,
6415
+ stableSelector: s.stableSelector || s.selector,
6416
+ selector: s.selector || s.stableSelector,
6417
+ value: s.value || '',
6418
+ assertType: s.assertType || null,
6419
+ description: s.description || s.action,
6420
+ })),
6421
+ }),
6481
6422
  });
6482
6423
  const data = await res.json();
6483
6424
  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();
6425
+ previewState.testerId = data.testerId;
6492
6426
  btn.textContent = '⏸ Previewing';
6427
+ btn.onclick = stopBuilderPreview;
6428
+ showToast('Browser open — use the toolbar in the browser to run steps');
6493
6429
  } catch (err) {
6494
6430
  showToast('Error: ' + err.message);
6495
6431
  btn.disabled = false;
@@ -6502,121 +6438,8 @@ async function stopBuilderPreview() {
6502
6438
  await fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/stop', { method: 'POST' }).catch(() => {});
6503
6439
  previewState.testerId = null;
6504
6440
  }
6505
- document.getElementById('builder-preview-overlay').style.display = 'none';
6506
6441
  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;
6442
+ if (btn) { btn.disabled = false; btn.textContent = 'Preview'; btn.onclick = startBuilderPreview; }
6620
6443
  }
6621
6444
 
6622
6445
  // ── SUITE TEST PICKER ────────────────────────────────────────────────────────