"""Hace un click de raton real en coordenadas (x, y) de una pestana de Chrome via CDP. Primitiva de input CDP: localiza un target (pestana) por substring de su URL, abre el WebSocket de depuracion y emite los eventos `Input.dispatchMouseEvent` que componen un click izquierdo sintetico real (mousePressed + mouseReleased), opcionalmente precedido de un mouseMoved para que la SPA registre el hover. Necesario porque `element.click()` de JavaScript NO dispara los handlers de React de muchas SPAs (WhatsApp Web entre ellas): abrir un chat de la lista o un resultado de busqueda requiere un click de raton sintetico real. El caller resuelve las coordenadas del elemento con `cdp_eval_py_browser` (getBoundingClientRect -> centro) y las pasa aqui. """ import json import time import urllib.request import websocket def cdp_click_xy( x: int, y: int, *, port: int = 9222, target_url_substr: str = "", move_first: bool = True, timeout_s: float = 10.0, ) -> dict: """Hace un click izquierdo real en (x, y) del viewport de una pestana de Chrome. Localiza el target `page` por substring de URL, abre el WebSocket CDP y emite un click izquierdo simple via `Input.dispatchMouseEvent`: si `move_first`, primero un `mouseMoved` a (x, y) (para que la SPA registre hover), luego `mousePressed` y `mouseReleased` con `button=left`, `buttons=1`, `clickCount=1`. Las coordenadas son CSS px del viewport; el caller las resuelve normalmente con `cdp_eval_py_browser` (getBoundingClientRect -> centro). Args: x: Coordenada X en CSS px del viewport donde hacer click. y: Coordenada Y en CSS px del viewport donde hacer click. port: Puerto de remote debugging de Chrome. Default 9222. target_url_substr: Substring que debe contener la URL del target (pestana). Si "", usa el primer target de tipo "page". move_first: Si True, emite un mouseMoved a (x, y) antes del click para que la SPA registre el hover. Default True. timeout_s: Timeout (segundos) para la conexion WebSocket. Default 10.0. Returns: dict con claves: ok: bool — True si los eventos de raton se emitieron sin error. error: str — mensaje de error (vacio si ok). x: int — coordenada X usada (eco del argumento). y: int — coordenada Y usada (eco del argumento). Nunca lanza: errores de red/conexion/transport se devuelven en "error" con ok=False. """ # 1. Listar targets via HTTP. try: with urllib.request.urlopen( f"http://127.0.0.1:{port}/json", timeout=5 ) as resp: targets = json.loads(resp.read().decode()) except Exception as e: # noqa: BLE001 — red/HTTP/JSON, no relanzar return {"ok": False, "error": str(e), "x": x, "y": y} # 2. Elegir el primer target type=="page" cuya url contenga el substring. chosen = None for t in targets: if t.get("type") != "page": continue url = t.get("url", "") if target_url_substr == "" or target_url_substr in url: chosen = t break if chosen is None: return { "ok": False, "error": f"no target matching {target_url_substr}", "x": x, "y": y, } ws_url = chosen.get("webSocketDebuggerUrl", "") # 3. Abrir WS. try: ws = websocket.create_connection(ws_url, timeout=timeout_s) except Exception as e: # noqa: BLE001 — conexion WS return {"ok": False, "error": str(e), "x": x, "y": y} try: msg_id = 1 # 3b. Hover previo opcional: ayuda a que la SPA registre el mouseover. if move_first: ws.send(json.dumps({ "id": msg_id, "method": "Input.dispatchMouseEvent", "params": {"type": "mouseMoved", "x": x, "y": y}, })) msg_id += 1 time.sleep(0.03) # 4. Click izquierdo real: mousePressed + mouseReleased. press_id = msg_id ws.send(json.dumps({ "id": press_id, "method": "Input.dispatchMouseEvent", "params": { "type": "mousePressed", "x": x, "y": y, "button": "left", "buttons": 1, "clickCount": 1, }, })) time.sleep(0.04) release_id = msg_id + 1 ws.send(json.dumps({ "id": release_id, "method": "Input.dispatchMouseEvent", "params": { "type": "mouseReleased", "x": x, "y": y, "button": "left", "buttons": 1, "clickCount": 1, }, })) # Drenar respuestas hasta ver el id del release (o agotar el stream). # Ignoramos eventos del server y frames no-JSON. while True: raw = ws.recv() if not raw: break try: parsed = json.loads(raw) except Exception: # noqa: BLE001 — frame no-JSON, ignorar continue if parsed.get("id") == release_id: break except Exception as e: # noqa: BLE001 — fallo de transport durante send/recv return {"ok": False, "error": str(e), "x": x, "y": y} finally: try: ws.close() except Exception: # noqa: BLE001 — cierre best-effort pass return {"ok": True, "error": "", "x": x, "y": y} if __name__ == "__main__": import sys arg_x = int(sys.argv[1]) if len(sys.argv) > 1 else 0 arg_y = int(sys.argv[2]) if len(sys.argv) > 2 else 0 substr = sys.argv[3] if len(sys.argv) > 3 else "" out = cdp_click_xy(arg_x, arg_y, port=9222, target_url_substr=substr) print(json.dumps(out, ensure_ascii=False, indent=2))