skopix 2.0.28 → 2.0.30

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 +56 -53
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skopix",
3
- "version": "2.0.28",
3
+ "version": "2.0.30",
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": {
@@ -1620,27 +1620,25 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
1620
1620
  <div class="modal-body" style="display:flex;flex-direction:column;gap:14px">
1621
1621
  <input type="hidden" id="lib-step-id">
1622
1622
  <div class="form-field">
1623
- <label class="form-label">Name</label>
1624
- <input class="form-input" id="lib-step-name" type="text" placeholder="e.g. Click chart settings icon">
1623
+ <label class="form-label">Element name</label>
1624
+ <input class="form-input" id="lib-step-name" type="text" placeholder="e.g. Chart settings icon, Username input">
1625
1625
  </div>
1626
1626
  <div class="form-field">
1627
- <label class="form-label">Action</label>
1627
+ <label class="form-label">Selector</label>
1628
+ <input class="form-input" id="lib-step-selector" type="text" placeholder="CSS selector" style="font-family:var(--mono)">
1629
+ <div class="form-help">The CSS selector that identifies this element</div>
1630
+ </div>
1631
+ <div class="form-field">
1632
+ <label class="form-label">Default action (optional)</label>
1628
1633
  <select class="form-select" id="lib-step-action">
1634
+ <option value="">— No default —</option>
1629
1635
  <option value="click">click</option>
1630
1636
  <option value="type">type</option>
1631
1637
  <option value="check">check</option>
1632
1638
  <option value="assert">assert</option>
1633
1639
  <option value="select">select</option>
1634
- <option value="scroll">scroll</option>
1635
1640
  </select>
1636
- </div>
1637
- <div class="form-field">
1638
- <label class="form-label">Selector</label>
1639
- <input class="form-input" id="lib-step-selector" type="text" placeholder="CSS selector" style="font-family:var(--mono)">
1640
- </div>
1641
- <div class="form-field" id="lib-step-value-row">
1642
- <label class="form-label">Value (for type/assert)</label>
1643
- <input class="form-input" id="lib-step-value" type="text" placeholder="Value to type or assert">
1641
+ <div class="form-help">Suggested action when used in the test builder — can be overridden</div>
1644
1642
  </div>
1645
1643
  <div class="form-field">
1646
1644
  <label class="form-label">Tags (comma separated)</label>
