chromex-mcp 1.0.0

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/package.json +50 -0
  4. package/plugins/chromex/skills/chromex/scripts/chromex.mjs +343 -0
  5. package/plugins/chromex/skills/chromex/scripts/lib/browser.mjs +66 -0
  6. package/plugins/chromex/skills/chromex/scripts/lib/client.mjs +98 -0
  7. package/plugins/chromex/skills/chromex/scripts/lib/commands/console.mjs +37 -0
  8. package/plugins/chromex/skills/chromex/scripts/lib/commands/cookies.mjs +77 -0
  9. package/plugins/chromex/skills/chromex/scripts/lib/commands/coverage.mjs +95 -0
  10. package/plugins/chromex/skills/chromex/scripts/lib/commands/cpu.mjs +14 -0
  11. package/plugins/chromex/skills/chromex/scripts/lib/commands/dialog.mjs +38 -0
  12. package/plugins/chromex/skills/chromex/scripts/lib/commands/domsnapshot.mjs +84 -0
  13. package/plugins/chromex/skills/chromex/scripts/lib/commands/download.mjs +25 -0
  14. package/plugins/chromex/skills/chromex/scripts/lib/commands/drag.mjs +71 -0
  15. package/plugins/chromex/skills/chromex/scripts/lib/commands/emulate.mjs +44 -0
  16. package/plugins/chromex/skills/chromex/scripts/lib/commands/evaluate.mjs +31 -0
  17. package/plugins/chromex/skills/chromex/scripts/lib/commands/form.mjs +163 -0
  18. package/plugins/chromex/skills/chromex/scripts/lib/commands/geo.mjs +37 -0
  19. package/plugins/chromex/skills/chromex/scripts/lib/commands/har.mjs +101 -0
  20. package/plugins/chromex/skills/chromex/scripts/lib/commands/heap.mjs +24 -0
  21. package/plugins/chromex/skills/chromex/scripts/lib/commands/highlight.mjs +36 -0
  22. package/plugins/chromex/skills/chromex/scripts/lib/commands/html.mjs +10 -0
  23. package/plugins/chromex/skills/chromex/scripts/lib/commands/inject.mjs +39 -0
  24. package/plugins/chromex/skills/chromex/scripts/lib/commands/interact.mjs +88 -0
  25. package/plugins/chromex/skills/chromex/scripts/lib/commands/intercept.mjs +99 -0
  26. package/plugins/chromex/skills/chromex/scripts/lib/commands/navigate.mjs +45 -0
  27. package/plugins/chromex/skills/chromex/scripts/lib/commands/network.mjs +13 -0
  28. package/plugins/chromex/skills/chromex/scripts/lib/commands/pdf.mjs +16 -0
  29. package/plugins/chromex/skills/chromex/scripts/lib/commands/perf.mjs +98 -0
  30. package/plugins/chromex/skills/chromex/scripts/lib/commands/refs.mjs +67 -0
  31. package/plugins/chromex/skills/chromex/scripts/lib/commands/screenshot.mjs +54 -0
  32. package/plugins/chromex/skills/chromex/scripts/lib/commands/scroll.mjs +44 -0
  33. package/plugins/chromex/skills/chromex/scripts/lib/commands/snapshot.mjs +100 -0
  34. package/plugins/chromex/skills/chromex/scripts/lib/commands/storage.mjs +47 -0
  35. package/plugins/chromex/skills/chromex/scripts/lib/commands/tab.mjs +31 -0
  36. package/plugins/chromex/skills/chromex/scripts/lib/commands/throttle.mjs +38 -0
  37. package/plugins/chromex/skills/chromex/scripts/lib/commands/touch.mjs +62 -0
  38. package/plugins/chromex/skills/chromex/scripts/lib/commands/trace.mjs +51 -0
  39. package/plugins/chromex/skills/chromex/scripts/lib/commands/upload.mjs +43 -0
  40. package/plugins/chromex/skills/chromex/scripts/lib/commands/wait.mjs +84 -0
  41. package/plugins/chromex/skills/chromex/scripts/lib/commands/webauthn.mjs +47 -0
  42. package/plugins/chromex/skills/chromex/scripts/lib/config.mjs +100 -0
  43. package/plugins/chromex/skills/chromex/scripts/lib/daemon.mjs +368 -0
  44. package/plugins/chromex/skills/chromex/scripts/lib/ipc.mjs +178 -0
  45. package/plugins/chromex/skills/chromex/scripts/lib/launcher.mjs +111 -0
  46. package/plugins/chromex/skills/chromex/scripts/lib/security.mjs +48 -0
  47. package/plugins/chromex/skills/chromex/scripts/lib/utils.mjs +47 -0
  48. package/plugins/chromex/skills/chromex/scripts/mcp-server.mjs +726 -0
