skopix 2.0.98 → 2.0.100
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 +26 -38
- package/core/recorder.js +162 -0
- package/package.json +1 -1
package/cli/commands/agent.js
CHANGED
|
@@ -382,18 +382,12 @@ export async function agentCommand(options) {
|
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
// ── DEBUG-RECORD JOB ────────────────────────────────────────────────────────
|
|
385
|
-
// Replays steps to a debug point on this agent (which has a display),
|
|
386
|
-
// then opens the recorder so user can add new steps from that state.
|
|
387
385
|
async function handleDebugRecord(msg) {
|
|
388
|
-
const { runId, recordingId, replaySteps, startUrl
|
|
386
|
+
const { runId, recordingId, replaySteps, startUrl } = msg;
|
|
389
387
|
console.log(chalk.cyan(' ⏸ Debug replay: ') + chalk.white(replaySteps.length + ' steps to debug point'));
|
|
390
388
|
|
|
391
|
-
const sendRun = (data) => {
|
|
392
|
-
|
|
393
|
-
};
|
|
394
|
-
const sendRec = (data) => {
|
|
395
|
-
try { ws.send(JSON.stringify({ type: 'recordingUpdate', recordingId, data })); } catch {}
|
|
396
|
-
};
|
|
389
|
+
const sendRun = (data) => { try { ws.send(JSON.stringify({ type: 'runUpdate', runId, data })); } catch {} };
|
|
390
|
+
const sendRec = (data) => { try { ws.send(JSON.stringify({ type: 'recordingUpdate', recordingId, data })); } catch {} };
|
|
397
391
|
|
|
398
392
|
sendRun({ type: 'stdout', text: '' });
|
|
399
393
|
sendRun({ type: 'stdout', text: ' Replaying ' + replaySteps.length + ' steps to reach debug point...' });
|
|
@@ -410,19 +404,18 @@ export async function agentCommand(options) {
|
|
|
410
404
|
await page.waitForTimeout(1000);
|
|
411
405
|
}
|
|
412
406
|
|
|
413
|
-
// Replay each step
|
|
407
|
+
// Replay each step to reach debug point
|
|
414
408
|
let stepNum = 0;
|
|
415
409
|
for (const step of replaySteps) {
|
|
416
410
|
stepNum++;
|
|
417
|
-
sendRun({ type: 'stdout', text: ' [' + stepNum + '/' + replaySteps.length + '] ' + (step.action || '').toUpperCase() + ' — ' + (step.description || step.selector || '') });
|
|
411
|
+
sendRun({ type: 'stdout', text: ' [' + stepNum + '/' + replaySteps.length + '] ' + (step.action || '').toUpperCase() + ' — ' + (step.description || step.stableSelector || step.selector || '') });
|
|
418
412
|
try {
|
|
419
413
|
const sel = step.stableSelector || step.selector;
|
|
420
414
|
await executeStep(step, sel, page, { url: startUrl });
|
|
421
|
-
sendRun({ type: 'stdout', text: chalk.green ? '✓ Done' : '✓ Done' });
|
|
422
415
|
} catch (err) {
|
|
423
|
-
sendRun({ type: 'stdout', text: '✖ FAILED: ' + err.message });
|
|
416
|
+
sendRun({ type: 'stdout', text: ' ✖ FAILED at step ' + stepNum + ': ' + err.message });
|
|
424
417
|
sendRun({ type: 'done', exitCode: 1, status: 'failed' });
|
|
425
|
-
sendRun({ type: 'stdout', text: '
|
|
418
|
+
sendRun({ type: 'stdout', text: '<span style="color:var(--red)">✗ Replay failed — could not reach debug point</span>' });
|
|
426
419
|
await browser.close().catch(() => {});
|
|
427
420
|
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
|
428
421
|
return;
|
|
@@ -432,38 +425,33 @@ export async function agentCommand(options) {
|
|
|
432
425
|
sendRun({ type: 'stdout', text: '' });
|
|
433
426
|
sendRun({ type: 'stdout', text: ' ✔ Reached debug point — starting recorder' });
|
|
434
427
|
|
|
435
|
-
//
|
|
428
|
+
// Attach RecordingSession to the existing browser — injects toolbar into current page
|
|
436
429
|
const screenshotDir = path.join(os.homedir(), '.skopix', 'recordings', recordingId);
|
|
437
430
|
await fs.ensureDir(screenshotDir);
|
|
438
431
|
|
|
439
|
-
const
|
|
440
|
-
const {
|
|
441
|
-
const child = spawn('node', [recorderPath, startUrl || '', recordingId, screenshotDir], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
432
|
+
const { RecordingSession } = await import('../../core/recorder.js');
|
|
433
|
+
const session = new RecordingSession({ url: startUrl || '', sessionId: recordingId, screenshotDir });
|
|
442
434
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
435
|
+
// Override emit to forward over WebSocket
|
|
436
|
+
session.emit = (obj) => {
|
|
437
|
+
sendRec(obj);
|
|
438
|
+
if (obj.type === 'step') process.stdout.write(chalk.cyan(' ⏺ ') + (obj.step?.action || '') + '\n');
|
|
439
|
+
if (obj.type === 'done') {
|
|
440
|
+
console.log(chalk.green(' ✔ Debug recording done — ' + (obj.steps?.length || 0) + ' new steps'));
|
|
441
|
+
browser.close().catch(() => {});
|
|
442
|
+
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
|
443
|
+
console.log(chalk.cyan(' ◆ Waiting for jobs\n'));
|
|
444
|
+
}
|
|
445
|
+
};
|
|
453
446
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
sendRec({ type: 'stopped' });
|
|
457
|
-
browser.close().catch(() => {});
|
|
458
|
-
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
|
459
|
-
console.log(chalk.cyan(' ◆ Waiting for jobs\n'));
|
|
460
|
-
});
|
|
447
|
+
// Attach to existing browser/context/page — injects toolbar without new browser
|
|
448
|
+
await session.attachTo(browser, ctx, page);
|
|
461
449
|
|
|
462
|
-
// Listen for stop signal
|
|
450
|
+
// Listen for stop signal from server
|
|
463
451
|
const stopHandler = (event) => {
|
|
464
452
|
let m; try { m = JSON.parse(event.data); } catch { return; }
|
|
465
453
|
if (m.type === 'stopRecord' && m.recordingId === recordingId) {
|
|
466
|
-
|
|
454
|
+
session.stop().catch(() => {});
|
|
467
455
|
ws.removeEventListener('message', stopHandler);
|
|
468
456
|
}
|
|
469
457
|
};
|
|
@@ -471,7 +459,7 @@ export async function agentCommand(options) {
|
|
|
471
459
|
|
|
472
460
|
} catch (err) {
|
|
473
461
|
console.error(chalk.red(' ✖ Debug error: ' + err.message));
|
|
474
|
-
sendRun({ type: 'stdout', text: '✖ Debug replay error: ' + err.message });
|
|
462
|
+
sendRun({ type: 'stdout', text: ' ✖ Debug replay error: ' + err.message });
|
|
475
463
|
sendRun({ type: 'done', exitCode: 1, status: 'failed' });
|
|
476
464
|
sendRun({ type: 'stdout', text: '<span style="color:var(--red)">✗ Replay failed — could not reach debug point</span>' });
|
|
477
465
|
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
package/core/recorder.js
CHANGED
|
@@ -597,6 +597,168 @@ export class RecordingSession {
|
|
|
597
597
|
this.emit({ type: 'ready' });
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
// Attach to an existing browser/context/page (used for debug mode)
|
|
601
|
+
// instead of launching a fresh browser via launch()
|
|
602
|
+
async attachTo(browser, context, page) {
|
|
603
|
+
this.browser = browser;
|
|
604
|
+
this.context = context;
|
|
605
|
+
this.page = page;
|
|
606
|
+
|
|
607
|
+
// Expose capture function on the existing context
|
|
608
|
+
await this.context.exposeFunction('__skopixCapture', async (actionData) => {
|
|
609
|
+
if (this._stopping) return;
|
|
610
|
+
if (actionData.action === 'stop') {
|
|
611
|
+
await this.stop();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
await this._captureStep(actionData);
|
|
615
|
+
}).catch(() => {}); // may already be exposed
|
|
616
|
+
|
|
617
|
+
// Inject the init script into the existing page
|
|
618
|
+
await this.context.addInitScript(() => {
|
|
619
|
+
if (window.__skopixRecording) return;
|
|
620
|
+
window.__skopixRecording = true;
|
|
621
|
+
|
|
622
|
+
function getSelector(el) {
|
|
623
|
+
if (!el || el === document.body) return 'body';
|
|
624
|
+
const testAttrs = ['data-testid', 'data-test', 'pi-test-identifier', 'data-cy', 'data-qa'];
|
|
625
|
+
for (const attr of testAttrs) {
|
|
626
|
+
const val = el.getAttribute(attr);
|
|
627
|
+
if (val) return '[' + attr + '="' + val + '"]';
|
|
628
|
+
}
|
|
629
|
+
if (el.id && !/^\d/.test(el.id)) return '#' + el.id;
|
|
630
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
631
|
+
if (ariaLabel && ['button', 'a', 'input'].includes(el.tagName.toLowerCase())) {
|
|
632
|
+
return el.tagName.toLowerCase() + '[aria-label="' + ariaLabel + '"]';
|
|
633
|
+
}
|
|
634
|
+
if (el.name && el.tagName === 'INPUT') return 'input[name="' + el.name + '"]';
|
|
635
|
+
const parts = []; let cur = el; let depth = 0;
|
|
636
|
+
while (cur && cur !== document.body && depth < 4) {
|
|
637
|
+
let seg = cur.tagName.toLowerCase();
|
|
638
|
+
if (cur.id && !/^\d/.test(cur.id)) { parts.unshift('#' + cur.id); break; }
|
|
639
|
+
const sib = Array.from(cur.parentElement ? cur.parentElement.children : []).filter(c => c.tagName === cur.tagName);
|
|
640
|
+
if (sib.length > 1) seg += ':nth-of-type(' + (sib.indexOf(cur) + 1) + ')';
|
|
641
|
+
parts.unshift(seg); cur = cur.parentElement; depth++;
|
|
642
|
+
}
|
|
643
|
+
return parts.join(' > ');
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function getElementInfo(el) {
|
|
647
|
+
const piTestId = el.getAttribute('pi-test-identifier') || null;
|
|
648
|
+
const title = el.getAttribute('title') || null;
|
|
649
|
+
const ariaLabel = el.getAttribute('aria-label') || null;
|
|
650
|
+
const dataTestId = el.getAttribute('data-testid') || el.getAttribute('data-test-id') || null;
|
|
651
|
+
const info = { tag: el.tagName.toLowerCase(), id: el.id || null, name: el.name || null, type: el.type || null, text: (el.innerText || el.value || el.placeholder || ariaLabel || title || '').trim().slice(0, 80), selector: getSelector(el), classes: el.className ? el.className.toString().trim().slice(0, 100) : null, title, ariaLabel, piTestId, dataTestId };
|
|
652
|
+
const isIcon = ['i', 'span', 'svg'].includes(info.tag) && !info.id && !info.text && !piTestId;
|
|
653
|
+
if (isIcon && el.parentElement) { const p = el.parentElement; info.parentTag = p.tagName.toLowerCase(); info.parentClasses = p.className ? p.className.toString().trim().slice(0, 100) : null; info.parentSelector = getSelector(p); info.parentAriaLabel = p.getAttribute('aria-label') || null; info.parentTitle = p.getAttribute('title') || null; info.parentTestId = p.getAttribute('pi-test-identifier') || p.getAttribute('data-testid') || null; }
|
|
654
|
+
return info;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
document.addEventListener('click', function(e) {
|
|
658
|
+
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; }
|
|
659
|
+
if (window.__skopixPickMode) return;
|
|
660
|
+
const el = e.target;
|
|
661
|
+
if (!el || el === document.body || el === document.documentElement) return;
|
|
662
|
+
const rect = el.getBoundingClientRect();
|
|
663
|
+
if (window.__skopixCapture) {
|
|
664
|
+
const isCheckable = el.type === 'checkbox' || el.type === 'radio';
|
|
665
|
+
let checkTarget = null;
|
|
666
|
+
if (!isCheckable && el.tagName === 'LABEL' && el.htmlFor) checkTarget = document.getElementById(el.htmlFor);
|
|
667
|
+
if (!isCheckable && el.tagName === 'LABEL' && !el.htmlFor) checkTarget = el.querySelector('input[type="checkbox"], input[type="radio"]');
|
|
668
|
+
const actualCheckable = isCheckable ? el : checkTarget;
|
|
669
|
+
if (actualCheckable && (actualCheckable.type === 'checkbox' || actualCheckable.type === 'radio')) {
|
|
670
|
+
window.__skopixCapture({ action: 'check', checked: actualCheckable.checked, element: getElementInfo(actualCheckable), 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) });
|
|
671
|
+
} else {
|
|
672
|
+
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) });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}, true);
|
|
676
|
+
|
|
677
|
+
let typeTimer = null, lastInputEl = null;
|
|
678
|
+
document.addEventListener('input', function(e) {
|
|
679
|
+
const el = e.target;
|
|
680
|
+
if (!el || !['INPUT', 'TEXTAREA'].includes(el.tagName)) return;
|
|
681
|
+
if (el.type === 'checkbox' || el.type === 'radio') return;
|
|
682
|
+
if (el.closest && (el.closest('#__skopix_toolbar') || el.closest('#__skopix_popover'))) return;
|
|
683
|
+
lastInputEl = el; clearTimeout(typeTimer);
|
|
684
|
+
typeTimer = setTimeout(function() {
|
|
685
|
+
if (!lastInputEl) return;
|
|
686
|
+
if (window.__skopixCapture) window.__skopixCapture({ action: 'type', element: getElementInfo(lastInputEl), value: lastInputEl.value, isPassword: lastInputEl.type === 'password' });
|
|
687
|
+
lastInputEl = null;
|
|
688
|
+
}, 600);
|
|
689
|
+
}, true);
|
|
690
|
+
|
|
691
|
+
document.addEventListener('change', function(e) {
|
|
692
|
+
const el = e.target;
|
|
693
|
+
if (!el || el.tagName !== 'SELECT') return;
|
|
694
|
+
if (el.closest && (el.closest('#__skopix_toolbar') || el.closest('#__skopix_popover'))) return;
|
|
695
|
+
const selected = el.options[el.selectedIndex];
|
|
696
|
+
if (window.__skopixCapture) window.__skopixCapture({ action: 'select', element: getElementInfo(el), value: selected ? selected.value : '', label: selected ? selected.text : '' });
|
|
697
|
+
}, true);
|
|
698
|
+
|
|
699
|
+
function createToolbar() {
|
|
700
|
+
if (document.getElementById('__skopix_toolbar')) return;
|
|
701
|
+
const toolbar = document.createElement('div');
|
|
702
|
+
toolbar.id = '__skopix_toolbar';
|
|
703
|
+
toolbar.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:2147483647;display:flex;align-items:center;gap:8px;background:#0d0d1a;border:1px solid rgba(0,212,255,0.3);border-radius:10px;padding:10px 14px;font-family:monospace;font-size:12px;box-shadow:0 4px 24px rgba(0,0,0,0.5);user-select:none;';
|
|
704
|
+
toolbar.innerHTML = '<span style="color:#00d4ff;font-weight:600;letter-spacing:0.08em">SKOPIX</span><span id="__skopix_step_count" style="color:#5a6180;font-size:11px">0 steps</span><button id="__skopix_assert_btn" style="background:#1a1d2e;border:1px solid rgba(0,212,255,0.2);color:#00d4ff;border-radius:6px;padding:5px 10px;cursor:pointer;font-family:monospace;font-size:11px">+ Assert</button><button id="__skopix_stop_btn" style="background:#1a1d2e;border:1px solid rgba(239,68,68,0.3);color:#ef4444;border-radius:6px;padding:5px 10px;cursor:pointer;font-family:monospace;font-size:11px">■ Stop</button>';
|
|
705
|
+
document.body.appendChild(toolbar);
|
|
706
|
+
document.getElementById('__skopix_stop_btn').addEventListener('click', function() { if (window.__skopixCapture) window.__skopixCapture({ action: 'stop' }); });
|
|
707
|
+
document.getElementById('__skopix_assert_btn').addEventListener('click', function() { window.__skopixPickMode = true; document.body.style.cursor = 'crosshair'; });
|
|
708
|
+
document.addEventListener('click', function assertPick(e) {
|
|
709
|
+
if (!window.__skopixPickMode) return;
|
|
710
|
+
if (e.target.closest && (e.target.closest('#__skopix_toolbar') || e.target.closest('#__skopix_popover'))) return;
|
|
711
|
+
e.stopPropagation(); e.preventDefault();
|
|
712
|
+
window.__skopixPickMode = false; document.body.style.cursor = '';
|
|
713
|
+
const el = e.target; const sel = getSelector(el); const currentText = (el.innerText || el.value || '').trim().slice(0, 80);
|
|
714
|
+
const suggestedType = el.tagName === 'IMG' ? 'visible' : currentText ? 'text_contains' : 'visible';
|
|
715
|
+
const suggestedValue = currentText || '';
|
|
716
|
+
let highlightEl = document.getElementById('__skopix_highlight');
|
|
717
|
+
if (!highlightEl) { highlightEl = document.createElement('div'); highlightEl.id = '__skopix_highlight'; highlightEl.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483646;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);border-radius:4px;transition:all 0.1s;'; document.body.appendChild(highlightEl); }
|
|
718
|
+
const rect = el.getBoundingClientRect(); highlightEl.style.cssText += 'left:' + rect.left + 'px;top:' + rect.top + 'px;width:' + rect.width + 'px;height:' + rect.height + 'px;display:block;';
|
|
719
|
+
let popover = document.getElementById('__skopix_popover');
|
|
720
|
+
if (popover) popover.remove();
|
|
721
|
+
popover = document.createElement('div'); popover.id = '__skopix_popover';
|
|
722
|
+
popover.style.cssText = 'position:fixed;z-index:2147483647;background:#0d0d1a;border:1px solid rgba(59,130,246,0.4);border-radius:10px;padding:16px;width:320px;box-shadow:0 8px 32px rgba(0,0,0,0.6);font-family:monospace;font-size:12px;color:#e5e7eb;';
|
|
723
|
+
const vw = window.innerWidth, vh = window.innerHeight;
|
|
724
|
+
const top = rect.bottom + 10 < vh - 200 ? rect.bottom + 10 : rect.top - 220;
|
|
725
|
+
const left = Math.min(Math.max(rect.left, 10), vw - 340);
|
|
726
|
+
popover.style.top = top + 'px'; popover.style.left = left + 'px';
|
|
727
|
+
popover.innerHTML = '<div style="color:#60a5fa;font-size:11px;letter-spacing:0.1em;margin-bottom:12px">ADD ASSERTION</div><div style="margin-bottom:10px"><div style="color:#9ca3af;font-size:10px;margin-bottom:4px">SELECTED 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" title="' + sel + '">' + sel + '</div>' + (currentText ? '<div style="color:#6b7280;font-size:10px;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">Current text: "' + currentText.slice(0, 50) + (currentText.length > 50 ? '...' : '') + '"</div>' : '') + '</div><div style="margin-bottom:10px"><div style="color:#9ca3af;font-size:10px;margin-bottom:4px">ASSERTION 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"' + (suggestedType === "visible" ? " selected" : "") + '>Element is visible</option><option value="text_contains"' + (suggestedType === "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><option value="attribute_contains">Attribute contains</option></select></div><div id="__skopix_value_row" style="margin-bottom:10px;' + (suggestedType === "visible" ? "display:none" : "") + '"><div style="color:#9ca3af;font-size:10px;margin-bottom:4px" id="__skopix_value_label">EXPECTED VALUE</div><input id="__skopix_assert_value" type="text" value="' + suggestedValue.replace(/"/g, """) + '" placeholder="Expected value..." 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><div style="display:flex;gap:8px;justify-content:flex-end"><button id="__skopix_assert_cancel" 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_assert_add" 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 ✓</button></div>';
|
|
728
|
+
document.body.appendChild(popover);
|
|
729
|
+
popover.querySelector('#__skopix_assert_cancel').addEventListener('click', function(e) { e.stopPropagation(); if (highlightEl) highlightEl.style.display = 'none'; popover.remove(); });
|
|
730
|
+
popover.querySelector('#__skopix_assert_add').addEventListener('click', function(e) {
|
|
731
|
+
e.stopPropagation();
|
|
732
|
+
const assertType = popover.querySelector('#__skopix_assert_type').value;
|
|
733
|
+
const value = popover.querySelector('#__skopix_assert_value') ? popover.querySelector('#__skopix_assert_value').value.trim() : '';
|
|
734
|
+
if (assertType !== 'visible' && assertType !== 'url_contains' && !value) { popover.querySelector('#__skopix_assert_value').style.borderColor = '#dc2626'; return; }
|
|
735
|
+
if (window.__skopixCapture) window.__skopixCapture({ action: 'assert', assertType, selector: assertType === 'url_contains' ? null : sel, value: value || null, element: assertType === 'url_contains' ? null : getElementInfo(el) });
|
|
736
|
+
if (highlightEl) highlightEl.style.display = 'none'; popover.remove();
|
|
737
|
+
});
|
|
738
|
+
}, true);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (document.body) { createToolbar(); } else { document.addEventListener('DOMContentLoaded', createToolbar); }
|
|
742
|
+
new MutationObserver(() => { if (!document.getElementById('__skopix_toolbar')) createToolbar(); }).observe(document.documentElement, { childList: true, subtree: false });
|
|
743
|
+
window.__skopixUpdateCount = function(n) { const el = document.getElementById('__skopix_step_count'); if (el) el.textContent = n + ' step' + (n === 1 ? '' : 's'); };
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Re-evaluate init script on the current page (since addInitScript only runs on navigation)
|
|
747
|
+
try { await this.page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }); } catch {}
|
|
748
|
+
|
|
749
|
+
this.page.on('framenavigated', async (frame) => {
|
|
750
|
+
if (frame !== this.page.mainFrame()) return;
|
|
751
|
+
const url = frame.url();
|
|
752
|
+
if (url === 'about:blank') return;
|
|
753
|
+
this.emit({ type: 'navigate', url });
|
|
754
|
+
setTimeout(async () => {
|
|
755
|
+
try { await this.page.evaluate((n) => { if (window.__skopixUpdateCount) window.__skopixUpdateCount(n); }, this.steps.length); } catch {}
|
|
756
|
+
}, 500);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
this.emit({ type: 'ready' });
|
|
760
|
+
}
|
|
761
|
+
|
|
600
762
|
async _captureStep(actionData) {
|
|
601
763
|
const id = this.nextId();
|
|
602
764
|
const page = this.page;
|
package/package.json
CHANGED