skopix 2.0.27 → 2.0.29

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 +189 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.27",
3
+ "version": "2.0.29",
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>
@@ -5943,6 +6008,129 @@ async function saveBuiltTest() {
5943
6008
  }
5944
6009
  }
5945
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
+
5946
6134
  // ── SUITE TEST PICKER ────────────────────────────────────────────────────────
5947
6135
  function openSuiteTestPicker() {
5948
6136
  const list = document.getElementById('stp-test-list');