chocolatito-code 1.1.0 → 1.2.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.
@@ -17,7 +17,245 @@
17
17
 
18
18
  const IS_TOP = window.top === window;
19
19
 
20
- // ======================================================== aviso en pagina
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
- function findByRef(ref) {
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
- const el = root.querySelector('[data-choco-ref="' + ref + '"]');
250
- if (el) return el;
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
- // El nodo se fue en un re-render: se busca otro con la misma identidad.
254
- const id = refIdentity.get(ref);
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
- const nodes = root.querySelectorAll(SELECTOR);
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 raton real. Se emite la secuencia completa de eventos porque
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 entere.
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
- switch (msg.cmd) {
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 el = findByRef(msg.ref);
435
- if (!el) { sendResponse({ ok: false, error: "ref no encontrado en este frame" }); break; }
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 el = findByRef(msg.ref);
444
- if (!el) { sendResponse({ ok: false, error: "ref no encontrado en este frame" }); break; }
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 el = findByRef(msg.ref);
457
- if (!el) { sendResponse({ ok: false, error: "ref no encontrado en este frame" }); break; }
458
- try { el.scrollIntoView({ block: "center", behavior: "instant" }); } catch (_) {}
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: accessibleName(el).slice(0, 60),
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 el = findByRef(msg.ref);
473
- if (!el) { sendResponse({ ok: false, error: "ref no encontrado" }); break; }
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
- const el = msg.ref ? findByRef(msg.ref) : null;
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); // referencias circulares
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