skopix 2.0.26 → 2.0.28

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 +199 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.26",
3
+ "version": "2.0.28",
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": {
@@ -1578,6 +1578,10 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1578
1578
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
1579
1579
  Sync tests to library
1580
1580
  </button>
1581
+ <button class="btn btn-ghost" onclick="openStepHarvester()" style="color:#f59e0b;border-color:rgba(245,158,11,0.3)">
1582
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
1583
+ Harvest step
1584
+ </button>
1581
1585
  <button class="btn btn-primary" onclick="openAddLibraryStep()">
1582
1586
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
1583
1587
  Add step
@@ -1711,7 +1715,68 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1711
1715
  </div>
1712
1716
  </div>
1713
1717
 
1714
- <!-- SUITES LIST -->
1718
+ <!-- STEP HARVESTER — START -->
1719
+ <div class="modal-overlay" id="harvester-start-modal" style="display:none" onclick="if(event.target===this)closeHarvester()">
1720
+ <div class="modal" style="max-width:480px;width:100%">
1721
+ <div class="modal-header">
1722
+ <div class="modal-title">Harvest a step</div>
1723
+ <button class="modal-close" onclick="closeHarvester()">✕</button>
1724
+ </div>
1725
+ <div class="modal-body" style="display:flex;flex-direction:column;gap:14px">
1726
+ <p style="font-family:var(--mono);font-size:12px;color:var(--muted);line-height:1.7;margin:0">
1727
+ Opens a browser at the URL you specify. Navigate to the element you want to capture, then click <strong style="color:#f59e0b">Capture</strong>. The next interaction you make gets saved directly to the step library.
1728
+ </p>
1729
+ <div class="form-field">
1730
+ <label class="form-label">URL to open</label>
1731
+ <input class="form-input" id="harvester-url" type="text" placeholder="http://localhost:8224/pi">
1732
+ </div>
1733
+ <div class="form-field">
1734
+ <label class="form-label">Step name (optional)</label>
1735
+ <input class="form-input" id="harvester-step-name" type="text" placeholder="e.g. Click chart settings icon">
1736
+ <div class="form-help">Leave blank to auto-generate from the interaction</div>
1737
+ </div>
1738
+ </div>
1739
+ <div class="modal-footer">
1740
+ <button class="btn btn-ghost" onclick="closeHarvester()">Cancel</button>
1741
+ <button class="btn btn-primary" style="background:#f59e0b;border-color:#f59e0b;color:#000" onclick="startHarvester()">Open browser</button>
1742
+ </div>
1743
+ </div>
1744
+ </div>
1745
+
1746
+ <!-- STEP HARVESTER — ACTIVE -->
1747
+ <div class="modal-overlay" id="harvester-active-modal" style="display:none" onclick="if(event.target===this)closeHarvester()">
1748
+ <div class="modal" style="max-width:480px;width:100%">
1749
+ <div class="modal-header">
1750
+ <div class="modal-title" style="color:#f59e0b">
1751
+ <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#f59e0b;margin-right:8px;animation:pulse 1s infinite"></span>
1752
+ Harvester active
1753
+ </div>
1754
+ <button class="modal-close" onclick="closeHarvester()">✕</button>
1755
+ </div>
1756
+ <div class="modal-body" style="display:flex;flex-direction:column;gap:16px">
1757
+ <p style="font-family:var(--mono);font-size:12px;color:var(--muted);line-height:1.7;margin:0">
1758
+ Browser is open. Navigate to the element you want to capture, then click <strong style="color:#f59e0b">Capture next interaction</strong>. The very next click, type, or check you make will be saved to the library.
1759
+ </p>
1760
+ <div id="harvester-status" style="font-family:var(--mono);font-size:12px;padding:12px 14px;background:var(--surface2);border-radius:8px;color:var(--muted)">
1761
+ Waiting — navigate to the right page first...
1762
+ </div>
1763
+ <div id="harvester-captured" style="display:none;flex-direction:column;gap:8px">
1764
+ <div style="font-family:var(--mono);font-size:10px;color:#34d399;letter-spacing:0.1em">✓ STEP CAPTURED</div>
1765
+ <div id="harvester-captured-desc" style="font-size:13px;color:var(--text)"></div>
1766
+ <code id="harvester-captured-sel" style="font-size:11px;color:var(--muted)"></code>
1767
+ </div>
1768
+ </div>
1769
+ <div class="modal-footer" style="justify-content:space-between">
1770
+ <button class="btn btn-ghost" onclick="closeHarvester()">Done</button>
1771
+ <div style="display:flex;gap:8px">
1772
+ <button class="btn btn-ghost" id="btn-harvest-another" style="display:none" onclick="harvestAnother()">Harvest another</button>
1773
+ <button class="btn btn-primary" id="btn-capture-next" style="background:#f59e0b;border-color:#f59e0b;color:#000" onclick="captureNextInteraction()">
1774
+ Capture next interaction
1775
+ </button>
1776
+ </div>
1777
+ </div>
1778
+ </div>
1779
+ </div>
1715
1780
  <div class="view" id="view-suites">
