skopix 2.0.35 → 2.0.37
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/cli/commands/dashboard.js +157 -0
- package/package.json +1 -1
- package/web/app/index.html +93 -0
|
@@ -1794,6 +1794,30 @@ export async function dashboardCommand(options) {
|
|
|
1794
1794
|
return;
|
|
1795
1795
|
}
|
|
1796
1796
|
|
|
1797
|
+
// ─── STEP TESTER ───────────────────────────────────────────────────
|
|
1798
|
+
if (pathname === '/api/step-tester/start' && method === 'POST') {
|
|
1799
|
+
const { url, selector } = JSON.parse(await readBody(req));
|
|
1800
|
+
try {
|
|
1801
|
+
const testerId = Math.random().toString(36).slice(2, 10);
|
|
1802
|
+
await startStepTester(testerId, url, selector);
|
|
1803
|
+
sendJSON(res, 200, { testerId });
|
|
1804
|
+
} catch (err) { sendJSON(res, 500, { error: err.message }); }
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (pathname.match(/^\/api\/step-tester\/[^/]+\/run$/) && method === 'POST') {
|
|
1808
|
+
const testerId = pathname.split('/')[3];
|
|
1809
|
+
const { selector, action, value, assertType } = JSON.parse(await readBody(req));
|
|
1810
|
+
const result = await runStepTesterAction(testerId, { selector, action, value, assertType });
|
|
1811
|
+
sendJSON(res, 200, result);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (pathname.match(/^\/api\/step-tester\/[^/]+\/stop$/) && method === 'POST') {
|
|
1815
|
+
const testerId = pathname.split('/')[3];
|
|
1816
|
+
await stopStepTester(testerId);
|
|
1817
|
+
sendJSON(res, 200, { stopped: true });
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1797
1821
|
// ─── STEP LIBRARY ──────────────────────────────────────────────────
|
|
1798
1822
|
if (pathname === '/api/step-library' && method === 'GET') {
|
|
1799
1823
|
sendJSON(res, 200, await listLibrarySteps(suitesDir));
|
|
@@ -3784,3 +3808,136 @@ async function syncTestsToLibrary(suitesDir) {
|
|
|
3784
3808
|
}
|
|
3785
3809
|
return { synced, updated };
|
|
3786
3810
|
}
|
|
3811
|
+
|
|
3812
|
+
// ═══════════════════════════════════════════════════════════════
|
|
3813
|
+
// STEP TESTER — live browser step testing
|
|
3814
|
+
// ═══════════════════════════════════════════════════════════════
|
|
3815
|
+
|
|
3816
|
+
const stepTesterSessions = new Map(); // testerId -> { browser, ctx, page }
|
|
3817
|
+
|
|
3818
|
+
async function startStepTester(testerId, url, selector) {
|
|
3819
|
+
const { chromium } = await import('playwright');
|
|
3820
|
+
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox'] });
|
|
3821
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
3822
|
+
const page = await ctx.newPage();
|
|
3823
|
+
|
|
3824
|
+
// Inject the tester toolbar on every page load
|
|
3825
|
+
await ctx.addInitScript((sel) => {
|
|
3826
|
+
window.__skopixTesterSelector = sel;
|
|
3827
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
3828
|
+
if (document.getElementById('__skopix_tester')) return;
|
|
3829
|
+
const tb = document.createElement('div');
|
|
3830
|
+
tb.id = '__skopix_tester';
|
|
3831
|
+
tb.style.cssText = [
|
|
3832
|
+
'position:fixed', 'bottom:20px', 'left:50%', 'transform:translateX(-50%)',
|
|
3833
|
+
'z-index:2147483647', 'background:#0f1117', 'border:1px solid #f59e0b',
|
|
3834
|
+
'border-radius:12px', 'padding:12px 16px', 'display:flex', 'align-items:center',
|
|
3835
|
+
'gap:10px', 'font-family:monospace', 'font-size:12px', 'color:#e5e7eb',
|
|
3836
|
+
'box-shadow:0 4px 24px rgba(0,0,0,0.7)', 'min-width:400px',
|
|
3837
|
+
].join(';');
|
|
3838
|
+
tb.innerHTML = `
|
|
3839
|
+
<span style="color:#f59e0b;font-size:14px">⚡</span>
|
|
3840
|
+
<span style="color:#9ca3af;font-size:11px">STEP TESTER</span>
|
|
3841
|
+
<input id="__skopix_sel" value="${(sel||'').replace(/"/g,'"')}" placeholder="selector..."
|
|
3842
|
+
style="flex:1;background:#1a1d2e;border:1px solid #374151;border-radius:6px;padding:4px 8px;
|
|
3843
|
+
color:#e5e7eb;font-family:monospace;font-size:11px;min-width:160px">
|
|
3844
|
+
<select id="__skopix_action" style="background:#1a1d2e;border:1px solid #374151;border-radius:6px;
|
|
3845
|
+
padding:4px 8px;color:#e5e7eb;font-family:monospace;font-size:11px">
|
|
3846
|
+
<option value="click">click</option>
|
|
3847
|
+
<option value="type">type</option>
|
|
3848
|
+
<option value="check">check</option>
|
|
3849
|
+
<option value="assert">assert (visible)</option>
|
|
3850
|
+
<option value="assert_text">assert (text)</option>
|
|
3851
|
+
</select>
|
|
3852
|
+
<input id="__skopix_value" placeholder="value..." style="width:100px;background:#1a1d2e;
|
|
3853
|
+
border:1px solid #374151;border-radius:6px;padding:4px 8px;color:#e5e7eb;
|
|
3854
|
+
font-family:monospace;font-size:11px;display:none">
|
|
3855
|
+
<button id="__skopix_run" style="background:#f59e0b;border:none;color:#000;
|
|
3856
|
+
border-radius:6px;padding:5px 14px;cursor:pointer;font-size:11px;font-weight:700;
|
|
3857
|
+
font-family:monospace">▶ Run</button>
|
|
3858
|
+
<span id="__skopix_result" style="font-size:13px;min-width:20px"></span>
|
|
3859
|
+
`;
|
|
3860
|
+
document.body.appendChild(tb);
|
|
3861
|
+
|
|
3862
|
+
// Show/hide value input based on action
|
|
3863
|
+
document.getElementById('__skopix_action').addEventListener('change', function() {
|
|
3864
|
+
const needsValue = ['type','assert_text'].includes(this.value);
|
|
3865
|
+
document.getElementById('__skopix_value').style.display = needsValue ? '' : 'none';
|
|
3866
|
+
});
|
|
3867
|
+
|
|
3868
|
+
// Run button — posts to parent via window.__skopixTesterRun
|
|
3869
|
+
document.getElementById('__skopix_run').addEventListener('click', function(e) {
|
|
3870
|
+
e.stopPropagation();
|
|
3871
|
+
const sel = document.getElementById('__skopix_sel').value.trim();
|
|
3872
|
+
const action = document.getElementById('__skopix_action').value;
|
|
3873
|
+
const value = document.getElementById('__skopix_value').value.trim();
|
|
3874
|
+
const resultEl = document.getElementById('__skopix_result');
|
|
3875
|
+
resultEl.textContent = '⏳';
|
|
3876
|
+
if (window.__skopixTesterRun) {
|
|
3877
|
+
window.__skopixTesterRun({ sel, action, value });
|
|
3878
|
+
}
|
|
3879
|
+
});
|
|
3880
|
+
});
|
|
3881
|
+
}, selector || '');
|
|
3882
|
+
|
|
3883
|
+
// Expose run function back to page
|
|
3884
|
+
await ctx.exposeFunction('__skopixTesterRun', async ({ sel, action, value }) => {
|
|
3885
|
+
const result = await executeStepTesterAction(page, { selector: sel, action, value });
|
|
3886
|
+
await page.evaluate((r) => {
|
|
3887
|
+
const el = document.getElementById('__skopix_result');
|
|
3888
|
+
if (el) el.textContent = r.passed ? '✓' : '✗';
|
|
3889
|
+
// Highlight element briefly
|
|
3890
|
+
try {
|
|
3891
|
+
const target = document.querySelector(r.selector);
|
|
3892
|
+
if (target) {
|
|
3893
|
+
const orig = target.style.outline;
|
|
3894
|
+
target.style.outline = r.passed ? '3px solid #34d399' : '3px solid #ef4444';
|
|
3895
|
+
setTimeout(() => { target.style.outline = orig; }, 1500);
|
|
3896
|
+
}
|
|
3897
|
+
} catch {}
|
|
3898
|
+
}, { ...result, selector: sel });
|
|
3899
|
+
});
|
|
3900
|
+
|
|
3901
|
+
if (url) await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {});
|
|
3902
|
+
|
|
3903
|
+
stepTesterSessions.set(testerId, { browser, ctx, page });
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
async function executeStepTesterAction(page, { selector, action, value }) {
|
|
3907
|
+
try {
|
|
3908
|
+
const { sanitiseSelector } = { sanitiseSelector: (s) => s }; // inline since not exported
|
|
3909
|
+
const sel = selector;
|
|
3910
|
+
if (action === 'click') {
|
|
3911
|
+
await page.locator(sel).first().click({ timeout: 5000 });
|
|
3912
|
+
} else if (action === 'type') {
|
|
3913
|
+
await page.locator(sel).first().fill(value || '', { timeout: 5000 });
|
|
3914
|
+
} else if (action === 'check') {
|
|
3915
|
+
await page.locator(sel).first().click({ timeout: 5000 });
|
|
3916
|
+
} else if (action === 'assert' || action === 'assert_text') {
|
|
3917
|
+
if (action === 'assert_text') {
|
|
3918
|
+
const text = await page.locator(sel).first().textContent({ timeout: 5000 });
|
|
3919
|
+
if (!text.includes(value)) throw new Error(`Text "${text}" does not contain "${value}"`);
|
|
3920
|
+
} else {
|
|
3921
|
+
await page.locator(sel).first().waitFor({ state: 'attached', timeout: 5000 });
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
const screenshot = await page.screenshot({ type: 'jpeg', quality: 70 }).catch(() => null);
|
|
3925
|
+
return { passed: true, screenshot: screenshot ? screenshot.toString('base64') : null };
|
|
3926
|
+
} catch (err) {
|
|
3927
|
+
const screenshot = await page.screenshot({ type: 'jpeg', quality: 70 }).catch(() => null);
|
|
3928
|
+
return { passed: false, error: err.message, screenshot: screenshot ? screenshot.toString('base64') : null };
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
async function runStepTesterAction(testerId, stepData) {
|
|
3933
|
+
const session = stepTesterSessions.get(testerId);
|
|
3934
|
+
if (!session) return { passed: false, error: 'Session not found' };
|
|
3935
|
+
return await executeStepTesterAction(session.page, stepData);
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
async function stopStepTester(testerId) {
|
|
3939
|
+
const session = stepTesterSessions.get(testerId);
|
|
3940
|
+
if (!session) return;
|
|
3941
|
+
try { await session.browser.close(); } catch {}
|
|
3942
|
+
stepTesterSessions.delete(testerId);
|
|
3943
|
+
}
|
package/package.json
CHANGED
package/web/app/index.html
CHANGED
|
@@ -1769,6 +1769,39 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1769
1769
|
</div>
|
|
1770
1770
|
</div>
|
|
1771
1771
|
</div>
|
|
1772
|
+
|
|
1773
|
+
<!-- STEP TESTER MODAL -->
|
|
1774
|
+
<div class="modal-overlay" id="step-tester-modal" style="display:none" onclick="if(event.target===this)closeStepTester()">
|
|
1775
|
+
<div class="modal" style="max-width:520px;width:100%">
|
|
1776
|
+
<div class="modal-header">
|
|
1777
|
+
<div class="modal-title" style="color:#f59e0b">
|
|
1778
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#f59e0b;margin-right:8px"></span>
|
|
1779
|
+
Step tester
|
|
1780
|
+
</div>
|
|
1781
|
+
<button class="modal-close" onclick="closeStepTester()">✕</button>
|
|
1782
|
+
</div>
|
|
1783
|
+
<div class="modal-body" style="display:flex;flex-direction:column;gap:14px">
|
|
1784
|
+
<p style="font-family:var(--mono);font-size:12px;color:var(--muted);line-height:1.7;margin:0">
|
|
1785
|
+
Opens a browser with a floating toolbar. Navigate to where the element is, then use the toolbar to test the step directly in the browser.
|
|
1786
|
+
</p>
|
|
1787
|
+
<div class="form-field">
|
|
1788
|
+
<label class="form-label">URL to open</label>
|
|
1789
|
+
<input class="form-input" id="tester-url" type="text" placeholder="http://localhost:8224/pi">
|
|
1790
|
+
</div>
|
|
1791
|
+
<div class="form-field">
|
|
1792
|
+
<label class="form-label">Selector</label>
|
|
1793
|
+
<input class="form-input" id="tester-selector" type="text" style="font-family:var(--mono)">
|
|
1794
|
+
<div class="form-help">Pre-filled from library — editable in the browser toolbar too</div>
|
|
1795
|
+
</div>
|
|
1796
|
+
<div id="tester-status" style="display:none;padding:12px 14px;border-radius:8px;font-family:var(--mono);font-size:12px"></div>
|
|
1797
|
+
</div>
|
|
1798
|
+
<div class="modal-footer">
|
|
1799
|
+
<button class="btn btn-ghost" onclick="closeStepTester()">Close</button>
|
|
1800
|
+
<button class="btn btn-primary" id="btn-open-tester" style="background:#f59e0b;border-color:#f59e0b;color:#000" onclick="openStepTesterBrowser()">Open browser</button>
|
|
1801
|
+
</div>
|
|
1802
|
+
</div>
|
|
1803
|
+
</div>
|
|
1804
|
+
|
|
1772
1805
|
<div class="view" id="view-suites">
|
|
1773
1806
|
<div class="topbar">
|
|
1774
1807
|
<div>
|
|
@@ -5614,6 +5647,7 @@ function renderLibrarySteps(steps) {
|
|
|
5614
5647
|
</td>
|
|
5615
5648
|
<td style="padding:12px 16px;font-family:var(--mono);font-size:12px;color:var(--muted)">${s.usageCount||0}</td>
|
|
5616
5649
|
<td style="padding:12px 16px;text-align:right">
|
|
5650
|
+
<button class="btn btn-ghost lib-test-btn" style="padding:4px 10px;font-size:11px;margin-right:4px;color:#f59e0b;border-color:rgba(245,158,11,0.3)" data-id="${escapeAttr(s.id)}" data-selector="${escapeAttr(sel)}">Test</button>
|
|
5617
5651
|
<button class="btn btn-ghost lib-edit-btn" style="padding:4px 10px;font-size:11px;margin-right:4px" data-id="${escapeAttr(s.id)}">Edit</button>
|
|
5618
5652
|
<button class="btn btn-ghost lib-delete-btn" style="padding:4px 10px;font-size:11px;color:var(--red)" data-id="${escapeAttr(s.id)}" data-name="${escapeAttr(s.name||'')}">Delete</button>
|
|
5619
5653
|
</td>
|
|
@@ -5733,10 +5767,69 @@ async function loadLibraryView() {
|
|
|
5733
5767
|
renderLibrarySteps(steps);
|
|
5734
5768
|
}
|
|
5735
5769
|
|
|
5770
|
+
// ── STEP TESTER ──────────────────────────────────────────────────────────────
|
|
5771
|
+
let stepTesterState = { testerId: null };
|
|
5772
|
+
|
|
5773
|
+
function openLibraryStepTester(id, selector) {
|
|
5774
|
+
const step = libraryStepsCache.find(s => s.id === id);
|
|
5775
|
+
document.getElementById('tester-selector').value = selector || (step ? (step.stableSelector||step.selector||'') : '');
|
|
5776
|
+
document.getElementById('tester-url').value = '';
|
|
5777
|
+
document.getElementById('tester-status').style.display = 'none';
|
|
5778
|
+
document.getElementById('btn-open-tester').disabled = false;
|
|
5779
|
+
document.getElementById('btn-open-tester').textContent = 'Open browser';
|
|
5780
|
+
document.getElementById('step-tester-modal').style.display = 'flex';
|
|
5781
|
+
setTimeout(() => document.getElementById('tester-url').focus(), 100);
|
|
5782
|
+
}
|
|
5783
|
+
|
|
5784
|
+
function closeStepTester() {
|
|
5785
|
+
document.getElementById('step-tester-modal').style.display = 'none';
|
|
5786
|
+
if (stepTesterState.testerId) {
|
|
5787
|
+
fetch(API_BASE + '/api/step-tester/' + stepTesterState.testerId + '/stop', { method: 'POST' }).catch(() => {});
|
|
5788
|
+
stepTesterState.testerId = null;
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
|
|
5792
|
+
async function openStepTesterBrowser() {
|
|
5793
|
+
const url = document.getElementById('tester-url').value.trim();
|
|
5794
|
+
const selector = document.getElementById('tester-selector').value.trim();
|
|
5795
|
+
const statusEl = document.getElementById('tester-status');
|
|
5796
|
+
const btn = document.getElementById('btn-open-tester');
|
|
5797
|
+
|
|
5798
|
+
if (!url) { showToast('Enter a URL first'); return; }
|
|
5799
|
+
|
|
5800
|
+
btn.disabled = true;
|
|
5801
|
+
btn.textContent = 'Opening...';
|
|
5802
|
+
statusEl.style.display = 'block';
|
|
5803
|
+
statusEl.style.background = 'var(--surface2)';
|
|
5804
|
+
statusEl.style.color = 'var(--muted)';
|
|
5805
|
+
statusEl.textContent = 'Opening browser...';
|
|
5806
|
+
|
|
5807
|
+
try {
|
|
5808
|
+
const res = await fetch(API_BASE + '/api/step-tester/start', {
|
|
5809
|
+
method: 'POST',
|
|
5810
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5811
|
+
body: JSON.stringify({ url, selector }),
|
|
5812
|
+
});
|
|
5813
|
+
const data = await res.json();
|
|
5814
|
+
if (!res.ok) throw new Error(data.error || 'Failed');
|
|
5815
|
+
stepTesterState.testerId = data.testerId;
|
|
5816
|
+
statusEl.textContent = '✓ Browser open — navigate to the element and use the toolbar to test';
|
|
5817
|
+
statusEl.style.color = '#34d399';
|
|
5818
|
+
btn.textContent = 'Browser open';
|
|
5819
|
+
} catch (err) {
|
|
5820
|
+
statusEl.textContent = '✗ ' + err.message;
|
|
5821
|
+
statusEl.style.color = 'var(--red)';
|
|
5822
|
+
btn.disabled = false;
|
|
5823
|
+
btn.textContent = 'Open browser';
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5736
5827
|
// Event delegation for library table buttons
|
|
5737
5828
|
document.addEventListener('click', (e) => {
|
|
5829
|
+
const testBtn = e.target.closest('.lib-test-btn');
|
|
5738
5830
|
const editBtn = e.target.closest('.lib-edit-btn');
|
|
5739
5831
|
const deleteBtn = e.target.closest('.lib-delete-btn');
|
|
5832
|
+
if (testBtn) { openLibraryStepTester(testBtn.dataset.id, testBtn.dataset.selector); }
|
|
5740
5833
|
if (editBtn) { editLibraryStep(editBtn.dataset.id); }
|
|
5741
5834
|
if (deleteBtn) { deleteLibraryStepUI(deleteBtn.dataset.id, deleteBtn.dataset.name); }
|
|
5742
5835
|
});
|