chocolatito-code 1.1.0 → 1.3.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.
- package/README.md +66 -0
- package/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +218 -41
- package/dist/agent/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +39 -0
- package/dist/agent/toolGate.js +6 -1
- package/dist/config/engine.d.ts +21 -1
- package/dist/config/engine.js +22 -1
- package/dist/config/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -0
- package/dist/index.js +22 -7
- package/dist/mcp/client.d.ts +28 -0
- package/dist/mcp/client.js +62 -8
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- package/dist/tools/browserExtension.js +153 -7
- package/dist/tools/computerUse.d.ts +1 -1
- package/dist/tools/computerUse.js +183 -2
- package/dist/tools/definitions.js +9 -2
- package/dist/tools/grepSearch.js +111 -23
- package/dist/tools/runner.js +1 -1
- package/dist/tools/toolDefsComputer.js +6 -2
- package/dist/tools/visionBridge.js +60 -3
- package/dist/tools/win/hostScript.js +154 -0
- package/dist/tools/writeFile.d.ts +16 -1
- package/dist/tools/writeFile.js +22 -1
- package/dist/ui/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +53 -4
- package/dist/ui/keyboardGuard.d.ts +21 -0
- package/dist/ui/keyboardGuard.js +30 -1
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.d.ts +5 -0
- package/dist/ui/prompt.js +109 -16
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/dist/ui/spinner.d.ts +15 -8
- package/dist/ui/spinner.js +80 -42
- package/extension/background.js +191 -14
- package/extension/content.js +446 -48
- package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/ORIGEN.md +48 -0
- package/extension/iconos/logotipo.png +0 -0
- package/extension/manifest.json +27 -4
- package/extension/popup.html +16 -6
- package/package.json +3 -2
package/extension/content.js
CHANGED
|
@@ -17,7 +17,245 @@
|
|
|
17
17
|
|
|
18
18
|
const IS_TOP = window.top === window;
|
|
19
19
|
|
|
20
|
-
// ========================================================
|
|
20
|
+
// ======================================================== overlay visual y borde naranja (#D97757)
|
|
21
|
+
|
|
22
|
+
let overlayHost = null;
|
|
23
|
+
let overlayShadow = null;
|
|
24
|
+
let overlayFrame = null;
|
|
25
|
+
let overlayLayers = null;
|
|
26
|
+
let overlayActivo = false;
|
|
27
|
+
|
|
28
|
+
// Icono oficial del zorro de Chocolatito a 16px en base64 para cargar de inmediato
|
|
29
|
+
// sin depender de peticiones de red ni fallar por políticas CSP de páginas externas.
|
|
30
|
+
const LOGO_16_DATA_URI =
|
|
31
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC10lEQVR4nI1RS2hUZxT+zv+4uZk7c5MxNk5SiI/4IhZUhIJ2IVnYRbGQUqQU3YhS64OgGxdubDcqWIraldVaSqmIYLGL4mPhA6QUtFIxLlQ0iQnJSHQyjncmM/fe/z/lDplixBY/OItzOOc73/kO8AoYoCSGNnf1DGzs6klqV9dC4X8gZmQ/wCGA2zP4YFlOXA92Lfqo9zpi3g35VgS0BbWez/dKN4o/iyrhbI+qv5e+nLeVjsD8vfPNJNSQ/vTAh2rOyN3VAB8MQn5PKe1PxYysI2J4/kr65t4An4egPtgZCniaxAkm3HIo9hVqojIZ0plSaK5VIi5DsAJP7a5369dObiho4OrXy5rcMe/d1mq+1wWnx628tEBUL85pc0P6dmhxsizx6I0e8A7I3v33aqNDw/mcinbOy9KRdnA1oKZ9qKKE/4CqD5+DoE9hCtuWLM+qqdNBzfQgtFYL/mSc5Y9zY1rKv0ChCMkjiOkQzL8EZzdAJsPV7d1rmmRwIYzIL4bxVFprx1ecLabn83Bx8tjCjYiBeuDVUyhJHm1+X3fqJ4PNSnaOldm0OCS9rEahEG9pOzl6ivs7V8PqNXBlBqT/pMP3LzZIVMIy1JFmb5xvWbbrNSEMGXefTcQ/NUs5Yr7ouoLY9iJRXbGAYxD1z/2Ojg33836IuoJne7r7oqJJF9kMxqGWSkf+LOKPlbBbs46gfMVYJsTMsIogcr50bJhaJY8/uK0SGWUrnqZaza8oRI/IMV6rI3KuBsZKZPOWI0cJp0WRozVQqplEjBE6XAHgtkhkeEcf/vEiNJc72nV3SlJusmo4XzZ4J0Wi0yPH0xQqKW5aS6cJdAOSZAD5pP6Fr6ZdjWa1bcKL4vcphXU1ZtdXFGhX34Gg880Z9RsdevA4GRjcsTwja89PDLTIG9NGznzLyLYFizIi9p8HaqL758f1LY0erIX8a0mHMxr7su/U/ZdJ7R8LXTi7zKe+1AAAAABJRU5ErkJggg==";
|
|
32
|
+
|
|
33
|
+
function ensureOverlay() {
|
|
34
|
+
if (overlayHost && document.documentElement && document.documentElement.contains(overlayHost)) {
|
|
35
|
+
return overlayShadow;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
overlayHost = document.createElement("div");
|
|
39
|
+
overlayHost.id = "__chocolatito_overlay__";
|
|
40
|
+
// CRÍTICO: pointer-events: none en el host y en todos los hijos para que
|
|
41
|
+
// JAMÁS capture clics ni altere el flujo de interacción de la página o del agente.
|
|
42
|
+
// all: initial aísla de las reglas CSS de la página que pudieran pisarlo.
|
|
43
|
+
overlayHost.style.cssText =
|
|
44
|
+
"all: initial !important; position: fixed !important; inset: 0 !important; " +
|
|
45
|
+
"width: 100vw !important; height: 100vh !important; pointer-events: none !important; " +
|
|
46
|
+
"z-index: 2147483646 !important; overflow: hidden !important;";
|
|
47
|
+
|
|
48
|
+
overlayShadow = overlayHost.attachShadow({ mode: "closed" });
|
|
49
|
+
|
|
50
|
+
const style = document.createElement("style");
|
|
51
|
+
style.textContent = `
|
|
52
|
+
:host {
|
|
53
|
+
all: initial !important;
|
|
54
|
+
pointer-events: none !important;
|
|
55
|
+
}
|
|
56
|
+
* {
|
|
57
|
+
box-sizing: border-box !important;
|
|
58
|
+
pointer-events: none !important;
|
|
59
|
+
}
|
|
60
|
+
/* Borde terracota (#D97757) hecho con box-shadow inset: no desplaza el layout ni el scroll */
|
|
61
|
+
.frame {
|
|
62
|
+
position: fixed !important;
|
|
63
|
+
inset: 0 !important;
|
|
64
|
+
pointer-events: none !important;
|
|
65
|
+
box-shadow: inset 0 0 0 3px #D97757 !important;
|
|
66
|
+
z-index: 2147483646 !important;
|
|
67
|
+
display: none;
|
|
68
|
+
animation: chocoFramePulse 2s ease-in-out infinite;
|
|
69
|
+
}
|
|
70
|
+
.frame.visible {
|
|
71
|
+
display: block !important;
|
|
72
|
+
}
|
|
73
|
+
@keyframes chocoFramePulse {
|
|
74
|
+
0%, 100% { opacity: 1; }
|
|
75
|
+
50% { opacity: 0.6; }
|
|
76
|
+
}
|
|
77
|
+
@media (prefers-reduced-motion: reduce) {
|
|
78
|
+
.frame {
|
|
79
|
+
animation: none !important;
|
|
80
|
+
opacity: 1 !important;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/* Etiqueta distintiva en la esquina superior */
|
|
84
|
+
.badge {
|
|
85
|
+
position: fixed !important;
|
|
86
|
+
top: 12px !important;
|
|
87
|
+
right: 14px !important;
|
|
88
|
+
display: inline-flex !important;
|
|
89
|
+
align-items: center !important;
|
|
90
|
+
gap: 7px !important;
|
|
91
|
+
padding: 5px 10px !important;
|
|
92
|
+
background: #D97757 !important;
|
|
93
|
+
color: #FFFFFF !important;
|
|
94
|
+
font: 600 12px/1.3 -apple-system, "Segoe UI", system-ui, sans-serif !important;
|
|
95
|
+
border-radius: 6px !important;
|
|
96
|
+
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.35) !important;
|
|
97
|
+
pointer-events: none !important;
|
|
98
|
+
letter-spacing: 0.2px !important;
|
|
99
|
+
z-index: 2147483647 !important;
|
|
100
|
+
}
|
|
101
|
+
.badge-icon {
|
|
102
|
+
width: 16px !important;
|
|
103
|
+
height: 16px !important;
|
|
104
|
+
display: block !important;
|
|
105
|
+
flex: 0 0 16px !important;
|
|
106
|
+
}
|
|
107
|
+
/* Capa de efectos efímeros (recuadros de selección y ondas de clic) */
|
|
108
|
+
.layers {
|
|
109
|
+
position: fixed !important;
|
|
110
|
+
inset: 0 !important;
|
|
111
|
+
pointer-events: none !important;
|
|
112
|
+
z-index: 2147483647 !important;
|
|
113
|
+
}
|
|
114
|
+
.highlight-box {
|
|
115
|
+
position: fixed !important;
|
|
116
|
+
outline: 2px solid #D97757 !important;
|
|
117
|
+
background: rgba(217, 119, 87, 0.12) !important;
|
|
118
|
+
border-radius: 3px !important;
|
|
119
|
+
pointer-events: none !important;
|
|
120
|
+
transition: opacity 0.6s ease;
|
|
121
|
+
opacity: 1;
|
|
122
|
+
}
|
|
123
|
+
.highlight-box.fade-out {
|
|
124
|
+
opacity: 0 !important;
|
|
125
|
+
}
|
|
126
|
+
.highlight-tag {
|
|
127
|
+
position: absolute !important;
|
|
128
|
+
bottom: 100% !important;
|
|
129
|
+
left: 0 !important;
|
|
130
|
+
margin-bottom: 3px !important;
|
|
131
|
+
background: #D97757 !important;
|
|
132
|
+
color: #FFFFFF !important;
|
|
133
|
+
font: 600 11px/1.2 -apple-system, system-ui, sans-serif !important;
|
|
134
|
+
padding: 2px 6px !important;
|
|
135
|
+
border-radius: 3px !important;
|
|
136
|
+
white-space: nowrap !important;
|
|
137
|
+
pointer-events: none !important;
|
|
138
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.25) !important;
|
|
139
|
+
}
|
|
140
|
+
.click-ripple {
|
|
141
|
+
position: fixed !important;
|
|
142
|
+
width: 20px !important;
|
|
143
|
+
height: 20px !important;
|
|
144
|
+
border-radius: 50% !important;
|
|
145
|
+
background: rgba(217, 119, 87, 0.35) !important;
|
|
146
|
+
border: 2px solid #D97757 !important;
|
|
147
|
+
transform: translate(-50%, -50%) scale(0.3);
|
|
148
|
+
pointer-events: none !important;
|
|
149
|
+
animation: chocoRipple 0.4s ease-out forwards;
|
|
150
|
+
}
|
|
151
|
+
@keyframes chocoRipple {
|
|
152
|
+
0% { transform: translate(-50%, -50%) scale(0.3); opacity: 1; }
|
|
153
|
+
100% { transform: translate(-50%, -50%) scale(2.2); opacity: 0; }
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
overlayShadow.appendChild(style);
|
|
158
|
+
|
|
159
|
+
// Solo el frame principal de la ventana (IS_TOP) dibuja el borde exterior y la insignia
|
|
160
|
+
// para no llenar de bordes anidados los iframes de la página.
|
|
161
|
+
if (IS_TOP) {
|
|
162
|
+
overlayFrame = document.createElement("div");
|
|
163
|
+
overlayFrame.className = "frame" + (overlayActivo ? " visible" : "");
|
|
164
|
+
overlayFrame.innerHTML = `
|
|
165
|
+
<div class="badge">
|
|
166
|
+
<img class="badge-icon" src="${LOGO_16_DATA_URI}" alt="" />
|
|
167
|
+
<span>Chocolatito está trabajando</span>
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
overlayShadow.appendChild(overlayFrame);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
overlayLayers = document.createElement("div");
|
|
174
|
+
overlayLayers.className = "layers";
|
|
175
|
+
overlayShadow.appendChild(overlayLayers);
|
|
176
|
+
|
|
177
|
+
(document.body || document.documentElement).appendChild(overlayHost);
|
|
178
|
+
return overlayShadow;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function setOverlayActive(activo) {
|
|
182
|
+
overlayActivo = Boolean(activo);
|
|
183
|
+
ensureOverlay();
|
|
184
|
+
if (overlayFrame) {
|
|
185
|
+
if (overlayActivo) overlayFrame.classList.add("visible");
|
|
186
|
+
else overlayFrame.classList.remove("visible");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isOverlayActive() {
|
|
191
|
+
return overlayActivo;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Dibuja un recuadro sobre el elemento objetivo antes de actuar (C1) */
|
|
195
|
+
function resaltarElemento(el, ref, role, name) {
|
|
196
|
+
try {
|
|
197
|
+
ensureOverlay();
|
|
198
|
+
if (!overlayLayers) return;
|
|
199
|
+
const r = el.getBoundingClientRect();
|
|
200
|
+
if (r.width < 1 && r.height < 1) return;
|
|
201
|
+
|
|
202
|
+
const box = document.createElement("div");
|
|
203
|
+
box.className = "highlight-box";
|
|
204
|
+
box.style.left = `${Math.round(r.left)}px`;
|
|
205
|
+
box.style.top = `${Math.round(r.top)}px`;
|
|
206
|
+
box.style.width = `${Math.round(r.width)}px`;
|
|
207
|
+
box.style.height = `${Math.round(r.height)}px`;
|
|
208
|
+
|
|
209
|
+
const tag = document.createElement("div");
|
|
210
|
+
tag.className = "highlight-tag";
|
|
211
|
+
const etiqueta = `[#${ref}] ${role || ""}${name ? ' "' + name.slice(0, 30) + '"' : ""}`;
|
|
212
|
+
tag.textContent = etiqueta.trim();
|
|
213
|
+
box.appendChild(tag);
|
|
214
|
+
|
|
215
|
+
overlayLayers.appendChild(box);
|
|
216
|
+
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
box.classList.add("fade-out");
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
try { if (box.parentNode) box.parentNode.removeChild(box); } catch (_) {}
|
|
221
|
+
}, 600);
|
|
222
|
+
}, 400);
|
|
223
|
+
} catch (_) {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Dibuja una onda en el punto exacto donde se pulsó (C2) */
|
|
227
|
+
function mostrarOndaClic(x, y) {
|
|
228
|
+
try {
|
|
229
|
+
ensureOverlay();
|
|
230
|
+
if (!overlayLayers) return;
|
|
231
|
+
const ripple = document.createElement("div");
|
|
232
|
+
ripple.className = "click-ripple";
|
|
233
|
+
ripple.style.left = `${Math.round(x)}px`;
|
|
234
|
+
ripple.style.top = `${Math.round(y)}px`;
|
|
235
|
+
|
|
236
|
+
overlayLayers.appendChild(ripple);
|
|
237
|
+
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
try { if (ripple.parentNode) ripple.parentNode.removeChild(ripple); } catch (_) {}
|
|
240
|
+
}, 400);
|
|
241
|
+
} catch (_) {}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// D3: Observador de mutaciones para saber cuándo la pantalla "está quieta" (reposo DOM)
|
|
245
|
+
let ultimaMutacion = Date.now();
|
|
246
|
+
try {
|
|
247
|
+
const mutObs = new MutationObserver(() => {
|
|
248
|
+
ultimaMutacion = Date.now();
|
|
249
|
+
});
|
|
250
|
+
mutObs.observe(document.documentElement || document, {
|
|
251
|
+
childList: true,
|
|
252
|
+
subtree: true,
|
|
253
|
+
attributes: true,
|
|
254
|
+
characterData: true,
|
|
255
|
+
});
|
|
256
|
+
} catch (_) {}
|
|
257
|
+
|
|
258
|
+
// ======================================================== aviso flotante inferior (banner)
|
|
21
259
|
|
|
22
260
|
let bannerHost = null;
|
|
23
261
|
let bannerBody = null;
|
|
@@ -29,8 +267,6 @@
|
|
|
29
267
|
|
|
30
268
|
bannerHost = document.createElement("div");
|
|
31
269
|
bannerHost.id = "__chocolatito_banner__";
|
|
32
|
-
// Shadow DOM: los estilos de la pagina no pueden romper el aviso, ni el
|
|
33
|
-
// aviso ensuciar la pagina.
|
|
34
270
|
const shadow = bannerHost.attachShadow({ mode: "closed" });
|
|
35
271
|
const style = document.createElement("style");
|
|
36
272
|
style.textContent = `
|
|
@@ -84,7 +320,6 @@
|
|
|
84
320
|
bannerBody.action.textContent = text || "";
|
|
85
321
|
bannerBody.wrap.classList.remove("hide");
|
|
86
322
|
clearTimeout(bannerHideTimer);
|
|
87
|
-
// Se desvanece solo si el agente deja de trabajar, para no molestar.
|
|
88
323
|
bannerHideTimer = setTimeout(() => {
|
|
89
324
|
if (bannerBody) bannerBody.wrap.classList.add("hide");
|
|
90
325
|
}, 12000);
|
|
@@ -244,19 +479,41 @@
|
|
|
244
479
|
*/
|
|
245
480
|
const refIdentity = new Map();
|
|
246
481
|
|
|
247
|
-
|
|
482
|
+
/**
|
|
483
|
+
* Busca elementos en el DOM que tengan el mismo rol o nombre similar
|
|
484
|
+
* para dar mensajes de error útiles si un ref no se encuentra (D5).
|
|
485
|
+
*/
|
|
486
|
+
function buscarSimilares(id) {
|
|
487
|
+
if (!id) return [];
|
|
488
|
+
const similares = [];
|
|
248
489
|
for (const root of allRoots()) {
|
|
249
|
-
|
|
250
|
-
|
|
490
|
+
let nodes;
|
|
491
|
+
try { nodes = root.querySelectorAll(SELECTOR); } catch (_) { continue; }
|
|
492
|
+
for (const el of nodes) {
|
|
493
|
+
const r = roleOf(el);
|
|
494
|
+
const n = accessibleName(el).slice(0, 60);
|
|
495
|
+
if (!n && r !== "textbox") continue;
|
|
496
|
+
const coincideRol = id.role && r === id.role;
|
|
497
|
+
const coincideNombre = id.name && n && (norm(n).includes(norm(id.name)) || norm(id.name).includes(norm(n)));
|
|
498
|
+
if (coincideRol || coincideNombre) {
|
|
499
|
+
similares.push({ role: r, name: n });
|
|
500
|
+
if (similares.length >= 3) return similares;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
251
503
|
}
|
|
504
|
+
return similares;
|
|
505
|
+
}
|
|
252
506
|
|
|
253
|
-
|
|
254
|
-
|
|
507
|
+
/**
|
|
508
|
+
* Reubica un elemento tras re-renders de frameworks reactivos (React/Vue/Flow)
|
|
509
|
+
* buscando candidatos con la misma identidad registrada.
|
|
510
|
+
*/
|
|
511
|
+
function reubicarPorIdentidad(id, ref) {
|
|
255
512
|
if (!id) return null;
|
|
256
|
-
|
|
257
513
|
const candidatos = [];
|
|
258
514
|
for (const root of allRoots()) {
|
|
259
|
-
|
|
515
|
+
let nodes;
|
|
516
|
+
try { nodes = root.querySelectorAll(SELECTOR); } catch (_) { continue; }
|
|
260
517
|
for (const el of nodes) {
|
|
261
518
|
if (roleOf(el) !== id.role) continue;
|
|
262
519
|
if (accessibleName(el).slice(0, 120) !== id.name) continue;
|
|
@@ -266,13 +523,68 @@
|
|
|
266
523
|
}
|
|
267
524
|
}
|
|
268
525
|
if (candidatos.length === 0) return null;
|
|
269
|
-
|
|
270
|
-
// Con varios iguales se elige el que ocupaba la misma posicion en la lista.
|
|
271
526
|
const elegido = candidatos[Math.min(id.idx, candidatos.length - 1)];
|
|
272
527
|
elegido.setAttribute("data-choco-ref", String(ref));
|
|
273
528
|
return elegido;
|
|
274
529
|
}
|
|
275
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Resuelve un ref verificando que el elemento siga siendo el mismo (D4).
|
|
533
|
+
* Si el DOM mutó drásticamente o el elemento desapareció, devuelve un error
|
|
534
|
+
* explicativo con sugerencias de elementos similares (D5).
|
|
535
|
+
*/
|
|
536
|
+
function resolverRef(ref) {
|
|
537
|
+
const id = refIdentity.get(ref);
|
|
538
|
+
let el = null;
|
|
539
|
+
for (const root of allRoots()) {
|
|
540
|
+
try {
|
|
541
|
+
el = root.querySelector('[data-choco-ref="' + ref + '"]');
|
|
542
|
+
if (el) break;
|
|
543
|
+
} catch (_) {}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (el) {
|
|
547
|
+
if (id) {
|
|
548
|
+
const actualRole = roleOf(el);
|
|
549
|
+
const actualName = accessibleName(el).slice(0, 120);
|
|
550
|
+
const rolCoincide = actualRole === id.role;
|
|
551
|
+
const nombreCoincide = !id.name || !actualName || norm(actualName).includes(norm(id.name)) || norm(id.name).includes(norm(actualName));
|
|
552
|
+
if (!rolCoincide && !nombreCoincide) {
|
|
553
|
+
// El elemento fue reciclado por otro componente del framework; reintentar por identidad
|
|
554
|
+
const reubicado = reubicarPorIdentidad(id, ref);
|
|
555
|
+
if (reubicado) return { ok: true, el: reubicado, id };
|
|
556
|
+
const similares = buscarSimilares(id);
|
|
557
|
+
const simTxt = similares.length > 0 ? ` Elementos parecidos: ${similares.map(s => `<${s.role}> "${s.name}"`).join(", ")}.` : "";
|
|
558
|
+
return {
|
|
559
|
+
ok: false,
|
|
560
|
+
error: `El DOM cambió desde el último snapshot: el ref [${ref}] apuntaba a <${id.role}> "${id.name}", pero ahora hay <${actualRole}> "${actualName}".${simTxt} Reház el snapshot antes de continuar.`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return { ok: true, el, id };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Si el nodo desapareció en un re-render, se busca por identidad
|
|
568
|
+
if (id) {
|
|
569
|
+
const reubicado = reubicarPorIdentidad(id, ref);
|
|
570
|
+
if (reubicado) return { ok: true, el: reubicado, id };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const similares = buscarSimilares(id);
|
|
574
|
+
const simTxt = similares.length > 0
|
|
575
|
+
? ` Elementos similares disponibles: ${similares.map(s => `<${s.role}> "${s.name}"`).join(", ")}.`
|
|
576
|
+
: " No se encontraron elementos similares en el DOM.";
|
|
577
|
+
return {
|
|
578
|
+
ok: false,
|
|
579
|
+
error: `Ref [${ref}] no encontrado en este frame. Se buscaba: <${id?.role || "elemento"}> "${id?.name || ""}".${simTxt} Reház el snapshot para actualizar los refs.`,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function findByRef(ref) {
|
|
584
|
+
const res = resolverRef(ref);
|
|
585
|
+
return res.ok ? res.el : null;
|
|
586
|
+
}
|
|
587
|
+
|
|
276
588
|
// ======================================================== acciones
|
|
277
589
|
|
|
278
590
|
function centerOf(el) {
|
|
@@ -280,9 +592,52 @@
|
|
|
280
592
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
281
593
|
}
|
|
282
594
|
|
|
595
|
+
/** Maneja de forma nativa la selección en menús desplegables <select> (D6) */
|
|
596
|
+
function seleccionarOpcion(selectEl, textoOValor) {
|
|
597
|
+
const needle = norm(textoOValor);
|
|
598
|
+
let elegidoIdx = -1;
|
|
599
|
+
for (let i = 0; i < selectEl.options.length; i++) {
|
|
600
|
+
const opt = selectEl.options[i];
|
|
601
|
+
if (norm(opt.value) === needle || norm(opt.text).includes(needle) || norm(opt.label).includes(needle)) {
|
|
602
|
+
elegidoIdx = i;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (elegidoIdx === -1) {
|
|
607
|
+
const num = Number(textoOValor);
|
|
608
|
+
if (!isNaN(num) && num >= 0 && num < selectEl.options.length) elegidoIdx = num;
|
|
609
|
+
}
|
|
610
|
+
if (elegidoIdx !== -1) {
|
|
611
|
+
selectEl.selectedIndex = elegidoIdx;
|
|
612
|
+
const valor = selectEl.options[elegidoIdx].value;
|
|
613
|
+
const proto = HTMLSelectElement.prototype;
|
|
614
|
+
const desc = Object.getOwnPropertyDescriptor(proto, "value");
|
|
615
|
+
if (desc && desc.set) desc.set.call(selectEl, valor);
|
|
616
|
+
else selectEl.value = valor;
|
|
617
|
+
}
|
|
618
|
+
selectEl.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
619
|
+
selectEl.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
620
|
+
return selectEl.value;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Conmuta casillas de verificación y radios usando descriptors nativos y eventos (D6) */
|
|
624
|
+
function conmutarCasilla(inputEl, estadoDeseado) {
|
|
625
|
+
const proximo = estadoDeseado !== undefined ? Boolean(estadoDeseado) : (inputEl.type === "checkbox" ? !inputEl.checked : true);
|
|
626
|
+
const proto = HTMLInputElement.prototype;
|
|
627
|
+
const desc = Object.getOwnPropertyDescriptor(proto, "checked");
|
|
628
|
+
if (desc && desc.set) desc.set.call(inputEl, proximo);
|
|
629
|
+
else inputEl.checked = proximo;
|
|
630
|
+
|
|
631
|
+
inputEl.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, composed: true }));
|
|
632
|
+
inputEl.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
633
|
+
inputEl.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
634
|
+
return inputEl.checked;
|
|
635
|
+
}
|
|
636
|
+
|
|
283
637
|
/**
|
|
284
|
-
* Pulsa sin
|
|
638
|
+
* Pulsa sin ratón real. Se emite la secuencia completa de eventos porque
|
|
285
639
|
* muchos componentes escuchan pointerdown/mouseup y no solo click.
|
|
640
|
+
* Realiza scroll al centro antes de pulsar (C3) y muestra onda de clic (C2).
|
|
286
641
|
*/
|
|
287
642
|
function clickElement(el) {
|
|
288
643
|
try {
|
|
@@ -291,6 +646,12 @@
|
|
|
291
646
|
try { el.scrollIntoView(); } catch (_) {}
|
|
292
647
|
}
|
|
293
648
|
const c = centerOf(el);
|
|
649
|
+
mostrarOndaClic(c.x, c.y);
|
|
650
|
+
|
|
651
|
+
if (el.type === "checkbox" || el.type === "radio") {
|
|
652
|
+
return conmutarCasilla(el);
|
|
653
|
+
}
|
|
654
|
+
|
|
294
655
|
const base = { bubbles: true, cancelable: true, composed: true, view: window, clientX: c.x, clientY: c.y, button: 0 };
|
|
295
656
|
const ptr = Object.assign({ pointerId: 1, isPrimary: true, pointerType: "mouse" }, base);
|
|
296
657
|
|
|
@@ -308,7 +669,7 @@
|
|
|
308
669
|
}
|
|
309
670
|
|
|
310
671
|
/**
|
|
311
|
-
* Escribe de forma que React se
|
|
672
|
+
* Escribe de forma que React/Vue se enteren.
|
|
312
673
|
*
|
|
313
674
|
* Asignar el.value directamente NO sirve: React guarda su propio estado y no
|
|
314
675
|
* ve el cambio, asi que el campo parece relleno pero el boton de enviar sigue
|
|
@@ -330,6 +691,16 @@
|
|
|
330
691
|
try { el.scrollIntoView({ block: "center", behavior: "instant" }); } catch (_) {}
|
|
331
692
|
try { el.focus({ preventScroll: true }); } catch (_) {}
|
|
332
693
|
|
|
694
|
+
// Soporte nativo para <select> (D6)
|
|
695
|
+
if (el.tagName.toLowerCase() === "select") {
|
|
696
|
+
return seleccionarOpcion(el, text);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Soporte para checkboxes y radios (D6)
|
|
700
|
+
if (el.type === "checkbox" || el.type === "radio") {
|
|
701
|
+
return conmutarCasilla(el);
|
|
702
|
+
}
|
|
703
|
+
|
|
333
704
|
if (el.isContentEditable) {
|
|
334
705
|
if (replace) {
|
|
335
706
|
const sel = window.getSelection();
|
|
@@ -338,7 +709,6 @@
|
|
|
338
709
|
sel.removeAllRanges();
|
|
339
710
|
sel.addRange(range);
|
|
340
711
|
}
|
|
341
|
-
// execCommand emite beforeinput/input igual que si se hubiera tecleado.
|
|
342
712
|
const ok = document.execCommand("insertText", false, text);
|
|
343
713
|
if (!ok) {
|
|
344
714
|
el.textContent = replace ? text : (el.textContent || "") + text;
|
|
@@ -357,11 +727,6 @@
|
|
|
357
727
|
|
|
358
728
|
/**
|
|
359
729
|
* Codigos de tecla.
|
|
360
|
-
*
|
|
361
|
-
* La tabla tenia siete entradas y todo lo demas salia con keyCode 0, que para
|
|
362
|
-
* un manejador que mira `e.keyCode` o `e.which` -que son casi todos los que
|
|
363
|
-
* existen desde antes de 2018- equivale a "no se ha pulsado nada". Escribir
|
|
364
|
-
* una letra suelta o un Espacio con press no hacia absolutamente nada.
|
|
365
730
|
*/
|
|
366
731
|
const CODIGOS = {
|
|
367
732
|
Enter: 13, Escape: 27, Esc: 27, Tab: 9, Backspace: 8, Delete: 46,
|
|
@@ -398,11 +763,8 @@
|
|
|
398
763
|
composed: true,
|
|
399
764
|
};
|
|
400
765
|
target.dispatchEvent(new KeyboardEvent("keydown", init));
|
|
401
|
-
// keypress solo existe para las teclas que producen caracter. Emitirlo para
|
|
402
|
-
// Escape o las flechas es inventarse un evento que el navegador nunca manda.
|
|
403
766
|
if (nombre.length === 1) target.dispatchEvent(new KeyboardEvent("keypress", init));
|
|
404
767
|
target.dispatchEvent(new KeyboardEvent("keyup", init));
|
|
405
|
-
// Si es un formulario y la tecla es Enter, se intenta enviar de verdad.
|
|
406
768
|
if (key === "Enter" && target.form && typeof target.form.requestSubmit === "function") {
|
|
407
769
|
try { target.form.requestSubmit(); } catch (_) {}
|
|
408
770
|
}
|
|
@@ -413,9 +775,16 @@
|
|
|
413
775
|
|
|
414
776
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
415
777
|
try {
|
|
416
|
-
|
|
778
|
+
const comando = msg.cmd || msg.tipo;
|
|
779
|
+
switch (comando) {
|
|
780
|
+
case "overlay": {
|
|
781
|
+
setOverlayActive(msg.activo === true);
|
|
782
|
+
sendResponse({ ok: true, activo: isOverlayActive() });
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
|
|
417
786
|
case "ping":
|
|
418
|
-
sendResponse({ ok: true, top: IS_TOP, url: location.href });
|
|
787
|
+
sendResponse({ ok: true, top: IS_TOP, url: location.href, overlay: isOverlayActive() });
|
|
419
788
|
break;
|
|
420
789
|
|
|
421
790
|
case "banner":
|
|
@@ -431,46 +800,51 @@
|
|
|
431
800
|
}
|
|
432
801
|
|
|
433
802
|
case "click": {
|
|
434
|
-
const
|
|
435
|
-
if (!
|
|
803
|
+
const resolucion = resolverRef(msg.ref);
|
|
804
|
+
if (!resolucion.ok) { sendResponse(resolucion); break; }
|
|
805
|
+
const el = resolucion.el;
|
|
436
806
|
const label = accessibleName(el).slice(0, 60);
|
|
807
|
+
resaltarElemento(el, msg.ref, resolucion.id?.role, label);
|
|
437
808
|
clickElement(el);
|
|
438
809
|
sendResponse({ ok: true, label });
|
|
439
810
|
break;
|
|
440
811
|
}
|
|
441
812
|
|
|
442
813
|
case "type": {
|
|
443
|
-
const
|
|
444
|
-
if (!
|
|
814
|
+
const resolucion = resolverRef(msg.ref);
|
|
815
|
+
if (!resolucion.ok) { sendResponse(resolucion); break; }
|
|
816
|
+
const el = resolucion.el;
|
|
817
|
+
const label = accessibleName(el).slice(0, 60);
|
|
818
|
+
resaltarElemento(el, msg.ref, resolucion.id?.role, label);
|
|
445
819
|
typeInto(el, msg.text, msg.replace !== false);
|
|
446
|
-
// Se devuelve el valor real tras escribir, para poder verificar.
|
|
447
820
|
const after = el.isContentEditable ? (el.textContent || "") : (el.value || "");
|
|
448
821
|
if (msg.submit) pressKey(el, "Enter");
|
|
449
|
-
sendResponse({ ok: true, value: String(after).slice(0, 200) });
|
|
822
|
+
sendResponse({ ok: true, value: String(after).slice(0, 200), label });
|
|
450
823
|
break;
|
|
451
824
|
}
|
|
452
825
|
|
|
453
|
-
// Devuelve el centro del elemento en coordenadas del viewport, que es lo
|
|
454
|
-
// que necesita el protocolo DevTools para entregar un clic real.
|
|
455
826
|
case "rect": {
|
|
456
|
-
const
|
|
457
|
-
if (!
|
|
458
|
-
|
|
827
|
+
const resolucion = resolverRef(msg.ref);
|
|
828
|
+
if (!resolucion.ok) { sendResponse(resolucion); break; }
|
|
829
|
+
const el = resolucion.el;
|
|
830
|
+
try { el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); } catch (_) {}
|
|
831
|
+
const label = accessibleName(el).slice(0, 60);
|
|
832
|
+
resaltarElemento(el, msg.ref, resolucion.id?.role, label);
|
|
459
833
|
const r = el.getBoundingClientRect();
|
|
460
834
|
sendResponse({
|
|
461
835
|
ok: true,
|
|
462
836
|
x: Math.round(r.left + r.width / 2),
|
|
463
837
|
y: Math.round(r.top + r.height / 2),
|
|
464
|
-
label
|
|
465
|
-
editable: el.isContentEditable || "value" in el,
|
|
838
|
+
label,
|
|
839
|
+
editable: el.isContentEditable || "value" in el || el.tagName.toLowerCase() === "select",
|
|
466
840
|
});
|
|
467
841
|
break;
|
|
468
842
|
}
|
|
469
843
|
|
|
470
|
-
// Solo lee el valor actual, para verificar despues de escribir.
|
|
471
844
|
case "value": {
|
|
472
|
-
const
|
|
473
|
-
if (!
|
|
845
|
+
const resolucion = resolverRef(msg.ref);
|
|
846
|
+
if (!resolucion.ok) { sendResponse(resolucion); break; }
|
|
847
|
+
const el = resolucion.el;
|
|
474
848
|
sendResponse({
|
|
475
849
|
ok: true,
|
|
476
850
|
value: String(el.isContentEditable ? el.textContent || "" : el.value || "").slice(0, 300),
|
|
@@ -479,12 +853,39 @@
|
|
|
479
853
|
}
|
|
480
854
|
|
|
481
855
|
case "press": {
|
|
482
|
-
|
|
856
|
+
let el = null;
|
|
857
|
+
if (msg.ref !== undefined && msg.ref !== null) {
|
|
858
|
+
const resolucion = resolverRef(msg.ref);
|
|
859
|
+
if (resolucion.ok) {
|
|
860
|
+
el = resolucion.el;
|
|
861
|
+
resaltarElemento(el, msg.ref, resolucion.id?.role, accessibleName(el));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
483
864
|
pressKey(el, msg.keys || "Enter");
|
|
484
865
|
sendResponse({ ok: true });
|
|
485
866
|
break;
|
|
486
867
|
}
|
|
487
868
|
|
|
869
|
+
case "waitForCondition": {
|
|
870
|
+
if (msg.selector) {
|
|
871
|
+
let found = false;
|
|
872
|
+
for (const root of allRoots()) {
|
|
873
|
+
try {
|
|
874
|
+
if (root.querySelector(msg.selector)) { found = true; break; }
|
|
875
|
+
} catch (_) {}
|
|
876
|
+
}
|
|
877
|
+
sendResponse({ ok: true, conditionMet: found });
|
|
878
|
+
break;
|
|
879
|
+
}
|
|
880
|
+
if (msg.stable) {
|
|
881
|
+
const quietFor = Date.now() - ultimaMutacion;
|
|
882
|
+
sendResponse({ ok: true, conditionMet: quietFor >= (msg.quietMs || 400) });
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
sendResponse({ ok: true, conditionMet: true });
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
|
|
488
889
|
case "getText":
|
|
489
890
|
sendResponse({
|
|
490
891
|
ok: true,
|
|
@@ -495,9 +896,6 @@
|
|
|
495
896
|
break;
|
|
496
897
|
|
|
497
898
|
case "eval": {
|
|
498
|
-
// Un objeto salia como "[object Object]", que no dice nada: quien
|
|
499
|
-
// pedia document.querySelectorAll('a').length lo tenia bien, pero
|
|
500
|
-
// quien pedia el objeto entero recibia ruido y volvia a intentarlo.
|
|
501
899
|
let out;
|
|
502
900
|
try {
|
|
503
901
|
const valor = eval(msg.script);
|
|
@@ -506,7 +904,7 @@
|
|
|
506
904
|
else if (typeof valor === "object") {
|
|
507
905
|
try {
|
|
508
906
|
out = JSON.stringify(valor, null, 2);
|
|
509
|
-
if (out === undefined) out = String(valor);
|
|
907
|
+
if (out === undefined) out = String(valor);
|
|
510
908
|
} catch (_) {
|
|
511
909
|
out = String(valor);
|
|
512
910
|
}
|
|
@@ -519,7 +917,7 @@
|
|
|
519
917
|
}
|
|
520
918
|
|
|
521
919
|
default:
|
|
522
|
-
sendResponse({ ok: false, error: "comando desconocido: " + msg.cmd });
|
|
920
|
+
sendResponse({ ok: false, error: "comando desconocido: " + (msg.cmd || msg.tipo) });
|
|
523
921
|
}
|
|
524
922
|
} catch (err) {
|
|
525
923
|
sendResponse({ ok: false, error: String((err && err.message) || err) });
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|