@@ -0,0 +1,95 @@
1
+ // CSS/JS coverage reporting via Profiler and CSS domains
2
+
3
+ let active = false;
4
+
5
+ export async function coverageStr(cdp, sid, action) {
6
+ if (!action) throw new Error('Usage: coverage <target> start | stop');
7
+
8
+ switch (action) {
9
+ case 'start': {
10
+ if (active) return 'Coverage collection already active.';
11
+ await cdp.send('DOM.enable', {}, sid);
12
+ await cdp.send('Profiler.enable', {}, sid);
13
+ await cdp.send('Debugger.enable', {}, sid);
14
+ await cdp.send('Profiler.startPreciseCoverage', { callCount: true, detailed: true }, sid);
15
+ await cdp.send('CSS.enable', {}, sid);
16
+ await cdp.send('CSS.startRuleUsageTracking', {}, sid);
17
+ active = true;
18
+ return 'Coverage collection started. Navigate/interact, then "coverage <target> stop" for report.';
19
+ }
20
+
21
+ case 'stop': {
22
+ if (!active) return 'No coverage collection active.';
23
+ active = false;
24
+
25
+ // JS Coverage
26
+ const { result: jsCoverage } = await cdp.send('Profiler.takePreciseCoverage', {}, sid);
27
+ await cdp.send('Profiler.stopPreciseCoverage', {}, sid);
28
+ await cdp.send('Profiler.disable', {}, sid);
29
+ await cdp.send('Debugger.disable', {}, sid);
30
+
31
+ // CSS Coverage
32
+ const { ruleUsage } = await cdp.send('CSS.stopRuleUsageTracking', {}, sid);
33
+ await cdp.send('CSS.disable', {}, sid);
34
+
35
+ // JS report
36
+ const jsFiles = [];
37
+ let jsTotalBytes = 0;
38
+ let jsUsedBytes = 0;
39
+ for (const script of jsCoverage) {
40
+ if (!script.url || script.url.startsWith('extensions://')) continue;
41
+ let scriptTotal = 0;
42
+ let scriptUsed = 0;
43
+ for (const fn of script.functions) {
44
+ for (const range of fn.ranges) {
45
+ const size = range.endOffset - range.startOffset;
46
+ scriptTotal += size;
47
+ if (range.count > 0) scriptUsed += size;
48
+ }
49
+ }
50
+ jsTotalBytes += scriptTotal;
51
+ jsUsedBytes += scriptUsed;
52
+ if (scriptTotal > 0) {
53
+ jsFiles.push({
54
+ url: script.url.substring(0, 80),
55
+ total: scriptTotal,
56
+ used: scriptUsed,
57
+ pct: Math.round((scriptUsed / scriptTotal) * 100),
58
+ });
59
+ }
60
+ }
61
+
62
+ // CSS report
63
+ const cssUsed = ruleUsage ? ruleUsage.filter(r => r.used).length : 0;
64
+ const cssTotal = ruleUsage ? ruleUsage.length : 0;
65
+
66
+ const lines = ['## JavaScript Coverage'];
67
+ lines.push(`Total: ${formatBytes(jsTotalBytes)}, Used: ${formatBytes(jsUsedBytes)} (${jsTotalBytes > 0 ? Math.round((jsUsedBytes / jsTotalBytes) * 100) : 0}%)`);
68
+ lines.push('');
69
+
70
+ // Top unused files
71
+ const unused = jsFiles.filter(f => f.pct < 50).sort((a, b) => (a.pct - b.pct));
72
+ if (unused.length > 0) {
73
+ lines.push('Files with <50% usage:');
74
+ for (const f of unused.slice(0, 10)) {
75
+ lines.push(` ${String(f.pct).padStart(3)}% ${formatBytes(f.total).padStart(8)} ${f.url}`);
76
+ }
77
+ lines.push('');
78
+ }
79
+
80
+ lines.push('## CSS Coverage');
81
+ lines.push(`Rules: ${cssTotal} total, ${cssUsed} used (${cssTotal > 0 ? Math.round((cssUsed / cssTotal) * 100) : 0}%)`);
82
+
83
+ return lines.join('\n');
84
+ }
85
+
86
+ default:
87
+ throw new Error('Usage: coverage <target> start | stop');
88
+ }
89
+ }
90
+
91
+ function formatBytes(bytes) {
92
+ if (bytes < 1024) return `${bytes}B`;
93
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
94
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
95
+ }
@@ -0,0 +1,14 @@
1
+ // CPU throttling via Emulation domain
2
+
3
+ export async function cpuStr(cdp, sid, rate) {
4
+ if (!rate || rate === 'reset') {
5
+ await cdp.send('Emulation.setCPUThrottlingRate', { rate: 1 }, sid);
6
+ return 'CPU throttling reset to normal.';
7
+ }
8
+
9
+ const r = parseFloat(rate);
10
+ if (isNaN(r) || r < 1) throw new Error('Rate must be >= 1 (1=normal, 4=4x slower, 6=mobile sim)');
11
+
12
+ await cdp.send('Emulation.setCPUThrottlingRate', { rate: r }, sid);
13
+ return `CPU throttled to ${r}x slower.`;
14
+ }
@@ -0,0 +1,38 @@
1
+ // Dialog handling (alert/confirm/prompt) via Page domain
2
+
3
+ export async function dialogStr(cdp, sid, action, text) {
4
+ await cdp.send('Page.enable', {}, sid);
5
+
6
+ switch (action) {
7
+ case 'accept':
8
+ await cdp.send('Page.handleJavaScriptDialog', {
9
+ accept: true,
10
+ promptText: text || '',
11
+ }, sid);
12
+ return `Dialog accepted${text ? ` with text "${text}"` : ''}.`;
13
+
14
+ case 'dismiss':
15
+ await cdp.send('Page.handleJavaScriptDialog', { accept: false }, sid);
16
+ return 'Dialog dismissed.';
17
+
18
+ case 'auto':
19
+ // Retorna flag para o daemon registrar auto-handler
20
+ return '__AUTO_DIALOG__';
21
+
22
+ default:
23
+ throw new Error('Usage: dialog <target> accept [text] | dismiss | auto');
24
+ }
25
+ }
26
+
27
+ // Registra handler permanente que auto-aceita dialogs
28
+ export function setupAutoDialog(cdp, sid) {
29
+ cdp.onEvent('Page.javascriptDialogOpening', async (params) => {
30
+ try {
31
+ await cdp.send('Page.handleJavaScriptDialog', {
32
+ accept: true,
33
+ promptText: '',
34
+ }, sid);
35
+ } catch { /* dialog ja foi tratado */ }
36
+ });
37
+ return 'Auto-dialog enabled: all dialogs (alert/confirm/prompt) will be auto-accepted.';
38
+ }
@@ -0,0 +1,84 @@
1
+ // Structured DOM snapshot via DOMSnapshot domain
2
+
3
+ export async function domsnapshotStr(cdp, sid, includeStyles) {
4
+ const computedStyles = includeStyles
5
+ ? ['display', 'visibility', 'opacity', 'overflow', 'position', 'z-index', 'font-size', 'color', 'background-color']
6
+ : ['display', 'visibility'];
7
+
8
+ const snapshot = await cdp.send('DOMSnapshot.captureSnapshot', {
9
+ computedStyles,
10
+ includeDOMRects: true,
11
+ includePaintOrder: true,
12
+ }, sid);
13
+
14
+ const { documents, strings } = snapshot;
15
+ if (!documents || documents.length === 0) return 'No DOM snapshot available.';
16
+
17
+ const doc = documents[0];
18
+ const nodes = doc.nodes;
19
+ const layout = doc.layout;
20
+ const lines = [];
21
+
22
+ // Build layout index by nodeIndex
23
+ const layoutMap = new Map();
24
+ if (layout && layout.nodeIndex && layout.bounds) {
25
+ for (let i = 0; i < layout.nodeIndex.length; i++) {
26
+ const ni = layout.nodeIndex[i];
27
+ const b = layout.bounds[i]; // Each bounds is [x, y, w, h]
28
+ if (Array.isArray(b) && b.length >= 4) {
29
+ layoutMap.set(ni, { bounds: b });
30
+ }
31
+ }
32
+ }
33
+
34
+ // Build indented tree
35
+ const parentIndex = nodes.parentIndex || [];
36
+ const nodeNames = nodes.nodeName || [];
37
+ const nodeValues = nodes.nodeValue || [];
38
+ const attrs = nodes.attributes || [];
39
+
40
+ // Calcular profundidade
41
+ const depth = new Array(nodeNames.length).fill(0);
42
+ for (let i = 1; i < nodeNames.length; i++) {
43
+ if (parentIndex[i] >= 0) depth[i] = depth[parentIndex[i]] + 1;
44
+ }
45
+
46
+ for (let i = 0; i < nodeNames.length; i++) {
47
+ const name = strings[nodeNames[i]] || '';
48
+ if (!name || name === '#document' || name === '#comment') continue;
49
+
50
+ const indent = ' '.repeat(Math.min(depth[i], 8));
51
+ const lay = layoutMap.get(i);
52
+
53
+ if (name === '#text') {
54
+ const text = strings[nodeValues[i]] || '';
55
+ if (text.trim()) {
56
+ lines.push(`${indent}"${text.trim().substring(0, 80)}"`);
57
+ }
58
+ continue;
59
+ }
60
+
61
+ // Atributos do nó
62
+ const nodeAttrs = attrs[i] || [];
63
+ let attrStr = '';
64
+ for (let a = 0; a < nodeAttrs.length; a += 2) {
65
+ const attrName = strings[nodeAttrs[a]] || '';
66
+ const attrVal = strings[nodeAttrs[a + 1]] || '';
67
+ if (['id', 'class', 'name', 'type', 'href', 'src', 'role'].includes(attrName) && attrVal) {
68
+ attrStr += ` ${attrName}="${attrVal.substring(0, 40)}"`;
69
+ }
70
+ }
71
+
72
+ let line = `${indent}<${name.toLowerCase()}${attrStr}>`;
73
+
74
+ // Adicionar bounding rect se disponível
75
+ if (lay?.bounds) {
76
+ const [x, y, w, h] = lay.bounds;
77
+ line += ` [${Math.round(x)},${Math.round(y)} ${Math.round(w)}x${Math.round(h)}]`;
78
+ }
79
+
80
+ lines.push(line);
81
+ }
82
+
83
+ return lines.join('\n');
84
+ }
@@ -0,0 +1,25 @@
1
+ // Download control via Browser domain
2
+
3
+ import { existsSync, mkdirSync } from 'fs';
4
+
5
+ export async function downloadStr(cdp, sid, action, path) {
6
+ switch (action) {
7
+ case 'allow': {
8
+ const downloadPath = path || '/tmp/chromex-downloads';
9
+ if (!existsSync(downloadPath)) mkdirSync(downloadPath, { recursive: true });
10
+ await cdp.send('Browser.setDownloadBehavior', {
11
+ behavior: 'allowAndName',
12
+ downloadPath,
13
+ }, sid);
14
+ return `Downloads allowed. Path: ${downloadPath}`;
15
+ }
16
+ case 'deny':
17
+ await cdp.send('Browser.setDownloadBehavior', { behavior: 'deny' }, sid);
18
+ return 'Downloads blocked.';
19
+ case 'reset':
20
+ await cdp.send('Browser.setDownloadBehavior', { behavior: 'default' }, sid);
21
+ return 'Download behavior reset to default.';
22
+ default:
23
+ throw new Error('Usage: download <target> allow [path] | deny | reset');
24
+ }
25
+ }
@@ -0,0 +1,71 @@
1
+ // Drag & Drop via Input domain
2
+
3
+ import { sleep } from '../utils.mjs';
4
+ import { evalStr } from './evaluate.mjs';
5
+
6
+ export async function dragStr(cdp, sid, from, to) {
7
+ if (!from || !to) throw new Error('Usage: drag <target> <from_selector> <to_selector> or drag <target> x1,y1 x2,y2');
8
+
9
+ let fromX, fromY, toX, toY;
10
+
11
+ // Coordenadas diretas: "100,200"
12
+ if (from.includes(',') && to.includes(',')) {
13
+ [fromX, fromY] = from.split(',').map(Number);
14
+ [toX, toY] = to.split(',').map(Number);
15
+ if (isNaN(fromX) || isNaN(fromY) || isNaN(toX) || isNaN(toY)) {
16
+ throw new Error('Invalid coordinates. Use: x1,y1 x2,y2');
17
+ }
18
+ } else {
19
+ // CSS selectors: resolver para coordenadas centrais
20
+ fromX = await getCenterX(cdp, sid, from);
21
+ fromY = await getCenterY(cdp, sid, from);
22
+ toX = await getCenterX(cdp, sid, to);
23
+ toY = await getCenterY(cdp, sid, to);
24
+ }
25
+
26
+ const base = { button: 'left', clickCount: 1, modifiers: 0 };
27
+
28
+ // Mouse down no ponto de origem
29
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mousePressed', x: fromX, y: fromY }, sid);
30
+ await sleep(100);
31
+
32
+ // Mover em passos intermediários para simular arraste real
33
+ const steps = 5;
34
+ for (let i = 1; i <= steps; i++) {
35
+ const x = fromX + (toX - fromX) * (i / steps);
36
+ const y = fromY + (toY - fromY) * (i / steps);
37
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mouseMoved', x, y }, sid);
38
+ await sleep(50);
39
+ }
40
+
41
+ // Mouse up no destino
42
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mouseReleased', x: toX, y: toY }, sid);
43
+
44
+ return `Dragged from (${Math.round(fromX)},${Math.round(fromY)}) to (${Math.round(toX)},${Math.round(toY)}).`;
45
+ }
46
+
47
+ async function getCenterX(cdp, sid, selector) {
48
+ const raw = await evalStr(cdp, sid, `
49
+ (function() {
50
+ const el = document.querySelector(${JSON.stringify(selector)});
51
+ if (!el) return null;
52
+ const r = el.getBoundingClientRect();
53
+ return Math.round(r.left + r.width / 2);
54
+ })()
55
+ `);
56
+ if (raw === 'null') throw new Error(`Element not found: ${selector}`);
57
+ return parseFloat(raw);
58
+ }
59
+
60
+ async function getCenterY(cdp, sid, selector) {
61
+ const raw = await evalStr(cdp, sid, `
62
+ (function() {
63
+ const el = document.querySelector(${JSON.stringify(selector)});
64
+ if (!el) return null;
65
+ const r = el.getBoundingClientRect();
66
+ return Math.round(r.top + r.height / 2);
67
+ })()
68
+ `);
69
+ if (raw === 'null') throw new Error(`Element not found: ${selector}`);
70
+ return parseFloat(raw);
71
+ }
@@ -0,0 +1,44 @@
1
+ // Device emulation via CDP
2
+
3
+ const DEVICES = {
4
+ 'iphone-14': { width: 390, height: 844, deviceScaleFactor: 3, mobile: true, ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },
5
+ 'iphone-15-pro': { width: 393, height: 852, deviceScaleFactor: 3, mobile: true, ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' },
6
+ 'ipad-pro': { width: 1024, height: 1366, deviceScaleFactor: 2, mobile: true, ua: 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' },
7
+ 'pixel-7': { width: 412, height: 915, deviceScaleFactor: 2.625, mobile: true, ua: 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36' },
8
+ 'galaxy-s23': { width: 360, height: 780, deviceScaleFactor: 3, mobile: true, ua: 'Mozilla/5.0 (Linux; Android 13; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36' },
9
+ 'macbook-air': { width: 1440, height: 900, deviceScaleFactor: 2, mobile: false, ua: '' },
10
+ 'desktop-1080p': { width: 1920, height: 1080, deviceScaleFactor: 1, mobile: false, ua: '' },
11
+ 'desktop-4k': { width: 3840, height: 2160, deviceScaleFactor: 1, mobile: false, ua: '' },
12
+ };
13
+
14
+ export async function emulateStr(cdp, sid, device) {
15
+ if (!device) {
16
+ const list = Object.keys(DEVICES).join(', ');
17
+ throw new Error(`Device name required. Available: ${list}, reset`);
18
+ }
19
+
20
+ if (device === 'reset') {
21
+ await cdp.send('Emulation.clearDeviceMetricsOverride', {}, sid);
22
+ await cdp.send('Emulation.setUserAgentOverride', { userAgent: '' }, sid);
23
+ return 'Device emulation reset to default.';
24
+ }
25
+
26
+ const preset = DEVICES[device.toLowerCase()];
27
+ if (!preset) {
28
+ const list = Object.keys(DEVICES).join(', ');
29
+ throw new Error(`Unknown device: ${device}. Available: ${list}, reset`);
30
+ }
31
+
32
+ await cdp.send('Emulation.setDeviceMetricsOverride', {
33
+ width: preset.width,
34
+ height: preset.height,
35
+ deviceScaleFactor: preset.deviceScaleFactor,
36
+ mobile: preset.mobile,
37
+ }, sid);
38
+
39
+ if (preset.ua) {
40
+ await cdp.send('Emulation.setUserAgentOverride', { userAgent: preset.ua }, sid);
41
+ }
42
+
43
+ return `Emulating ${device}: ${preset.width}x${preset.height} @${preset.deviceScaleFactor}x${preset.mobile ? ' (mobile)' : ''}`;
44
+ }
@@ -0,0 +1,31 @@
1
+ // Eval JS + evalraw CDP
2
+
3
+ import { isCdpMethodBlocked } from '../security.mjs';
4
+
5
+ export async function evalStr(cdp, sid, expression) {
6
+ await cdp.send('Runtime.enable', {}, sid);
7
+ const result = await cdp.send('Runtime.evaluate', {
8
+ expression, returnByValue: true, awaitPromise: true,
9
+ }, sid);
10
+ if (result.exceptionDetails) {
11
+ throw new Error(result.exceptionDetails.text || result.exceptionDetails.exception?.description);
12
+ }
13
+ const val = result.result.value;
14
+ return typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val ?? '');
15
+ }
16
+
17
+ export async function evalRawStr(cdp, sid, method, paramsJson, config) {
18
+ if (!method) throw new Error('CDP method required (e.g. "DOM.getDocument")');
19
+
20
+ if (isCdpMethodBlocked(method, config)) {
21
+ throw new Error(`CDP method "${method}" is blocked by security config. Edit ~/.chromex/config.json to change.`);
22
+ }
23
+
24
+ let params = {};
25
+ if (paramsJson) {
26
+ try { params = JSON.parse(paramsJson); }
27
+ catch { throw new Error(`Invalid JSON params: ${paramsJson}`); }
28
+ }
29
+ const result = await cdp.send(method, params, sid);
30
+ return JSON.stringify(result, null, 2);
31
+ }
@@ -0,0 +1,163 @@
1
+ // Form filling: fill, clear, select, check, form (batch)
2
+
3
+ import { sleep } from '../utils.mjs';
4
+ import { evalStr } from './evaluate.mjs';
5
+
6
+ export async function fillStr(cdp, sid, selector, value) {
7
+ if (!selector) throw new Error('CSS selector required');
8
+ if (value == null) throw new Error('Value required');
9
+
10
+ // Focar e limpar o campo
11
+ const info = await evalStr(cdp, sid, `
12
+ (function() {
13
+ const el = document.querySelector(${JSON.stringify(selector)});
14
+ if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
15
+ el.scrollIntoView({ block: 'center' });
16
+ el.focus();
17
+ if (el.select) el.select();
18
+ return { ok: true, tag: el.tagName, type: el.type || '', name: el.name || '' };
19
+ })()
20
+ `);
21
+ const r = JSON.parse(info);
22
+ if (!r.ok) throw new Error(r.error);
23
+
24
+ // Selecionar tudo e deletar (funciona em campos React controlled)
25
+ await cdp.send('Input.dispatchKeyEvent', {
26
+ type: 'keyDown', key: 'a', code: 'KeyA', modifiers: getModifierKey(),
27
+ }, sid);
28
+ await cdp.send('Input.dispatchKeyEvent', {
29
+ type: 'keyUp', key: 'a', code: 'KeyA', modifiers: 0,
30
+ }, sid);
31
+ await sleep(50);
32
+
33
+ // Inserir texto
34
+ await cdp.send('Input.insertText', { text: String(value) }, sid);
35
+
36
+ // Disparar eventos de mudança para frameworks reativos
37
+ await evalStr(cdp, sid, `
38
+ (function() {
39
+ const el = document.querySelector(${JSON.stringify(selector)});
40
+ if (!el) return;
41
+ el.dispatchEvent(new Event('input', { bubbles: true }));
42
+ el.dispatchEvent(new Event('change', { bubbles: true }));
43
+ })()
44
+ `);
45
+
46
+ return `Filled <${r.tag}${r.name ? ` name="${r.name}"` : ''}> with "${String(value).substring(0, 50)}"`;
47
+ }
48
+
49
+ export async function clearStr(cdp, sid, selector) {
50
+ if (!selector) throw new Error('CSS selector required');
51
+
52
+ const info = await evalStr(cdp, sid, `
53
+ (function() {
54
+ const el = document.querySelector(${JSON.stringify(selector)});
55
+ if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
56
+ el.scrollIntoView({ block: 'center' });
57
+ el.focus();
58
+ if (el.select) el.select();
59
+ return { ok: true, tag: el.tagName, name: el.name || '' };
60
+ })()
61
+ `);
62
+ const r = JSON.parse(info);
63
+ if (!r.ok) throw new Error(r.error);
64
+
65
+ // Selecionar tudo + Delete
66
+ await cdp.send('Input.dispatchKeyEvent', {
67
+ type: 'keyDown', key: 'a', code: 'KeyA', modifiers: getModifierKey(),
68
+ }, sid);
69
+ await cdp.send('Input.dispatchKeyEvent', {
70
+ type: 'keyUp', key: 'a', code: 'KeyA', modifiers: 0,
71
+ }, sid);
72
+ await cdp.send('Input.dispatchKeyEvent', {
73
+ type: 'keyDown', key: 'Delete', code: 'Delete',
74
+ }, sid);
75
+ await cdp.send('Input.dispatchKeyEvent', {
76
+ type: 'keyUp', key: 'Delete', code: 'Delete',
77
+ }, sid);
78
+
79
+ await evalStr(cdp, sid, `
80
+ (function() {
81
+ const el = document.querySelector(${JSON.stringify(selector)});
82
+ if (!el) return;
83
+ el.dispatchEvent(new Event('input', { bubbles: true }));
84
+ el.dispatchEvent(new Event('change', { bubbles: true }));
85
+ })()
86
+ `);
87
+
88
+ return `Cleared <${r.tag}${r.name ? ` name="${r.name}"` : ''}>`;
89
+ }
90
+
91
+ export async function selectStr(cdp, sid, selector, value) {
92
+ if (!selector) throw new Error('CSS selector required');
93
+ if (value == null) throw new Error('Value required');
94
+
95
+ const result = await evalStr(cdp, sid, `
96
+ (function() {
97
+ const el = document.querySelector(${JSON.stringify(selector)});
98
+ if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
99
+ if (el.tagName !== 'SELECT') return { ok: false, error: 'Element is not a <select>' };
100
+ const val = ${JSON.stringify(String(value))};
101
+ const option = Array.from(el.options).find(o => o.value === val || o.textContent.trim() === val);
102
+ if (!option) {
103
+ const opts = Array.from(el.options).map(o => o.value || o.textContent.trim()).slice(0, 10);
104
+ return { ok: false, error: 'Option not found: ' + val + '. Available: ' + opts.join(', ') };
105
+ }
106
+ el.value = option.value;
107
+ el.dispatchEvent(new Event('change', { bubbles: true }));
108
+ el.dispatchEvent(new Event('input', { bubbles: true }));
109
+ return { ok: true, selected: option.textContent.trim(), value: option.value };
110
+ })()
111
+ `);
112
+ const r = JSON.parse(result);
113
+ if (!r.ok) throw new Error(r.error);
114
+ return `Selected "${r.selected}" (value="${r.value}")`;
115
+ }
116
+
117
+ export async function checkStr(cdp, sid, selector, checked = true) {
118
+ if (!selector) throw new Error('CSS selector required');
119
+
120
+ const result = await evalStr(cdp, sid, `
121
+ (function() {
122
+ const el = document.querySelector(${JSON.stringify(selector)});
123
+ if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
124
+ const want = ${JSON.stringify(checked)};
125
+ if (el.type !== 'checkbox' && el.type !== 'radio') {
126
+ return { ok: false, error: 'Element is not a checkbox/radio (type: ' + el.type + ')' };
127
+ }
128
+ if (el.checked !== want) {
129
+ el.click();
130
+ el.dispatchEvent(new Event('change', { bubbles: true }));
131
+ }
132
+ return { ok: true, type: el.type, checked: el.checked, name: el.name || '' };
133
+ })()
134
+ `);
135
+ const r = JSON.parse(result);
136
+ if (!r.ok) throw new Error(r.error);
137
+ return `${r.type} ${r.name ? `"${r.name}" ` : ''}is now ${r.checked ? 'checked' : 'unchecked'}`;
138
+ }
139
+
140
+ export async function formStr(cdp, sid, fieldsJson) {
141
+ let fields;
142
+ try {
143
+ fields = JSON.parse(fieldsJson);
144
+ } catch {
145
+ throw new Error(`Invalid JSON: ${fieldsJson}`);
146
+ }
147
+
148
+ const results = [];
149
+ for (const [selector, value] of Object.entries(fields)) {
150
+ if (typeof value === 'boolean') {
151
+ results.push(await checkStr(cdp, sid, selector, value));
152
+ } else {
153
+ results.push(await fillStr(cdp, sid, selector, String(value)));
154
+ }
155
+ await sleep(100); // Pausa entre campos para frameworks reativos
156
+ }
157
+ return results.join('\n');
158
+ }
159
+
160
+ // macOS usa Meta (Cmd), Linux usa Control
161
+ function getModifierKey() {
162
+ return process.platform === 'darwin' ? 4 : 2; // 4 = Meta, 2 = Control
163
+ }
@@ -0,0 +1,37 @@
1
+ // Geolocation, timezone, and locale override via Emulation domain
2
+
3
+ export async function geoStr(cdp, sid, lat, lon, accuracy) {
4
+ if (lat === 'reset') {
5
+ await cdp.send('Emulation.clearGeolocationOverride', {}, sid);
6
+ return 'Geolocation override cleared.';
7
+ }
8
+
9
+ const latitude = parseFloat(lat);
10
+ const longitude = parseFloat(lon);
11
+ if (isNaN(latitude) || isNaN(longitude)) {
12
+ throw new Error('Usage: geo <target> <latitude> <longitude> [accuracy] | reset');
13
+ }
14
+
15
+ await cdp.send('Emulation.setGeolocationOverride', {
16
+ latitude, longitude, accuracy: parseFloat(accuracy) || 100,
17
+ }, sid);
18
+ return `Geolocation set to (${latitude}, ${longitude}).`;
19
+ }
20
+
21
+ export async function timezoneStr(cdp, sid, tz) {
22
+ if (!tz || tz === 'reset') {
23
+ await cdp.send('Emulation.setTimezoneOverride', { timezoneId: '' }, sid);
24
+ return 'Timezone override cleared.';
25
+ }
26
+ await cdp.send('Emulation.setTimezoneOverride', { timezoneId: tz }, sid);
27
+ return `Timezone set to ${tz}.`;
28
+ }
29
+
30
+ export async function localeStr(cdp, sid, locale) {
31
+ if (!locale || locale === 'reset') {
32
+ await cdp.send('Emulation.setLocaleOverride', { locale: '' }, sid);
33
+ return 'Locale override cleared.';
34
+ }
35
+ await cdp.send('Emulation.setLocaleOverride', { locale }, sid);
36
+ return `Locale set to ${locale}.`;
37
+ }