---
name: cdp_type_chars
kind: function
lang: py
domain: browser
version: "1.0.0"
purity: impure
signature: "def cdp_type_chars(text: str, *, port: int = 9222, target_url_substr: str = '', delay_ms: int = 12, timeout_s: float = 10.0) -> dict"
description: "Escribe texto caracter a caracter en el elemento ENFOCADO de una pestana via CDP (Input.dispatchKeyEvent keyDown/keyUp por char). Unico metodo validado para el editor Lexical de WhatsApp Web, donde document.execCommand/el.value no funcionan. El caller debe enfocar el elemento antes."
tags: [cdp, browser, automation, python, navegator]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["urllib.request", "json", "time", "websocket"]
tested: true
tests: ["escribir ab envia 4 dispatchKeyEvent con text correctos", "text vacio no envia nada y devuelve ok", "fallo de create_connection devuelve ok False"]
test_file_path: "python/functions/browser/cdp_type_chars_test.py"
file_path: "python/functions/browser/cdp_type_chars.py"
params:
  - name: text
    desc: "Texto a escribir, caracter a caracter. Cada char emite un par keyDown/keyUp."
  - name: port
    desc: "Puerto de depuracion remota de Chrome (DevTools). Default 9222."
  - name: target_url_substr
    desc: "Substring para elegir el target page por su URL. Si vacio, usa el primer page disponible."
  - name: delay_ms
    desc: "Pausa en milisegundos entre cada par de teclas. Humaniza y da tiempo al re-render de editores como Lexical. Default 12."
  - name: timeout_s
    desc: "Timeout en segundos para el GET /json y la apertura del WebSocket. Default 10.0."
output: "dict {ok: bool, chars_sent: int, error: str}. ok True si todos los chars se enviaron; chars_sent cuenta los enviados (incluso si falla a mitad); error con el mensaje o cadena vacia. Nunca lanza."
---

## Ejemplo

Requiere Chrome lanzado con remote debugging (`--remote-debugging-port=9222`)
y un elemento editable ENFOCADO en la pestana. Primero se enfoca con cdp_eval
(ejecutando `.focus()` sobre el composer), luego se escribe:

```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from browser.cdp_eval import cdp_eval            # enfoca el elemento
from browser.cdp_type_chars import cdp_type_chars

# 1. Enfocar el composer (contenteditable de Lexical) de WhatsApp Web.
cdp_eval(
    "document.querySelector('footer div[contenteditable=\"true\"]').focus()",
    target_url_substr="whatsapp",
)

# 2. Escribir caracter a caracter en el elemento enfocado.
res = cdp_type_chars("hola", target_url_substr="whatsapp")
print(res)  # {'ok': True, 'chars_sent': 4, 'error': ''}
```

> Nota: esta funcion NO enfoca por si misma. El enfoque previo lo hace el
> caller con `cdp_eval_py_browser` (ejecutando `.focus()` sobre el selector).
> El contrato de `cdp_type_chars` es solo "escribir en lo que ya esta
> enfocado".

## Cuando usarla

- Para escribir en inputs, textarea y contenteditable de una pestana ya
  abierta: el composer Lexical de WhatsApp Web, su search box, o cualquier
  campo que requiera key events reales en vez de asignacion directa de valor.
- SIEMPRE despues de enfocar el elemento destino con cdp_eval ejecutando
  `.focus()` sobre el selector.
- Cuando `el.value = ...` o `document.execCommand('insertText')` no surten
  efecto porque el editor escucha eventos de teclado (caso Lexical).

## Gotchas

- Escribe en el elemento ACTUALMENTE ENFOCADO, no recibe selector: enfoca
  antes con cdp_eval (`...focus()`). Sin foco, los chars se pierden o van al
  body.
- NO mezclar con `document.execCommand('insertText')`: en Lexical produce
  texto duplicado o intercalado (gotcha real observado). Usa solo una via.
- `delay_ms` muy bajo (cerca de 0) puede perder caracteres en Lexical, que
  necesita un re-render entre teclas. 12 ms es un default seguro; subir si
  ves chars perdidos.
- Solo inserta texto plano via el campo `text`. Para Enter, Backspace, flechas
  u otras teclas especiales (incluido enviar el mensaje) usa
  `cdp_press_key_py_browser`.
- Impura: depende de un Chrome con remote debugging accesible en `port`. Si no
  hay target page valido devuelve `{"ok": False, ...}` (no lanza).
