skopix 2.0.25 → 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.
- package/package.json +1 -1
- package/web/app/index.html +264 -0
- package/web/index.html +595 -6491
package/package.json
CHANGED
package/web/app/index.html
CHANGED
|
@@ -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');
|