Selenium Shadow DOM: búsquedas y raíces desconectadas
Diagnostica búsquedas en Shadow DOM con Selenium y una prueba local en Python: límites del documento, raíces anidadas y cerradas, y reemplazo de componentes.
BotBrowser Team
Quieres la documentación estructurada de Primeros pasos?
Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.
Cuando Selenium no encuentra un elemento de Shadow DOM visible en DevTools, comprueba el contexto de búsqueda antes de cambiar el selector o el tiempo de espera. Localiza el anfitrión del componente, obtén su shadow_root nativa y busca dentro de esa raíz; repite el proceso para cada anfitrión anidado. BotBrowser proporciona ajustes declarados del entorno del navegador para comparar reproducciones, pero no cruza los límites de los componentes por un selector y no garantiza que un componente haya terminado de renderizarse.
TL;DR
- Una consulta del documento no atraviesa automáticamente las raíces shadow. Un selector CSS de descendientes más largo sigue buscando en el árbol equivocado.
- Recorre cada anfitrión con el comando nativo
shadow_rootde Selenium. Cambiar de marco y recorrer raíces shadow resuelve límites diferentes. - Una raíz cerrada hace que el acceso JavaScript a
.shadowRootdevuelvanull; eso no demuestra que el acceso nativo de WebDriver deba fallar. - Cuando se reemplaza el anfitrión de un componente, vuelve a obtener la ruta completa de anfitriones y raíces y verifica el resultado de la aplicación. Reutilizar una raíz anterior puede lanzar
DetachedShadowRootException.
Contenido
- Clasifica el límite que falla antes de cambiar el selector
- Reproduce localmente las raíces del documento, anidadas y reemplazadas
- Separa el acceso JavaScript a raíces cerradas del acceso nativo de WebDriver
- Vuelve a obtener un componente reemplazado sin repetir su acción
- Conclusión práctica
Clasifica el límite que falla antes de cambiar el selector
La documentación de búsqueda de elementos de Selenium usa un elemento anfitrión seguido de su raíz shadow como contexto de búsqueda independiente. El anfitrión es un elemento normal dentro del árbol que lo contiene; la raíz adjunta contiene otro árbol. Un botón coincidente dentro de esa raíz no es un descendiente normal disponible para una búsqueda CSS del documento. DevTools puede mostrar ambos árboles juntos sin convertir sus contextos de búsqueda de WebDriver en intercambiables.
Conserva el comando, el selector y el contexto de la excepción original. Una NoSuchElementException de driver.find_element() y la misma excepción de root.find_element() dejan preguntas diferentes abiertas: la primera puede estar usando el árbol equivocado, mientras que la segunda puede haber llegado a la raíz correcta antes de buscar demasiado pronto o con el selector de descendiente equivocado. Una captura por sí sola no distingue esos casos. Registra el anfitrión identificado y si se obtuvo correctamente la raíz nativa.
| Observación | Primera comprobación | Qué significa una corrección válida |
|---|---|---|
| La consulta del documento no devuelve ningún botón coincidente | ¿El botón está dentro de un árbol shadow creado por la aplicación? | La raíz del anfitrión previsto contiene el destino |
host.shadow_root lanza NoSuchShadowRootException | Anfitrión correcto, momento en que se adjunta la raíz y compatibilidad del controlador | Se obtiene una raíz de ese anfitrión |
La consulta de la raíz lanza NoSuchElementException | Selector del descendiente, otro anfitrión anidado o disponibilidad | La raíz prevista contiene el descendiente necesario |
Un elemento anterior lanza StaleElementReferenceException | ¿Se reemplazó su anfitrión o documento? | Una ruta nueva identifica el componente actual |
Una raíz anterior lanza DetachedShadowRootException | ¿Se desconectó el anfitrión de esa raíz? | Una raíz nueva pertenece al anfitrión conectado actual |
La búsqueda funciona, pero click() falla | Identidad del destino, estado de interacción y obstrucciones | La aplicación registra la acción prevista |
Los comandos de raíces shadow de WebDriver distinguen entre obtener una raíz y encontrar un elemento dentro de ella. Un <div> normal sin raíz adjunta es un elemento válido, pero no un anfitrión shadow válido para ese comando. A la inversa, una raíz existente puede no contener ningún elemento .save coincidente. Son fallos distintos; capturar ambos y comunicar «Shadow DOM no compatible» descarta esa diferencia útil.
Mantén los marcos y las raíces anidadas en la ruta
Un iframe cambia el documento actual mediante driver.switch_to.frame(...). Un anfitrión shadow cambia el contexto de búsqueda mediante host.shadow_root; no requiere cambiar de marco. Si un componente está dentro de un iframe, entra primero en el marco correcto y después recorre sus anfitriones. Si un iframe está dentro de una raíz shadow, localiza el elemento del marco a través de la raíz antes de cambiar a él. Conserva el orden real de los límites en lugar de aplicar un cambio genérico de marco a cada elemento ausente.
Los componentes anidados requieren obtener otra raíz nativa en cada límite. La raíz exterior puede localizar un anfitrión interior, pero un único selector CSS como #outer #inner #save no cruza ambas raíces. Prefiere una ruta breve y con nombres claros: documento, anfitrión exterior, raíz exterior, anfitrión interior, raíz interior y botón, para que el fallo identifique el límite ausente. Inspecciona el número de coincidencias cuando una aplicación pueda renderizar varias copias; un tipo de raíz correcto no demuestra que el componente seleccionado represente el registro previsto.
Una vez que funciona el recorrido de raíces, la interacción sigue siendo una cuestión aparte. El diagnóstico de element not interactable en Selenium distingue los controles ocultos, los centros de clic interceptados y la editabilidad. Que una búsqueda en la raíz devuelva un botón no demuestra que este sea visible, esté habilitado o no tenga obstrucciones. Conserva la evidencia de búsqueda antes de pasar a esas comprobaciones específicas de cada comando; de lo contrario, un fallo de clic podría comunicarse como un árbol shadow inaccesible.
Reproduce localmente las raíces del documento, anidadas y reemplazadas
Instala Selenium en un entorno virtual de Python con python -m pip install selenium y guarda lo siguiente como shadow_roots.py. Usa una página data: local integrada, no un destino de terceros. webdriver.Chrome() necesita un navegador instalado y un controlador compatible; la gestión de controladores de Selenium puede descargar un controlador durante la preparación. En un entorno sin conexión, configura explícitamente un ejecutable compatible conocido y Service. Un error de arranque antes de la navegación no aporta evidencia sobre la búsqueda en raíces shadow.
from urllib.parse import quote
from selenium import webdriver
from selenium.common.exceptions import (
DetachedShadowRootException,
NoSuchElementException,
NoSuchShadowRootException,
StaleElementReferenceException,
)
from selenium.webdriver.common.by import By
HTML = """
<!doctype html><meta charset="utf-8">
<div id="open"></div><div id="outer"></div><div id="closed"></div>
<script>
function populate(host, mode) {
const root = host.attachShadow({mode});
root.innerHTML = `<button id="save"
onclick="this.getRootNode().host.dataset.saved='yes'">Save</button>`;
return root;
}
populate(document.getElementById('open'), 'open');
const outer = document.getElementById('outer').attachShadow({mode:'open'});
outer.innerHTML = '<div id="inner"></div>';
populate(outer.getElementById('inner'), 'open');
populate(document.getElementById('closed'), 'closed');
window.replaceOpen = () => {
window.oldHost = document.getElementById('open');
const fresh = document.createElement('div');
fresh.id = 'open'; fresh.dataset.version = 'new';
populate(fresh, 'open'); window.oldHost.replaceWith(fresh);
};
</script>
"""
def rejects(exception, command):
try:
command()
except exception:
return
raise AssertionError(f"Expected {exception.__name__}")
options = webdriver.ChromeOptions()
options.add_argument("--headless")
options.add_argument("--window-size=1280,900")
driver = webdriver.Chrome(options=options)
try:
driver.get("data:text/html;charset=utf-8," + quote(HTML))
assert driver.find_elements(By.CSS_SELECTOR, "#save") == []
rejects(NoSuchElementException,
lambda: driver.find_element(By.CSS_SELECTOR, "#save"))
host = driver.find_element(By.ID, "open")
root = host.shadow_root
button = root.find_element(By.CSS_SELECTOR, "#save")
button.click()
assert host.get_attribute("data-saved") == "yes"
outer = driver.find_element(By.ID, "outer").shadow_root
rejects(NoSuchElementException,
lambda: outer.find_element(By.CSS_SELECTOR, "#save"))
inner = outer.find_element(By.CSS_SELECTOR, "#inner")
inner.shadow_root.find_element(By.CSS_SELECTOR, "#save").click()
assert inner.get_attribute("data-saved") == "yes"
closed = driver.find_element(By.ID, "closed")
assert driver.execute_script("return arguments[0].shadowRoot", closed) is None
try:
closed_root = closed.shadow_root
except NoSuchShadowRootException:
closed_native = "unavailable"
assert closed.get_attribute("data-saved") is None
else:
closed_root.find_element(By.CSS_SELECTOR, "#save").click()
assert closed.get_attribute("data-saved") == "yes"
closed_native = "saved"
driver.execute_script("window.replaceOpen()")
assert driver.execute_script("return window.oldHost.isConnected") is False
rejects(StaleElementReferenceException, button.click)
rejects(DetachedShadowRootException,
lambda: root.find_element(By.CSS_SELECTOR, "#save"))
current = driver.find_element(By.ID, "open")
current.shadow_root.find_element(By.CSS_SELECTOR, "#save").click()
assert current.get_attribute("data-version") == "new"
assert current.get_attribute("data-saved") == "yes"
print(f"open=saved nested=saved closed_native={closed_native} reacquired=saved")
finally:
driver.quit()
Ejecuta python shadow_roots.py. Un controlador que admite la rama de raíces cerradas imprime open=saved nested=saved closed_native=saved reacquired=saved. Si la obtención nativa de una raíz cerrada informa de que no existe tal raíz, la prueba registra closed_native=unavailable en su lugar; esa rama no cuenta como una interacción correcta con una raíz cerrada. Cualquier otra excepción inesperada sigue siendo un fallo. Los valores id="save" deliberadamente repetidos pertenecen a árboles shadow diferentes, lo que permite comprobar el ámbito sin cambiar el selector del descendiente.
El manejador del botón establece data-saved en su propio anfitrión, por lo que el resultado es específico del componente. Las aserciones de las raíces abiertas y anidadas fallarían si una acción seleccionara silenciosamente otro componente. Después del reemplazo, data-version=new distingue el anfitrión nuevo del anterior desconectado. Una aplicación de producción necesita su propia identidad de operación o registro; copiar el marcador de la prueba a un test sin un contrato de aplicación no demostraría un guardado real.
Qué demuestran los casos locales
La ejecución preparatoria de cinco casos del 2026-11-11 usó Selenium 4.52.0, Chrome estándar 157.0.8103.79 y ChromeDriver 157.0.8103.50 en macOS arm64. La búsqueda del documento no encontró un descendiente de una raíz abierta; el recorrido nativo permitió hacer clic correctamente en él. Otro caso cruzó dos raíces anidadas. El acceso JavaScript a la raíz cerrada devolvió None, mientras que el acceso nativo encontró su botón y permitió hacer clic en él. Un anfitrión normal lanzó NoSuchShadowRootException, mientras que buscar un descendiente ausente en una raíz existente lanzó NoSuchElementException.
Reemplazar un anfitrión dejó obsoleto el botón almacenado y desconectó la raíz almacenada; volver a obtener el anfitrión y la raíz permitió llegar al componente nuevo y guardar correctamente. Son observaciones sintéticas con el navegador estándar, no una ejecución de capacidades de BotBrowser ni evidencia sobre la implementación de componentes de un sitio externo. Conserva las versiones del navegador, del controlador y de los enlaces de Selenium con cada reproducción, especialmente cuando la diferencia concierne a la compatibilidad con raíces cerradas y no a la sintaxis del selector.
Separa el acceso JavaScript a raíces cerradas del acceso nativo de WebDriver
El acceso a shadowRoot definido en la especificación DOM devuelve null cuando no hay raíz o su modo es cerrado. Por tanto, execute_script("return arguments[0].shadowRoot", host) no puede distinguir por sí solo esos dos casos. Un resultado JavaScript vacío es una observación sobre ese acceso, no una prueba de que el anfitrión carezca de raíz interna. DevTools puede mostrar la raíz y un script devolver null, y ambos resultados ser correctos.
El algoritmo nativo Get Element Shadow Root de WebDriver trabaja con la raíz shadow interna del elemento, en lugar de imponer ese acceso JavaScript. El cambio de compatibilidad con raíces shadow cerradas de ChromeDriver, incorporado el 2024-10-14, añadió explícitamente esa compatibilidad. Las implementaciones anteriores pueden comportarse de otro modo. Registra el resultado real del comando y las versiones de implementación; ni «cerrado siempre funciona» ni «cerrado siempre es inaccesible para Selenium» es una regla válida para todos los controladores.
Separa la evidencia del comando y de la implementación
En una máquina donde falle, ejecuta la misma prueba propia con .shadow_root nativa y conserva la clase de excepción. Si difiere del entorno registrado, compara las versiones del navegador, del controlador y de los enlaces de Selenium antes de reescribir la página. Un anfitrión obsoleto, uno incorrecto o una condición de carrera al adjuntar la raíz también pueden hacer fallar su obtención; la palabra cerrado no sustituye la identificación del nodo seleccionado. La prueba crea cada raíz de forma síncrona para no confundir compatibilidad con tiempos de la aplicación.
El resultado registrado para la raíz cerrada corresponde a un componente creado por la aplicación con attachShadow({mode:'closed'}). No demuestra que todos los controles internos del navegador expongan su árbol shadow del agente de usuario al mismo comando de automatización. La compatibilidad nativa con una prueba tampoco dice nada sobre el modelo de permisos de la aplicación ni sobre si debe ejecutarse una operación. Limita el diagnóstico a componentes propios o cuya prueba esté autorizada.
Usa CSS dentro de una raíz conocida
La API ShadowRoot de Selenium para Python proporciona métodos de búsqueda de elementos. El botón devuelto es un WebElement con click(); la propia raíz no es un elemento sobre el que se pueda hacer clic. CSS es la estrategia que ejercita esta prueba. No supongas que una expresión XPath puede salir de un árbol shadow o atravesar el siguiente solo porque XPath funcione en el contexto del documento. Los errores de estrategia no compatible y de ausencia de coincidencias deben conservar sus categorías reales de protocolo.
Evita sustituir el comando nativo por un recorrido JavaScript largo sin registrar el cambio. Una cadena de scripts que usa .shadowRoot hereda el comportamiento del acceso a raíces cerradas y puede fallar en un límite diferente. Un script que invoca directamente el manejador del botón también convierte una prueba de interacción en una activación mediante script. La búsqueda nativa seguida de un clic normal de WebDriver conserva la distinción entre llegar al nodo e interactuar con él.
Vuelve a obtener un componente reemplazado sin repetir su acción
Una referencia a una raíz pertenece a una instancia concreta de anfitrión, no a todos los elementos futuros con el mismo ID. Cuando una aplicación reemplaza <settings-panel> por un elemento nuevo, el selector de texto puede seguir siendo válido mientras las referencias anteriores al anfitrión y a la raíz dejan de ser utilizables. La definición de raíces shadow de WebDriver incluye comprobaciones de desconexión. Volver a buscar solo el botón a través de una raíz anterior conserva la parte rota de la ruta.
La prueba conserva deliberadamente window.oldHost después del reemplazo. Esa referencia JavaScript mantiene accesible el objeto anterior, pero no lo mantiene conectado al documento; isConnected es false. Es un contraejemplo útil de «el objeto todavía existe, así que Selenium debería usarlo». El botón anterior provoca un error de elemento obsoleto y una búsqueda a través de la raíz anterior provoca un error de raíz desconectada en el controlador registrado. Ambos requieren la identidad actual del componente, no otro selector de descendientes sobre el objeto anterior.
Espera a obtener el elemento y ejecuta la acción una sola vez
Para una aplicación que adjunta su raíz de forma asíncrona, una espera explícita puede volver a obtener el anfitrión, la raíz y el descendiente en cada consulta. La siguiente función auxiliar supone que #open identifica un componente previsto único y que su descendiente shadow tiene el ID save. Devuelve un destino visible y habilitado; no promete que el punto de clic esté libre de obstrucciones. Solo reintenta la búsqueda, de modo que las consultas de espera no puedan enviar repetidamente la misma operación.
from selenium.webdriver.support.ui import WebDriverWait
def current_save(driver):
try:
host = driver.find_element(By.ID, "open")
root = host.shadow_root
button = root.find_element(By.CSS_SELECTOR, "#save")
if button.is_displayed() and button.is_enabled():
return button
return False
except (NoSuchElementException, NoSuchShadowRootException,
StaleElementReferenceException, DetachedShadowRootException):
return False
save = WebDriverWait(driver, 5).until(current_save)
save.click()
Sigue existiendo una condición de carrera entre el elemento devuelto y el clic posterior. Si la aplicación puede reemplazar el componente en ese intervalo, usa una condición de estado estable controlada por la aplicación o comunica el nuevo error de elemento obsoleto en lugar de repetir silenciosamente una compra o un guardado completo. El diagnóstico de esperas y pruebas estables de Selenium desarrolla los contratos de disponibilidad y resultado; aquí, el requisito adicional es volver a obtener cada límite shadow que pertenezca al componente actual.
Después de la acción, espera su resultado real por separado. Una respuesta de red, un registro guardado o una confirmación del componente deben corresponder a la operación recién ejecutada. Un data-saved=yes permanente que dejó una interacción anterior no basta para pruebas de guardados repetidos. Cuando importen los envíos duplicados, conserva una identidad de operación e inspecciona el resultado antes de decidir si otro clic es seguro. Un bucle genérico de captura y reintento no puede tomar esa decisión de negocio.
Diagnostica las esperas agotadas persistentes con el límite que falla
La función auxiliar convierte deliberadamente los fallos esperados al obtener el elemento en otra consulta, por lo que un agotamiento final de la espera debe conservar contexto de diagnóstico en otro lugar. Registra si el intento más reciente falló al buscar el anfitrión, al obtener la raíz o al buscar el descendiente, y captura una imagen junto con atributos seguros del componente. No registres todo el marcado shadow si contiene credenciales o datos de clientes. Ejecutar temporalmente una sola obtención sin la captura de excepciones también puede exponer la excepción de protocolo subyacente en una prueba propia.
Si nunca se adjunta una raíz, inspecciona la señal de inicialización de la aplicación y los fallos anteriores de scripts o red. Si se adjunta, pero el destino sigue ausente, inspecciona el estado previsto y el selector dentro de esa raíz. Si el destino aparece y falla la interacción, pasa al diagnóstico de visibilidad y obstrucciones. Esas ramas permiten actuar ante una espera agotada; aumentar simplemente WebDriverWait(driver, 5) oculta qué estado debía cambiar.
Compara el entorno después de la ruta del componente
Las versiones del navegador y los entornos de renderizado pueden cambiar el comportamiento o el diseño de un componente, así que consérvalos una vez establecida la identidad del documento y de la raíz. BotBrowser permite declarar ajustes de pantalla y fuentes mediante su referencia de configuración de perfiles, pero no corrige un selector de raíz y no garantiza compatibilidad nativa con raíces cerradas en todas las combinaciones de controladores. La documentación de integración cubre el arranque y la conexión; esta ruta de diagnóstico trata los contextos de búsqueda cuando la conexión ya funciona.
La prueba local no carga scripts remotos, no autentica ninguna cuenta ni ejercita la hidratación de un framework. Aísla el recorrido y la duración de las referencias. La actualización asíncrona de elementos personalizados, la navegación o la duplicación adaptable de una aplicación necesitan una reproducción propia adicional con los marcadores de estado correspondientes. Mantén esa prueba añadida separada de las aserciones síncronas del ejemplo para que una afirmación de compatibilidad del controlador no se convierta accidentalmente en una promesa de disponibilidad de la aplicación.
Preguntas frecuentes
¿Por qué un botón visible no devuelve coincidencias?
La visibilidad en DevTools no identifica el árbol de búsqueda actual de Selenium. Confirma el marco, obtén la raíz del anfitrión y busca allí. Si la raíz exterior contiene otro anfitrión, obtén esa raíz interior antes de consultar el botón.
¿Java o C# evitan el límite de Python?
Cambiar los enlaces de Selenium cambia la sintaxis, no los contextos de búsqueda del documento y de las raíces shadow. Conserva la misma ruta de anfitriones y raíces y el comando real de WebDriver al comparar lenguajes, y registra las versiones de los enlaces si difieren las API que exponen.
¿Una raíz cerrada es un contrato de capacidad de prueba?
El modo cerrado restringe el acceso JavaScript de la página; no es una afirmación universal sobre la compatibilidad con automatización. Para un componente propio, prefiere un contrato de prueba explícito y compatible, y verifica el controlador real en lugar de depender de una solución alternativa de acceso no registrada.
Fuentes
- Selenium: evaluar Shadow DOM
- W3C WebDriver: comandos y referencias de raíces shadow
- WHATWG DOM: acceso a shadowRoot
- Chromium: compatibilidad con raíces shadow cerradas en ChromeDriver
- Selenium para Python: API ShadowRoot
- BotBrowser: límites de configuración de perfiles
Conclusión práctica
Conserva el contexto que falló, recorre explícitamente cada raíz nativa y vuelve a obtener la ruta completa cuando se reemplace el anfitrión. Ejecuta la prueba local antes de atribuir un fallo al modo cerrado o al entorno del navegador. Una búsqueda correcta establece una referencia al destino; el resultado de la operación actual del componente demuestra si funcionó la acción prevista.
Este artículo forma parte de la guía: Primeros pasos con automatización de navegador: mapa inicial
Artículos Relacionados
Selenium element not interactable: causas y soluciones
Primeros pasosEsperas de Selenium WebDriver para pruebas estables
PlataformaRevisión de compatibilidad del navegador para Web Components
Primeros pasosPlaywright strict mode violation: corregir duplicados
Primeros pasosIntegración de perfiles de navegador con Selenium
Primeros pasosScraping con Selenium: capacidades y registros
Ejecuta la misma comprobación en BotBrowser
Carga un perfil, mantén dentro del navegador las señales que acabas de leer y confirma el resultado en tu propia máquina antes del despliegue.