skopix 2.0.24 → 2.0.26

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/web/app/index.html +264 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.24",
3
+ "version": "2.0.26",
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": {
@@ -1516,6 +1516,10 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1516
1516
  <div class="topbar-sub">Every test across every suite — filter, search, and manage them in one place</div>
1517
1517
  </div>
1518
1518
  <div style="display:flex;gap:8px">
1519
+ <button class="btn btn-ghost" onclick="openTestBuilder()">
1520
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="4" rx="1"/><rect x="3" y="10" width="18" height="4" rx="1"/><path d="M3 17h8M16 17h5M19 14v6"/></svg>
1521
+ Build from library
1522
+ </button>
1519
1523
  <button class="btn btn-primary" onclick="openRecorder()" style="background:#dc2626;border-color:#dc2626">
1520
1524
  <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10"/></svg>
1521
1525
  Record test
@@ -1646,6 +1650,67 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1646
1650
  </div>
1647
1651
  </div>
1648
1652
 
1653
+ <!-- STEP LIBRARY modal is above -->
1654
+
1655
+ <!-- TEST BUILDER -->
1656
+ <div class="view" id="view-test-builder" style="display:none;flex-direction:column;height:100%">
1657
+ <div class="topbar" style="flex-shrink:0">
1658
+ <div style="display:flex;align-items:center;gap:12px">
1659
+ <button class="btn btn-ghost" onclick="closeTestBuilder()" style="padding:6px 10px">
1660
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
1661
+ Back
1662
+ </button>
1663
+ <div>
1664
+ <h1 style="margin:0">Test builder</h1>
1665
+ <div class="topbar-sub">Build a test from your step library — no recording needed</div>
1666
+ </div>
1667
+ </div>
1668
+ <div style="display:flex;gap:8px;align-items:center">
1669
+ <input class="form-input" id="builder-test-name" type="text" placeholder="Test name..." style="width:220px">
1670
+ <button class="btn btn-primary" onclick="saveBuiltTest()">Save test</button>
1671
+ </div>
1672
+ </div>
1673
+
1674
+ <div style="display:grid;grid-template-columns:380px 1fr;gap:0;flex:1;overflow:hidden;border-top:1px solid var(--border)">
1675
+
1676
+ <!-- LEFT: Step Library panel -->
1677
+ <div style="border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden">
1678
+ <div style="padding:14px 16px;border-bottom:1px solid var(--border);flex-shrink:0">
1679
+ <div style="font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:0.1em;margin-bottom:8px">STEP LIBRARY</div>
1680
+ <input class="form-input" id="builder-library-search" type="text" placeholder="Search steps..." oninput="filterBuilderLibrary()" style="width:100%">
1681
+ <div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap">
1682
+ <button class="btn btn-ghost builder-action-filter active" data-action="" onclick="setBuilderActionFilter(this,'')" style="padding:3px 8px;font-size:11px">All</button>
1683
+ <button class="btn btn-ghost builder-action-filter" data-action="click" onclick="setBuilderActionFilter(this,'click')" style="padding:3px 8px;font-size:11px;color:#22d3ee">click</button>
1684
+ <button class="btn btn-ghost builder-action-filter" data-action="type" onclick="setBuilderActionFilter(this,'type')" style="padding:3px 8px;font-size:11px;color:#a78bfa">type</button>
1685
+ <button class="btn btn-ghost builder-action-filter" data-action="check" onclick="setBuilderActionFilter(this,'check')" style="padding:3px 8px;font-size:11px;color:#34d399">check</button>
1686
+ <button class="btn btn-ghost builder-action-filter" data-action="assert" onclick="setBuilderActionFilter(this,'assert')" style="padding:3px 8px;font-size:11px;color:#fb923c">assert</button>
1687
+ </div>
1688
+ </div>
1689
+ <div id="builder-library-list" style="overflow-y:auto;flex:1;padding:8px 0"></div>
1690
+ </div>
1691
+
1692
+ <!-- RIGHT: Test steps being built -->
1693
+ <div style="display:flex;flex-direction:column;overflow:hidden">
1694
+ <div style="padding:14px 20px;border-bottom:1px solid var(--border);flex-shrink:0;display:flex;align-items:center;justify-content:space-between">
1695
+ <div>
1696
+ <div style="font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:0.1em">TEST STEPS</div>
1697
+ <div style="font-family:var(--mono);font-size:11px;color:var(--muted);margin-top:2px" id="builder-step-count">0 steps — click steps from the library to add them</div>
1698
+ </div>
1699
+ <div style="display:flex;gap:8px">
1700
+ <input class="form-input" id="builder-url" type="text" placeholder="Start URL (optional)" style="width:260px;font-size:12px">
1701
+ <button class="btn btn-ghost" style="font-size:11px;padding:4px 10px" onclick="addBuilderStep()">+ Custom step</button>
1702
+ </div>
1703
+ </div>
1704
+ <div id="builder-steps-list" style="overflow-y:auto;flex:1;padding:16px 20px">
1705
+ <div id="builder-empty" style="text-align:center;padding:60px 20px;color:var(--muted);font-family:var(--mono);font-size:13px">
1706
+ ← Click steps from the library to add them here
1707
+ </div>
1708
+ </div>
1709
+ </div>
1710
+
1711
+ </div>
1712
+ </div>
1713
+
1649
1714
  <!-- SUITES LIST -->
