"""Tests para whatsapp_send_message. whatsapp_send_message compone whatsapp_open_chat con tres primitivas CDP (cdp_eval, cdp_type_chars, cdp_press_key) y requiere un Chrome vivo. Aqui se mockean las cuatro con monkeypatch sobre el modulo `browser.whatsapp_send_message` (donde quedan ligados los nombres por el `from browser.X import Y`), de modo que NO hace falta Chrome. Las llamadas a cdp_eval que importan distinguen por el contenido de la expresion: - 'aria-label' -> verificacion del destinatario (open_first=False). - 'innerText' (composer) -> contenido tecleado, comparado contra el texto. - 'role="row"' (#main) -> ultima fila renderizada tras enviar (last_row). - cualquier otra (focus del composer) -> value inocuo. """ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import browser.whatsapp_send_message as wsm from browser.whatsapp_send_message import whatsapp_send_message # --- Fakes ----------------------------------------------------------------- def _fake_cdp_eval_factory(*, label_value=None, composer_value="", last_row_value=None): """Devuelve un fake de cdp_eval que distingue cada expresion por su contenido.""" def _fake(expr, *, port=9222, target_url_substr=""): if "aria-label" in expr: return {"ok": True, "value": label_value, "error": "", "target_url": ""} if "innerText.replace(/\\n/g" in expr or "innerText.replace(/\n/g" in expr: return {"ok": True, "value": composer_value, "error": "", "target_url": ""} if 'role=\\"row\\"' in expr or 'role="row"' in expr: return {"ok": True, "value": last_row_value, "error": "", "target_url": ""} return {"ok": True, "value": None, "error": "", "target_url": ""} return _fake class _Spy: """Registra los argumentos de cada llamada y devuelve un valor fijo.""" def __init__(self, ret=None): self.calls = [] self.ret = ret if ret is not None else {"ok": True} def __call__(self, *args, **kwargs): self.calls.append((args, kwargs)) return self.ret # --- Tests ----------------------------------------------------------------- def test_golden_envia_mensaje_y_devuelve_last_row(monkeypatch): text = "hola desde el registry" last = "hola desde el registry 11:40" # open_first=True (default): whatsapp_open_chat abre con exito. monkeypatch.setattr(wsm, "whatsapp_open_chat", lambda *a, **k: {"opened": True, "name": a[0]}) # composer devuelve exactamente el texto; #main devuelve la ultima fila. monkeypatch.setattr(wsm, "cdp_eval", _fake_cdp_eval_factory(composer_value=text, last_row_value=last)) type_spy = _Spy(ret={"ok": True}) press_spy = _Spy(ret={"ok": True}) monkeypatch.setattr(wsm, "cdp_type_chars", type_spy) monkeypatch.setattr(wsm, "cdp_press_key", press_spy) monkeypatch.setattr(wsm.time, "sleep", lambda *a, **k: None) res = whatsapp_send_message("NOTAS WASAP", text, port=9222, target_url_substr="whatsapp") assert res["sent"] is True assert res["name"] == "NOTAS WASAP" assert res["last_row"] == last # Se tecleo el texto y se pulso Enter una vez. assert len(type_spy.calls) == 1 assert type_spy.calls[0][0][0] == text assert len(press_spy.calls) == 1 assert press_spy.calls[0][0][0] == "Enter" def test_edge_open_fallido_sent_false_reason(monkeypatch): # whatsapp_open_chat no abre: aborta sin tocar el composer. monkeypatch.setattr( wsm, "whatsapp_open_chat", lambda *a, **k: {"opened": False, "name": a[0], "reason": "chat no encontrado en la lista (no cargado o nombre inexacto)"}, ) type_spy = _Spy(ret={"ok": True}) press_spy = _Spy(ret={"ok": True}) monkeypatch.setattr(wsm, "cdp_eval", _fake_cdp_eval_factory()) monkeypatch.setattr(wsm, "cdp_type_chars", type_spy) monkeypatch.setattr(wsm, "cdp_press_key", press_spy) monkeypatch.setattr(wsm.time, "sleep", lambda *a, **k: None) res = whatsapp_send_message("Contacto Inexistente", "hola", port=9222, target_url_substr="whatsapp") assert res["sent"] is False assert res["name"] == "Contacto Inexistente" assert "no encontrado" in res["reason"] # No se intento escribir ni enviar cuando el chat no abrio. assert len(type_spy.calls) == 0 assert len(press_spy.calls) == 0 assert "last_row" not in res def test_seguridad_open_first_false_label_no_coincide_aborta_sin_escribir(monkeypatch): # open_first=False y el aria-label del composer NO contiene el name -> abort. monkeypatch.setattr(wsm, "whatsapp_open_chat", lambda *a, **k: {"opened": True, "name": a[0]}) monkeypatch.setattr( wsm, "cdp_eval", _fake_cdp_eval_factory(label_value="Escribir un mensaje para el grupo OTRO CHAT"), ) type_spy = _Spy(ret={"ok": True}) press_spy = _Spy(ret={"ok": True}) monkeypatch.setattr(wsm, "cdp_type_chars", type_spy) monkeypatch.setattr(wsm, "cdp_press_key", press_spy) monkeypatch.setattr(wsm.time, "sleep", lambda *a, **k: None) res = whatsapp_send_message("NOTAS WASAP", "hola", port=9222, target_url_substr="whatsapp", open_first=False) assert res["sent"] is False assert res["name"] == "NOTAS WASAP" assert "abortado por seguridad" in res["reason"] # SEGURIDAD: no se llamo a cdp_type_chars ni a cdp_press_key. assert len(type_spy.calls) == 0 assert len(press_spy.calls) == 0 def test_mismatch_composer_sent_false_sin_press_enter(monkeypatch): # El composer no contiene el texto esperado tras teclear -> no se envia. text = "hola desde el registry" composer_real = "holaa desde ell registryy" # texto distinto (Lexical duplicando) monkeypatch.setattr(wsm, "whatsapp_open_chat", lambda *a, **k: {"opened": True, "name": a[0]}) monkeypatch.setattr(wsm, "cdp_eval", _fake_cdp_eval_factory(composer_value=composer_real)) type_spy = _Spy(ret={"ok": True}) press_spy = _Spy(ret={"ok": True}) monkeypatch.setattr(wsm, "cdp_type_chars", type_spy) monkeypatch.setattr(wsm, "cdp_press_key", press_spy) monkeypatch.setattr(wsm.time, "sleep", lambda *a, **k: None) res = whatsapp_send_message("NOTAS WASAP", text, port=9222, target_url_substr="whatsapp") assert res["sent"] is False assert res["name"] == "NOTAS WASAP" assert "no contiene el texto esperado" in res["reason"] assert res["composer"] == composer_real # Se tecleo pero NO se pulso Enter por el mismatch. assert len(type_spy.calls) == 1 assert len(press_spy.calls) == 0