skopix 2.0.37 → 2.0.39
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 +145 -15
- package/package.json +1 -1
- package/web/app/index.html +393 -12
|
@@ -1811,6 +1811,25 @@ export async function dashboardCommand(options) {
|
|
|
1811
1811
|
sendJSON(res, 200, result);
|
|
1812
1812
|
return;
|
|
1813
1813
|
}
|
|
1814
|
+
if (pathname.match(/^\/api\/step-tester\/[^/]+\/run-sequence$/) && method === 'POST') {
|
|
1815
|
+
const testerId = pathname.split('/')[3];
|
|
1816
|
+
const { steps } = JSON.parse(await readBody(req));
|
|
1817
|
+
const session = stepTesterSessions.get(testerId);
|
|
1818
|
+
if (!session) { sendJSON(res, 404, { error: 'Session not found' }); return; }
|
|
1819
|
+
// Run steps one at a time, streaming results via SSE
|
|
1820
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' });
|
|
1821
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1822
|
+
const step = steps[i];
|
|
1823
|
+
res.write(`data: ${JSON.stringify({ type: 'step-start', index: i })}\n\n`);
|
|
1824
|
+
const result = await executeStepTesterAction(session.page, step);
|
|
1825
|
+
res.write(`data: ${JSON.stringify({ type: 'step-result', index: i, ...result })}\n\n`);
|
|
1826
|
+
if (!result.passed) break; // stop on first failure
|
|
1827
|
+
await new Promise(r => setTimeout(r, 300)); // small gap between steps
|
|
1828
|
+
}
|
|
1829
|
+
res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
|
|
1830
|
+
res.end();
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1814
1833
|
if (pathname.match(/^\/api\/step-tester\/[^/]+\/stop$/) && method === 'POST') {
|
|
1815
1834
|
const testerId = pathname.split('/')[3];
|
|
1816
1835
|
await stopStepTester(testerId);
|
|
@@ -1823,6 +1842,32 @@ export async function dashboardCommand(options) {
|
|
|
1823
1842
|
sendJSON(res, 200, await listLibrarySteps(suitesDir));
|
|
1824
1843
|
return;
|
|
1825
1844
|
}
|
|
1845
|
+
if (pathname === '/api/step-library/pending' && method === 'GET') {
|
|
1846
|
+
sendJSON(res, 200, await listPendingSteps());
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
if (pathname.match(/^\/api\/step-library\/pending\/[^/]+\/approve$/) && method === 'POST') {
|
|
1850
|
+
const id = decodeURIComponent(pathname.split('/')[4]);
|
|
1851
|
+
const step = await approvePendingStep(suitesDir, id);
|
|
1852
|
+
sendJSON(res, 200, step || { error: 'Not found' });
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (pathname.match(/^\/api\/step-library\/pending\/[^/]+\/dismiss$/) && method === 'POST') {
|
|
1856
|
+
const id = decodeURIComponent(pathname.split('/')[4]);
|
|
1857
|
+
await dismissPendingStep(id);
|
|
1858
|
+
sendJSON(res, 200, { dismissed: true });
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (pathname === '/api/step-library/pending/approve-all' && method === 'POST') {
|
|
1862
|
+
const count = await approveAllPending(suitesDir);
|
|
1863
|
+
sendJSON(res, 200, { approved: count });
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (pathname === '/api/step-library/pending/dismiss-all' && method === 'POST') {
|
|
1867
|
+
await dismissAllPending();
|
|
1868
|
+
sendJSON(res, 200, { dismissed: true });
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1826
1871
|
if (pathname === '/api/step-library' && method === 'POST') {
|
|
1827
1872
|
const data = JSON.parse(await readBody(req));
|
|
1828
1873
|
const step = await addLibraryStep(suitesDir, data);
|
|
@@ -3652,6 +3697,66 @@ async function syncIssuesStatus() {
|
|
|
3652
3697
|
// ═══════════════════════════════════════════════════════════════
|
|
3653
3698
|
|
|
3654
3699
|
const LIBRARY_FILE = () => path.join(os.homedir(), '.skopix', 'step-library.yaml');
|
|
3700
|
+
const LIBRARY_PENDING_FILE = () => path.join(os.homedir(), '.skopix', 'step-library-pending.yaml');
|
|
3701
|
+
|
|
3702
|
+
async function listPendingSteps() {
|
|
3703
|
+
const file = LIBRARY_PENDING_FILE();
|
|
3704
|
+
if (!await fs.pathExists(file)) return [];
|
|
3705
|
+
try {
|
|
3706
|
+
const content = await fs.readFile(file, 'utf8');
|
|
3707
|
+
const data = yaml.parse(content);
|
|
3708
|
+
return Array.isArray(data) ? data : [];
|
|
3709
|
+
} catch { return []; }
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
async function savePendingSteps(steps) {
|
|
3713
|
+
const file = LIBRARY_PENDING_FILE();
|
|
3714
|
+
await fs.ensureDir(path.dirname(file));
|
|
3715
|
+
await fs.writeFile(file, yaml.stringify(steps));
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
async function addToPending(suitesDir, pendingStep) {
|
|
3719
|
+
const pending = await listPendingSteps();
|
|
3720
|
+
const existing = await listLibrarySteps(suitesDir);
|
|
3721
|
+
// Check not already in library or pending
|
|
3722
|
+
const allSels = [...existing, ...pending].map(e => (e.stableSelector||e.selector||'').toLowerCase());
|
|
3723
|
+
const selLower = (pendingStep.stableSelector||pendingStep.selector||'').toLowerCase();
|
|
3724
|
+
if (allSels.includes(selLower)) return; // already exists
|
|
3725
|
+
// Check similarity
|
|
3726
|
+
const similar = [...existing, ...pending].find(e => stepSimilarity(e, pendingStep) >= 0.85);
|
|
3727
|
+
if (similar) return;
|
|
3728
|
+
pending.push({ ...pendingStep, id: 'pending-' + Math.random().toString(36).slice(2, 10), addedAt: new Date().toISOString() });
|
|
3729
|
+
await savePendingSteps(pending);
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
async function approvePendingStep(suitesDir, id) {
|
|
3733
|
+
const pending = await listPendingSteps();
|
|
3734
|
+
const step = pending.find(s => s.id === id);
|
|
3735
|
+
if (!step) return null;
|
|
3736
|
+
const { id: _id, addedAt: _a, ...stepData } = step;
|
|
3737
|
+
const added = await addLibraryStep(suitesDir, stepData);
|
|
3738
|
+
await savePendingSteps(pending.filter(s => s.id !== id));
|
|
3739
|
+
return added;
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
async function dismissPendingStep(id) {
|
|
3743
|
+
const pending = await listPendingSteps();
|
|
3744
|
+
await savePendingSteps(pending.filter(s => s.id !== id));
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
async function approveAllPending(suitesDir) {
|
|
3748
|
+
const pending = await listPendingSteps();
|
|
3749
|
+
for (const step of pending) {
|
|
3750
|
+
const { id: _id, addedAt: _a, ...stepData } = step;
|
|
3751
|
+
await addLibraryStep(suitesDir, stepData);
|
|
3752
|
+
}
|
|
3753
|
+
await savePendingSteps([]);
|
|
3754
|
+
return pending.length;
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
async function dismissAllPending() {
|
|
3758
|
+
await savePendingSteps([]);
|
|
3759
|
+
}
|
|
3655
3760
|
|
|
3656
3761
|
async function listLibrarySteps(suitesDir) {
|
|
3657
3762
|
const file = LIBRARY_FILE();
|
|
@@ -3733,19 +3838,41 @@ async function extractStepsToLibrary(suitesDir, steps, testName) {
|
|
|
3733
3838
|
const sel = step.stableSelector || step.selector;
|
|
3734
3839
|
if (!sel) continue;
|
|
3735
3840
|
const selLower = sel.toLowerCase();
|
|
3736
|
-
|
|
3841
|
+
|
|
3842
|
+
// Check exact match first
|
|
3737
3843
|
if (seenSelectors.has(selLower)) {
|
|
3738
|
-
const
|
|
3739
|
-
|
|
3740
|
-
if (
|
|
3844
|
+
const match = existing.find(e => (e.stableSelector||e.selector||'').toLowerCase() === selLower) ||
|
|
3845
|
+
toAdd.find(e => (e.stableSelector||e.selector||'').toLowerCase() === selLower);
|
|
3846
|
+
if (match) match.usageCount = (match.usageCount || 0) + 1;
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
// Check similarity against existing library entries (catches slight variations)
|
|
3851
|
+
const similarExisting = existing.find(e => stepSimilarity(e, step) >= 0.85);
|
|
3852
|
+
if (similarExisting) {
|
|
3853
|
+
similarExisting.usageCount = (similarExisting.usageCount || 0) + 1;
|
|
3854
|
+
// If new selector is more stable (has pi-test-identifier), upgrade it
|
|
3855
|
+
if (sel.includes('pi-test-identifier') && !(similarExisting.stableSelector||'').includes('pi-test-identifier')) {
|
|
3856
|
+
similarExisting.stableSelector = sel;
|
|
3857
|
+
similarExisting.selector = sel;
|
|
3858
|
+
}
|
|
3859
|
+
seenSelectors.add(selLower);
|
|
3741
3860
|
continue;
|
|
3742
3861
|
}
|
|
3862
|
+
|
|
3863
|
+
// Check similarity against steps we're about to add
|
|
3864
|
+
const similarNew = toAdd.find(e => stepSimilarity(e, step) >= 0.85);
|
|
3865
|
+
if (similarNew) {
|
|
3866
|
+
similarNew.usageCount = (similarNew.usageCount || 0) + 1;
|
|
3867
|
+
seenSelectors.add(selLower);
|
|
3868
|
+
continue;
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3743
3871
|
seenSelectors.add(selLower);
|
|
3744
3872
|
// Generate clean name — strip action prefix
|
|
3745
3873
|
let name = step.description || '';
|
|
3746
3874
|
name = name.replace(/^(click|type|check|assert|select)\s+/i, '').trim() || sel.slice(0, 50);
|
|
3747
3875
|
toAdd.push({
|
|
3748
|
-
id: 'lib-' + Math.random().toString(36).slice(2, 10),
|
|
3749
3876
|
name,
|
|
3750
3877
|
selector: sel,
|
|
3751
3878
|
stableSelector: step.stableSelector || sel,
|
|
@@ -3757,29 +3884,32 @@ async function extractStepsToLibrary(suitesDir, steps, testName) {
|
|
|
3757
3884
|
tags: [],
|
|
3758
3885
|
usageCount: 1,
|
|
3759
3886
|
sourceTest: testName,
|
|
3760
|
-
createdAt: new Date().toISOString(),
|
|
3761
3887
|
});
|
|
3762
3888
|
}
|
|
3763
3889
|
|
|
3764
|
-
|
|
3765
|
-
|
|
3890
|
+
// Add new elements to pending review queue (not directly to library)
|
|
3891
|
+
for (const step of toAdd) {
|
|
3892
|
+
await addToPending(suitesDir, step);
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
// Save updated usage counts for existing entries
|
|
3896
|
+
if (existing.some(e => e.usageCount !== undefined)) {
|
|
3897
|
+
await saveLibrarySteps(existing);
|
|
3766
3898
|
}
|
|
3767
3899
|
}
|
|
3768
3900
|
|
|
3769
3901
|
// Import steps from all existing tests into library
|
|
3770
3902
|
async function importStepsFromTests(suitesDir) {
|
|
3771
3903
|
const allTests = await listAllTests(suitesDir);
|
|
3772
|
-
let
|
|
3773
|
-
let skipped = 0;
|
|
3904
|
+
let queued = 0;
|
|
3774
3905
|
for (const test of allTests) {
|
|
3775
3906
|
if (!test.steps || test.steps.length === 0) continue;
|
|
3776
|
-
const before = (await
|
|
3907
|
+
const before = (await listPendingSteps()).length;
|
|
3777
3908
|
await extractStepsToLibrary(suitesDir, test.steps, test.name);
|
|
3778
|
-
const after = (await
|
|
3779
|
-
|
|
3780
|
-
skipped += test.steps.length - (after - before);
|
|
3909
|
+
const after = (await listPendingSteps()).length;
|
|
3910
|
+
queued += after - before;
|
|
3781
3911
|
}
|
|
3782
|
-
return {
|
|
3912
|
+
return { queued, message: `${queued} elements added to review queue` };
|
|
3783
3913
|
}
|
|
3784
3914
|
|
|
3785
3915
|
// Sync existing tests — replace matching steps with library steps
|
package/package.json
CHANGED
package/web/app/index.html
CHANGED
|
@@ -1321,6 +1321,7 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1321
1321
|
<a class="nav-item" data-view="step-library">
|
|
1322
1322
|
<span class="nav-icon"><svg 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"/><rect x="3" y="17" width="18" height="4" rx="1"/></svg></span>
|
|
1323
1323
|
Step library
|
|
1324
|
+
<span id="lib-pending-badge" style="display:none;background:#f59e0b;color:#000;font-size:9px;font-weight:700;padding:1px 5px;border-radius:8px;margin-left:4px;font-family:var(--mono)"></span>
|
|
1324
1325
|
</a>
|
|
1325
1326
|
|
|
1326
1327
|
|
|
@@ -1589,6 +1590,23 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1589
1590
|
</div>
|
|
1590
1591
|
</div>
|
|
1591
1592
|
|
|
1593
|
+
<!-- Pending review section -->
|
|
1594
|
+
<div id="library-pending-section" style="display:none;margin-bottom:16px">
|
|
1595
|
+
<div class="card" style="border-color:rgba(245,158,11,0.3);background:rgba(245,158,11,0.04)">
|
|
1596
|
+
<div class="card-header" style="border-color:rgba(245,158,11,0.2)">
|
|
1597
|
+
<div class="card-title" style="color:#f59e0b">
|
|
1598
|
+
⏳ Pending review
|
|
1599
|
+
<span id="pending-count-label" style="font-family:var(--mono);font-size:11px;color:var(--muted);font-weight:400;margin-left:8px"></span>
|
|
1600
|
+
</div>
|
|
1601
|
+
<div style="display:flex;gap:8px">
|
|
1602
|
+
<button class="btn btn-ghost" style="font-size:11px;padding:4px 10px" onclick="dismissAllPendingUI()">Dismiss all</button>
|
|
1603
|
+
<button class="btn btn-primary" style="font-size:11px;padding:4px 10px;background:#f59e0b;border-color:#f59e0b;color:#000" onclick="approveAllPendingUI()">Add all to library</button>
|
|
1604
|
+
</div>
|
|
1605
|
+
</div>
|
|
1606
|
+
<div id="pending-steps-list" style="padding:0"></div>
|
|
1607
|
+
</div>
|
|
1608
|
+
</div>
|
|
1609
|
+
|
|
1592
1610
|
<!-- Search + filter -->
|
|
1593
1611
|
<div style="display:flex;gap:10px;margin-bottom:16px">
|
|
1594
1612
|
<input class="form-input" id="library-search" type="text" placeholder="Search elements..." oninput="filterLibrarySteps()" style="flex:1">
|
|
@@ -1663,6 +1681,10 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1663
1681
|
</div>
|
|
1664
1682
|
<div style="display:flex;gap:8px;align-items:center">
|
|
1665
1683
|
<input class="form-input" id="builder-test-name" type="text" placeholder="Test name..." style="width:220px">
|
|
1684
|
+
<button class="btn btn-ghost" id="btn-preview-test" onclick="startBuilderPreview()" style="color:#f59e0b;border-color:rgba(245,158,11,0.3)">
|
|
1685
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
|
1686
|
+
Preview
|
|
1687
|
+
</button>
|
|
1666
1688
|
<button class="btn btn-primary" onclick="saveBuiltTest()">Save test</button>
|
|
1667
1689
|
</div>
|
|
1668
1690
|
</div>
|
|
@@ -1686,7 +1708,7 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1686
1708
|
</div>
|
|
1687
1709
|
|
|
1688
1710
|
<!-- RIGHT: Test steps being built -->
|
|
1689
|
-
<div style="display:flex;flex-direction:column;overflow:hidden">
|
|
1711
|
+
<div style="display:flex;flex-direction:column;overflow:hidden;position:relative">
|
|
1690
1712
|
<div style="padding:14px 20px;border-bottom:1px solid var(--border);flex-shrink:0;display:flex;align-items:center;justify-content:space-between">
|
|
1691
1713
|
<div>
|
|
1692
1714
|
<div style="font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:0.1em">TEST STEPS</div>
|
|
@@ -1702,6 +1724,36 @@ body.viewer-mode .saved-test-row { cursor: default !important; }
|
|
|
1702
1724
|
← Click steps from the library to add them here
|
|
1703
1725
|
</div>
|
|
1704
1726
|
</div>
|
|
1727
|
+
|
|
1728
|
+
<!-- PREVIEW OVERLAY -->
|
|
1729
|
+
<div id="builder-preview-overlay" style="display:none;position:absolute;inset:0;background:var(--bg);flex-direction:column;z-index:10">
|
|
1730
|
+
<div style="padding:14px 20px;border-bottom:1px solid var(--border);flex-shrink:0;display:flex;align-items:center;justify-content:space-between;background:rgba(245,158,11,0.06)">
|
|
1731
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
1732
|
+
<span style="color:#f59e0b;font-size:14px" id="preview-status-dot">⏸</span>
|
|
1733
|
+
<div>
|
|
1734
|
+
<div style="font-family:var(--mono);font-size:10px;color:#f59e0b;letter-spacing:0.1em">PREVIEW MODE</div>
|
|
1735
|
+
<div style="font-family:var(--mono);font-size:11px;color:var(--muted);margin-top:2px" id="preview-status-text">Browser open — ready to run</div>
|
|
1736
|
+
</div>
|
|
1737
|
+
</div>
|
|
1738
|
+
<div style="display:flex;gap:8px">
|
|
1739
|
+
<button class="btn btn-ghost" id="btn-preview-run-all" onclick="previewRunAll()" style="font-size:11px;padding:4px 12px;color:#f59e0b;border-color:rgba(245,158,11,0.3)">
|
|
1740
|
+
▶▶ Run all
|
|
1741
|
+
</button>
|
|
1742
|
+
<button class="btn btn-ghost" id="btn-preview-run-next" onclick="previewRunNext()" style="font-size:11px;padding:4px 12px;color:#f59e0b;border-color:rgba(245,158,11,0.3)">
|
|
1743
|
+
▶ Run next
|
|
1744
|
+
</button>
|
|
1745
|
+
<button class="btn btn-ghost" onclick="stopBuilderPreview()" style="font-size:11px;padding:4px 10px">
|
|
1746
|
+
✕ Stop preview
|
|
1747
|
+
</button>
|
|
1748
|
+
</div>
|
|
1749
|
+
</div>
|
|
1750
|
+
<div id="preview-steps-list" style="overflow-y:auto;flex:1;padding:8px 0"></div>
|
|
1751
|
+
<div style="padding:12px 20px;border-top:1px solid var(--border);flex-shrink:0;display:flex;align-items:center;gap:10px" id="preview-summary" style="display:none">
|
|
1752
|
+
<span id="preview-passed" style="font-family:var(--mono);font-size:12px;color:#34d399"></span>
|
|
1753
|
+
<span id="preview-failed" style="font-family:var(--mono);font-size:12px;color:#ef4444"></span>
|
|
1754
|
+
</div>
|
|
1755
|
+
</div>
|
|
1756
|
+
|
|
1705
1757
|
</div>
|
|
1706
1758
|
|
|
1707
1759
|
</div>
|
|
@@ -2791,6 +2843,7 @@ async function refreshAll() {
|
|
|
2791
2843
|
renderConfig();
|
|
2792
2844
|
renderSuites();
|
|
2793
2845
|
populateSuiteSelect();
|
|
2846
|
+
checkPendingCount();
|
|
2794
2847
|
}
|
|
2795
2848
|
|
|
2796
2849
|
function renderStats() {
|
|
@@ -5549,8 +5602,122 @@ async function saveOllamaConfig() {
|
|
|
5549
5602
|
}
|
|
5550
5603
|
}
|
|
5551
5604
|
|
|
5552
|
-
// ── STEP LIBRARY
|
|
5553
|
-
let
|
|
5605
|
+
// ── STEP LIBRARY PENDING REVIEW ──────────────────────────────────────────────
|
|
5606
|
+
let pendingStepsCache = [];
|
|
5607
|
+
|
|
5608
|
+
async function fetchPendingSteps() {
|
|
5609
|
+
try {
|
|
5610
|
+
const res = await fetch(API_BASE + '/api/step-library/pending');
|
|
5611
|
+
if (!res.ok) return [];
|
|
5612
|
+
pendingStepsCache = await res.json();
|
|
5613
|
+
return pendingStepsCache;
|
|
5614
|
+
} catch { return []; }
|
|
5615
|
+
}
|
|
5616
|
+
|
|
5617
|
+
function updatePendingBadge(count) {
|
|
5618
|
+
const badge = document.getElementById('lib-pending-badge');
|
|
5619
|
+
if (!badge) return;
|
|
5620
|
+
if (count > 0) { badge.textContent = count; badge.style.display = ''; }
|
|
5621
|
+
else { badge.style.display = 'none'; }
|
|
5622
|
+
}
|
|
5623
|
+
|
|
5624
|
+
function renderPendingSteps(steps) {
|
|
5625
|
+
const section = document.getElementById('library-pending-section');
|
|
5626
|
+
const list = document.getElementById('pending-steps-list');
|
|
5627
|
+
const countLabel = document.getElementById('pending-count-label');
|
|
5628
|
+
if (!section || !list) return;
|
|
5629
|
+
|
|
5630
|
+
if (!steps || steps.length === 0) {
|
|
5631
|
+
section.style.display = 'none';
|
|
5632
|
+
updatePendingBadge(0);
|
|
5633
|
+
return;
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
section.style.display = '';
|
|
5637
|
+
updatePendingBadge(steps.length);
|
|
5638
|
+
if (countLabel) countLabel.textContent = `${steps.length} element${steps.length > 1 ? 's' : ''} waiting`;
|
|
5639
|
+
|
|
5640
|
+
list.innerHTML = steps.map(s => {
|
|
5641
|
+
const sel = s.stableSelector || s.selector || '';
|
|
5642
|
+
const fragile = isSelectorFragile(sel);
|
|
5643
|
+
return `
|
|
5644
|
+
<div style="display:flex;align-items:center;gap:12px;padding:12px 20px;border-bottom:1px solid rgba(245,158,11,0.1)" data-pending-id="${escapeAttr(s.id)}">
|
|
5645
|
+
<div style="flex:1;min-width:0">
|
|
5646
|
+
<div style="font-size:13px;color:var(--text);margin-bottom:3px">${escapeHtml(s.name||'')}</div>
|
|
5647
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
5648
|
+
${fragile ? '<span style="color:#f59e0b;font-size:12px" title="Fragile selector">⚠</span>' : ''}
|
|
5649
|
+
<code style="font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;max-width:400px">${escapeHtml(sel)}</code>
|
|
5650
|
+
</div>
|
|
5651
|
+
${s.sourceTest ? `<div style="font-family:var(--mono);font-size:10px;color:var(--muted2);margin-top:2px">from: ${escapeHtml(s.sourceTest)}</div>` : ''}
|
|
5652
|
+
</div>
|
|
5653
|
+
<div style="display:flex;gap:6px;flex-shrink:0">
|
|
5654
|
+
<button class="btn btn-ghost pending-edit-btn" style="padding:4px 10px;font-size:11px" data-id="${escapeAttr(s.id)}">Edit</button>
|
|
5655
|
+
<button class="btn btn-ghost pending-dismiss-btn" style="padding:4px 10px;font-size:11px;color:var(--red)" data-id="${escapeAttr(s.id)}">Dismiss</button>
|
|
5656
|
+
<button class="btn btn-primary pending-approve-btn" style="padding:4px 10px;font-size:11px;background:#f59e0b;border-color:#f59e0b;color:#000" data-id="${escapeAttr(s.id)}">Add</button>
|
|
5657
|
+
</div>
|
|
5658
|
+
</div>`;
|
|
5659
|
+
}).join('');
|
|
5660
|
+
}
|
|
5661
|
+
|
|
5662
|
+
async function loadLibraryView() {
|
|
5663
|
+
const [steps, pending] = await Promise.all([fetchLibrarySteps(), fetchPendingSteps()]);
|
|
5664
|
+
populateLibraryTagFilter();
|
|
5665
|
+
renderPendingSteps(pending);
|
|
5666
|
+
renderLibrarySteps(steps);
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
async function approvePendingUI(id) {
|
|
5670
|
+
await fetch(API_BASE + '/api/step-library/pending/' + encodeURIComponent(id) + '/approve', { method: 'POST' });
|
|
5671
|
+
await loadLibraryView();
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5674
|
+
async function dismissPendingUI(id) {
|
|
5675
|
+
await fetch(API_BASE + '/api/step-library/pending/' + encodeURIComponent(id) + '/dismiss', { method: 'POST' });
|
|
5676
|
+
const pending = await fetchPendingSteps();
|
|
5677
|
+
renderPendingSteps(pending);
|
|
5678
|
+
}
|
|
5679
|
+
|
|
5680
|
+
async function approveAllPendingUI() {
|
|
5681
|
+
const res = await fetch(API_BASE + '/api/step-library/pending/approve-all', { method: 'POST' });
|
|
5682
|
+
const data = await res.json();
|
|
5683
|
+
showToast(`Added ${data.approved} elements to library`);
|
|
5684
|
+
await loadLibraryView();
|
|
5685
|
+
}
|
|
5686
|
+
|
|
5687
|
+
async function dismissAllPendingUI() {
|
|
5688
|
+
showConfirm('Dismiss all?', 'Dismiss all pending elements without adding them to the library?', async () => {
|
|
5689
|
+
await fetch(API_BASE + '/api/step-library/pending/dismiss-all', { method: 'POST' });
|
|
5690
|
+
await loadLibraryView();
|
|
5691
|
+
});
|
|
5692
|
+
}
|
|
5693
|
+
|
|
5694
|
+
// Also check pending count on page load and periodically
|
|
5695
|
+
async function checkPendingCount() {
|
|
5696
|
+
const pending = await fetchPendingSteps();
|
|
5697
|
+
updatePendingBadge(pending.length);
|
|
5698
|
+
}
|
|
5699
|
+
|
|
5700
|
+
// Event delegation for pending buttons
|
|
5701
|
+
document.addEventListener('click', (e) => {
|
|
5702
|
+
const approveBtn = e.target.closest('.pending-approve-btn');
|
|
5703
|
+
const dismissBtn = e.target.closest('.pending-dismiss-btn');
|
|
5704
|
+
const editBtn = e.target.closest('.pending-edit-btn');
|
|
5705
|
+
if (approveBtn) approvePendingUI(approveBtn.dataset.id);
|
|
5706
|
+
if (dismissBtn) dismissPendingUI(dismissBtn.dataset.id);
|
|
5707
|
+
if (editBtn) {
|
|
5708
|
+
// Open edit modal pre-filled with pending step data
|
|
5709
|
+
const step = pendingStepsCache.find(s => s.id === editBtn.dataset.id);
|
|
5710
|
+
if (step) {
|
|
5711
|
+
document.getElementById('lib-step-id').value = 'pending:' + step.id;
|
|
5712
|
+
document.getElementById('lib-step-name').value = step.name || '';
|
|
5713
|
+
document.getElementById('lib-step-selector').value = step.stableSelector || step.selector || '';
|
|
5714
|
+
document.getElementById('lib-step-action').value = step.defaultAction || '';
|
|
5715
|
+
document.getElementById('lib-step-tags').value = (step.tags || []).join(', ');
|
|
5716
|
+
document.getElementById('library-step-modal-title').textContent = 'Edit before adding';
|
|
5717
|
+
document.getElementById('library-step-modal').style.display = 'flex';
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
});
|
|
5554
5721
|
|
|
5555
5722
|
async function fetchLibrarySteps() {
|
|
5556
5723
|
try {
|
|
@@ -5861,7 +6028,9 @@ function closeLibraryStepModal() {
|
|
|
5861
6028
|
}
|
|
5862
6029
|
|
|
5863
6030
|
async function saveLibraryStep() {
|
|
5864
|
-
const
|
|
6031
|
+
const rawId = document.getElementById('lib-step-id').value;
|
|
6032
|
+
const isPending = rawId.startsWith('pending:');
|
|
6033
|
+
const id = isPending ? rawId.slice(8) : rawId;
|
|
5865
6034
|
const name = document.getElementById('lib-step-name').value.trim();
|
|
5866
6035
|
const selector = document.getElementById('lib-step-selector').value.trim();
|
|
5867
6036
|
const defaultAction = document.getElementById('lib-step-action').value;
|
|
@@ -5871,14 +6040,23 @@ async function saveLibraryStep() {
|
|
|
5871
6040
|
if (!selector) { showToast('Selector is required'); return; }
|
|
5872
6041
|
|
|
5873
6042
|
try {
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
6043
|
+
if (isPending) {
|
|
6044
|
+
// Dismiss pending and add directly to library with edits
|
|
6045
|
+
await fetch(API_BASE + '/api/step-library/pending/' + encodeURIComponent(id) + '/dismiss', { method: 'POST' });
|
|
6046
|
+
const body = { name, selector, stableSelector: selector, defaultAction: defaultAction || null, tags };
|
|
6047
|
+
await fetch(API_BASE + '/api/step-library', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
6048
|
+
} else if (id) {
|
|
6049
|
+
const body = { name, selector, stableSelector: selector, defaultAction: defaultAction || null, tags };
|
|
6050
|
+
const res = await fetch(API_BASE + '/api/step-library/' + encodeURIComponent(id), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
6051
|
+
if (!res.ok) throw new Error(await res.text());
|
|
6052
|
+
} else {
|
|
6053
|
+
const body = { name, selector, stableSelector: selector, defaultAction: defaultAction || null, tags };
|
|
6054
|
+
const res = await fetch(API_BASE + '/api/step-library', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
6055
|
+
if (!res.ok) throw new Error(await res.text());
|
|
6056
|
+
}
|
|
5879
6057
|
closeLibraryStepModal();
|
|
5880
6058
|
await loadLibraryView();
|
|
5881
|
-
showToast(id ? 'Element updated' : 'Element added to library');
|
|
6059
|
+
showToast(isPending ? 'Element added to library' : id ? 'Element updated' : 'Element added to library');
|
|
5882
6060
|
} catch (err) { showToast('Error: ' + err.message); }
|
|
5883
6061
|
}
|
|
5884
6062
|
|
|
@@ -5896,7 +6074,7 @@ async function importStepsFromTests() {
|
|
|
5896
6074
|
try {
|
|
5897
6075
|
const res = await fetch(API_BASE + '/api/step-library/import', { method: 'POST' });
|
|
5898
6076
|
const data = await res.json();
|
|
5899
|
-
showToast(
|
|
6077
|
+
showToast(data.queued > 0 ? `${data.queued} elements added to review queue` : 'No new elements found');
|
|
5900
6078
|
await loadLibraryView();
|
|
5901
6079
|
} catch (err) { showToast('Error: ' + err.message); }
|
|
5902
6080
|
finally { if (btn) { btn.disabled = false; btn.textContent = 'Import from all tests'; } }
|
|
@@ -5939,9 +6117,14 @@ function openTestBuilder() {
|
|
|
5939
6117
|
}
|
|
5940
6118
|
|
|
5941
6119
|
function closeTestBuilder() {
|
|
6120
|
+
// Stop preview if active
|
|
6121
|
+
if (previewState.testerId) {
|
|
6122
|
+
fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/stop', { method: 'POST' }).catch(() => {});
|
|
6123
|
+
previewState.testerId = null;
|
|
6124
|
+
}
|
|
6125
|
+
document.getElementById('builder-preview-overlay').style.display = 'none';
|
|
5942
6126
|
const builder = document.getElementById('view-test-builder');
|
|
5943
6127
|
if (builder) builder.style.display = 'none';
|
|
5944
|
-
// Restore all views to their default display state before switching
|
|
5945
6128
|
document.querySelectorAll('.view').forEach(v => {
|
|
5946
6129
|
if (v.id !== 'view-test-builder') v.style.display = '';
|
|
5947
6130
|
});
|
|
@@ -6238,6 +6421,204 @@ function harvestAnother() {
|
|
|
6238
6421
|
document.getElementById('harvester-status').textContent = 'Navigate to the next element, then click Capture.';
|
|
6239
6422
|
}
|
|
6240
6423
|
|
|
6424
|
+
// ── TEST BUILDER PREVIEW ─────────────────────────────────────────────────────
|
|
6425
|
+
let previewState = { testerId: null, currentStep: 0, results: [], running: false };
|
|
6426
|
+
|
|
6427
|
+
function renderPreviewSteps() {
|
|
6428
|
+
const container = document.getElementById('preview-steps-list');
|
|
6429
|
+
if (!container) return;
|
|
6430
|
+
const actionColors = { click:'#22d3ee', type:'#a78bfa', check:'#34d399', assert:'#fb923c', select:'#f472b6', scroll:'#6b7280' };
|
|
6431
|
+
|
|
6432
|
+
container.innerHTML = builderSteps.map((s, i) => {
|
|
6433
|
+
const result = previewState.results[i];
|
|
6434
|
+
const isCurrent = i === previewState.currentStep && !result;
|
|
6435
|
+
const status = result ? (result.passed ? '✓' : '✗') : (isCurrent ? '▶' : '○');
|
|
6436
|
+
const statusColor = result ? (result.passed ? '#34d399' : '#ef4444') : (isCurrent ? '#f59e0b' : 'var(--muted2)');
|
|
6437
|
+
const bg = isCurrent ? 'rgba(245,158,11,0.06)' : result && !result.passed ? 'rgba(239,68,68,0.04)' : '';
|
|
6438
|
+
return `<div style="display:flex;align-items:flex-start;gap:12px;padding:10px 20px;border-bottom:1px solid var(--border);background:${bg}">
|
|
6439
|
+
<span style="font-family:var(--mono);font-size:13px;color:${statusColor};min-width:20px;margin-top:1px">${status}</span>
|
|
6440
|
+
<div style="flex:1;min-width:0">
|
|
6441
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
6442
|
+
<span style="font-family:var(--mono);font-size:10px;color:${actionColors[s.action]||'var(--muted)'}">${s.action}</span>
|
|
6443
|
+
<span style="font-size:12px;color:var(--text)">${escapeHtml(s.description||'')}</span>
|
|
6444
|
+
</div>
|
|
6445
|
+
<code style="font-size:10px;color:var(--muted2)">${escapeHtml(s.stableSelector||s.selector||'')}</code>
|
|
6446
|
+
${result && !result.passed ? `<div style="font-family:var(--mono);font-size:11px;color:#ef4444;margin-top:4px">✗ ${escapeHtml(result.error||'Failed')}</div>` : ''}
|
|
6447
|
+
${result && result.screenshot ? `<img src="data:image/jpeg;base64,${result.screenshot}" style="width:100%;max-width:300px;border-radius:6px;margin-top:8px;border:1px solid var(--border)" loading="lazy">` : ''}
|
|
6448
|
+
</div>
|
|
6449
|
+
</div>`;
|
|
6450
|
+
}).join('');
|
|
6451
|
+
|
|
6452
|
+
// Scroll current step into view
|
|
6453
|
+
const items = container.querySelectorAll('div[style*="border-bottom"]');
|
|
6454
|
+
if (items[previewState.currentStep]) items[previewState.currentStep].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
6455
|
+
}
|
|
6456
|
+
|
|
6457
|
+
function updatePreviewSummary() {
|
|
6458
|
+
const passed = previewState.results.filter(r => r && r.passed).length;
|
|
6459
|
+
const failed = previewState.results.filter(r => r && !r.passed).length;
|
|
6460
|
+
const total = previewState.results.filter(r => r).length;
|
|
6461
|
+
if (total === 0) return;
|
|
6462
|
+
document.getElementById('preview-passed').textContent = `✓ ${passed} passed`;
|
|
6463
|
+
document.getElementById('preview-failed').textContent = failed > 0 ? `✗ ${failed} failed` : '';
|
|
6464
|
+
document.getElementById('preview-summary').style.display = 'flex';
|
|
6465
|
+
}
|
|
6466
|
+
|
|
6467
|
+
async function startBuilderPreview() {
|
|
6468
|
+
const url = document.getElementById('builder-url')?.value?.trim();
|
|
6469
|
+
if (builderSteps.length === 0) { showToast('Add steps first'); return; }
|
|
6470
|
+
if (!url) { showToast('Set a start URL first'); document.getElementById('builder-url').focus(); return; }
|
|
6471
|
+
|
|
6472
|
+
const btn = document.getElementById('btn-preview-test');
|
|
6473
|
+
btn.disabled = true;
|
|
6474
|
+
btn.textContent = 'Opening...';
|
|
6475
|
+
|
|
6476
|
+
try {
|
|
6477
|
+
const res = await fetch(API_BASE + '/api/step-tester/start', {
|
|
6478
|
+
method: 'POST',
|
|
6479
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6480
|
+
body: JSON.stringify({ url, selector: builderSteps[0]?.stableSelector || '' }),
|
|
6481
|
+
});
|
|
6482
|
+
const data = await res.json();
|
|
6483
|
+
if (!res.ok) throw new Error(data.error || 'Failed');
|
|
6484
|
+
|
|
6485
|
+
previewState = { testerId: data.testerId, currentStep: 0, results: [], running: false };
|
|
6486
|
+
|
|
6487
|
+
// Show overlay
|
|
6488
|
+
document.getElementById('builder-preview-overlay').style.display = 'flex';
|
|
6489
|
+
document.getElementById('preview-status-text').textContent = 'Browser open — navigate to start, then click Run next or Run all';
|
|
6490
|
+
document.getElementById('preview-summary').style.display = 'none';
|
|
6491
|
+
renderPreviewSteps();
|
|
6492
|
+
btn.textContent = '⏸ Previewing';
|
|
6493
|
+
} catch (err) {
|
|
6494
|
+
showToast('Error: ' + err.message);
|
|
6495
|
+
btn.disabled = false;
|
|
6496
|
+
btn.textContent = 'Preview';
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
|
|
6500
|
+
async function stopBuilderPreview() {
|
|
6501
|
+
if (previewState.testerId) {
|
|
6502
|
+
await fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/stop', { method: 'POST' }).catch(() => {});
|
|
6503
|
+
previewState.testerId = null;
|
|
6504
|
+
}
|
|
6505
|
+
document.getElementById('builder-preview-overlay').style.display = 'none';
|
|
6506
|
+
const btn = document.getElementById('btn-preview-test');
|
|
6507
|
+
btn.disabled = false;
|
|
6508
|
+
btn.textContent = 'Preview';
|
|
6509
|
+
}
|
|
6510
|
+
|
|
6511
|
+
async function previewRunNext() {
|
|
6512
|
+
if (!previewState.testerId || previewState.running) return;
|
|
6513
|
+
if (previewState.currentStep >= builderSteps.length) return;
|
|
6514
|
+
|
|
6515
|
+
previewState.running = true;
|
|
6516
|
+
const step = builderSteps[previewState.currentStep];
|
|
6517
|
+
document.getElementById('preview-status-text').textContent = `Running step ${previewState.currentStep + 1}/${builderSteps.length}...`;
|
|
6518
|
+
document.getElementById('btn-preview-run-next').disabled = true;
|
|
6519
|
+
document.getElementById('btn-preview-run-all').disabled = true;
|
|
6520
|
+
renderPreviewSteps();
|
|
6521
|
+
|
|
6522
|
+
try {
|
|
6523
|
+
const res = await fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/run', {
|
|
6524
|
+
method: 'POST',
|
|
6525
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6526
|
+
body: JSON.stringify({ selector: step.stableSelector || step.selector, action: step.action, value: step.value || '', assertType: step.assertType }),
|
|
6527
|
+
});
|
|
6528
|
+
const result = await res.json();
|
|
6529
|
+
previewState.results[previewState.currentStep] = result;
|
|
6530
|
+
|
|
6531
|
+
if (result.passed) {
|
|
6532
|
+
previewState.currentStep++;
|
|
6533
|
+
document.getElementById('preview-status-text').textContent =
|
|
6534
|
+
previewState.currentStep >= builderSteps.length
|
|
6535
|
+
? `✓ All ${builderSteps.length} steps passed!`
|
|
6536
|
+
: `✓ Step ${previewState.currentStep} passed — ready for next`;
|
|
6537
|
+
document.getElementById('preview-status-dot').textContent = previewState.currentStep >= builderSteps.length ? '✓' : '⏸';
|
|
6538
|
+
document.getElementById('preview-status-dot').style.color = previewState.currentStep >= builderSteps.length ? '#34d399' : '#f59e0b';
|
|
6539
|
+
} else {
|
|
6540
|
+
document.getElementById('preview-status-text').textContent = `✗ Step ${previewState.currentStep + 1} failed — fix the step and try again`;
|
|
6541
|
+
document.getElementById('preview-status-dot').textContent = '✗';
|
|
6542
|
+
document.getElementById('preview-status-dot').style.color = '#ef4444';
|
|
6543
|
+
}
|
|
6544
|
+
} catch (err) {
|
|
6545
|
+
previewState.results[previewState.currentStep] = { passed: false, error: err.message };
|
|
6546
|
+
}
|
|
6547
|
+
|
|
6548
|
+
previewState.running = false;
|
|
6549
|
+
document.getElementById('btn-preview-run-next').disabled = previewState.currentStep >= builderSteps.length;
|
|
6550
|
+
document.getElementById('btn-preview-run-all').disabled = false;
|
|
6551
|
+
renderPreviewSteps();
|
|
6552
|
+
updatePreviewSummary();
|
|
6553
|
+
}
|
|
6554
|
+
|
|
6555
|
+
async function previewRunAll() {
|
|
6556
|
+
if (!previewState.testerId || previewState.running) return;
|
|
6557
|
+
previewState.running = true;
|
|
6558
|
+
previewState.results = [];
|
|
6559
|
+
previewState.currentStep = 0;
|
|
6560
|
+
document.getElementById('btn-preview-run-all').disabled = true;
|
|
6561
|
+
document.getElementById('btn-preview-run-next').disabled = true;
|
|
6562
|
+
document.getElementById('preview-summary').style.display = 'none';
|
|
6563
|
+
|
|
6564
|
+
const steps = builderSteps.map(s => ({
|
|
6565
|
+
selector: s.stableSelector || s.selector,
|
|
6566
|
+
action: s.action,
|
|
6567
|
+
value: s.value || '',
|
|
6568
|
+
assertType: s.assertType,
|
|
6569
|
+
}));
|
|
6570
|
+
|
|
6571
|
+
try {
|
|
6572
|
+
const es = new EventSource(API_BASE + '/api/step-tester/' + previewState.testerId + '/run-sequence?' + new URLSearchParams({ steps: JSON.stringify(steps) }));
|
|
6573
|
+
|
|
6574
|
+
// Use POST with EventSource workaround — send steps via a separate POST then listen
|
|
6575
|
+
const res = await fetch(API_BASE + '/api/step-tester/' + previewState.testerId + '/run-sequence', {
|
|
6576
|
+
method: 'POST',
|
|
6577
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6578
|
+
body: JSON.stringify({ steps }),
|
|
6579
|
+
});
|
|
6580
|
+
|
|
6581
|
+
const reader = res.body.getReader();
|
|
6582
|
+
const decoder = new TextDecoder();
|
|
6583
|
+
let buffer = '';
|
|
6584
|
+
|
|
6585
|
+
while (true) {
|
|
6586
|
+
const { done, value } = await reader.read();
|
|
6587
|
+
if (done) break;
|
|
6588
|
+
buffer += decoder.decode(value, { stream: true });
|
|
6589
|
+
const lines = buffer.split('\n');
|
|
6590
|
+
buffer = lines.pop();
|
|
6591
|
+
for (const line of lines) {
|
|
6592
|
+
if (!line.startsWith('data: ')) continue;
|
|
6593
|
+
try {
|
|
6594
|
+
const msg = JSON.parse(line.slice(6));
|
|
6595
|
+
if (msg.type === 'step-start') {
|
|
6596
|
+
previewState.currentStep = msg.index;
|
|
6597
|
+
document.getElementById('preview-status-text').textContent = `Running step ${msg.index + 1}/${builderSteps.length}...`;
|
|
6598
|
+
renderPreviewSteps();
|
|
6599
|
+
} else if (msg.type === 'step-result') {
|
|
6600
|
+
previewState.results[msg.index] = { passed: msg.passed, error: msg.error, screenshot: msg.screenshot };
|
|
6601
|
+
previewState.currentStep = msg.passed ? msg.index + 1 : msg.index;
|
|
6602
|
+
renderPreviewSteps();
|
|
6603
|
+
updatePreviewSummary();
|
|
6604
|
+
} else if (msg.type === 'done') {
|
|
6605
|
+
const allPassed = previewState.results.every(r => r && r.passed);
|
|
6606
|
+
document.getElementById('preview-status-text').textContent = allPassed ? `✓ All ${builderSteps.length} steps passed!` : `✗ Preview complete — some steps failed`;
|
|
6607
|
+
document.getElementById('preview-status-dot').textContent = allPassed ? '✓' : '✗';
|
|
6608
|
+
document.getElementById('preview-status-dot').style.color = allPassed ? '#34d399' : '#ef4444';
|
|
6609
|
+
}
|
|
6610
|
+
} catch {}
|
|
6611
|
+
}
|
|
6612
|
+
}
|
|
6613
|
+
} catch (err) {
|
|
6614
|
+
document.getElementById('preview-status-text').textContent = 'Error: ' + err.message;
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6617
|
+
previewState.running = false;
|
|
6618
|
+
document.getElementById('btn-preview-run-all').disabled = false;
|
|
6619
|
+
document.getElementById('btn-preview-run-next').disabled = previewState.currentStep >= builderSteps.length;
|
|
6620
|
+
}
|
|
6621
|
+
|
|
6241
6622
|
// ── SUITE TEST PICKER ────────────────────────────────────────────────────────
|
|
6242
6623
|
function openSuiteTestPicker() {
|
|
6243
6624
|
const list = document.getElementById('stp-test-list');
|