362 lines
18 KiB
Python
362 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"},
|
|
]
|
|
|
|
|
|
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": "footer div[aria-placeholder*='mensaje' i]", "desc": "Footer placeholder mensaje"},
|
|
{"by": By.CSS_SELECTOR, "selector": "footer div[aria-label*='mensaje' i]", "desc": "Footer label mensaje"},
|
|
{"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)
|