skopix 2.0.105 → 2.0.107
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/agent.js +224 -51
- package/package.json +1 -1
package/cli/commands/agent.js
CHANGED
|
@@ -385,89 +385,262 @@ export async function agentCommand(options) {
|
|
|
385
385
|
async function handleDebugRecord(msg) {
|
|
386
386
|
const { runId, recordingId, replaySteps } = msg;
|
|
387
387
|
const startUrl = (msg.startUrl && msg.startUrl.startsWith('http')) ? msg.startUrl : null;
|
|
388
|
-
console.log(chalk.cyan(' ⏸ Debug replay: ') + chalk.white(replaySteps.length + ' steps
|
|
388
|
+
console.log(chalk.cyan(' ⏸ Debug replay: ') + chalk.white(replaySteps.length + ' steps') + (startUrl ? chalk.dim(' @ ' + startUrl) : ''));
|
|
389
389
|
|
|
390
390
|
const sendRun = (data) => { try { ws.send(JSON.stringify({ type: 'runUpdate', runId, data })); } catch {} };
|
|
391
391
|
const sendRec = (data) => { try { ws.send(JSON.stringify({ type: 'recordingUpdate', recordingId, data })); } catch {} };
|
|
392
|
+
const steps = [];
|
|
393
|
+
let chromiumBrowser = null;
|
|
394
|
+
let page = null;
|
|
395
|
+
let ctx = null;
|
|
392
396
|
|
|
393
397
|
sendRun({ type: 'stdout', text: '' });
|
|
394
398
|
sendRun({ type: 'stdout', text: ' Replaying ' + replaySteps.length + ' steps to reach debug point...' });
|
|
395
|
-
sendRun({ type: 'stdout', text: '
|
|
399
|
+
sendRun({ type: 'stdout', text: '\u2501'.repeat(60) });
|
|
396
400
|
|
|
397
401
|
try {
|
|
398
402
|
const { chromium } = await import('playwright');
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
403
|
+
chromiumBrowser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--disable-blink-features=AutomationControlled', '--allow-insecure-localhost'] });
|
|
404
|
+
ctx = await chromiumBrowser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
405
|
+
page = await ctx.newPage();
|
|
406
|
+
|
|
407
|
+
// Wire up capture FIRST — must be on context before any page loads
|
|
408
|
+
await ctx.exposeFunction('__skopixCapture', async (actionData) => {
|
|
409
|
+
if (actionData.action === 'stop') {
|
|
410
|
+
// Mirror stopDebugRecording: send done with steps, then stopped
|
|
411
|
+
sendRec({ type: 'done', steps });
|
|
412
|
+
sendRec({ type: 'stopped' });
|
|
413
|
+
try { await ctx.close(); } catch {}
|
|
414
|
+
try { await chromiumBrowser.close(); } catch {}
|
|
415
|
+
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
|
416
|
+
console.log(chalk.green(' \u2714 Debug recording done \u2014 ' + steps.length + ' new steps'));
|
|
417
|
+
console.log(chalk.cyan(' \u25c6 Waiting for jobs\n'));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const id = 'step-' + String(steps.length + 1).padStart(3, '0') + '-dbg';
|
|
421
|
+
const recStep = {
|
|
422
|
+
id, action: actionData.action, assertType: actionData.assertType || null,
|
|
423
|
+
attribute: actionData.attribute || null,
|
|
424
|
+
selector: actionData.selector || (actionData.element ? actionData.element.selector : null),
|
|
425
|
+
element: actionData.element || null, value: actionData.value || null,
|
|
426
|
+
isPassword: actionData.isPassword || false, label: actionData.label || null,
|
|
427
|
+
clickX: actionData.clickX || null, clickY: actionData.clickY || null,
|
|
428
|
+
elementX: actionData.elementX || null, elementY: actionData.elementY || null,
|
|
429
|
+
description: actionData.description || null,
|
|
430
|
+
url: page ? page.url() : '', timestamp: Date.now(), stableSelector: null,
|
|
431
|
+
};
|
|
432
|
+
steps.push(recStep);
|
|
433
|
+
sendRec({ type: 'step', step: recStep });
|
|
434
|
+
process.stdout.write(chalk.cyan(' \u23fa ') + (recStep.action || '') + '\n');
|
|
435
|
+
try { await page.evaluate(n => { if (window.__skopixUpdateCount) window.__skopixUpdateCount(n); }, steps.length); } catch {}
|
|
436
|
+
setTimeout(async () => {
|
|
437
|
+
try {
|
|
438
|
+
const screenshotDir = path.join(os.homedir(), '.skopix', 'recordings', recordingId);
|
|
439
|
+
await fs.ensureDir(screenshotDir);
|
|
440
|
+
const sp = path.join(screenshotDir, id + '.png');
|
|
441
|
+
await page.screenshot({ path: sp, fullPage: false }).catch(() => {});
|
|
442
|
+
recStep.screenshotPath = sp;
|
|
443
|
+
sendRec({ type: 'screenshot', stepId: id, path: sp });
|
|
444
|
+
} catch {}
|
|
445
|
+
}, 400);
|
|
446
|
+
});
|
|
402
447
|
|
|
403
448
|
if (startUrl) {
|
|
404
449
|
await page.goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
405
|
-
await page.waitForTimeout(
|
|
450
|
+
await page.waitForTimeout(500);
|
|
406
451
|
}
|
|
407
452
|
|
|
408
|
-
//
|
|
453
|
+
// Inject toolbar into page BEFORE replay — visible throughout
|
|
454
|
+
await page.evaluate(() => {
|
|
455
|
+
if (window.__skopixRecording) return;
|
|
456
|
+
window.__skopixRecording = true;
|
|
457
|
+
|
|
458
|
+
function getSelector(el) {
|
|
459
|
+
if (!el || el === document.body) return 'body';
|
|
460
|
+
const testAttrs = ['data-testid','data-test','pi-test-identifier','data-cy','data-qa'];
|
|
461
|
+
for (const attr of testAttrs) { const val = el.getAttribute(attr); if (val) return '['+attr+'="'+val+'"]'; }
|
|
462
|
+
if (el.id && !/^\d/.test(el.id)) return '#'+el.id;
|
|
463
|
+
const al = el.getAttribute('aria-label');
|
|
464
|
+
if (al && ['button','a','input'].includes(el.tagName.toLowerCase())) return el.tagName.toLowerCase()+'[aria-label="'+al+'"]';
|
|
465
|
+
if (el.name && el.tagName === 'INPUT') return 'input[name="'+el.name+'"]';
|
|
466
|
+
const parts = []; let cur = el, depth = 0;
|
|
467
|
+
while (cur && cur !== document.body && depth < 4) {
|
|
468
|
+
let seg = cur.tagName.toLowerCase();
|
|
469
|
+
if (cur.id && !/^\d/.test(cur.id)) { parts.unshift('#'+cur.id); break; }
|
|
470
|
+
const sib = Array.from(cur.parentElement ? cur.parentElement.children : []).filter(c => c.tagName === cur.tagName);
|
|
471
|
+
if (sib.length > 1) seg += ':nth-of-type('+(sib.indexOf(cur)+1)+')';
|
|
472
|
+
parts.unshift(seg); cur = cur.parentElement; depth++;
|
|
473
|
+
}
|
|
474
|
+
return parts.join(' > ');
|
|
475
|
+
}
|
|
476
|
+
function getElementInfo(el) {
|
|
477
|
+
return { tag: el.tagName.toLowerCase(), id: el.id||null, name: el.name||null, type: el.type||null,
|
|
478
|
+
text: (el.innerText||el.value||el.placeholder||el.getAttribute('aria-label')||'').trim().slice(0,80),
|
|
479
|
+
selector: getSelector(el), classes: el.className ? el.className.toString().trim().slice(0,100) : null,
|
|
480
|
+
piTestId: el.getAttribute('pi-test-identifier')||null, title: el.getAttribute('title')||null,
|
|
481
|
+
ariaLabel: el.getAttribute('aria-label')||null, dataTestId: el.getAttribute('data-testid')||null };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
document.addEventListener('click', function(e) {
|
|
485
|
+
if (e.target && e.target.closest) { if (e.target.closest('#__skopix_toolbar')) return; if (e.target.closest('#__skopix_popover')) return; if (e.target.closest('#__skopix_hint')) return; }
|
|
486
|
+
if (window.__skopixPickMode) return;
|
|
487
|
+
const el = e.target;
|
|
488
|
+
if (!el || el === document.body || el === document.documentElement) return;
|
|
489
|
+
const rect = el.getBoundingClientRect();
|
|
490
|
+
const isCheckable = el.type === 'checkbox' || el.type === 'radio';
|
|
491
|
+
let checkTarget = null;
|
|
492
|
+
if (!isCheckable && el.tagName === 'LABEL' && el.htmlFor) checkTarget = document.getElementById(el.htmlFor);
|
|
493
|
+
if (!isCheckable && el.tagName === 'LABEL' && !el.htmlFor) checkTarget = el.querySelector('input[type="checkbox"],input[type="radio"]');
|
|
494
|
+
const actual = isCheckable ? el : checkTarget;
|
|
495
|
+
if (actual && (actual.type === 'checkbox' || actual.type === 'radio')) {
|
|
496
|
+
window.__skopixCapture && window.__skopixCapture({ action:'check', checked:actual.checked, element:getElementInfo(actual), clickX:Math.round(e.clientX), clickY:Math.round(e.clientY), elementX:Math.round(rect.left+rect.width/2), elementY:Math.round(rect.top+rect.height/2) });
|
|
497
|
+
} else {
|
|
498
|
+
window.__skopixCapture && window.__skopixCapture({ action:'click', element:getElementInfo(el), clickX:Math.round(e.clientX), clickY:Math.round(e.clientY), elementX:Math.round(rect.left+rect.width/2), elementY:Math.round(rect.top+rect.height/2) });
|
|
499
|
+
}
|
|
500
|
+
}, true);
|
|
501
|
+
|
|
502
|
+
let typeTimer = null, lastInputEl = null;
|
|
503
|
+
document.addEventListener('input', function(e) {
|
|
504
|
+
const el = e.target;
|
|
505
|
+
if (!el || !['INPUT','TEXTAREA'].includes(el.tagName)) return;
|
|
506
|
+
if (el.type === 'checkbox' || el.type === 'radio') return;
|
|
507
|
+
if (el.closest && (el.closest('#__skopix_toolbar') || el.closest('#__skopix_popover'))) return;
|
|
508
|
+
lastInputEl = el; clearTimeout(typeTimer);
|
|
509
|
+
typeTimer = setTimeout(() => { if (!lastInputEl) return; window.__skopixCapture && window.__skopixCapture({ action:'type', element:getElementInfo(lastInputEl), value:lastInputEl.value, isPassword:lastInputEl.type==='password' }); lastInputEl = null; }, 600);
|
|
510
|
+
}, true);
|
|
511
|
+
|
|
512
|
+
document.addEventListener('change', function(e) {
|
|
513
|
+
const el = e.target; if (!el || el.tagName !== 'SELECT') return;
|
|
514
|
+
if (el.closest && (el.closest('#__skopix_toolbar') || el.closest('#__skopix_popover'))) return;
|
|
515
|
+
const sel2 = el.options[el.selectedIndex];
|
|
516
|
+
window.__skopixCapture && window.__skopixCapture({ action:'select', element:getElementInfo(el), value:el.value, label:sel2?sel2.text:el.value });
|
|
517
|
+
}, true);
|
|
518
|
+
|
|
519
|
+
// Toolbar
|
|
520
|
+
const tb = document.createElement('div');
|
|
521
|
+
tb.id = '__skopix_toolbar';
|
|
522
|
+
tb.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:2147483647;background:#0f1117;border:2px solid #f59e0b;border-radius:10px;padding:10px 14px;display:flex;align-items:center;gap:10px;font-family:monospace;font-size:12px;color:#e5e7eb;box-shadow:0 4px 24px rgba(0,0,0,0.6);user-select:none';
|
|
523
|
+
tb.innerHTML = '<span style="color:#f59e0b;font-size:14px;animation:skopix_pulse 1s infinite">\u25cf</span>'
|
|
524
|
+
+ '<span style="color:#9ca3af">Debug recording</span>'
|
|
525
|
+
+ '<span id="__skopix_count" style="color:#22d3ee;font-weight:700;min-width:20px;text-align:center">0</span>'
|
|
526
|
+
+ '<span style="color:#4b5563">steps</span>'
|
|
527
|
+
+ '<button id="__skopix_assert_btn" style="background:#1e3a5f;border:1px solid #2563eb;color:#60a5fa;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:11px;font-family:monospace">+ Assert</button>'
|
|
528
|
+
+ '<button id="__skopix_stop_btn" style="background:#3f0d0d;border:1px solid #dc2626;color:#f87171;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:11px;font-family:monospace">\u25a0 Stop</button>';
|
|
529
|
+
const sty = document.createElement('style');
|
|
530
|
+
sty.textContent = '@keyframes skopix_pulse{0%,100%{opacity:1}50%{opacity:0.3}}';
|
|
531
|
+
document.head.appendChild(sty);
|
|
532
|
+
document.body.appendChild(tb);
|
|
533
|
+
tb.querySelector('#__skopix_stop_btn').addEventListener('click', e => { e.stopPropagation(); window.__skopixCapture && window.__skopixCapture({ action:'stop' }); });
|
|
534
|
+
window.__skopixUpdateCount = n => { const el = document.getElementById('__skopix_count'); if (el) el.textContent = n; };
|
|
535
|
+
|
|
536
|
+
// Assert picker
|
|
537
|
+
tb.querySelector('#__skopix_assert_btn').addEventListener('click', function(e) {
|
|
538
|
+
e.stopPropagation();
|
|
539
|
+
window.__skopixPickMode = true;
|
|
540
|
+
document.body.style.cursor = 'crosshair';
|
|
541
|
+
const hint = document.createElement('div'); hint.id = '__skopix_hint';
|
|
542
|
+
hint.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#1e3a5f;border:1px solid #2563eb;color:#60a5fa;padding:8px 18px;border-radius:8px;font-family:monospace;font-size:13px;pointer-events:none';
|
|
543
|
+
hint.textContent = 'Click any element to add an assertion \u2014 Esc to cancel';
|
|
544
|
+
document.body.appendChild(hint);
|
|
545
|
+
const overlay = document.createElement('div');
|
|
546
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:auto;cursor:crosshair';
|
|
547
|
+
document.body.appendChild(overlay);
|
|
548
|
+
const hl = document.createElement('div');
|
|
549
|
+
hl.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483645;background:rgba(37,99,235,0.15);border:2px solid #2563eb;border-radius:3px;transition:all 0.1s;display:none';
|
|
550
|
+
document.body.appendChild(hl);
|
|
551
|
+
overlay.addEventListener('mousemove', function(e2) {
|
|
552
|
+
overlay.style.pointerEvents = 'none';
|
|
553
|
+
const el2 = document.elementFromPoint(e2.clientX, e2.clientY);
|
|
554
|
+
overlay.style.pointerEvents = 'auto';
|
|
555
|
+
if (!el2 || el2 === document.body) { hl.style.display = 'none'; return; }
|
|
556
|
+
const r = el2.getBoundingClientRect();
|
|
557
|
+
hl.style.display = 'block'; hl.style.top = r.top+'px'; hl.style.left = r.left+'px'; hl.style.width = r.width+'px'; hl.style.height = r.height+'px';
|
|
558
|
+
});
|
|
559
|
+
overlay.addEventListener('click', function(e2) {
|
|
560
|
+
e2.preventDefault(); e2.stopPropagation();
|
|
561
|
+
overlay.style.pointerEvents = 'none';
|
|
562
|
+
const el2 = document.elementFromPoint(e2.clientX, e2.clientY);
|
|
563
|
+
overlay.style.pointerEvents = 'auto';
|
|
564
|
+
window.__skopixPickMode = false; document.body.style.cursor = '';
|
|
565
|
+
overlay.remove(); const h2 = document.getElementById('__skopix_hint'); if (h2) h2.remove();
|
|
566
|
+
if (!el2 || el2 === document.body) { hl.style.display = 'none'; return; }
|
|
567
|
+
const sel2 = getSelector(el2);
|
|
568
|
+
const txt = (el2.innerText||el2.textContent||'').trim().slice(0,100);
|
|
569
|
+
const rect2 = el2.getBoundingClientRect();
|
|
570
|
+
hl.style.background = 'rgba(34,197,94,0.15)'; hl.style.borderColor = '#22c55e';
|
|
571
|
+
hl.style.display = 'block'; hl.style.top = rect2.top+'px'; hl.style.left = rect2.left+'px'; hl.style.width = rect2.width+'px'; hl.style.height = rect2.height+'px';
|
|
572
|
+
let sugType = 'visible', sugVal = '';
|
|
573
|
+
if (txt && txt.length > 0 && txt.length < 80) { sugType = 'text_contains'; sugVal = txt.replace(/\s+/g,' ').trim(); }
|
|
574
|
+
const popover = document.createElement('div'); popover.id = '__skopix_popover';
|
|
575
|
+
const topPos = rect2.bottom+8+280 > window.innerHeight ? Math.max(8,rect2.top-288) : rect2.bottom+8;
|
|
576
|
+
popover.style.cssText = 'position:fixed;z-index:2147483647;top:'+topPos+'px;left:'+Math.min(rect2.left,window.innerWidth-360)+'px;width:350px;background:#0f1117;border:1px solid #2563eb;border-radius:10px;padding:16px;font-family:monospace;font-size:12px;color:#e5e7eb;box-shadow:0 8px 32px rgba(0,0,0,0.7)';
|
|
577
|
+
popover.innerHTML = '<div style="color:#60a5fa;font-size:11px;letter-spacing:0.1em;margin-bottom:12px">ADD ASSERTION</div>'
|
|
578
|
+
+ '<div style="margin-bottom:10px"><div style="color:#9ca3af;font-size:10px;margin-bottom:4px">ELEMENT</div><div style="background:#1a1d2e;padding:6px 10px;border-radius:6px;color:#22d3ee;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+sel2+'</div>'+(txt?'<div style="color:#6b7280;font-size:10px;margin-top:3px">Text: "'+txt.slice(0,50)+'"</div>':'')+'</div>'
|
|
579
|
+
+ '<div style="margin-bottom:10px"><div style="color:#9ca3af;font-size:10px;margin-bottom:4px">TYPE</div><select id="__skopix_assert_type" style="width:100%;background:#1a1d2e;border:1px solid #374151;color:#e5e7eb;padding:6px 8px;border-radius:6px;font-family:monospace;font-size:12px"><option value="visible"'+(sugType==="visible"?" selected":"")+'>Element is visible</option><option value="text_contains"'+(sugType==="text_contains"?" selected":"")+'>Text contains</option><option value="text_equals">Text equals</option><option value="url_contains">URL contains</option><option value="element_count">Element count</option></select></div>'
|
|
580
|
+
+ '<div id="__skopix_value_row" style="margin-bottom:10px;'+(sugType==="visible"?"display:none":"")+'"><div style="color:#9ca3af;font-size:10px;margin-bottom:4px">EXPECTED VALUE</div><input id="__skopix_assert_value" type="text" value="'+sugVal.replace(/"/g,""")+'" style="width:100%;box-sizing:border-box;background:#1a1d2e;border:1px solid #374151;color:#e5e7eb;padding:6px 8px;border-radius:6px;font-family:monospace;font-size:12px"></div>'
|
|
581
|
+
+ '<div style="display:flex;gap:8px;justify-content:flex-end"><button id="__skopix_cancel_btn" style="background:transparent;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:6px 14px;cursor:pointer;font-family:monospace;font-size:12px">Cancel</button><button id="__skopix_add_btn" style="background:#1e3a5f;border:1px solid #2563eb;color:#60a5fa;border-radius:6px;padding:6px 14px;cursor:pointer;font-family:monospace;font-size:12px;font-weight:700">Add \u2713</button></div>';
|
|
582
|
+
document.body.appendChild(popover);
|
|
583
|
+
popover.querySelector('#__skopix_assert_type').addEventListener('change', function() {
|
|
584
|
+
const t = this.value; popover.querySelector('#__skopix_value_row').style.display = t==='visible'?'none':'';
|
|
585
|
+
});
|
|
586
|
+
popover.querySelector('#__skopix_cancel_btn').addEventListener('click', function(e3) { e3.stopPropagation(); hl.style.display='none'; popover.remove(); });
|
|
587
|
+
popover.querySelector('#__skopix_add_btn').addEventListener('click', function(e3) {
|
|
588
|
+
e3.stopPropagation();
|
|
589
|
+
const assertType = popover.querySelector('#__skopix_assert_type').value;
|
|
590
|
+
const value = popover.querySelector('#__skopix_assert_value').value.trim();
|
|
591
|
+
if (assertType !== 'visible' && assertType !== 'url_contains' && !value) { popover.querySelector('#__skopix_assert_value').style.borderColor='#dc2626'; return; }
|
|
592
|
+
window.__skopixCapture && window.__skopixCapture({ action:'assert', assertType, selector:assertType==='url_contains'?null:sel2, value:value||null, element:assertType==='url_contains'?null:{tag:el2.tagName.toLowerCase(),text:txt} });
|
|
593
|
+
hl.style.display='none'; popover.remove();
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
document.addEventListener('keydown', function escH(e2) { if (e2.key==='Escape') { window.__skopixPickMode=false; document.body.style.cursor=''; overlay.remove(); const h2=document.getElementById('__skopix_hint');if(h2)h2.remove(); hl.style.display='none'; document.removeEventListener('keydown',escH); } });
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
// Replay steps to debug point
|
|
409
603
|
let stepNum = 0;
|
|
604
|
+
let browserClosed = false;
|
|
410
605
|
for (const step of replaySteps) {
|
|
606
|
+
if (browserClosed) break;
|
|
411
607
|
stepNum++;
|
|
412
|
-
|
|
608
|
+
const sel = sanitiseSelector(step.stableSelector || step.selector);
|
|
609
|
+
const desc = step.description || (step.action + ' ' + (sel || ''));
|
|
610
|
+
sendRun({ type: 'stdout', text: ' [' + stepNum + '/' + replaySteps.length + '] ' + step.action.toUpperCase() + ' \u2014 ' + desc });
|
|
413
611
|
try {
|
|
414
|
-
const sel = step.stableSelector || step.selector;
|
|
415
612
|
await executeStep(step, sel, page, { url: startUrl || '' });
|
|
613
|
+
sendRun({ type: 'stdout', text: ' \u2713 Done' });
|
|
416
614
|
} catch (err) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
615
|
+
const emsg = err.message || '';
|
|
616
|
+
if (emsg.includes('closed') || emsg.includes('destroyed') || emsg.includes('detached')) {
|
|
617
|
+
browserClosed = true;
|
|
618
|
+
sendRun({ type: 'stdout', text: ' \u2716 Browser closed' });
|
|
619
|
+
} else {
|
|
620
|
+
sendRun({ type: 'stdout', text: ' \u26a0 Skipped: ' + emsg.slice(0, 100) });
|
|
621
|
+
}
|
|
423
622
|
}
|
|
424
623
|
}
|
|
425
624
|
|
|
426
625
|
sendRun({ type: 'stdout', text: '' });
|
|
427
|
-
sendRun({ type: 'stdout', text: '
|
|
428
|
-
|
|
429
|
-
// Attach RecordingSession to the existing browser — injects toolbar into current page
|
|
430
|
-
const screenshotDir = path.join(os.homedir(), '.skopix', 'recordings', recordingId);
|
|
431
|
-
await fs.ensureDir(screenshotDir);
|
|
432
|
-
|
|
433
|
-
const { RecordingSession } = await import('../../core/recorder.js');
|
|
434
|
-
const session = new RecordingSession({ url: startUrl || msg.startUrl || '', sessionId: recordingId, screenshotDir });
|
|
435
|
-
|
|
436
|
-
// Override emit to forward over WebSocket
|
|
437
|
-
session.emit = (obj) => {
|
|
438
|
-
sendRec(obj);
|
|
439
|
-
if (obj.type === 'step') process.stdout.write(chalk.cyan(' ⏺ ') + (obj.step?.action || '') + '\n');
|
|
440
|
-
if (obj.type === 'done' || obj.type === 'stopped') {
|
|
441
|
-
console.log(chalk.green(' ✔ Debug recording done — ' + (obj.steps?.length || 0) + ' new steps'));
|
|
442
|
-
browser.close().catch(() => {});
|
|
443
|
-
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
|
444
|
-
console.log(chalk.cyan(' ◆ Waiting for jobs\n'));
|
|
445
|
-
}
|
|
446
|
-
};
|
|
626
|
+
sendRun({ type: 'stdout', text: ' \u2714 Reached debug point \u2014 now recording' });
|
|
627
|
+
sendRun({ type: 'stdout', text: ' Use the browser, then click Stop.' });
|
|
447
628
|
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// Listen for stop signal from server
|
|
452
|
-
const stopHandler = (event) => {
|
|
453
|
-
let m; try { m = JSON.parse(event.data); } catch { return; }
|
|
454
|
-
if (m.type === 'stopRecord' && m.recordingId === recordingId) {
|
|
455
|
-
session.stop().catch(() => {});
|
|
456
|
-
ws.removeEventListener('message', stopHandler);
|
|
457
|
-
}
|
|
458
|
-
};
|
|
459
|
-
ws.addEventListener('message', stopHandler);
|
|
629
|
+
// Tell dashboard replay is done — frontend switches to recording SSE
|
|
630
|
+
sendRun({ type: 'done', exitCode: 0, status: 'passed', recordingId });
|
|
631
|
+
process.stderr.write('[debug] toolbar injected, waiting for user actions\n');
|
|
460
632
|
|
|
461
633
|
} catch (err) {
|
|
462
|
-
console.error(chalk.red('
|
|
463
|
-
sendRun({ type: 'stdout', text: '
|
|
634
|
+
console.error(chalk.red(' \u2716 Debug error: ' + err.message));
|
|
635
|
+
sendRun({ type: 'stdout', text: ' \u2716 Debug replay error: ' + err.message });
|
|
464
636
|
sendRun({ type: 'done', exitCode: 1, status: 'failed' });
|
|
465
|
-
|
|
637
|
+
sendRec({ type: 'stopped' });
|
|
638
|
+
if (chromiumBrowser) try { await chromiumBrowser.close(); } catch {}
|
|
466
639
|
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
|
467
640
|
}
|
|
468
641
|
}
|
|
469
642
|
|
|
470
|
-
|
|
643
|
+
// ── HELPERS ────────────────────────────────────────────────────────────────
|
|
471
644
|
function sanitiseSelector(sel) {
|
|
472
645
|
if (!sel) return sel;
|
|
473
646
|
return sel.replace(/\[([a-zA-Z_-]+)="([^"]+\.\d{5,})"\]/g, (_, attr, val) => '[' + attr + '*="' + val.replace(/\.\d{5,}$/, '') + '"]');
|
package/package.json
CHANGED