"""Tests para cdp_click_xy — mockean urlopen + create_connection. Mockean la capa de red de CDP: urllib.request.urlopen (lista de targets) y websocket.create_connection (un fake que captura los mensajes enviados y devuelve las respuestas CDP con el id correspondiente). """ import json import os import sys from contextlib import contextmanager sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from browser import cdp_click_xy as mod # noqa: E402 from browser.cdp_click_xy import cdp_click_xy # noqa: E402 class _FakeResp: """Context manager que imita la respuesta de urllib.request.urlopen.""" def __init__(self, payload: list): self._payload = payload def __enter__(self): return self def __exit__(self, *exc): return False def read(self): return json.dumps(self._payload).encode() class _FakeWS: """WebSocket falso: captura los mensajes enviados y responde por id.""" def __init__(self): self.sent = [] self._inbox = [] self.closed = False def send(self, raw: str): msg = json.loads(raw) self.sent.append(msg) # Encola una respuesta CDP vacia con el mismo id (como Chrome devuelve). self._inbox.append(json.dumps({"id": msg["id"], "result": {}})) def recv(self): if self._inbox: return self._inbox.pop(0) return "" def close(self): self.closed = True @contextmanager def _patch(targets, ws_obj=None, urlopen_exc=None, create_conn_exc=None): """Parchea urlopen y create_connection del modulo. Restaura al salir.""" orig_urlopen = mod.urllib.request.urlopen orig_create = mod.websocket.create_connection def fake_urlopen(url, timeout=5): if urlopen_exc is not None: raise urlopen_exc return _FakeResp(targets) def fake_create(ws_url, timeout=10): if create_conn_exc is not None: raise create_conn_exc return ws_obj mod.urllib.request.urlopen = fake_urlopen mod.websocket.create_connection = fake_create try: yield finally: mod.urllib.request.urlopen = orig_urlopen mod.websocket.create_connection = orig_create _TARGETS = [ {"type": "page", "url": "https://web.whatsapp.com/", "webSocketDebuggerUrl": "ws://x/1"}, ] def test_golden_click_emite_movido_pressed_released_left(): """Click en (100, 200) emite mouseMoved + mousePressed + mouseReleased correctos.""" ws = _FakeWS() with _patch(_TARGETS, ws_obj=ws): res = cdp_click_xy(100, 200, target_url_substr="whatsapp") assert res == {"ok": True, "error": "", "x": 100, "y": 200} assert len(ws.sent) == 3 moved, pressed, released = ws.sent assert moved["method"] == "Input.dispatchMouseEvent" assert moved["params"]["type"] == "mouseMoved" assert moved["params"]["x"] == 100 assert moved["params"]["y"] == 200 assert pressed["params"]["type"] == "mousePressed" assert pressed["params"]["x"] == 100 assert pressed["params"]["y"] == 200 assert pressed["params"]["button"] == "left" assert pressed["params"]["buttons"] == 1 assert pressed["params"]["clickCount"] == 1 assert released["params"]["type"] == "mouseReleased" assert released["params"]["x"] == 100 assert released["params"]["y"] == 200 assert released["params"]["button"] == "left" assert released["params"]["buttons"] == 1 assert released["params"]["clickCount"] == 1 assert ws.closed is True def test_edge_move_first_false_omite_mousemoved(): """Con move_first=False no se emite el mouseMoved previo, solo press + release.""" ws = _FakeWS() with _patch(_TARGETS, ws_obj=ws): res = cdp_click_xy(50, 60, target_url_substr="whatsapp", move_first=False) assert res["ok"] is True assert len(ws.sent) == 2 types = [m["params"]["type"] for m in ws.sent] assert types == ["mousePressed", "mouseReleased"] assert all(m["params"]["type"] != "mouseMoved" for m in ws.sent) def test_error_create_connection_lanza_ok_false(): """Si create_connection lanza, se captura y devuelve ok=False sin relanzar.""" with _patch(_TARGETS, create_conn_exc=ConnectionRefusedError("ws down")): res = cdp_click_xy(10, 20, target_url_substr="whatsapp") assert res["ok"] is False assert "ws down" in res["error"] assert res["x"] == 10 assert res["y"] == 20