skopix 2.0.97 → 2.0.99
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 +86 -0
- package/cli/commands/dashboard.js +19 -3
- package/core/recorder.js +162 -0
- package/package.json +1 -1
package/cli/commands/agent.js
CHANGED
|
@@ -190,6 +190,7 @@ export async function agentCommand(options) {
|
|
|
190
190
|
if (msg.type === 'ping') { ws.send(JSON.stringify({ type: 'pong' })); return; }
|
|
191
191
|
if (msg.type === 'record') { await handleRecord(msg); return; }
|
|
192
192
|
if (msg.type === 'replay') { await handleReplay(msg); return; }
|
|
193
|
+
if (msg.type === 'debug-record') { await handleDebugRecord(msg); return; }
|
|
193
194
|
});
|
|
194
195
|
|
|
195
196
|
ws.addEventListener('close', () => {
|
|
@@ -380,6 +381,91 @@ export async function agentCommand(options) {
|
|
|
380
381
|
}
|
|
381
382
|
}
|
|
382
383
|
|
|
384
|
+
// ── DEBUG-RECORD JOB ────────────────────────────────────────────────────────
|
|
385
|
+
async function handleDebugRecord(msg) {
|
|
386
|
+
const { runId, recordingId, replaySteps, startUrl } = msg;
|
|
387
|
+
console.log(chalk.cyan(' ⏸ Debug replay: ') + chalk.white(replaySteps.length + ' steps to debug point'));
|
|
388
|
+
|
|
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 {} };
|
|
391
|
+
|
|
392
|
+
sendRun({ type: 'stdout', text: '' });
|
|
393
|
+
sendRun({ type: 'stdout', text: ' Replaying ' + replaySteps.length + ' steps to reach debug point...' });
|
|
394
|
+
sendRun({ type: 'stdout', text: '━'.repeat(60) });
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const { chromium } = await import('playwright');
|
|
398
|
+
const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--disable-blink-features=AutomationControlled', '--allow-insecure-localhost'] });
|
|
399
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
400
|
+
const page = await ctx.newPage();
|
|
401
|
+
|
|
402
|
+
if (startUrl) {
|
|
403
|
+
await page.goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
404
|
+
await page.waitForTimeout(1000);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Replay each step to reach debug point
|
|
408
|
+
let stepNum = 0;
|
|
409
|
+
for (const step of replaySteps) {
|
|
410
|
+
stepNum++;
|
|
411
|
+
sendRun({ type: 'stdout', text: ' [' + stepNum + '/' + replaySteps.length + '] ' + (step.action || '').toUpperCase() + ' — ' + (step.description || step.stableSelector || step.selector || '') });
|
|
412
|
+
try {
|
|
413
|
+
const sel = step.stableSelector || step.selector;
|
|
414
|
+
await executeStep(step, sel, page, { url: startUrl });
|
|
415
|
+
} catch (err) {
|
|
416
|
+
sendRun({ type: 'stdout', text: ' ✖ FAILED at step ' + stepNum + ': ' + err.message });
|
|
417
|
+
sendRun({ type: 'done', exitCode: 1, status: 'failed' });
|
|
418
|
+
sendRun({ type: 'stdout', text: '<span style="color:var(--red)">✗ Replay failed — could not reach debug point</span>' });
|
|
419
|
+
await browser.close().catch(() => {});
|
|
420
|
+
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
sendRun({ type: 'stdout', text: '' });
|
|
426
|
+
sendRun({ type: 'stdout', text: ' ✔ Reached debug point — starting recorder' });
|
|
427
|
+
|
|
428
|
+
// Attach RecordingSession to the existing browser — injects toolbar into current page
|
|
429
|
+
const screenshotDir = path.join(os.homedir(), '.skopix', 'recordings', recordingId);
|
|
430
|
+
await fs.ensureDir(screenshotDir);
|
|
431
|
+
|
|
432
|
+
const { RecordingSession } = await import('../../core/recorder.js');
|
|
433
|
+
const session = new RecordingSession({ url: startUrl || '', sessionId: recordingId, screenshotDir });
|
|
434
|
+
|
|
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
|
+
};
|
|
446
|
+
|
|
447
|
+
// Attach to existing browser/context/page — injects toolbar without new browser
|
|
448
|
+
await session.attachTo(browser, ctx, page);
|
|
449
|
+
|
|
450
|
+
// Listen for stop signal from server
|
|
451
|
+
const stopHandler = (event) => {
|
|
452
|
+
let m; try { m = JSON.parse(event.data); } catch { return; }
|
|
453
|
+
if (m.type === 'stopRecord' && m.recordingId === recordingId) {
|
|
454
|
+
session.stop().catch(() => {});
|
|
455
|
+
ws.removeEventListener('message', stopHandler);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
ws.addEventListener('message', stopHandler);
|
|
459
|
+
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.error(chalk.red(' ✖ Debug error: ' + err.message));
|
|
462
|
+
sendRun({ type: 'stdout', text: ' ✖ Debug replay error: ' + err.message });
|
|
463
|
+
sendRun({ type: 'done', exitCode: 1, status: 'failed' });
|
|
464
|
+
sendRun({ type: 'stdout', text: '<span style="color:var(--red)">✗ Replay failed — could not reach debug point</span>' });
|
|
465
|
+
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
383
469
|
// ── HELPERS ────────────────────────────────────────────────────────────────
|
|
384
470
|
function sanitiseSelector(sel) {
|
|
385
471
|
if (!sel) return sel;
|
|
@@ -1249,17 +1249,33 @@ export async function dashboardCommand(options) {
|
|
|
1249
1249
|
const broadcast = (line) => { run.output.push(line); run.listeners.forEach(l => l(line)); };
|
|
1250
1250
|
const broadcastRec = (line) => { recording.output.push(line); recording.listeners.forEach(l => l(line)); };
|
|
1251
1251
|
|
|
1252
|
-
// In team mode,
|
|
1252
|
+
// In team mode, dispatch to a headed agent — never run locally on the server
|
|
1253
1253
|
if (teamMode) {
|
|
1254
1254
|
const headedAgent = getLeastBusyAgent(currentUser?.id, true);
|
|
1255
1255
|
if (!headedAgent) {
|
|
1256
|
-
sendJSON(res, 503, { error: 'No agent with a display connected. Run "skopix agent --server ' + (req.headers.host || 'localhost:9000') + '" on your local machine first to use debug mode.' });
|
|
1256
|
+
sendJSON(res, 503, { error: 'No agent with a display connected. Run "skopix agent --server http://' + (req.headers.host || 'localhost:9000') + '" on your local machine first to use debug mode.' });
|
|
1257
1257
|
activeRuns.delete(runId);
|
|
1258
1258
|
activeRecordings.delete(recordingId);
|
|
1259
1259
|
return;
|
|
1260
1260
|
}
|
|
1261
|
+
// Dispatch to the headed agent
|
|
1262
|
+
headedAgent.status = 'recording';
|
|
1263
|
+
headedAgent.currentJob = { type: 'debug-record', runId, recordingId };
|
|
1264
|
+
broadcastAgentList();
|
|
1265
|
+
sendToAgent(headedAgent, {
|
|
1266
|
+
type: 'debug-record',
|
|
1267
|
+
runId,
|
|
1268
|
+
recordingId,
|
|
1269
|
+
replaySteps,
|
|
1270
|
+
startUrl: test.url,
|
|
1271
|
+
env: { ...process.env, ...(await resolveUserSecretsEnv(currentUser?.id, teamMode) || {}) },
|
|
1272
|
+
});
|
|
1273
|
+
sendJSON(res, 200, { runId, recordingId, agent: { id: headedAgent.id, name: headedAgent.name } });
|
|
1274
|
+
return;
|
|
1261
1275
|
}
|
|
1262
1276
|
|
|
1277
|
+
// Solo mode — run locally
|
|
1278
|
+
sendJSON(res, 200, { runId, recordingId });
|
|
1263
1279
|
// Start the debug session asynchronously
|
|
1264
1280
|
(async () => {
|
|
1265
1281
|
const sessionDir = path.join(reportsDir, runId);
|
|
@@ -2353,7 +2369,7 @@ export async function dashboardCommand(options) {
|
|
|
2353
2369
|
|
|
2354
2370
|
if (msg.type === 'register') {
|
|
2355
2371
|
const agentId = msg.agentId || crypto.randomUUID();
|
|
2356
|
-
const agent = { id: agentId, name: msg.name || msg.machine, machine: msg.machine, userId: msg.userId || null, ws, status: 'idle', currentJob: null, connectedAt: Date.now(), hasDisplay: msg.hasDisplay
|
|
2372
|
+
const agent = { id: agentId, name: msg.name || msg.machine, machine: msg.machine, userId: msg.userId || null, ws, status: 'idle', currentJob: null, connectedAt: Date.now(), hasDisplay: msg.hasDisplay === true };
|
|
2357
2373
|
agents.set(agentId, agent);
|
|
2358
2374
|
ws.agentId = agentId;
|
|
2359
2375
|
ws.send(JSON.stringify({ type: 'registered', agentId }));
|
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