Files
Agent-IA/Python/whatsapp_modular_v3_6/scripts/validar_selectores.py

367 lines
18 KiB
Python

#!/usr/bin/env python3
"""
Validador integral de selectores para WhatsApp Web.
Prueba todas las estrategias de localización usadas en whatsapp_sender/
y reporta cuáles siguen funcionando y cuáles no.
"""
import os
import time
from datetime import datetime
from collections import OrderedDict
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from whatsapp_sender.driver_session import ensure_session_active, iniciar_driver
from whatsapp_sender.utils import log_text
DIAGNOSTICOS_DIR = "diagnosticos_selectores"
os.makedirs(DIAGNOSTICOS_DIR, exist_ok=True)
def captura(driver, nombre):
ts = datetime.now().strftime("%H%M%S")
path = os.path.join(DIAGNOSTICOS_DIR, f"{nombre}_{ts}.png")
try:
driver.save_screenshot(path)
except Exception:
pass
return path
def probar_selector(driver, by, selector, desc, categoria):
"""
Prueba un selector y devuelve un dict con resultados.
"""
resultado = {
"categoria": categoria,
"descripcion": desc,
"estrategia": str(by),
"selector": selector,
"total": 0,
"visibles": 0,
"coincide": False,
"error": None,
}
try:
elementos = driver.find_elements(by, selector)
resultado["total"] = len(elementos)
visibles = 0
for elem in elementos:
try:
if elem.is_displayed():
visibles += 1
except Exception:
continue
resultado["visibles"] = visibles
resultado["coincide"] = visibles > 0
except Exception as exc:
resultado["error"] = str(exc)
return resultado
def probar_selectores(driver, selectores, categoria):
resultados = []
for item in selectores:
res = probar_selector(driver, item["by"], item["selector"], item["desc"], categoria)
resultados.append(res)
return resultados
def imprimir_resultados(resultados, titulo):
print(f"\n{'=' * 70}")
print(f" {titulo}")
print(f"{'=' * 70}")
ok = sum(1 for r in resultados if r["coincide"])
total = len(resultados)
print(f" {ok}/{total} selectores funcionan\n")
for r in resultados:
icono = "OK" if r["coincide"] else "---"
print(f" [{icono}] {r['descripcion']}")
print(f" Selector: {r['selector']}")
print(f" By: {r['estrategia']} | Total: {r['total']} | Visibles: {r['visibles']}")
if r["error"]:
print(f" ERROR: {r['error']}")
print()
return ok, total
def construir_selectores_session():
return [
{"by": By.ID, "selector": "pane-side", "desc": "Panel lateral #pane-side"},
{"by": By.ID, "selector": "side", "desc": "Panel lateral #side"},
{"by": By.CSS_SELECTOR, "selector": "[data-testid='chat-list']", "desc": "Chat list data-testid"},
{"by": By.CSS_SELECTOR, "selector": "[data-testid='cell-frame-container']", "desc": "Cell frame container"},
{"by": By.CSS_SELECTOR, "selector": "div[role='grid']", "desc": "role=grid"},
{"by": By.CSS_SELECTOR, "selector": "div[aria-label*='chat' i][role='grid']", "desc": "Grid con aria-label chat"},
{"by": By.CSS_SELECTOR, "selector": "div[contenteditable='true'][role='textbox'][aria-label*='buscar' i]", "desc": "Busqueda contenteditable label=buscar"},
{"by": By.CSS_SELECTOR, "selector": "div[contenteditable='true'][role='textbox'][aria-label*='search' i]", "desc": "Busqueda contenteditable label=search"},
]
def construir_selectores_qr():
return [
{"by": By.CSS_SELECTOR, "selector": "canvas[aria-label*='Scan']", "desc": "QR canvas aria-label Scan"},
{"by": By.CSS_SELECTOR, "selector": "canvas[aria-label*='escan' i]", "desc": "QR canvas aria-label escan"},
{"by": By.CSS_SELECTOR, "selector": "canvas[aria-label='Scan me!']", "desc": "QR canvas exacto Scan me!"},
{"by": By.CSS_SELECTOR, "selector": "div[data-ref] canvas", "desc": "QR div[data-ref] canvas"},
]
def construir_selectores_busqueda():
return [
{"by": By.CSS_SELECTOR, "selector": "input[role='textbox'][data-tab='3']", "desc": "Input textbox data-tab=3"},
{"by": By.CSS_SELECTOR, "selector": "input[aria-label*='Buscar' i]", "desc": "Input aria-label Buscar"},
{"by": By.CSS_SELECTOR, "selector": "input[placeholder*='Buscar' i]", "desc": "Input placeholder Buscar"},
{"by": By.CSS_SELECTOR, "selector": "input[aria-label*='Search' i]", "desc": "Input aria-label Search"},
{"by": By.CSS_SELECTOR, "selector": "input[placeholder*='Search' i]", "desc": "Input placeholder Search"},
{"by": By.CSS_SELECTOR, "selector": "[data-testid='chat-list-search']", "desc": "data-testid chat-list-search"},
{"by": By.CSS_SELECTOR, "selector": "div[role='search']", "desc": "div role=search"},
{"by": By.CSS_SELECTOR, "selector": "div[contenteditable='true'][data-tab='3']", "desc": "Contenteditable data-tab=3"},
{"by": By.CSS_SELECTOR, "selector": "div[contenteditable='true'][role='textbox'][aria-label*='buscar' i]", "desc": "Contenteditable label buscar"},
{"by": By.CSS_SELECTOR, "selector": "div[contenteditable='true'][role='textbox'][aria-label*='search' i]", "desc": "Contenteditable label search"},
{"by": By.CSS_SELECTOR, "selector": "div[contenteditable='true'][role='textbox'][aria-placeholder*='buscar' i]", "desc": "Contenteditable placeholder buscar"},
{"by": By.CSS_SELECTOR, "selector": "div[contenteditable='true'][role='textbox'][aria-placeholder*='search' i]", "desc": "Contenteditable placeholder search"},
{"by": By.XPATH, "selector": "//*[@contenteditable='true' and ancestor::*[@id='side' or @id='pane-side']]", "desc": "XPath contenteditable dentro de #side/#pane-side"},
]
def construir_selectores_busqueda_botones():
return [
{"by": By.CSS_SELECTOR, "selector": "button[aria-label*='buscar' i]", "desc": "Boton aria-label buscar"},
{"by": By.CSS_SELECTOR, "selector": "button[aria-label*='search' i]", "desc": "Boton aria-label search"},
{"by": By.CSS_SELECTOR, "selector": "div[role='button'][aria-label*='buscar' i]", "desc": "Div role=button label buscar"},
{"by": By.CSS_SELECTOR, "selector": "div[role='button'][aria-label*='search' i]", "desc": "Div role=button label search"},
{"by": By.CSS_SELECTOR, "selector": "span[data-icon='search']", "desc": "Span data-icon=search"},
]
def construir_selectores_resultados():
return [
{"by": By.CSS_SELECTOR, "selector": "[data-testid='cell-frame-container']", "desc": "cell-frame-container"},
{"by": By.CSS_SELECTOR, "selector": "#pane-side div[tabindex='-1']", "desc": "#pane-side div tabindex=-1"},
{"by": By.CSS_SELECTOR, "selector": "#side div[tabindex='-1']", "desc": "#side div tabindex=-1"},
{"by": By.CSS_SELECTOR, "selector": "div[role='grid'] div[role='row']", "desc": "role=grid > role=row"},
{"by": By.CSS_SELECTOR, "selector": "div[aria-label][role='grid'] div[role='row']", "desc": "aria-label grid > role=row"},
]
def construir_selectores_caja_mensaje():
return [
{"by": By.CSS_SELECTOR, "selector": "[data-testid='conversation-compose-box-input']", "desc": "Compose box data-testid"},
{"by": By.CSS_SELECTOR, "selector": "footer div[contenteditable='true'][role='textbox']", "desc": "Footer contenteditable textbox"},
{"by": By.CSS_SELECTOR, "selector": "footer div[contenteditable='true'][data-lexical-editor='true']", "desc": "Footer lexical editor"},
{"by": By.CSS_SELECTOR, "selector": "footer div[contenteditable='true']", "desc": "Footer contenteditable generico"},
{"by": By.CSS_SELECTOR, "selector": "div[contenteditable='true'][data-tab='10']", "desc": "Contenteditable data-tab=10"},
{"by": By.CSS_SELECTOR, "selector": "div[contenteditable='true'][data-tab='9']", "desc": "Contenteditable data-tab=9"},
{"by": By.CSS_SELECTOR, "selector": "footer div[aria-placeholder*='mensaje' i]", "desc": "Footer placeholder mensaje"},
{"by": By.CSS_SELECTOR, "selector": "footer div[aria-placeholder*='message' i]", "desc": "Footer placeholder message"},
{"by": By.CSS_SELECTOR, "selector": "footer div[aria-label*='mensaje' i]", "desc": "Footer label mensaje"},
{"by": By.CSS_SELECTOR, "selector": "footer div[aria-label*='message' i]", "desc": "Footer label message"},
{"by": By.XPATH, "selector": "//footer//*[@contenteditable='true']", "desc": "XPath footer contenteditable"},
]
def construir_selectores_enviar():
return [
{"by": By.CSS_SELECTOR, "selector": "button[aria-label*='enviar' i]", "desc": "Boton aria-label enviar"},
{"by": By.CSS_SELECTOR, "selector": "button[aria-label*='send' i]", "desc": "Boton aria-label send"},
{"by": By.CSS_SELECTOR, "selector": "[data-icon*='send']", "desc": "Elemento con data-icon=send/wds-ic-send-filled"},
{"by": By.CSS_SELECTOR, "selector": "span[data-testid*='send']", "desc": "Span con data-testid=send"},
]
def construir_selectores_ticks():
return [
{"by": By.CSS_SELECTOR, "selector": "[data-testid='msg-meta'] svg title", "desc": "SVG title dentro de msg-meta"},
{"by": By.CSS_SELECTOR, "selector": "[data-testid='msg-meta']", "desc": "Contenedor msg-meta"},
{"by": By.CSS_SELECTOR, "selector": "span[data-icon='message-fail']", "desc": "Icono de mensaje fallido"},
]
def construir_selectores_mensaje_saliente():
return [
{"by": By.CSS_SELECTOR, "selector": "div[data-testid='msg-container'] span.selectable-text", "desc": "msg-container span.selectable-text"},
{"by": By.CSS_SELECTOR, "selector": "div[data-testid='msg-container'] [data-testid='selectable-text']", "desc": "msg-container testid selectable-text"},
{"by": By.CSS_SELECTOR, "selector": "span.selectable-text", "desc": "span.selectable-text generico"},
]
def construir_selectores_nombre_chat():
return [
{"by": By.XPATH, "selector": "//header//span[@dir='auto' and @title]", "desc": "XPath header span title"},
{"by": By.CSS_SELECTOR, "selector": "header span[title]", "desc": "Header span[title]"},
{"by": By.CSS_SELECTOR, "selector": "header span[dir='auto']", "desc": "Header span[dir=auto]"},
{"by": By.CSS_SELECTOR, "selector": "header div[role='button'] span", "desc": "Header role=button span"},
]
def construir_selectores_nombre_resultado():
return [
{"by": By.CSS_SELECTOR, "selector": "span[title]", "desc": "span[title] generico"},
{"by": By.CSS_SELECTOR, "selector": "span[dir='auto']", "desc": "span[dir=auto] generico"},
{"by": By.CSS_SELECTOR, "selector": "div[role='gridcell'] span[dir='auto']", "desc": "gridcell span[dir=auto]"},
]
def generar_reporte_markdown(resultados_por_categoria, archivo):
with open(archivo, "w", encoding="utf-8") as f:
f.write("# Reporte de validacion de selectores - WhatsApp Web\n\n")
f.write(f"Generado: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
total_ok = 0
total_all = 0
for categoria, resultados in resultados_por_categoria:
ok = sum(1 for r in resultados if r["coincide"])
total_ok += ok
total_all += len(resultados)
pct = ok / len(resultados) * 100 if resultados else 0
f.write(f"## {categoria}\n")
f.write(f"- **Selectores funcionales:** {ok}/{len(resultados)} ({pct:.1f}%)\n\n")
f.write("| Estado | Descripcion | Estrategia | Selector | Total | Visibles |\n")
f.write("|--------|-------------|------------|----------|-------|----------|\n")
for r in resultados:
estado = ":white_check_mark:" if r["coincide"] else ":x:"
error_str = f" ERROR: {r['error']}" if r["error"] else ""
f.write(f"| {estado} | {r['descripcion']}{error_str} | {r['estrategia']} | `{r['selector']}` | {r['total']} | {r['visibles']} |\n")
f.write("\n")
f.write(f"---\n\n## Resumen global\n\n")
f.write(f"**{total_ok}/{total_all} selectores funcionales ({total_ok/total_all*100:.1f}%)**\n")
def validar_selectores(destino_busqueda=None, cerrar=False):
driver = iniciar_driver()
driver.get("https://web.whatsapp.com")
try:
print("Esperando sesion activa de WhatsApp Web...")
if not ensure_session_active(driver):
print("No se detecto sesion activa. Escanea el QR.")
captura(driver, "sin_sesion")
return False
captura(driver, "sesion_activa")
categorias = [
("Sesion / Panel lateral", construir_selectores_session()),
("QR detection", construir_selectores_qr()),
("Buscador - campo principal", construir_selectores_busqueda()),
("Buscador - botones alternativos", construir_selectores_busqueda_botones()),
("Resultados de busqueda", construir_selectores_resultados()),
("Caja de mensaje", construir_selectores_caja_mensaje()),
("Boton enviar", construir_selectores_enviar()),
("Ticks / estado de mensaje", construir_selectores_ticks()),
("Texto mensaje saliente", construir_selectores_mensaje_saliente()),
("Nombre de chat (header)", construir_selectores_nombre_chat()),
("Nombre en resultados", construir_selectores_nombre_resultado()),
]
resultados_por_categoria = []
for categoria, selectores in categorias:
resultados = probar_selectores(driver, selectores, categoria)
ok, total = imprimir_resultados(resultados, categoria)
resultados_por_categoria.append((categoria, resultados))
# Si se proporcionó un destino, probar búsqueda real y apertura de chat
if destino_busqueda:
print(f"\n{'=' * 70}")
print(f" PRUEBA DE BUSQUEDA REAL con destino: '{destino_busqueda}'")
print(f"{'=' * 70}")
from whatsapp_sender.whatsapp import (abrir_buscador, buscar_resultados_chat, buscar_caja_mensaje,
click_seguro, limpiar_campo)
buscador = abrir_buscador(driver)
if buscador:
print(" Buscador abierto correctamente.")
limpiar_campo(driver, buscador)
for c in destino_busqueda:
buscador.send_keys(c)
time.sleep(0.05)
time.sleep(2)
resultados_busqueda = probar_selectores(driver, construir_selectores_resultados(), "Resultados post-busqueda")
ok_b, total_b = imprimir_resultados(resultados_busqueda, "Resultados de busqueda (despues de escribir)")
resultados_por_categoria.append((f"Resultados post-busqueda ('{destino_busqueda}')", resultados_busqueda))
captura(driver, "post_busqueda")
# Abrir chat por enlace directo (send?phone=) para validar
# selectores de caja de mensaje, header y boton enviar
import re
numeros = re.findall(r"\d{7,15}", destino_busqueda)
if numeros:
telefono = numeros[0]
print(f" Abriendo chat directo con numero {telefono}...")
driver.get(f"https://web.whatsapp.com/send?phone={telefono}")
time.sleep(5)
captura(driver, "chat_directo")
categorias_chat = [
("Caja de mensaje (con chat abierto)", construir_selectores_caja_mensaje()),
("Nombre de chat (header, con chat abierto)", construir_selectores_nombre_chat()),
("Ticks / estado de mensaje", construir_selectores_ticks()),
("Texto mensaje saliente", construir_selectores_mensaje_saliente()),
]
for cat, selectores in categorias_chat:
resultados = probar_selectores(driver, selectores, cat)
ok, total = imprimir_resultados(resultados, cat)
resultados_por_categoria.append((cat, resultados))
# Type in compose box so send button replaces mic button
compose = driver.find_elements(By.CSS_SELECTOR,
"[data-testid='conversation-compose-box-input'], "
"footer div[contenteditable='true']")
if compose:
compose[0].click()
compose[0].send_keys("test")
time.sleep(1)
resultados_envio = probar_selectores(driver, construir_selectores_enviar(), "Boton enviar (con texto en caja)")
ok_e, total_e = imprimir_resultados(resultados_envio, "Boton enviar (con texto en caja)")
resultados_por_categoria.append(("Boton enviar (con texto en caja)", resultados_envio))
if compose:
driver.execute_script("arguments[0].innerHTML = '';", compose[0])
else:
print(" No se encontro numero en el destino para abrir chat directo.")
else:
print(" No se pudo abrir el buscador.")
# Generar reporte
reporte_path = os.path.join(DIAGNOSTICOS_DIR, "reporte_selectores.md")
generar_reporte_markdown(resultados_por_categoria, reporte_path)
print(f"\nReporte guardado: {reporte_path}")
# Resumen final
total_ok = 0
total_all = 0
for cat, res in resultados_por_categoria:
total_ok += sum(1 for r in res if r["coincide"])
total_all += len(res)
print(f"\n{'=' * 70}")
print(f" RESUMEN GLOBAL: {total_ok}/{total_all} selectores funcionales ({total_ok/total_all*100:.1f}%)")
print(f"{'=' * 70}")
return True
finally:
if cerrar:
try:
driver.quit()
except Exception:
pass
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Valida todos los selectores CSS/XPath usados en whatsapp_sender/."
)
parser.add_argument("destino", nargs="?", help="Contacto/grupo para prueba de busqueda real")
parser.add_argument("--cerrar", action="store_true", help="Cerrar Chrome al terminar")
args = parser.parse_args()
validar_selectores(destino_busqueda=args.destino, cerrar=args.cerrar)