skopix 2.0.23 → 2.0.25
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 +137 -4
- package/web/index.html +6490 -594
package/package.json
CHANGED
package/web/app/index.html
CHANGED
|
@@ -5401,6 +5401,23 @@ async function fetchLibrarySteps() {
|
|
|
5401
5401
|
} catch { return []; }
|
|
5402
5402
|
}
|
|
5403
5403
|
|
|
5404
|
+
// Detect fragile/dynamic selectors
|
|
5405
|
+
function isSelectorFragile(sel) {
|
|
5406
|
+
if (!sel) return false;
|
|
5407
|
+
if (/nth-of-type|nth-child/.test(sel)) return true; // positional
|
|
5408
|
+
if (/#[a-z]+-\d+/.test(sel)) return true; // dynamic IDs like #cell-446
|
|
5409
|
+
if (/\[\w+=["'][^"']*\d{3,}[^"']*["']\]/.test(sel)) return true; // long numeric attr values
|
|
5410
|
+
return false;
|
|
5411
|
+
}
|
|
5412
|
+
|
|
5413
|
+
function getSelectorWarning(sel) {
|
|
5414
|
+
if (!sel) return null;
|
|
5415
|
+
if (/nth-of-type|nth-child/.test(sel)) return 'Positional selector — may break if page layout changes';
|
|
5416
|
+
if (/#[a-z]+-\d+/.test(sel)) return 'Dynamic ID — this number changes between runs';
|
|
5417
|
+
if (/\[\w+=["'][^"']*\d{3,}[^"']*["']\]/.test(sel)) return 'Contains a dynamic numeric value';
|
|
5418
|
+
return null;
|
|
5419
|
+
}
|
|
5420
|
+
|
|
5404
5421
|
function renderLibrarySteps(steps) {
|
|
5405
5422
|
const container = document.getElementById('library-steps-container');
|
|
5406
5423
|
if (!container) return;
|
|
@@ -5415,9 +5432,27 @@ function renderLibrarySteps(steps) {
|
|
|
5415
5432
|
}
|
|
5416
5433
|
|
|
5417
5434
|
const actionColors = { click:'#22d3ee', type:'#a78bfa', check:'#34d399', assert:'#fb923c', select:'#f472b6', scroll:'#6b7280' };
|
|
5435
|
+
const fragileCount = steps.filter(s => isSelectorFragile(s.stableSelector || s.selector)).length;
|
|
5436
|
+
|
|
5418
5437
|
container.innerHTML = `
|
|
5438
|
+
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-bottom:1px solid var(--border);background:var(--surface2)">
|
|
5439
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
5440
|
+
<label style="display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:12px;color:var(--muted);cursor:pointer">
|
|
5441
|
+
<input type="checkbox" id="lib-select-all" onchange="toggleSelectAllSteps(this.checked)" style="width:14px;height:14px">
|
|
5442
|
+
Select all
|
|
5443
|
+
</label>
|
|
5444
|
+
<button class="btn btn-ghost" id="btn-merge-steps" style="display:none;padding:4px 10px;font-size:11px" onclick="openMergeSteps()">
|
|
5445
|
+
Merge selected
|
|
5446
|
+
</button>
|
|
5447
|
+
<button class="btn btn-ghost" id="btn-delete-selected" style="display:none;padding:4px 10px;font-size:11px;color:var(--red)" onclick="deleteSelectedSteps()">
|
|
5448
|
+
Delete selected
|
|
5449
|
+
</button>
|
|
5450
|
+
</div>
|
|
5451
|
+
${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>'}
|
|
5452
|
+
</div>
|
|
5419
5453
|
<table style="width:100%;border-collapse:collapse">
|
|
5420
5454
|
<thead><tr style="font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:0.1em">
|
|
5455
|
+
<th style="padding:10px 16px;width:32px;border-bottom:1px solid var(--border)"></th>
|
|
5421
5456
|
<th style="padding:10px 20px;text-align:left;border-bottom:1px solid var(--border)">NAME</th>
|
|
5422
5457
|
<th style="padding:10px 16px;text-align:left;border-bottom:1px solid var(--border)">ACTION</th>
|
|
5423
5458
|
<th style="padding:10px 16px;text-align:left;border-bottom:1px solid var(--border)">SELECTOR</th>
|
|
@@ -5425,24 +5460,122 @@ function renderLibrarySteps(steps) {
|
|
|
5425
5460
|
<th style="padding:10px 16px;text-align:right;border-bottom:1px solid var(--border)"></th>
|
|
5426
5461
|
</tr></thead>
|
|
5427
5462
|
<tbody>
|
|
5428
|
-
${steps.map(s =>
|
|
5429
|
-
|
|
5463
|
+
${steps.map(s => {
|
|
5464
|
+
const sel = s.stableSelector || s.selector || '';
|
|
5465
|
+
const fragile = isSelectorFragile(sel);
|
|
5466
|
+
const warning = getSelectorWarning(sel);
|
|
5467
|
+
return `
|
|
5468
|
+
<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)':''}'">
|
|
5469
|
+
<td style="padding:12px 16px">
|
|
5470
|
+
<input type="checkbox" class="lib-step-checkbox" data-id="${escapeAttr(s.id)}" onchange="onLibStepCheck()" style="width:14px;height:14px">
|
|
5471
|
+
</td>
|
|
5430
5472
|
<td style="padding:12px 20px">
|
|
5431
5473
|
<div style="font-size:13px;color:var(--text)">${escapeHtml(s.name || s.description || '')}</div>
|
|
5432
5474
|
${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>` : ''}
|
|
5433
5475
|
</td>
|
|
5434
5476
|
<td style="padding:12px 16px"><span style="font-family:var(--mono);font-size:11px;color:${actionColors[s.action]||'var(--muted)'}">${s.action||''}</span></td>
|
|
5435
|
-
<td style="padding:12px 16px;max-width:280px"
|
|
5477
|
+
<td style="padding:12px 16px;max-width:280px">
|
|
5478
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
5479
|
+
${fragile ? `<span title="${escapeAttr(warning||'')}" style="color:#f59e0b;font-size:14px;cursor:help;flex-shrink:0">⚠</span>` : ''}
|
|
5480
|
+
<code style="font-size:11px;color:${fragile?'#f59e0b':'var(--muted)'};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block">${escapeHtml(sel || '—')}</code>
|
|
5481
|
+
</div>
|
|
5482
|
+
</td>
|
|
5436
5483
|
<td style="padding:12px 16px;font-family:var(--mono);font-size:12px;color:var(--muted)">${s.usageCount || 0}</td>
|
|
5437
5484
|
<td style="padding:12px 16px;text-align:right">
|
|
5438
5485
|
<button class="btn btn-ghost" style="padding:4px 10px;font-size:11px;margin-right:4px" onclick="editLibraryStep('${escapeAttr(s.id)}')">Edit</button>
|
|
5439
5486
|
<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>
|
|
5440
5487
|
</td>
|
|
5441
|
-
</tr
|
|
5488
|
+
</tr>`;
|
|
5489
|
+
}).join('')}
|
|
5442
5490
|
</tbody>
|
|
5443
5491
|
</table>`;
|
|
5444
5492
|
}
|
|
5445
5493
|
|
|
5494
|
+
function onLibStepCheck() {
|
|
5495
|
+
const checked = document.querySelectorAll('.lib-step-checkbox:checked');
|
|
5496
|
+
const mergeBtn = document.getElementById('btn-merge-steps');
|
|
5497
|
+
const deleteBtn = document.getElementById('btn-delete-selected');
|
|
5498
|
+
if (mergeBtn) mergeBtn.style.display = checked.length >= 2 ? '' : 'none';
|
|
5499
|
+
if (deleteBtn) deleteBtn.style.display = checked.length >= 1 ? '' : 'none';
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5502
|
+
function toggleSelectAllSteps(checked) {
|
|
5503
|
+
document.querySelectorAll('.lib-step-checkbox').forEach(cb => cb.checked = checked);
|
|
5504
|
+
onLibStepCheck();
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
function getSelectedStepIds() {
|
|
5508
|
+
return Array.from(document.querySelectorAll('.lib-step-checkbox:checked')).map(cb => cb.dataset.id);
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
async function deleteSelectedSteps() {
|
|
5512
|
+
const ids = getSelectedStepIds();
|
|
5513
|
+
if (!ids.length) return;
|
|
5514
|
+
showConfirm('Delete steps?', `Delete ${ids.length} selected step${ids.length>1?'s':''}?`, async () => {
|
|
5515
|
+
for (const id of ids) {
|
|
5516
|
+
await fetch(API_BASE + '/api/step-library/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
5517
|
+
}
|
|
5518
|
+
showToast(`Deleted ${ids.length} steps`);
|
|
5519
|
+
await loadLibraryView();
|
|
5520
|
+
});
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
function openMergeSteps() {
|
|
5524
|
+
const ids = getSelectedStepIds();
|
|
5525
|
+
if (ids.length < 2) return;
|
|
5526
|
+
const selected = ids.map(id => libraryStepsCache.find(s => s.id === id)).filter(Boolean);
|
|
5527
|
+
|
|
5528
|
+
// Use the confirm overlay but inject HTML into desc
|
|
5529
|
+
const descEl = document.getElementById('confirm-desc');
|
|
5530
|
+
if (descEl) {
|
|
5531
|
+
descEl.innerHTML = `
|
|
5532
|
+
<div style="font-family:var(--mono);font-size:12px;color:var(--muted);margin-bottom:16px">
|
|
5533
|
+
Select which step to keep. The others will be deleted.
|
|
5534
|
+
</div>
|
|
5535
|
+
<div style="display:flex;flex-direction:column;gap:8px;text-align:left">
|
|
5536
|
+
${selected.map((s, i) => `
|
|
5537
|
+
<label style="display:flex;align-items:flex-start;gap:10px;padding:10px 14px;border-radius:8px;border:1px solid var(--border);cursor:pointer">
|
|
5538
|
+
<input type="radio" name="merge-canonical" value="${escapeAttr(s.id)}" ${i===0?'checked':''} style="margin-top:3px;flex-shrink:0">
|
|
5539
|
+
<div>
|
|
5540
|
+
<div style="font-size:13px;color:var(--text);margin-bottom:4px">${escapeHtml(s.name||'')}</div>
|
|
5541
|
+
<code style="font-size:11px;color:var(--muted);word-break:break-all">${escapeHtml(s.stableSelector||s.selector||'')}</code>
|
|
5542
|
+
<span style="font-family:var(--mono);font-size:10px;color:var(--muted);margin-left:8px">${s.usageCount||0} uses</span>
|
|
5543
|
+
</div>
|
|
5544
|
+
</label>`).join('')}
|
|
5545
|
+
</div>`;
|
|
5546
|
+
}
|
|
5547
|
+
document.getElementById('confirm-title').textContent = `Merge ${selected.length} steps`;
|
|
5548
|
+
|
|
5549
|
+
// Override the confirm button
|
|
5550
|
+
const actions = document.querySelector('.confirm-actions');
|
|
5551
|
+
if (actions) {
|
|
5552
|
+
actions.innerHTML = `
|
|
5553
|
+
<button class="btn btn-ghost" onclick="closeConfirm()">Cancel</button>
|
|
5554
|
+
<button class="btn btn-primary" onclick="confirmMergeSteps(${JSON.stringify(ids)})">Merge</button>`;
|
|
5555
|
+
}
|
|
5556
|
+
document.getElementById('confirm-overlay').classList.add('open');
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
async function confirmMergeSteps(ids) {
|
|
5560
|
+
const selected = ids.map(id => libraryStepsCache.find(s => s.id === id)).filter(Boolean);
|
|
5561
|
+
const canonicalId = document.querySelector('input[name="merge-canonical"]:checked')?.value;
|
|
5562
|
+
if (!canonicalId) return;
|
|
5563
|
+
const canonical = selected.find(s => s.id === canonicalId);
|
|
5564
|
+
const toDelete = selected.filter(s => s.id !== canonicalId);
|
|
5565
|
+
const totalUses = selected.reduce((sum, s) => sum + (s.usageCount||0), 0);
|
|
5566
|
+
closeConfirm();
|
|
5567
|
+
await fetch(API_BASE + '/api/step-library/' + encodeURIComponent(canonicalId), {
|
|
5568
|
+
method: 'PUT',
|
|
5569
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5570
|
+
body: JSON.stringify({ ...canonical, usageCount: totalUses })
|
|
5571
|
+
});
|
|
5572
|
+
for (const s of toDelete) {
|
|
5573
|
+
await fetch(API_BASE + '/api/step-library/' + encodeURIComponent(s.id), { method: 'DELETE' });
|
|
5574
|
+
}
|
|
5575
|
+
showToast(`Merged ${selected.length} steps into "${canonical.name}"`);
|
|
5576
|
+
await loadLibraryView();
|
|
5577
|
+
}
|
|
5578
|
+
|
|
5446
5579
|
function filterLibrarySteps() {
|
|
5447
5580
|
const q = (document.getElementById('library-search')?.value || '').toLowerCase();
|
|
5448
5581
|
const action = document.getElementById('library-filter-action')?.value || '';
|