@@ -5583,14 +5581,14 @@ function renderLibrarySteps(steps) {
5583
5581
  Delete selected
5584
5582
  </button>
5585
5583
  </div>
5586
- ${fragileCount > 0 ? `<div style="font-family:var(--mono);font-size:11px;color:#f59e0b">⚠ ${fragileCount} step${fragileCount>1?'s':''} with fragile selectors</div>` : '<div style="font-family:var(--mono);font-size:11px;color:#34d399">✓ All selectors look stable</div>'}
5584
+ ${fragileCount > 0 ? `<div style="font-family:var(--mono);font-size:11px;color:#f59e0b">⚠ ${fragileCount} element${fragileCount>1?'s':''} with fragile selectors</div>` : '<div style="font-family:var(--mono);font-size:11px;color:#34d399">✓ All selectors look stable</div>'}
5587
5585
  </div>
5588
5586
  <table style="width:100%;border-collapse:collapse">
5589
5587
  <thead><tr style="font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:0.1em">
5590
5588
  <th style="padding:10px 16px;width:32px;border-bottom:1px solid var(--border)"></th>
5591
- <th style="padding:10px 20px;text-align:left;border-bottom:1px solid var(--border)">NAME</th>
5592
- <th style="padding:10px 16px;text-align:left;border-bottom:1px solid var(--border)">ACTION</th>
5589
+ <th style="padding:10px 20px;text-align:left;border-bottom:1px solid var(--border)">ELEMENT NAME</th>
5593
5590
  <th style="padding:10px 16px;text-align:left;border-bottom:1px solid var(--border)">SELECTOR</th>
5591
+ <th style="padding:10px 16px;text-align:left;border-bottom:1px solid var(--border)">DEFAULT ACTION</th>
5594
5592
  <th style="padding:10px 16px;text-align:left;border-bottom:1px solid var(--border)">USES</th>
5595
5593
  <th style="padding:10px 16px;text-align:right;border-bottom:1px solid var(--border)"></th>
5596
5594
  </tr></thead>
@@ -5599,23 +5597,28 @@ function renderLibrarySteps(steps) {
5599
5597
  const sel = s.stableSelector || s.selector || '';
5600
5598
  const fragile = isSelectorFragile(sel);
5601
5599
  const warning = getSelectorWarning(sel);
5600
+ const actionColors = { click:'#22d3ee', type:'#a78bfa', check:'#34d399', assert:'#fb923c', select:'#f472b6', scroll:'#6b7280' };
5602
5601
  return `
5603
5602
  <tr data-step-id="${escapeAttr(s.id)}" style="border-bottom:1px solid var(--border);transition:background 0.15s${fragile?';background:rgba(245,158,11,0.04)':''}" onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background='${fragile?'rgba(245,158,11,0.04)':''}'">
5604
5603
  <td style="padding:12px 16px">
5605
5604
  <input type="checkbox" class="lib-step-checkbox" data-id="${escapeAttr(s.id)}" onchange="onLibStepCheck()" style="width:14px;height:14px">
5606
5605
  </td>
5607
5606
  <td style="padding:12px 20px">
5608
- <div style="font-size:13px;color:var(--text)">${escapeHtml(s.name || s.description || '')}</div>
5607
+ <div style="font-size:13px;color:var(--text)">${escapeHtml(s.name||'')}</div>
5609
5608
  ${s.tags && s.tags.length ? `<div style="margin-top:4px;display:flex;gap:4px">${s.tags.map(t=>`<span style="font-family:var(--mono);font-size:10px;padding:2px 6px;background:var(--surface2);border-radius:4px;color:var(--muted)">${escapeHtml(t)}</span>`).join('')}</div>` : ''}
5610
5609
  </td>
5611
- <td style="padding:12px 16px"><span style="font-family:var(--mono);font-size:11px;color:${actionColors[s.action]||'var(--muted)'}">${s.action||''}</span></td>
5612
- <td style="padding:12px 16px;max-width:280px">
5610
+ <td style="padding:12px 16px;max-width:300px">
5613
5611
  <div style="display:flex;align-items:center;gap:6px">
5614
5612
  ${fragile ? `<span title="${escapeAttr(warning||'')}" style="color:#f59e0b;font-size:14px;cursor:help;flex-shrink:0">⚠</span>` : ''}
5615
- <code style="font-size:11px;color:${fragile?'#f59e0b':'var(--muted)'};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block">${escapeHtml(sel || '—')}</code>
5613
+ <code style="font-size:11px;color:${fragile?'#f59e0b':'var(--muted)'};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block">${escapeHtml(sel||'—')}</code>
5616
5614
  </div>
5617
5615
  </td>
5618
- <td style="padding:12px 16px;font-family:var(--mono);font-size:12px;color:var(--muted)">${s.usageCount || 0}</td>
5616
+ <td style="padding:12px 16px">
5617
+ <span style="font-family:var(--mono);font-size:11px;color:${actionColors[s.defaultAction]||'var(--muted2)'}">
5618
+ ${s.defaultAction||'—'}
5619
+ </span>
5620
+ </td>
5621
+ <td style="padding:12px 16px;font-family:var(--mono);font-size:12px;color:var(--muted)">${s.usageCount||0}</td>
5619
5622
  <td style="padding:12px 16px;text-align:right">
5620
5623
  <button class="btn btn-ghost" style="padding:4px 10px;font-size:11px;margin-right:4px" onclick="editLibraryStep('${escapeAttr(s.id)}')">Edit</button>
5621
5624
  <button class="btn btn-ghost" style="padding:4px 10px;font-size:11px;color:var(--red)" onclick="deleteLibraryStepUI('${escapeAttr(s.id)}','${escapeAttr(s.name||'')}')">Delete</button>
@@ -5730,11 +5733,10 @@ async function loadLibraryView() {
5730
5733
  function openAddLibraryStep() {
5731
5734
  document.getElementById('lib-step-id').value = '';
5732
5735
  document.getElementById('lib-step-name').value = '';
5733
- document.getElementById('lib-step-action').value = 'click';
5734
5736
  document.getElementById('lib-step-selector').value = '';
5735
- document.getElementById('lib-step-value').value = '';
5737
+ document.getElementById('lib-step-action').value = '';
5736
5738
  document.getElementById('lib-step-tags').value = '';
5737
- document.getElementById('library-step-modal-title').textContent = 'Add step to library';
5739
+ document.getElementById('library-step-modal-title').textContent = 'Add element to library';
5738
5740
  document.getElementById('library-step-modal').style.display = 'flex';
5739
5741
  }
5740
5742
 
@@ -5743,11 +5745,10 @@ function editLibraryStep(id) {
5743
5745
  if (!step) return;
5744
5746
  document.getElementById('lib-step-id').value = step.id;
5745
5747
  document.getElementById('lib-step-name').value = step.name || '';
5746
- document.getElementById('lib-step-action').value = step.action || 'click';
5747
5748
  document.getElementById('lib-step-selector').value = step.stableSelector || step.selector || '';
5748
- document.getElementById('lib-step-value').value = step.value || '';
5749
+ document.getElementById('lib-step-action').value = step.defaultAction || '';
5749
5750
  document.getElementById('lib-step-tags').value = (step.tags || []).join(', ');
5750
- document.getElementById('library-step-modal-title').textContent = 'Edit step';
5751
+ document.getElementById('library-step-modal-title').textContent = 'Edit element';
5751
5752
  document.getElementById('library-step-modal').style.display = 'flex';
5752
5753
  }
5753
5754
 
@@ -5758,22 +5759,22 @@ function closeLibraryStepModal() {
5758
5759
  async function saveLibraryStep() {
5759
5760
  const id = document.getElementById('lib-step-id').value;
5760
5761
  const name = document.getElementById('lib-step-name').value.trim();
5761
- const action = document.getElementById('lib-step-action').value;
5762
5762
  const selector = document.getElementById('lib-step-selector').value.trim();
5763
- const value = document.getElementById('lib-step-value').value.trim();
5763
+ const defaultAction = document.getElementById('lib-step-action').value;
5764
5764
  const tags = (document.getElementById('lib-step-tags').value || '').split(',').map(t=>t.trim()).filter(Boolean);
5765
5765
 
5766
- if (!name) { showToast('Name is required'); return; }
5766
+ if (!name) { showToast('Element name is required'); return; }
5767
+ if (!selector) { showToast('Selector is required'); return; }
5767
5768
 
5768
5769
  try {
5769
- const body = { name, action, selector, stableSelector: selector, value: value || null, tags };
5770
+ const body = { name, selector, stableSelector: selector, defaultAction: defaultAction || null, tags };
5770
5771
  const url = id ? `${API_BASE}/api/step-library/${encodeURIComponent(id)}` : `${API_BASE}/api/step-library`;
5771
5772
  const method = id ? 'PUT' : 'POST';
5772
5773
  const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
5773
5774
  if (!res.ok) throw new Error(await res.text());
5774
5775
  closeLibraryStepModal();
5775
5776
  await loadLibraryView();
5776
- showToast(id ? 'Step updated' : 'Step added to library');
5777
+ showToast(id ? 'Element updated' : 'Element added to library');
5777
5778
  } catch (err) { showToast('Error: ' + err.message); }
5778
5779
  }
5779
5780
 
@@ -5857,23 +5858,26 @@ function renderBuilderLibrary() {
5857
5858
  const actionColors = { click:'#22d3ee', type:'#a78bfa', check:'#34d399', assert:'#fb923c', select:'#f472b6', scroll:'#6b7280' };
5858
5859
 
5859
5860
  let steps = libraryStepsCache;
5860
- if (builderActionFilter) steps = steps.filter(s => s.action === builderActionFilter);
5861
+ if (builderActionFilter) steps = steps.filter(s => s.defaultAction === builderActionFilter);
5861
5862
  if (q) steps = steps.filter(s => (s.name||'').toLowerCase().includes(q) || (s.selector||'').toLowerCase().includes(q));
5862
5863
 
5863
5864
  if (steps.length === 0) {
5864
- container.innerHTML = `<div style="padding:24px 16px;text-align:center;color:var(--muted);font-family:var(--mono);font-size:12px">No steps match</div>`;
5865
+ container.innerHTML = `<div style="padding:24px 16px;text-align:center;color:var(--muted);font-family:var(--mono);font-size:12px">No elements match</div>`;
5865
5866
  return;
5866
5867
  }
5867
5868
 
5868
5869
  container.innerHTML = steps.map(s => `
5869
- <div onclick="addBuilderStepFromLibrary('${escapeAttr(s.id)}')"
5870
- style="padding:10px 16px;cursor:pointer;border-bottom:1px solid var(--border);transition:background 0.1s"
5871
- onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background=''">
5872
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:3px">
5873
- <span style="font-family:var(--mono);font-size:10px;color:${actionColors[s.action]||'var(--muted)'};min-width:40px">${s.action||''}</span>
5874
- <span style="font-size:12px;color:var(--text)">${escapeHtml(s.name||s.description||'')}</span>
5870
+ <div style="padding:10px 16px;border-bottom:1px solid var(--border)">
5871
+ <div style="font-size:12px;color:var(--text);margin-bottom:4px">${escapeHtml(s.name||'')}</div>
5872
+ <code style="font-size:10px;color:var(--muted2);display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:8px">${escapeHtml(s.stableSelector||s.selector||'')}</code>
5873
+ <div style="display:flex;gap:4px;flex-wrap:wrap">
5874
+ ${['click','type','check','assert','select'].map(a => `
5875
+ <button onclick="addBuilderStepFromLibrary('${escapeAttr(s.id)}','${a}')"
5876
+ style="font-family:var(--mono);font-size:10px;padding:3px 8px;border-radius:4px;border:1px solid ${actionColors[a]};background:transparent;color:${actionColors[a]};cursor:pointer;transition:background 0.1s"
5877
+ onmouseover="this.style.background='${actionColors[a]}22'" onmouseout="this.style.background='transparent'">
5878
+ ${a}
5879
+ </button>`).join('')}
5875
5880
  </div>
5876
- <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>
5877
5881
  </div>`).join('');
5878
5882
  }
5879
5883
 
@@ -5881,17 +5885,16 @@ function filterBuilderLibrary() {
5881
5885
  renderBuilderLibrary();
5882
5886
  }
5883
5887
 
5884
- function addBuilderStepFromLibrary(id) {
5888
+ function addBuilderStepFromLibrary(id, action) {
5885
5889
  const libStep = libraryStepsCache.find(s => s.id === id);
5886
5890
  if (!libStep) return;
5887
5891
  const step = {
5888
5892
  id: 'bs-' + Math.random().toString(36).slice(2, 8),
5889
- action: libStep.action,
5893
+ action: action || libStep.defaultAction || 'click',
5890
5894
  stableSelector: libStep.stableSelector || libStep.selector,
5891
5895
  selector: libStep.selector || libStep.stableSelector,
5892
- value: libStep.value || null,
5893
- assertType: libStep.assertType || null,
5894
- attribute: libStep.attribute || null,
5896
+ value: action === 'type' ? '' : null,
5897
+ assertType: action === 'assert' ? 'visible' : null,
5895
5898
  description: libStep.name || libStep.description,
5896
5899
  libraryId: libStep.id,
5897
5900
  timestamp: Date.now(),
@@ -6082,8 +6085,10 @@ function captureNextInteraction() {
6082
6085
 
6083
6086
  async function saveHarvestedStep(step) {
6084
6087
  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
6088
  const sel = step.stableSelector || step.selector || '';
6089
+ // Strip action prefix from description for cleaner element name
6090
+ let name = harvesterState.stepName || step.description || '';
6091
+ name = name.replace(/^(click|type|check|assert|select|scroll)\s+/i, '').trim() || sel.slice(0, 50);
6087
6092
 
6088
6093
  try {
6089
6094
  const res = await fetch(API_BASE + '/api/step-library', {
@@ -6091,10 +6096,10 @@ async function saveHarvestedStep(step) {
6091
6096
  headers: { 'Content-Type': 'application/json' },
6092
6097
  body: JSON.stringify({
6093
6098
  name,
6094
- action: step.action,
6095
6099
  selector: sel,
6096
6100
  stableSelector: sel,
6097
- value: step.value || null,
6101
+ defaultAction: step.action || null,
6102
+ defaultValue: step.value || null,
6098
6103
  assertType: step.assertType || null,
6099
6104
  description: step.description || null,
6100
6105
  tags: [],
@@ -6102,19 +6107,17 @@ async function saveHarvestedStep(step) {
6102
6107
  });
6103
6108
  if (!res.ok) throw new Error(await res.text());
6104
6109
 
6105
- // Show captured step
6106
- document.getElementById('harvester-status').textContent = 'Step saved to library ✓';
6110
+ document.getElementById('harvester-status').textContent = 'Element saved to library ✓';
6107
6111
  document.getElementById('harvester-captured').style.display = 'flex';
6108
6112
  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)}`;
6113
+ `<span style="font-family:var(--mono);font-size:11px;color:${actionColors[step.action]||'var(--muted)'};margin-right:8px">${step.action||''}</span>${escapeHtml(name)}`;
6110
6114
  document.getElementById('harvester-captured-sel').textContent = sel;
6111
6115
  document.getElementById('btn-harvest-another').style.display = '';
6112
6116
  document.getElementById('btn-capture-next').style.display = 'none';
6113
6117
 
6114
- // Refresh library if visible
6115
6118
  if (libraryStepsCache.length > 0) fetchLibrarySteps().then(() => { if (document.getElementById('view-step-library')?.classList.contains('active')) filterLibrarySteps(); });
6116
6119
  } catch (err) {
6117
- showToast('Failed to save step: ' + err.message);
6120
+ showToast('Failed to save element: ' + err.message);
6118
6121
  document.getElementById('btn-capture-next').disabled = false;
6119
6122
  document.getElementById('btn-capture-next').textContent = 'Capture next interaction';
6120
6123
  }