#!/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)