Buscar en el árbol: texto, selectores CSS y XPath
Los tres lenguajes de búsqueda del panel de Elements, qué puede expresar cada uno, y los cuatro casos donde XPath resuelve lo que CSS no puede.
El buscador del panel de Elements acepta tres cosas distintas por el mismo campo y decide sola cuál es: si lo que escribes parece un selector CSS válido, lo trata como selector; si empieza por barra o parece una expresión XPath, la evalúa; y si no, busca como texto en el marcado. Esa ambigüedad es cómoda y también es la fuente de la mayoría de las búsquedas que no encuentran nada. Entender qué puede expresar cada lenguaje convierte el buscador en la herramienta de acotación más rápida del panel.
- Elegir entre búsqueda por texto, por selector CSS o por XPath según lo que quieres encontrar.
- Escribir expresiones XPath para los casos que CSS no puede expresar.
- Combinar la búsqueda del panel con las utilidades de consulta de la consola.
- Diagnosticar por qué una búsqueda que debería encontrar algo devuelve cero resultados.
Los tres modos y sus fronteras
Se abre con Cmd+F o Ctrl+F con el foco en el árbol. Aparece un campo con un contador de coincidencias y flechas para recorrerlas.
La búsqueda por texto casa contra el marcado serializado, lo que significa que encuentra tanto contenido de texto como nombres de atributos y sus valores. Buscar guardar encuentra el botón cuyo texto es Guardar y también el que tiene data-action="guardar". Es el modo más tolerante y el que usarás la mitad de las veces.
La búsqueda por selector CSS admite todo lo que el navegador soporta, incluidos los selectores modernos. .card:has(> img) funciona. input:not([disabled]) funciona. Es un querySelectorAll con interfaz visual.
La búsqueda por XPath admite expresiones completas, y su valor está exactamente en lo que CSS no puede expresar.
La búsqueda por selector CSS opera sobre el documento principal. Si el elemento que buscas está dentro de un iframe o de un shadow DOM, no aparecerá aunque lo estés viendo en el árbol. Para el shadow DOM abierto la vía es consultar desde la consola con elemento.shadowRoot.querySelector(...); para el iframe hay que cambiar de contexto de ejecución.
Los cuatro casos donde XPath gana
CSS es un lenguaje de selección descendente: puedes describir un elemento por sus antepasados, por sus atributos y por su posición entre hermanos. XPath es un lenguaje de navegación en cualquier dirección sobre un documento, y eso le da cuatro capacidades que CSS no tiene.
Seleccionar por contenido de texto. El caso más frecuente con diferencia. CSS no puede mirar dentro del texto de un nodo; XPath sí.
//button[contains(text(), "Guardar")]
//*[normalize-space(text()) = "Sin resultados"]
//a[starts-with(@href, "/api/")]
La función normalize-space es la que hace útil el segundo: colapsa espacios y quita los de los extremos, que es lo que hace falta cuando el HTML viene indentado y el texto tiene saltos de línea alrededor.
Subir en el árbol. CSS tiene :has(), que resuelve una parte del problema, pero XPath tiene ejes explícitos: ancestor, parent, preceding-sibling, following-sibling. Encontrar la fila de tabla que contiene una celda con un texto concreto es directo.
//td[contains(., "pendiente")]/ancestor::tr
Combinar condiciones sobre distintos ejes. Un predicado XPath puede contener otra expresión completa, lo que permite condiciones que en CSS requerirían varios pasos.
//div[@class="fila"][.//input[@checked]][not(.//span[@class="error"])]
Contar y posicionar con aritmética. [last()], [position() > 3], [count(./li) > 5]. CSS tiene nth-child con su notación, que es menos expresiva y no puede contar descendientes.
//ul[count(./li) > 10]
Cuándo no usar XPath
Con todo lo anterior, la conclusión tentadora sería usar XPath siempre. Sería un error por tres razones.
Es más lento de evaluar que un selector CSS sobre documentos grandes, porque el motor de CSS está optimizado hasta el absurdo y el de XPath no.
Es mucho menos legible para la mayoría de los equipos. Un //div[@class="fila"] tiene además una trampa clásica: @class="fila" exige que el atributo sea exactamente esa cadena, mientras que el .fila de CSS casa con cualquier elemento que tenga esa clase entre otras. El equivalente correcto en XPath es feo: //div[contains(concat(" ", normalize-space(@class), " "), " fila ")].
Y no soporta los selectores modernos de CSS ni la semántica de pseudo-clases de estado. :focus-visible no tiene equivalente XPath.
La regla práctica: CSS por defecto, XPath cuando necesites texto, ejes ascendentes o aritmética. Que son exactamente los cuatro casos de arriba.
Del panel a la consola
Las mismas capacidades existen en la consola con las utilidades de selección, y ahí se pueden encadenar con código.
$$(selector) devuelve un array real —no una NodeList— con todos los elementos que casan, lo que permite encadenar métodos de array directamente.
$x(expresion) evalúa XPath y devuelve un array de nodos.
La combinación de ambas con console.table produce diagnósticos completos en una línea.
// Todos los enlaces que abren en otra pestaña sin la protección de rel
console.table(
$$('a[target="_blank"]')
.filter(a => !/noopener/.test(a.rel))
.map(a => ({ texto: a.textContent.trim().slice(0, 40), href: a.href, rel: a.rel }))
);
// Botones sin texto accesible, por contenido o por atributo
console.table(
$x('//button[not(normalize-space(text()))][not(@aria-label)][not(@aria-labelledby)]')
.map(b => ({ html: b.outerHTML.slice(0, 80) }))
);
Cuatro causas explican casi todos los casos de “está ahí y el buscador dice cero”, y ninguna es un fallo de la herramienta. La primera es que el texto que ves no está en el DOM: viene de un pseudo-elemento con content, y los pseudo-elementos no son nodos de texto buscables. Se reconoce porque tampoco puedes seleccionarlo con el ratón en la página. La segunda es que el texto está partido entre varios nodos: si el marcado es Total seguido de un span con la cifra, buscar la frase completa falla porque no existe ningún nodo que la contenga entera, y hay que buscar el fragmento que sí está junto. La tercera es que el texto está transformado por CSS: text-transform: uppercase hace que veas GUARDAR y que en el DOM ponga Guardar, así que buscar en mayúsculas no encuentra nada. Esta es especialmente cruel porque la búsqueda del panel no distingue mayúsculas en modo texto, pero sí lo hace en modo selector y en XPath, donde contains(text(), "GUARDAR") falla en silencio. La cuarta es que el nodo está en otro árbol: iframe o shadow DOM. La heurística que las cubre todas es empezar buscando el fragmento más corto y más raro que veas —tres o cuatro caracteres poco comunes, sin mayúsculas y sin espacios— y solo después refinar. Si un fragmento corto tampoco aparece, entonces sí tienes una de las cuatro causas y ya sabes cuál comprobar primero.
En cualquier página con una tabla, escribe la expresión XPath que devuelve las filas cuya última celda esté vacía. Luego escribe el equivalente en CSS moderno con :has() y compara: una de las dos versiones no se puede escribir sin conocer el número de columnas, y esa asimetría es exactamente el argumento de esta lección.