1716
1781
  <div class="topbar">
1717
1782
  <div>
@@ -5753,11 +5818,12 @@ function openTestBuilder() {
5753
5818
  builderSteps = [];
5754
5819
  document.getElementById('builder-test-name').value = '';
5755
5820
  document.getElementById('builder-url').value = '';
5756
- // Show builder view
5757
- document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
5821
+ // Deactivate all views and nav items
5822
+ document.querySelectorAll('.view').forEach(v => { v.classList.remove('active'); v.style.display = ''; });
5823
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
5824
+ // Show builder
5758
5825
  const bv = document.getElementById('view-test-builder');
5759
5826
  bv.style.display = 'flex';
5760
- document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
5761
5827
  renderBuilderSteps();
5762
5828
  // Load library if not cached
5763
5829
  if (libraryStepsCache.length === 0) {
@@ -5768,7 +5834,12 @@ function openTestBuilder() {
5768
5834
  }
5769
5835
 
5770
5836
  function closeTestBuilder() {
5771
- document.getElementById('view-test-builder').style.display = 'none';
5837
+ const builder = document.getElementById('view-test-builder');
5838
+ if (builder) builder.style.display = 'none';
5839
+ // Restore all views to their default display state before switching
5840
+ document.querySelectorAll('.view').forEach(v => {
5841
+ if (v.id !== 'view-test-builder') v.style.display = '';
5842
+ });
5772
5843
  switchView(previousView);
5773
5844
  }
5774
5845
 
@@ -5937,6 +6008,129 @@ async function saveBuiltTest() {
5937
6008
  }
5938
6009
  }
5939
6010
 
6011
+ // ── STEP HARVESTER ───────────────────────────────────────────────────────────
6012
+ let harvesterState = { recordingId: null, eventSource: null, capturing: false, stepName: '' };
6013
+
6014
+ function openStepHarvester() {
6015
+ document.getElementById('harvester-url').value = '';
6016
+ document.getElementById('harvester-step-name').value = '';
6017
+ document.getElementById('harvester-start-modal').style.display = 'flex';
6018
+ setTimeout(() => document.getElementById('harvester-url').focus(), 100);
6019
+ }
6020
+
6021
+ function closeHarvester() {
6022
+ document.getElementById('harvester-start-modal').style.display = 'none';
6023
+ document.getElementById('harvester-active-modal').style.display = 'none';
6024
+ // Stop recording if active
6025
+ if (harvesterState.recordingId) {
6026
+ fetch(API_BASE + '/api/record/' + harvesterState.recordingId + '/stop', { method: 'POST' }).catch(() => {});
6027
+ if (harvesterState.eventSource) { harvesterState.eventSource.close(); harvesterState.eventSource = null; }
6028
+ harvesterState.recordingId = null;
6029
+ }
6030
+ harvesterState.capturing = false;
6031
+ }
6032
+
6033
+ async function startHarvester() {
6034
+ const url = document.getElementById('harvester-url').value.trim();
6035
+ harvesterState.stepName = document.getElementById('harvester-step-name').value.trim();
6036
+ if (!url) { showToast('Enter a URL first'); return; }
6037
+
6038
+ document.getElementById('harvester-start-modal').style.display = 'none';
6039
+ document.getElementById('harvester-active-modal').style.display = 'flex';
6040
+ document.getElementById('harvester-status').textContent = 'Opening browser...';
6041
+ document.getElementById('harvester-captured').style.display = 'none';
6042
+ document.getElementById('btn-harvest-another').style.display = 'none';
6043
+ document.getElementById('btn-capture-next').style.display = '';
6044
+ harvesterState.capturing = false;
6045
+
6046
+ try {
6047
+ const res = await fetch(API_BASE + '/api/record/start', {
6048
+ method: 'POST',
6049
+ headers: { 'Content-Type': 'application/json' },
6050
+ body: JSON.stringify({ url }),
6051
+ });
6052
+ const data = await res.json();
6053
+ if (!res.ok) { showToast(data.error || 'Failed to open browser'); closeHarvester(); return; }
6054
+ harvesterState.recordingId = data.recordingId;
6055
+ document.getElementById('harvester-status').textContent = 'Browser open — navigate to the element you want, then click Capture.';
6056
+
6057
+ // Listen to the stream but only capture one step when in capture mode
6058
+ const es = new EventSource(API_BASE + '/api/record/' + data.recordingId + '/stream');
6059
+ harvesterState.eventSource = es;
6060
+ es.onmessage = (e) => {
6061
+ try {
6062
+ const msg = JSON.parse(e.data);
6063
+ if (msg.type === 'step' && harvesterState.capturing) {
6064
+ harvesterState.capturing = false;
6065
+ saveHarvestedStep(msg.step);
6066
+ }
6067
+ } catch {}
6068
+ };
6069
+ es.onerror = () => {};
6070
+ } catch (err) {
6071
+ showToast('Error: ' + err.message);
6072
+ closeHarvester();
6073
+ }
6074
+ }
6075
+
6076
+ function captureNextInteraction() {
6077
+ harvesterState.capturing = true;
6078
+ document.getElementById('harvester-status').textContent = '⏺ Capturing — make your interaction in the browser now...';
6079
+ document.getElementById('btn-capture-next').textContent = 'Waiting...';
6080
+ document.getElementById('btn-capture-next').disabled = true;
6081
+ }
6082
+
6083
+ async function saveHarvestedStep(step) {
6084
+ const actionColors = { click:'#22d3ee', type:'#a78bfa', check:'#34d399', assert:'#fb923c', select:'#f472b6', scroll:'#6b7280' };
6085
+ const name = harvesterState.stepName || step.description || (step.action + ' ' + (step.stableSelector || step.selector || '').slice(0, 40));
6086
+ const sel = step.stableSelector || step.selector || '';
6087
+
6088
+ try {
6089
+ const res = await fetch(API_BASE + '/api/step-library', {
6090
+ method: 'POST',
6091
+ headers: { 'Content-Type': 'application/json' },
6092
+ body: JSON.stringify({
6093
+ name,
6094
+ action: step.action,
6095
+ selector: sel,
6096
+ stableSelector: sel,
6097
+ value: step.value || null,
6098
+ assertType: step.assertType || null,
6099
+ description: step.description || null,
6100
+ tags: [],
6101
+ }),
6102
+ });
6103
+ if (!res.ok) throw new Error(await res.text());
6104
+
6105
+ // Show captured step
6106
+ document.getElementById('harvester-status').textContent = 'Step saved to library ✓';
6107
+ document.getElementById('harvester-captured').style.display = 'flex';
6108
+ document.getElementById('harvester-captured-desc').innerHTML =
6109
+ `<span style="font-family:var(--mono);font-size:11px;color:${actionColors[step.action]||'var(--muted)'};margin-right:8px">${step.action}</span>${escapeHtml(name)}`;
6110
+ document.getElementById('harvester-captured-sel').textContent = sel;
6111
+ document.getElementById('btn-harvest-another').style.display = '';
6112
+ document.getElementById('btn-capture-next').style.display = 'none';
6113
+
6114
+ // Refresh library if visible
6115
+ if (libraryStepsCache.length > 0) fetchLibrarySteps().then(() => { if (document.getElementById('view-step-library')?.classList.contains('active')) filterLibrarySteps(); });
6116
+ } catch (err) {
6117
+ showToast('Failed to save step: ' + err.message);
6118
+ document.getElementById('btn-capture-next').disabled = false;
6119
+ document.getElementById('btn-capture-next').textContent = 'Capture next interaction';
6120
+ }
6121
+ }
6122
+
6123
+ function harvestAnother() {
6124
+ harvesterState.stepName = '';
6125
+ document.getElementById('harvester-step-name') && (document.getElementById('harvester-step-name').value = '');
6126
+ document.getElementById('harvester-captured').style.display = 'none';
6127
+ document.getElementById('btn-harvest-another').style.display = 'none';
6128
+ document.getElementById('btn-capture-next').style.display = '';
6129
+ document.getElementById('btn-capture-next').disabled = false;
6130
+ document.getElementById('btn-capture-next').textContent = 'Capture next interaction';
6131
+ document.getElementById('harvester-status').textContent = 'Navigate to the next element, then click Capture.';
6132
+ }
6133
+
5940
6134
  // ── SUITE TEST PICKER ────────────────────────────────────────────────────────
5941
6135
  function openSuiteTestPicker() {
5942
6136
  const list = document.getElementById('stp-test-list');