feat(browser): auto-commit con 3 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,10 +74,16 @@ usa `whatsapp_send_message`; para leer/confirmar lo enviado, `whatsapp_read_chat
|
|||||||
Hay que desbloquearlo primero (teclear el password en `input[type=password]` + boton
|
Hay que desbloquearlo primero (teclear el password en `input[type=password]` + boton
|
||||||
"Desbloquear"). Sintoma: `whatsapp_open_chat` devuelve `opened: False` y la lista lateral sale
|
"Desbloquear"). Sintoma: `whatsapp_open_chat` devuelve `opened: False` y la lista lateral sale
|
||||||
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 menu "Adjuntar" es un TOGGLE (`aria-expanded`).** El `<input type=file>` solo queda "vivo"
|
||||||
el boton "Adjuntar" antes de `setFileInputFiles`; asignar al input persistente decoy no abre el
|
mientras el menu esta ABIERTO; asignar al input con el menu cerrado es un decoy (no abre preview).
|
||||||
preview. La WhatsApp Web actual usa una **bandeja de medios INLINE compacta** sobre el composer
|
Clickar "Adjuntar" cuando YA esta abierto lo CIERRA. Por eso la funcion clicka solo si
|
||||||
(no un drawer a pantalla completa).
|
`aria-expanded != "true"` y reintenta hasta verlo abierto (no un click ciego). La WhatsApp Web
|
||||||
|
actual usa una **bandeja de medios INLINE compacta** sobre el composer (no un drawer a pantalla
|
||||||
|
completa).
|
||||||
|
- **El envio se verifica por la ultima fila de `#main`, NO por contar filas.** Las filas de `#main`
|
||||||
|
se VIRTUALIZAN (las antiguas se desmontan al llegar nuevas), asi que el total se mantiene casi
|
||||||
|
constante. La funcion confirma el envio comprobando que la bandeja se vacio (adjuntos=0) Y que la
|
||||||
|
ultima fila renderizada es ya una imagen (`img[src^="blob:"]`).
|
||||||
- **El caption NO se embebe en la imagen: viaja como mensaje de texto de seguimiento.** En esta
|
- **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
|
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,
|
la bandeja, IGNORA el texto del composer) y "Enviar"/Enter (envia el texto como burbuja aparte,
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ anti-envio-al-contacto-equivocado:
|
|||||||
1. Abre el chat por su nombre exacto (`open_first=True`). Si no abre, aborta. Con
|
1. Abre el chat por su nombre exacto (`open_first=True`). Si no abre, aborta. Con
|
||||||
`open_first=False`, asume el chat abierto pero VERIFICA que el aria-label del composer
|
`open_first=False`, asume el chat abierto pero VERIFICA que el aria-label del composer
|
||||||
contiene el nombre; si no, aborta por seguridad.
|
contiene el nombre; si no, aborta por seguridad.
|
||||||
2. Hace click de raton real en el boton "Adjuntar" del footer: esto expone el
|
2. ASEGURA que el menu "Adjuntar" esta ABIERTO (es un toggle `aria-expanded`: clickar
|
||||||
`<input type=file>` "vivo" que escucha la SPA (antes de pulsarlo el input persistente
|
cuando ya esta abierto lo cierra). Solo entonces el `<input type=file>` queda "vivo".
|
||||||
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.
|
imagen aparece como miniatura en la bandeja inline.
|
||||||
4. Espera a que la bandeja aparezca (boton "Quitar archivo adjunto" presente) y hace click
|
4. Espera a que la bandeja aparezca (boton "Quitar archivo adjunto" presente) y hace click
|
||||||
real en el boton de enviar la bandeja (icono `wds-ic-send-filled`); verifica que la
|
real en el boton de enviar la bandeja (icono `wds-ic-send-filled`).
|
||||||
bandeja se cerro (sin adjuntos) para confirmar el envio de la imagen.
|
5. Verifica el envio comprobando que la bandeja se cerro Y que la ultima fila de `#main`
|
||||||
5. Si `caption` no esta vacio, lo envia como un MENSAJE DE TEXTO de seguimiento via
|
es ahora una imagen (las filas se virtualizan, asi que NO sirve contar filas).
|
||||||
|
6. Si `caption` no esta vacio, lo envia como un MENSAJE DE TEXTO de seguimiento via
|
||||||
`whatsapp_send_message` (con `open_first=False`, el chat ya esta abierto). En la
|
`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
|
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].
|
forma fiable, asi que la descripcion viaja como una segunda burbuja: [imagen][caption].
|
||||||
@@ -54,6 +54,16 @@ def _center(expr: str, port: int, substr: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _adj_expanded(port: int, substr: str) -> str:
|
||||||
|
"""Estado aria-expanded del boton 'Adjuntar' ('true'/'false'/'no-btn')."""
|
||||||
|
r = cdp_eval(
|
||||||
|
'/*ADJEXP*/var e=document.querySelector(\'button[aria-label="Adjuntar"]\'); '
|
||||||
|
"e?e.getAttribute('aria-expanded'):'no-btn'",
|
||||||
|
port=port, target_url_substr=substr,
|
||||||
|
)
|
||||||
|
return r.get("value") or "no-btn"
|
||||||
|
|
||||||
|
|
||||||
def _attachment_count(port: int, substr: str) -> int:
|
def _attachment_count(port: int, substr: str) -> int:
|
||||||
"""Numero de adjuntos en la bandeja inline (botones 'Quitar archivo adjunto')."""
|
"""Numero de adjuntos en la bandeja inline (botones 'Quitar archivo adjunto')."""
|
||||||
r = cdp_eval(
|
r = cdp_eval(
|
||||||
@@ -64,6 +74,16 @@ def _attachment_count(port: int, substr: str) -> int:
|
|||||||
return v if isinstance(v, int) else 0
|
return v if isinstance(v, int) else 0
|
||||||
|
|
||||||
|
|
||||||
|
def _last_row_is_image(port: int, substr: str) -> bool:
|
||||||
|
"""True si la ultima fila renderizada de #main contiene una imagen (blob)."""
|
||||||
|
r = cdp_eval(
|
||||||
|
'/*LASTIMG*/(() => {const r=[...document.querySelectorAll(\'#main [role="row"]\')]'
|
||||||
|
".slice(-1)[0]; return r?!!r.querySelector('img[src^=\"blob:\"]'):false;})()",
|
||||||
|
port=port, target_url_substr=substr,
|
||||||
|
)
|
||||||
|
return bool(r.get("value"))
|
||||||
|
|
||||||
|
|
||||||
def whatsapp_send_image(
|
def whatsapp_send_image(
|
||||||
name: str,
|
name: str,
|
||||||
image_path: str,
|
image_path: str,
|
||||||
@@ -131,7 +151,9 @@ def whatsapp_send_image(
|
|||||||
if name not in (chk.get("value") or ""):
|
if name not in (chk.get("value") or ""):
|
||||||
return fail("el chat abierto no coincide con el destinatario; abortado por seguridad")
|
return fail("el chat abierto no coincide con el destinatario; abortado por seguridad")
|
||||||
|
|
||||||
# 2. Click real en "Adjuntar" para exponer el <input type=file> vivo.
|
# 2. Asegurar el menu "Adjuntar" ABIERTO. Es un TOGGLE (aria-expanded): clickar cuando
|
||||||
|
# ya esta abierto lo cierra y el input vivo desaparece. Por eso clickamos SOLO si no
|
||||||
|
# esta expandido y reintentamos hasta verlo abierto.
|
||||||
adj = _center(
|
adj = _center(
|
||||||
'/*ADJUNTAR*/(() => {const e=document.querySelector(\'button[aria-label="Adjuntar"]\');'
|
'/*ADJUNTAR*/(() => {const e=document.querySelector(\'button[aria-label="Adjuntar"]\');'
|
||||||
"if(!e)return null;const b=e.getBoundingClientRect();"
|
"if(!e)return null;const b=e.getBoundingClientRect();"
|
||||||
@@ -140,21 +162,23 @@ def whatsapp_send_image(
|
|||||||
)
|
)
|
||||||
if not adj:
|
if not adj:
|
||||||
return fail("boton 'Adjuntar' no encontrado en el footer")
|
return fail("boton 'Adjuntar' no encontrado en el footer")
|
||||||
cdp_click_xy(adj["x"], adj["y"], port=port, target_url_substr=S)
|
for _ in range(3):
|
||||||
time.sleep(0.8)
|
if _adj_expanded(port, S) == "true":
|
||||||
|
break
|
||||||
|
cdp_click_xy(adj["x"], adj["y"], port=port, target_url_substr=S)
|
||||||
|
time.sleep(0.6)
|
||||||
|
if _adj_expanded(port, S) != "true":
|
||||||
|
return fail("no se pudo abrir el menu 'Adjuntar' (aria-expanded sigue en false)")
|
||||||
|
|
||||||
# 3. Asignar la imagen al input multiple (el que se activa tras Adjuntar);
|
# 3. Asignar la imagen al primer <input type=file> (el "vivo" mientras el menu esta
|
||||||
# fallback al primer input file si el selector con [multiple] no resuelve.
|
# abierto). El composer queda VACIO, asi que luego el unico wds-ic-send-filled es el
|
||||||
r = cdp_set_file_input('input[type="file"][multiple]', abs_img,
|
# de enviar la bandeja.
|
||||||
|
r = cdp_set_file_input('input[type="file"]', abs_img,
|
||||||
port=port, target_url_substr=S)
|
port=port, target_url_substr=S)
|
||||||
if not r.get("ok"):
|
|
||||||
r = cdp_set_file_input('input[type="file"]', abs_img,
|
|
||||||
port=port, target_url_substr=S)
|
|
||||||
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 la bandeja aparezca (adjunto presente). El composer queda VACIO,
|
# 4. Esperar a que la bandeja aparezca (adjunto presente).
|
||||||
# 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)
|
||||||
@@ -163,6 +187,7 @@ def whatsapp_send_image(
|
|||||||
break
|
break
|
||||||
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")
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
# 5. Click real en el boton de enviar la bandeja (icono wds-ic-send-filled).
|
# 5. Click real en el boton de enviar la bandeja (icono wds-ic-send-filled).
|
||||||
snd = _center(
|
snd = _center(
|
||||||
@@ -176,15 +201,16 @@ 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)
|
||||||
|
|
||||||
# 6. Verificar que la bandeja se cerro (sin adjuntos) -> imagen enviada.
|
# 6. Confirmar envio: la bandeja se cierra (adjuntos=0) Y la ultima fila de #main es ya
|
||||||
|
# una imagen. Las filas de #main se VIRTUALIZAN, asi que contar filas no sirve.
|
||||||
image_sent = False
|
image_sent = False
|
||||||
for _ in range(15):
|
for _ in range(20):
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
if _attachment_count(port, S) == 0:
|
if _attachment_count(port, S) == 0 and _last_row_is_image(port, S):
|
||||||
image_sent = True
|
image_sent = True
|
||||||
break
|
break
|
||||||
if not image_sent:
|
if not image_sent:
|
||||||
return fail("la bandeja no se cerro tras pulsar enviar; envio incierto")
|
return fail("no se confirmo la imagen en el chat tras pulsar enviar; envio incierto")
|
||||||
|
|
||||||
# 7. Caption opcional como mensaje de texto de seguimiento (segunda burbuja).
|
# 7. Caption opcional como mensaje de texto de seguimiento (segunda burbuja).
|
||||||
caption_sent = False
|
caption_sent = False
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ modulo `browser.whatsapp_send_image` (donde quedan ligados los nombres por el
|
|||||||
`from browser.X import Y`), de modo que NO hace falta 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*/`, `/*SEND*/`, `/*LABEL*/`). El estado `after_send` (que
|
ella (`/*ADJUNTAR*/`, `/*ADJEXP*/`, `/*PREVIEW*/`, `/*SEND*/`, `/*LASTIMG*/`, `/*LABEL*/`).
|
||||||
/*SEND*/ activa) hace que /*PREVIEW*/ devuelva 0 adjuntos tras el envio de la imagen,
|
El estado simula el ciclo real: el menu Adjuntar arranca cerrado y se abre tras un click;
|
||||||
simulando el cierre de la bandeja.
|
tras pulsar enviar (/*SEND*/) la bandeja se vacia (/*PREVIEW*/ -> 0) y la ultima fila pasa a
|
||||||
|
ser una imagen (/*LASTIMG*/ -> True).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -41,11 +42,17 @@ def _make_eval(label="Escribir un mensaje para el grupo NOTAS WASAP", state=None
|
|||||||
def fake(expr, *, port=9222, target_url_substr=""):
|
def fake(expr, *, port=9222, target_url_substr=""):
|
||||||
if "/*ADJUNTAR*/" in expr:
|
if "/*ADJUNTAR*/" in expr:
|
||||||
return {"ok": True, "value": json.dumps({"x": 575, "y": 589}), "error": ""}
|
return {"ok": True, "value": json.dumps({"x": 575, "y": 589}), "error": ""}
|
||||||
|
if "/*ADJEXP*/" in expr:
|
||||||
|
n = state.get("adjexp_n", 0)
|
||||||
|
state["adjexp_n"] = n + 1
|
||||||
|
return {"ok": True, "value": ("false" if n == 0 else "true"), "error": ""}
|
||||||
if "/*SEND*/" in expr:
|
if "/*SEND*/" in expr:
|
||||||
state["after_send"] = True
|
state["after_send"] = True
|
||||||
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 "/*LASTIMG*/" in expr:
|
||||||
|
return {"ok": True, "value": bool(state.get("after_send")), "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": ""}
|
||||||
@@ -83,10 +90,11 @@ def test_golden_envia_imagen_y_caption_de_seguimiento(monkeypatch):
|
|||||||
assert res["image"] == _IMG
|
assert res["image"] == _IMG
|
||||||
assert res["caption"] == cap
|
assert res["caption"] == cap
|
||||||
assert res["error"] == ""
|
assert res["error"] == ""
|
||||||
# Se adjunto la imagen (ruta absoluta) por setFileInputFiles.
|
# Se adjunto la imagen (ruta absoluta) por setFileInputFiles, una sola vez.
|
||||||
assert set_spy.calls[0][0][0] == 'input[type="file"][multiple]'
|
assert len(set_spy.calls) == 1
|
||||||
|
assert set_spy.calls[0][0][0] == 'input[type="file"]'
|
||||||
assert set_spy.calls[0][0][1] == _IMG
|
assert set_spy.calls[0][0][1] == _IMG
|
||||||
# Dos clicks reales: Adjuntar y Enviar (bandeja).
|
# Dos clicks reales: abrir Adjuntar (menu cerrado al inicio) 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.
|
# El caption viaja como mensaje de texto de seguimiento, open_first=False.
|
||||||
assert len(sendmsg_spy.calls) == 1
|
assert len(sendmsg_spy.calls) == 1
|
||||||
@@ -107,7 +115,7 @@ def test_envia_sin_caption_no_manda_texto(monkeypatch):
|
|||||||
assert res["caption_sent"] is False
|
assert res["caption_sent"] is False
|
||||||
# Sin caption: NO se manda mensaje de texto de seguimiento.
|
# Sin caption: NO se manda mensaje de texto de seguimiento.
|
||||||
assert len(sendmsg_spy.calls) == 0
|
assert len(sendmsg_spy.calls) == 0
|
||||||
assert len(click_spy.calls) == 2
|
assert len(set_spy.calls) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_edge_imagen_no_existe_error_sin_abrir(monkeypatch):
|
def test_edge_imagen_no_existe_error_sin_abrir(monkeypatch):
|
||||||
@@ -167,7 +175,7 @@ def test_error_set_file_input_falla_no_envia(monkeypatch):
|
|||||||
|
|
||||||
assert res["sent"] is False
|
assert res["sent"] is False
|
||||||
assert "no se pudo adjuntar" in res["error"]
|
assert "no se pudo adjuntar" in res["error"]
|
||||||
# Se intento adjuntar (dos selectores) pero no se llego a enviar (solo el click de Adjuntar).
|
# Se abrio Adjuntar (1 click) e intento adjuntar (1 set), pero NO se envio nada.
|
||||||
assert len(set_spy.calls) == 2
|
assert len(set_spy.calls) == 1
|
||||||
assert len(click_spy.calls) == 1
|
assert len(click_spy.calls) == 1
|
||||||
assert len(sendmsg_spy.calls) == 0
|
assert len(sendmsg_spy.calls) == 0
|
||||||
|
|||||||
Reference in New Issue
Block a user