1650
1715
  <div class="view" id="view-suites">
1651
1716
  <div class="topbar">
@@ -2721,12 +2786,17 @@ function switchView(name) {
2721
2786
  console.warn('View not found:', name);
2722
2787
  return;
2723
2788
  }
2789
+ // Close test builder if open
2790
+ const builder = document.getElementById('view-test-builder');
2791
+ if (builder) builder.style.display = 'none';
2724
2792
  document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
2725
2793
  document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
2726
2794
  target.classList.add('active');
2727
2795
  document.querySelectorAll(`[data-view="${name}"]`).forEach(n => n.classList.add('active'));
2728
2796
  window.scrollTo({ top: 0, behavior: 'smooth' });
2797
+ currentView = name;
2729
2798
  }
2799
+ let currentView = 'dashboard';
2730
2800
 
2731
2801
  document.querySelectorAll('.nav-item').forEach(item => {
2732
2802
  item.addEventListener('click', (e) => {
@@ -5673,6 +5743,200 @@ async function syncTestsToLibrary() {
5673
5743
  finally { if (btn) { btn.disabled = false; btn.textContent = 'Sync tests to library'; } }
5674
5744
  }
5675
5745
 
5746
+ // ── TEST BUILDER ─────────────────────────────────────────────────────────────
5747
+ let builderSteps = [];
5748
+ let builderActionFilter = '';
5749
+ let previousView = 'all-tests';
5750
+
5751
+ function openTestBuilder() {
5752
+ previousView = currentView || 'all-tests';
5753
+ builderSteps = [];
5754
+ document.getElementById('builder-test-name').value = '';
5755
+ document.getElementById('builder-url').value = '';
5756
+ // Show builder view
5757
+ document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
5758
+ const bv = document.getElementById('view-test-builder');
5759
+ bv.style.display = 'flex';
5760
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
5761
+ renderBuilderSteps();
5762
+ // Load library if not cached
5763
+ if (libraryStepsCache.length === 0) {
5764
+ fetchLibrarySteps().then(() => renderBuilderLibrary());
5765
+ } else {
5766
+ renderBuilderLibrary();
5767
+ }
5768
+ }
5769
+
5770
+ function closeTestBuilder() {
5771
+ document.getElementById('view-test-builder').style.display = 'none';
5772
+ switchView(previousView);
5773
+ }
5774
+
5775
+ function setBuilderActionFilter(btn, action) {
5776
+ builderActionFilter = action;
5777
+ document.querySelectorAll('.builder-action-filter').forEach(b => b.classList.remove('active'));
5778
+ btn.classList.add('active');
5779
+ renderBuilderLibrary();
5780
+ }
5781
+
5782
+ function renderBuilderLibrary() {
5783
+ const container = document.getElementById('builder-library-list');
5784
+ if (!container) return;
5785
+ const q = (document.getElementById('builder-library-search')?.value || '').toLowerCase();
5786
+ const actionColors = { click:'#22d3ee', type:'#a78bfa', check:'#34d399', assert:'#fb923c', select:'#f472b6', scroll:'#6b7280' };
5787
+
5788
+ let steps = libraryStepsCache;
5789
+ if (builderActionFilter) steps = steps.filter(s => s.action === builderActionFilter);
5790
+ if (q) steps = steps.filter(s => (s.name||'').toLowerCase().includes(q) || (s.selector||'').toLowerCase().includes(q));
5791
+
5792
+ if (steps.length === 0) {
5793
+ container.innerHTML = `<div style="padding:24px 16px;text-align:center;color:var(--muted);font-family:var(--mono);font-size:12px">No steps match</div>`;
5794
+ return;
5795
+ }
5796
+
5797
+ container.innerHTML = steps.map(s => `
5798
+ <div onclick="addBuilderStepFromLibrary('${escapeAttr(s.id)}')"
5799
+ style="padding:10px 16px;cursor:pointer;border-bottom:1px solid var(--border);transition:background 0.1s"
5800
+ onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background=''">
5801
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:3px">
5802
+ <span style="font-family:var(--mono);font-size:10px;color:${actionColors[s.action]||'var(--muted)'};min-width:40px">${s.action||''}</span>
5803
+ <span style="font-size:12px;color:var(--text)">${escapeHtml(s.name||s.description||'')}</span>
5804
+ </div>
5805
+ <code style="font-size:10px;color:var(--muted2);margin-left:48px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(s.stableSelector||s.selector||'')}</code>
5806
+ </div>`).join('');
5807
+ }
5808
+
5809
+ function filterBuilderLibrary() {
5810
+ renderBuilderLibrary();
5811
+ }
5812
+
5813
+ function addBuilderStepFromLibrary(id) {
5814
+ const libStep = libraryStepsCache.find(s => s.id === id);
5815
+ if (!libStep) return;
5816
+ const step = {
5817
+ id: 'bs-' + Math.random().toString(36).slice(2, 8),
5818
+ action: libStep.action,
5819
+ stableSelector: libStep.stableSelector || libStep.selector,
5820
+ selector: libStep.selector || libStep.stableSelector,
5821
+ value: libStep.value || null,
5822
+ assertType: libStep.assertType || null,
5823
+ attribute: libStep.attribute || null,
5824
+ description: libStep.name || libStep.description,
5825
+ libraryId: libStep.id,
5826
+ timestamp: Date.now(),
5827
+ };
5828
+ builderSteps.push(step);
5829
+ renderBuilderSteps();
5830
+ }
5831
+
5832
+ function addBuilderStep() {
5833
+ const step = {
5834
+ id: 'bs-' + Math.random().toString(36).slice(2, 8),
5835
+ action: 'click',
5836
+ stableSelector: '',
5837
+ selector: '',
5838
+ value: null,
5839
+ description: 'Custom step',
5840
+ timestamp: Date.now(),
5841
+ };
5842
+ builderSteps.push(step);
5843
+ renderBuilderSteps();
5844
+ }
5845
+
5846
+ function removeBuilderStep(id) {
5847
+ builderSteps = builderSteps.filter(s => s.id !== id);
5848
+ renderBuilderSteps();
5849
+ }
5850
+
5851
+ function moveBuilderStep(id, dir) {
5852
+ const idx = builderSteps.findIndex(s => s.id === id);
5853
+ if (idx === -1) return;
5854
+ const newIdx = idx + dir;
5855
+ if (newIdx < 0 || newIdx >= builderSteps.length) return;
5856
+ const tmp = builderSteps[idx];
5857
+ builderSteps[idx] = builderSteps[newIdx];
5858
+ builderSteps[newIdx] = tmp;
5859
+ renderBuilderSteps();
5860
+ }
5861
+
5862
+ function updateBuilderStep(id, field, value) {
5863
+ const step = builderSteps.find(s => s.id === id);
5864
+ if (step) step[field] = value;
5865
+ }
5866
+
5867
+ function renderBuilderSteps() {
5868
+ const container = document.getElementById('builder-steps-list');
5869
+ const countEl = document.getElementById('builder-step-count');
5870
+ if (!container) return;
5871
+
5872
+ if (countEl) countEl.textContent = builderSteps.length === 0
5873
+ ? '0 steps — click steps from the library to add them'
5874
+ : `${builderSteps.length} step${builderSteps.length > 1 ? 's' : ''}`;
5875
+
5876
+ if (builderSteps.length === 0) {
5877
+ container.innerHTML = `<div id="builder-empty" style="text-align:center;padding:60px 20px;color:var(--muted);font-family:var(--mono);font-size:13px">← Click steps from the library to add them here</div>`;
5878
+ return;
5879
+ }
5880
+
5881
+ const actionColors = { click:'#22d3ee', type:'#a78bfa', check:'#34d399', assert:'#fb923c', select:'#f472b6', scroll:'#6b7280' };
5882
+
5883
+ container.innerHTML = builderSteps.map((s, i) => `
5884
+ <div style="display:flex;gap:12px;padding:10px 0;border-bottom:1px solid var(--border);align-items:flex-start">
5885
+ <div style="font-family:var(--mono);font-size:11px;color:var(--muted2);min-width:28px;padding-top:8px;text-align:right">${String(i+1).padStart(2,'0')}</div>
5886
+ <div style="flex:1;display:flex;flex-direction:column;gap:6px">
5887
+ <div style="display:flex;gap:8px;align-items:center">
5888
+ <select style="font-family:var(--mono);font-size:11px;padding:4px 6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:${actionColors[s.action]||'var(--text)'}" onchange="updateBuilderStep('${s.id}','action',this.value);renderBuilderSteps()">
5889
+ ${['click','type','check','assert','select','scroll','navigate'].map(a=>`<option value="${a}" ${s.action===a?'selected':''}>${a}</option>`).join('')}
5890
+ </select>
5891
+ <input type="text" value="${escapeAttr(s.description||'')}" placeholder="Description..." style="flex:1;font-size:12px;padding:4px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--text)" onchange="updateBuilderStep('${s.id}','description',this.value)">
5892
+ </div>
5893
+ <input type="text" value="${escapeAttr(s.stableSelector||s.selector||'')}" placeholder="CSS selector..." style="font-family:var(--mono);font-size:11px;padding:4px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted)" onchange="updateBuilderStep('${s.id}','stableSelector',this.value);updateBuilderStep('${s.id}','selector',this.value)">
5894
+ ${s.action === 'type' || s.action === 'assert' || s.action === 'select' ? `<input type="text" value="${escapeAttr(s.value||'')}" placeholder="${s.action==='type'?'Value to type...':s.action==='assert'?'Expected value...':'Option value...'}" style="font-size:12px;padding:4px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--text)" onchange="updateBuilderStep('${s.id}','value',this.value)">` : ''}
5895
+ </div>
5896
+ <div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
5897
+ <button class="btn-icon" style="width:22px;height:22px" onclick="moveBuilderStep('${s.id}',-1)" ${i===0?'disabled':''}>↑</button>
5898
+ <button class="btn-icon" style="width:22px;height:22px" onclick="moveBuilderStep('${s.id}',1)" ${i===builderSteps.length-1?'disabled':''}>↓</button>
5899
+ <button class="btn-icon" style="width:22px;height:22px;color:var(--red)" onclick="removeBuilderStep('${s.id}')">✕</button>
5900
+ </div>
5901
+ </div>`).join('');
5902
+ }
5903
+
5904
+ async function saveBuiltTest() {
5905
+ const name = document.getElementById('builder-test-name')?.value?.trim();
5906
+ const url = document.getElementById('builder-url')?.value?.trim() || '';
5907
+
5908
+ if (!name) { showToast('Enter a test name first'); document.getElementById('builder-test-name').focus(); return; }
5909
+ if (builderSteps.length === 0) { showToast('Add at least one step'); return; }
5910
+
5911
+ // Clean up steps for saving
5912
+ const steps = builderSteps.map((s, i) => ({
5913
+ id: 'step-' + String(i+1).padStart(3,'0'),
5914
+ action: s.action,
5915
+ stableSelector: s.stableSelector || s.selector || null,
5916
+ selector: s.selector || s.stableSelector || null,
5917
+ value: s.value || null,
5918
+ assertType: s.assertType || null,
5919
+ attribute: s.attribute || null,
5920
+ description: s.description || (s.action + ' step ' + (i+1)),
5921
+ libraryId: s.libraryId || null,
5922
+ timestamp: s.timestamp || Date.now(),
5923
+ }));
5924
+
5925
+ try {
5926
+ const res = await fetch(API_BASE + '/api/record/save', {
5927
+ method: 'POST',
5928
+ headers: { 'Content-Type': 'application/json' },
5929
+ body: JSON.stringify({ scope: 'saved', name, url, steps, tags: [] }),
5930
+ });
5931
+ if (!res.ok) throw new Error(await res.text());
5932
+ showToast(`Test "${name}" saved`);
5933
+ closeTestBuilder();
5934
+ switchView('all-tests');
5935
+ } catch (err) {
5936
+ showToast('Error saving: ' + err.message);
5937
+ }
5938
+ }
5939
+
5676
5940
  // ── SUITE TEST PICKER ────────────────────────────────────────────────────────
5677
5941
  function openSuiteTestPicker() {
5678
5942
  const list = document.getElementById('stp-test-list');