feat(browser): auto-commit con 3 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 23:15:30 +02:00
parent 0dd2718c95
commit d0960bed70
3 changed files with 93 additions and 84 deletions
@@ -6,9 +6,9 @@ domain: browser
version: "1.0.0" version: "1.0.0"
purity: impure purity: impure
signature: "def whatsapp_send_image(name: str, image_path: str, *, caption: str = '', port: int = 9222, target_url_substr: str = 'whatsapp', open_first: bool = True) -> dict" signature: "def whatsapp_send_image(name: str, image_path: str, *, caption: str = '', port: int = 9222, target_url_substr: str = 'whatsapp', open_first: bool = True) -> dict"
description: "Envia una imagen (con caption opcional) a un chat de WhatsApp Web en una pestana ya logueada del navegador diario via CDP, sin abrir ventana nueva ni darle foco. Abre el chat por nombre exacto (whatsapp_open_chat) y verifica el destinatario (salvaguarda anti-envio-equivocado), hace click real en 'Adjuntar' para exponer el <input type=file> vivo, asigna la imagen con cdp_set_file_input (DOM.setFileInputFiles), espera el preview de la bandeja inline, teclea el caption opcional con teclado CDP real, y hace click en el boton enviar (icono wds-ic-send-filled) verificando que la bandeja se cerro. Accion con efecto: envia la imagen DE VERDAD, no reversible." description: "Envia una imagen (con caption opcional) a un chat de WhatsApp Web en una pestana ya logueada del navegador diario via CDP, sin abrir ventana nueva ni darle foco. Abre el chat por nombre exacto (whatsapp_open_chat) y verifica el destinatario (salvaguarda anti-envio-equivocado), hace click real en 'Adjuntar' para exponer el <input type=file> vivo, asigna la imagen con cdp_set_file_input (DOM.setFileInputFiles), espera la bandeja inline y hace click en el boton enviar (icono wds-ic-send-filled) verificando que la bandeja se cerro. Si hay caption, lo envia como mensaje de texto de seguimiento via whatsapp_send_message (en la WhatsApp Web compacta actual el caption embebido en la imagen no es automatizable de forma fiable, asi que viaja como segunda burbuja [imagen][caption]). Accion con efecto: envia la imagen DE VERDAD, no reversible."
tags: [whatsapp, cdp, browser, automation, image, upload, python, navegator] tags: [whatsapp, cdp, browser, automation, image, upload, python, navegator]
uses_functions: [whatsapp_open_chat_py_browser, cdp_eval_py_browser, cdp_click_xy_py_browser, cdp_type_chars_py_browser, cdp_set_file_input_py_browser] uses_functions: [whatsapp_open_chat_py_browser, cdp_eval_py_browser, cdp_click_xy_py_browser, cdp_set_file_input_py_browser, whatsapp_send_message_py_browser]
uses_types: [] uses_types: []
returns: [] returns: []
returns_optional: false returns_optional: false
@@ -21,16 +21,16 @@ params_schema:
- name: image_path - name: image_path
desc: "Ruta de la imagen a enviar. Se expande (~) y se convierte a ruta ABSOLUTA; debe existir en disco o aborta con error sin abrir el chat." desc: "Ruta de la imagen a enviar. Se expande (~) y se convierte a ruta ABSOLUTA; debe existir en disco o aborta con error sin abrir el chat."
- name: caption - name: caption
desc: "Texto opcional que acompana la imagen. Se teclea en el composer con teclado CDP real caracter a caracter; '' (default) envia la imagen sin caption." desc: "Texto opcional descriptivo. Se envia como un MENSAJE DE TEXTO de seguimiento (segunda burbuja [imagen][caption]) via whatsapp_send_message; '' (default) envia solo la imagen. La WhatsApp Web compacta actual no permite automatizar el caption embebido en la imagen de forma fiable."
- name: port - name: port
desc: "Puerto de remote debugging de Chrome. Default 9222." desc: "Puerto de remote debugging de Chrome. Default 9222."
- name: target_url_substr - name: target_url_substr
desc: "Substring que debe contener la URL del target (pestana). Default 'whatsapp'." desc: "Substring que debe contener la URL del target (pestana). Default 'whatsapp'."
- name: open_first - name: open_first
desc: "Si True (default), abre el chat por su nombre antes de adjuntar. Si False, asume el chat ya abierto pero verifica el aria-label del composer contra name (aborta si no coincide)." desc: "Si True (default), abre el chat por su nombre antes de adjuntar. Si False, asume el chat ya abierto pero verifica el aria-label del composer contra name (aborta si no coincide)."
output: "dict {ok: bool (alias de sent), sent: bool, recipient: str, image: str (ruta absoluta), caption: str, error: str (motivo del fallo, vacio si sent=True)}. sent=True solo si la imagen se adjunto, el caption (si lo hay) se verifico y se pulso enviar dejando la bandeja vacia. Nunca lanza: los fallos se reportan en 'sent'/'ok' + 'error'." output: "dict {ok: bool (imagen + caption enviados), sent: bool (imagen enviada), caption_sent: bool (caption de seguimiento enviado, False si no habia o fallo), recipient: str, image: str (ruta absoluta), caption: str, error: str (motivo del fallo, vacio si todo ok)}. sent=True solo si la imagen se adjunto y se envio dejando la bandeja vacia. Nunca lanza: los fallos se reportan en 'sent'/'ok' + 'error'."
tested: true tested: true
tests: ["test_golden_adjunta_caption_y_envia", "test_envia_sin_caption_no_teclea", "test_edge_imagen_no_existe_error_sin_abrir", "test_edge_open_fallido_error_sin_adjuntar", "test_seguridad_open_first_false_label_no_coincide_aborta", "test_error_set_file_input_falla_no_envia"] tests: ["test_golden_envia_imagen_y_caption_de_seguimiento", "test_envia_sin_caption_no_manda_texto", "test_edge_imagen_no_existe_error_sin_abrir", "test_edge_open_fallido_error_sin_adjuntar", "test_seguridad_open_first_false_label_no_coincide_aborta", "test_error_set_file_input_falla_no_envia"]
test_file_path: "python/functions/browser/whatsapp_send_image_test.py" test_file_path: "python/functions/browser/whatsapp_send_image_test.py"
file_path: "python/functions/browser/whatsapp_send_image.py" file_path: "python/functions/browser/whatsapp_send_image.py"
--- ---
@@ -76,8 +76,15 @@ usa `whatsapp_send_message`; para leer/confirmar lo enviado, `whatsapp_read_chat
vacia aunque la sesion siga logueada. vacia aunque la sesion siga logueada.
- **El input vivo solo existe tras pulsar "Adjuntar".** Por eso la funcion hace el click real en - **El input vivo solo existe tras pulsar "Adjuntar".** Por eso la funcion hace el click real en
el boton "Adjuntar" antes de `setFileInputFiles`; asignar al input persistente decoy no abre el el boton "Adjuntar" antes de `setFileInputFiles`; asignar al input persistente decoy no abre el
preview. La WhatsApp Web actual usa una **bandeja de medios INLINE** sobre el composer (no un preview. La WhatsApp Web actual usa una **bandeja de medios INLINE compacta** sobre el composer
drawer a pantalla completa): el caption se escribe en el MISMO composer del footer. (no un drawer a pantalla completa).
- **El caption NO se embebe en la imagen: viaja como mensaje de texto de seguimiento.** En esta
WhatsApp Web compacta hay dos botones de envio cuando hay media: "Enviar N seleccionados" (envia
la bandeja, IGNORA el texto del composer) y "Enviar"/Enter (envia el texto como burbuja aparte,
descartando la media en cola). No hay un campo de caption por-imagen automatizable de forma
fiable. Por eso la funcion envia primero la imagen (boton de la bandeja) y, si hay `caption`, lo
manda despues como mensaje de texto via `whatsapp_send_message` (`open_first=False`): el resultado
es [imagen][caption] como dos burbujas. `caption_sent` indica si esa segunda burbuja salio.
- **Selector de aria-label en espanol.** El preview se detecta por `[aria-label="Quitar archivo - **Selector de aria-label en espanol.** El preview se detecta por `[aria-label="Quitar archivo
adjunto"]` y el boton de adjuntar por `[aria-label="Adjuntar"]`: dependen del idioma de la UI adjunto"]` y el boton de adjuntar por `[aria-label="Adjuntar"]`: dependen del idioma de la UI
(espanol). En otro locale habria que ajustar los aria-labels. (espanol). En otro locale habria que ajustar los aria-labels.
@@ -87,8 +94,6 @@ usa `whatsapp_send_message`; para leer/confirmar lo enviado, `whatsapp_read_chat
`sent=False` con "envio incierto". `sent=False` con "envio incierto".
- **Salvaguarda anti-destinatario-equivocado**: con `open_first=True` abre y verifica el chat; con - **Salvaguarda anti-destinatario-equivocado**: con `open_first=True` abre y verifica el chat; con
`open_first=False` lee el aria-label del composer y aborta si no contiene `name`. `open_first=False` lee el aria-label del composer y aborta si no contiene `name`.
- **Caption verificado**: tras teclear, re-lee el `innerText` del composer y solo envia si coincide
EXACTAMENTE con `caption`; si no, devuelve `sent=False` sin enviar.
- **Funciona con la ventana minimizada o sin foco**: CDP opera la pestana sin traerla a primer plano. - **Funciona con la ventana minimizada o sin foco**: CDP opera la pestana sin traerla a primer plano.
- **Viola los ToS de WhatsApp**: automatizar la web tiene riesgo de ban del numero personal. Usar - **Viola los ToS de WhatsApp**: automatizar la web tiene riesgo de ban del numero personal. Usar
con cautela y bajo tu responsabilidad. con cautela y bajo tu responsabilidad.
+45 -43
View File
@@ -1,8 +1,9 @@
"""Envia una imagen (con caption opcional) a un chat de WhatsApp Web via Chrome DevTools Protocol. """Envia una imagen (con caption opcional) a un chat de WhatsApp Web via Chrome DevTools Protocol.
Compone `whatsapp_open_chat` (abrir + verificar destinatario) con primitivas CDP del Compone `whatsapp_open_chat` (abrir + verificar destinatario) con primitivas CDP del
registry (`cdp_eval`, `cdp_click_xy`, `cdp_set_file_input`, `cdp_type_chars`) para adjuntar registry (`cdp_eval`, `cdp_click_xy`, `cdp_set_file_input`) y `whatsapp_send_message` para
y enviar una imagen a un contacto/grupo SIN abrir ventana nueva ni darle foco al sistema. adjuntar y enviar una imagen a un contacto/grupo SIN abrir ventana nueva ni darle foco al
sistema.
Flujo (modelo de bandeja de medios INLINE de la WhatsApp Web actual), con salvaguarda Flujo (modelo de bandeja de medios INLINE de la WhatsApp Web actual), con salvaguarda
anti-envio-al-contacto-equivocado: anti-envio-al-contacto-equivocado:
@@ -14,15 +15,17 @@ anti-envio-al-contacto-equivocado:
`<input type=file>` "vivo" que escucha la SPA (antes de pulsarlo el input persistente `<input type=file>` "vivo" que escucha la SPA (antes de pulsarlo el input persistente
no dispara el preview). no dispara el preview).
3. Asigna la imagen al input via `cdp_set_file_input` (`DOM.setFileInputFiles`): la 3. Asigna la imagen al input via `cdp_set_file_input` (`DOM.setFileInputFiles`): la
imagen aparece como miniatura en la bandeja inline sobre el composer. imagen aparece como miniatura en la bandeja inline.
4. Espera a que el preview aparezca (boton "Quitar archivo adjunto" presente). 4. Espera a que la bandeja aparezca (boton "Quitar archivo adjunto" presente) y hace click
5. Si `caption` no esta vacio, enfoca el composer del footer y teclea el texto con real en el boton de enviar la bandeja (icono `wds-ic-send-filled`); verifica que la
teclado CDP real (`cdp_type_chars`); verifica el `innerText` antes de enviar. bandeja se cerro (sin adjuntos) para confirmar el envio de la imagen.
6. Click de raton real en el boton enviar (icono `wds-ic-send-filled`) y verifica que la 5. Si `caption` no esta vacio, lo envia como un MENSAJE DE TEXTO de seguimiento via
bandeja se cerro (sin adjuntos) para confirmar el envio. `whatsapp_send_message` (con `open_first=False`, el chat ya esta abierto). En la
WhatsApp Web compacta actual el caption embebido en la imagen no es automatizable de
forma fiable, asi que la descripcion viaja como una segunda burbuja: [imagen][caption].
Validado contra WhatsApp Web real. Accion CON EFECTO REAL E IRREVERSIBLE: envia la imagen Validado contra WhatsApp Web real. Accion CON EFECTO REAL E IRREVERSIBLE: envia la imagen
de verdad. (y el caption) de verdad.
""" """
import json import json
@@ -34,9 +37,9 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from browser.cdp_eval import cdp_eval from browser.cdp_eval import cdp_eval
from browser.cdp_click_xy import cdp_click_xy from browser.cdp_click_xy import cdp_click_xy
from browser.cdp_type_chars import cdp_type_chars
from browser.cdp_set_file_input import cdp_set_file_input from browser.cdp_set_file_input import cdp_set_file_input
from browser.whatsapp_open_chat import whatsapp_open_chat from browser.whatsapp_open_chat import whatsapp_open_chat
from browser.whatsapp_send_message import whatsapp_send_message
def _center(expr: str, port: int, substr: str): def _center(expr: str, port: int, substr: str):
@@ -70,7 +73,7 @@ def whatsapp_send_image(
target_url_substr: str = "whatsapp", target_url_substr: str = "whatsapp",
open_first: bool = True, open_first: bool = True,
) -> dict: ) -> dict:
"""Envia una imagen (con caption opcional) a un chat de WhatsApp Web ya logueado. """Envia una imagen (con caption opcional de seguimiento) a un chat de WhatsApp Web.
Accion CON EFECTO: envia la imagen DE VERDAD (no reversible). Verifica `name`. Accion CON EFECTO: envia la imagen DE VERDAD (no reversible). Verifica `name`.
@@ -80,8 +83,9 @@ def whatsapp_send_image(
al destinatario correcto antes de adjuntar. al destinatario correcto antes de adjuntar.
image_path: Ruta de la imagen a enviar. Se expande (`~`) y se convierte a ruta image_path: Ruta de la imagen a enviar. Se expande (`~`) y se convierte a ruta
ABSOLUTA; debe existir en disco. ABSOLUTA; debe existir en disco.
caption: Texto opcional que acompana la imagen. Se teclea en el composer con teclado caption: Texto opcional descriptivo. Se envia como un MENSAJE DE TEXTO de seguimiento
CDP real; "" (default) envia la imagen sin caption. (segunda burbuja [imagen][caption]) via `whatsapp_send_message`; "" (default)
envia solo la imagen.
port: Puerto de remote debugging de Chrome. Default 9222. port: Puerto de remote debugging de Chrome. Default 9222.
target_url_substr: Substring que debe contener la URL del target (pestana). Default target_url_substr: Substring que debe contener la URL del target (pestana). Default
"whatsapp". "whatsapp".
@@ -91,12 +95,14 @@ def whatsapp_send_image(
Returns: Returns:
dict con claves: dict con claves:
ok: bool — alias de sent (True si la imagen se envio). ok: bool — True si la imagen se envio y (si habia caption) el caption tambien.
sent: bool — True si la imagen se adjunto, (caption escrito) y se envio. sent: bool — True si la IMAGEN se envio.
caption_sent: bool — True si el caption de seguimiento se envio (False si no
habia caption o si fallo).
recipient: str — el nombre solicitado. recipient: str — el nombre solicitado.
image: str — ruta absoluta de la imagen. image: str — ruta absoluta de la imagen.
caption: str — caption solicitado. caption: str — caption solicitado.
error: str — motivo del fallo (vacio si sent=True). error: str — motivo del fallo (vacio si todo ok).
Nunca lanza: los fallos se reportan en "sent"/"ok" + "error". Nunca lanza: los fallos se reportan en "sent"/"ok" + "error".
""" """
@@ -104,7 +110,7 @@ def whatsapp_send_image(
abs_img = os.path.abspath(os.path.expanduser(image_path)) abs_img = os.path.abspath(os.path.expanduser(image_path))
def fail(error: str) -> dict: def fail(error: str) -> dict:
return {"ok": False, "sent": False, "recipient": name, return {"ok": False, "sent": False, "caption_sent": False, "recipient": name,
"image": abs_img, "caption": caption, "error": error} "image": abs_img, "caption": caption, "error": error}
# 0. La imagen debe existir. # 0. La imagen debe existir.
@@ -147,7 +153,8 @@ def whatsapp_send_image(
if not r.get("ok"): if not r.get("ok"):
return fail("no se pudo adjuntar la imagen: " + r.get("error", "")) return fail("no se pudo adjuntar la imagen: " + r.get("error", ""))
# 4. Esperar a que el preview/bandeja aparezca (adjunto presente). # 4. Esperar a que la bandeja aparezca (adjunto presente). El composer queda VACIO,
# asi que el unico boton wds-ic-send-filled es el de enviar la bandeja.
attached = False attached = False
for _ in range(15): for _ in range(15):
time.sleep(0.2) time.sleep(0.2)
@@ -157,27 +164,7 @@ def whatsapp_send_image(
if not attached: if not attached:
return fail("el preview no aparecio tras adjuntar la imagen") return fail("el preview no aparecio tras adjuntar la imagen")
# 5. Caption opcional: enfocar el composer y teclear con teclado real. # 5. Click real en el boton de enviar la bandeja (icono wds-ic-send-filled).
if caption:
cdp_eval(
'/*FOCUS*/var b=document.querySelector(\'div[contenteditable="true"]\'); '
"if(b){b.focus();}",
port=port, target_url_substr=S,
)
time.sleep(0.25)
cdp_type_chars(caption, port=port, target_url_substr=S, delay_ms=15)
time.sleep(0.3)
chk = cdp_eval(
'/*COMPOSER*/var b=document.querySelector(\'div[contenteditable="true"]\'); '
"b?b.innerText.replace(/\\n/g,''):''",
port=port, target_url_substr=S,
)
composer = chk.get("value") or ""
if composer != caption:
return fail("el composer no contiene el caption esperado (no enviado): "
+ repr(composer))
# 6. Click real en el boton enviar (icono wds-ic-send-filled).
snd = _center( snd = _center(
'/*SEND*/(() => {const e=document.querySelector(\'span[data-icon="wds-ic-send-filled"]\');' '/*SEND*/(() => {const e=document.querySelector(\'span[data-icon="wds-ic-send-filled"]\');'
"if(!e)return null;const b=e.getBoundingClientRect();" "if(!e)return null;const b=e.getBoundingClientRect();"
@@ -189,15 +176,30 @@ def whatsapp_send_image(
return fail("boton de enviar (wds-ic-send-filled) no encontrado") return fail("boton de enviar (wds-ic-send-filled) no encontrado")
cdp_click_xy(snd["x"], snd["y"], port=port, target_url_substr=S) cdp_click_xy(snd["x"], snd["y"], port=port, target_url_substr=S)
# 7. Verificar que la bandeja se cerro (sin adjuntos) -> envio confirmado. # 6. Verificar que la bandeja se cerro (sin adjuntos) -> imagen enviada.
image_sent = False
for _ in range(15): for _ in range(15):
time.sleep(0.2) time.sleep(0.2)
if _attachment_count(port, S) == 0: if _attachment_count(port, S) == 0:
return {"ok": True, "sent": True, "recipient": name, image_sent = True
"image": abs_img, "caption": caption, "error": ""} break
if not image_sent:
return fail("la bandeja no se cerro tras pulsar enviar; envio incierto") return fail("la bandeja no se cerro tras pulsar enviar; envio incierto")
# 7. Caption opcional como mensaje de texto de seguimiento (segunda burbuja).
caption_sent = False
if caption:
m = whatsapp_send_message(name, caption, port=port, target_url_substr=S,
open_first=False)
caption_sent = bool(m.get("sent"))
if not caption_sent:
return {"ok": False, "sent": True, "caption_sent": False, "recipient": name,
"image": abs_img, "caption": caption,
"error": "imagen enviada pero el caption fallo: " + m.get("reason", "")}
return {"ok": True, "sent": True, "caption_sent": caption_sent, "recipient": name,
"image": abs_img, "caption": caption, "error": ""}
if __name__ == "__main__": if __name__ == "__main__":
chat = sys.argv[1] if len(sys.argv) > 1 else "NOTAS WASAP" chat = sys.argv[1] if len(sys.argv) > 1 else "NOTAS WASAP"
@@ -1,14 +1,14 @@
"""Tests para whatsapp_send_image. """Tests para whatsapp_send_image.
whatsapp_send_image compone whatsapp_open_chat con cuatro primitivas CDP (cdp_eval, whatsapp_send_image compone whatsapp_open_chat con tres primitivas CDP (cdp_eval,
cdp_click_xy, cdp_type_chars, cdp_set_file_input) y requiere un Chrome vivo. Aqui se cdp_click_xy, cdp_set_file_input) y `whatsapp_send_message` (para el caption de
mockean las cinco con monkeypatch sobre el modulo `browser.whatsapp_send_image` (donde seguimiento), y requiere un Chrome vivo. Aqui se mockean todas con monkeypatch sobre el
quedan ligados los nombres por el `from browser.X import Y`), de modo que NO hace falta modulo `browser.whatsapp_send_image` (donde quedan ligados los nombres por el
Chrome. `from browser.X import Y`), de modo que NO hace falta Chrome.
El fake de cdp_eval distingue cada expresion por un marcador de comentario JS embebido en El fake de cdp_eval distingue cada expresion por un marcador de comentario JS embebido en
ella (`/*ADJUNTAR*/`, `/*PREVIEW*/`, `/*COMPOSER*/`, `/*SEND*/`, `/*LABEL*/`). El estado ella (`/*ADJUNTAR*/`, `/*PREVIEW*/`, `/*SEND*/`, `/*LABEL*/`). El estado `after_send` (que
`after_send` (que /*SEND*/ activa) hace que /*PREVIEW*/ devuelva 0 adjuntos tras el envio, /*SEND*/ activa) hace que /*PREVIEW*/ devuelva 0 adjuntos tras el envio de la imagen,
simulando el cierre de la bandeja. simulando el cierre de la bandeja.
""" """
@@ -35,8 +35,7 @@ class _Spy:
return self.ret return self.ret
def _make_eval(caption_value="", label="Escribir un mensaje para el grupo NOTAS WASAP", def _make_eval(label="Escribir un mensaje para el grupo NOTAS WASAP", state=None):
state=None):
state = state if state is not None else {} state = state if state is not None else {}
def fake(expr, *, port=9222, target_url_substr=""): def fake(expr, *, port=9222, target_url_substr=""):
@@ -47,8 +46,6 @@ def _make_eval(caption_value="", label="Escribir un mensaje para el grupo NOTAS
return {"ok": True, "value": json.dumps({"x": 1136, "y": 578}), "error": ""} return {"ok": True, "value": json.dumps({"x": 1136, "y": 578}), "error": ""}
if "/*PREVIEW*/" in expr: if "/*PREVIEW*/" in expr:
return {"ok": True, "value": 0 if state.get("after_send") else 1, "error": ""} return {"ok": True, "value": 0 if state.get("after_send") else 1, "error": ""}
if "/*COMPOSER*/" in expr:
return {"ok": True, "value": caption_value, "error": ""}
if "/*LABEL*/" in expr: if "/*LABEL*/" in expr:
return {"ok": True, "value": label, "error": ""} return {"ok": True, "value": label, "error": ""}
return {"ok": True, "value": None, "error": ""} return {"ok": True, "value": None, "error": ""}
@@ -56,30 +53,32 @@ def _make_eval(caption_value="", label="Escribir un mensaje para el grupo NOTAS
return fake return fake
def _patch_common(monkeypatch, *, eval_fn, set_ret={"ok": True}, open_ret=None): def _patch_common(monkeypatch, *, eval_fn, set_ret={"ok": True}, open_ret=None,
send_msg_ret={"sent": True}):
open_spy = _Spy(ret=open_ret if open_ret is not None else {"opened": True, "name": "x"}) open_spy = _Spy(ret=open_ret if open_ret is not None else {"opened": True, "name": "x"})
click_spy = _Spy(ret={"ok": True}) click_spy = _Spy(ret={"ok": True})
type_spy = _Spy(ret={"ok": True})
set_spy = _Spy(ret=set_ret) set_spy = _Spy(ret=set_ret)
sendmsg_spy = _Spy(ret=send_msg_ret)
monkeypatch.setattr(wsi, "whatsapp_open_chat", open_spy) monkeypatch.setattr(wsi, "whatsapp_open_chat", open_spy)
monkeypatch.setattr(wsi, "cdp_eval", eval_fn) monkeypatch.setattr(wsi, "cdp_eval", eval_fn)
monkeypatch.setattr(wsi, "cdp_click_xy", click_spy) monkeypatch.setattr(wsi, "cdp_click_xy", click_spy)
monkeypatch.setattr(wsi, "cdp_type_chars", type_spy)
monkeypatch.setattr(wsi, "cdp_set_file_input", set_spy) monkeypatch.setattr(wsi, "cdp_set_file_input", set_spy)
monkeypatch.setattr(wsi, "whatsapp_send_message", sendmsg_spy)
monkeypatch.setattr(wsi.time, "sleep", lambda *a, **k: None) monkeypatch.setattr(wsi.time, "sleep", lambda *a, **k: None)
return open_spy, click_spy, type_spy, set_spy return open_spy, click_spy, set_spy, sendmsg_spy
def test_golden_adjunta_caption_y_envia(monkeypatch): def test_golden_envia_imagen_y_caption_de_seguimiento(monkeypatch):
cap = "item icon: potion" cap = "item icon: potion"
state = {} state = {}
open_spy, click_spy, type_spy, set_spy = _patch_common( open_spy, click_spy, set_spy, sendmsg_spy = _patch_common(
monkeypatch, eval_fn=_make_eval(caption_value=cap, state=state)) monkeypatch, eval_fn=_make_eval(state=state))
res = whatsapp_send_image("NOTAS WASAP", _IMG, caption=cap, res = whatsapp_send_image("NOTAS WASAP", _IMG, caption=cap,
port=9222, target_url_substr="whatsapp") port=9222, target_url_substr="whatsapp")
assert res["sent"] is True and res["ok"] is True assert res["sent"] is True and res["ok"] is True
assert res["caption_sent"] is True
assert res["recipient"] == "NOTAS WASAP" assert res["recipient"] == "NOTAS WASAP"
assert res["image"] == _IMG assert res["image"] == _IMG
assert res["caption"] == cap assert res["caption"] == cap
@@ -87,30 +86,32 @@ def test_golden_adjunta_caption_y_envia(monkeypatch):
# Se adjunto la imagen (ruta absoluta) por setFileInputFiles. # Se adjunto la imagen (ruta absoluta) por setFileInputFiles.
assert set_spy.calls[0][0][0] == 'input[type="file"][multiple]' assert set_spy.calls[0][0][0] == 'input[type="file"][multiple]'
assert set_spy.calls[0][0][1] == _IMG assert set_spy.calls[0][0][1] == _IMG
# Se tecleo el caption una vez. # Dos clicks reales: Adjuntar y Enviar (bandeja).
assert len(type_spy.calls) == 1
assert type_spy.calls[0][0][0] == cap
# Dos clicks reales: Adjuntar y Enviar.
assert len(click_spy.calls) == 2 assert len(click_spy.calls) == 2
# El caption viaja como mensaje de texto de seguimiento, open_first=False.
assert len(sendmsg_spy.calls) == 1
assert sendmsg_spy.calls[0][0][0] == "NOTAS WASAP"
assert sendmsg_spy.calls[0][0][1] == cap
assert sendmsg_spy.calls[0][1].get("open_first") is False
def test_envia_sin_caption_no_teclea(monkeypatch): def test_envia_sin_caption_no_manda_texto(monkeypatch):
state = {} state = {}
_, click_spy, type_spy, set_spy = _patch_common( _, click_spy, set_spy, sendmsg_spy = _patch_common(
monkeypatch, eval_fn=_make_eval(caption_value="", state=state)) monkeypatch, eval_fn=_make_eval(state=state))
res = whatsapp_send_image("NOTAS WASAP", _IMG, caption="", res = whatsapp_send_image("NOTAS WASAP", _IMG, caption="",
port=9222, target_url_substr="whatsapp") port=9222, target_url_substr="whatsapp")
assert res["sent"] is True assert res["sent"] is True
# Sin caption: NO se teclea nada, pero si se adjunta y se envia. assert res["caption_sent"] is False
assert len(type_spy.calls) == 0 # Sin caption: NO se manda mensaje de texto de seguimiento.
assert len(set_spy.calls) >= 1 assert len(sendmsg_spy.calls) == 0
assert len(click_spy.calls) == 2 assert len(click_spy.calls) == 2
def test_edge_imagen_no_existe_error_sin_abrir(monkeypatch): def test_edge_imagen_no_existe_error_sin_abrir(monkeypatch):
open_spy, click_spy, type_spy, set_spy = _patch_common( open_spy, click_spy, set_spy, sendmsg_spy = _patch_common(
monkeypatch, eval_fn=_make_eval()) monkeypatch, eval_fn=_make_eval())
res = whatsapp_send_image("NOTAS WASAP", "/no/existe/foo.png", res = whatsapp_send_image("NOTAS WASAP", "/no/existe/foo.png",
@@ -124,7 +125,7 @@ def test_edge_imagen_no_existe_error_sin_abrir(monkeypatch):
def test_edge_open_fallido_error_sin_adjuntar(monkeypatch): def test_edge_open_fallido_error_sin_adjuntar(monkeypatch):
open_spy, click_spy, type_spy, set_spy = _patch_common( open_spy, click_spy, set_spy, sendmsg_spy = _patch_common(
monkeypatch, eval_fn=_make_eval(), monkeypatch, eval_fn=_make_eval(),
open_ret={"opened": False, "name": "x", open_ret={"opened": False, "name": "x",
"reason": "chat no encontrado en la lista (no cargado o nombre inexacto)"}) "reason": "chat no encontrado en la lista (no cargado o nombre inexacto)"})
@@ -140,7 +141,7 @@ def test_edge_open_fallido_error_sin_adjuntar(monkeypatch):
def test_seguridad_open_first_false_label_no_coincide_aborta(monkeypatch): def test_seguridad_open_first_false_label_no_coincide_aborta(monkeypatch):
open_spy, click_spy, type_spy, set_spy = _patch_common( open_spy, click_spy, set_spy, sendmsg_spy = _patch_common(
monkeypatch, monkeypatch,
eval_fn=_make_eval(label="Escribir un mensaje para el grupo OTRO CHAT")) eval_fn=_make_eval(label="Escribir un mensaje para el grupo OTRO CHAT"))
@@ -157,7 +158,7 @@ def test_seguridad_open_first_false_label_no_coincide_aborta(monkeypatch):
def test_error_set_file_input_falla_no_envia(monkeypatch): def test_error_set_file_input_falla_no_envia(monkeypatch):
state = {} state = {}
open_spy, click_spy, type_spy, set_spy = _patch_common( open_spy, click_spy, set_spy, sendmsg_spy = _patch_common(
monkeypatch, eval_fn=_make_eval(state=state), monkeypatch, eval_fn=_make_eval(state=state),
set_ret={"ok": False, "error": "no element matches selector"}) set_ret={"ok": False, "error": "no element matches selector"})
@@ -169,3 +170,4 @@ def test_error_set_file_input_falla_no_envia(monkeypatch):
# Se intento adjuntar (dos selectores) pero no se llego a enviar (solo el click de Adjuntar). # Se intento adjuntar (dos selectores) pero no se llego a enviar (solo el click de Adjuntar).
assert len(set_spy.calls) == 2 assert len(set_spy.calls) == 2
assert len(click_spy.calls) == 1 assert len(click_spy.calls) == 1
assert len(sendmsg_spy.calls) == 0