¿Te ha pasado alguna vez? Estás automatizando un sitio web con Puppeteer, encuentras un elemento que parece contener información importante, pero al intentar acceder a su shadowRoot solo obtienes null. La página muestra el contenido, pero tu script no puede verlo. Bienvenido al mundo de los Web Components con Shadow DOM cerrado.
En este artículo te llevo paso a paso a través de un laboratorio real que he construido para demostrar cómo interceptar la creación de un ShadowRoot cerrado con Puppeteer, utilizando la poderosa función page.evaluateOnNewDocument(). No es magia, es timing.
Introducción a los Web Components y Shadow DOM
Los Web Components son un conjunto de estándares que permiten crear elementos HTML reutilizables y encapsulados. Uno de los pilares de esta tecnología es el Shadow DOM, que permite aislar el estilo y la estructura de un componente del resto del documento. Esto es fantástico para el desarrollo de aplicaciones modulares, pero presenta un desafío significativo para la automatización del navegador.
Cuando un componente utiliza attachShadow({ mode: 'closed' }), el navegador impide que el código externo (incluyendo el de Puppeteer) acceda a su DOM interno a través de la propiedad .shadowRoot. El resultado es simple y frustrante:
> document.querySelector('product-card').shadowRoot
null
> document.querySelector('product-card').innerHTML
""
El contenido se renderiza en pantalla, pero es inalcanzable por métodos convencionales. Esto afecta a:
- Pruebas automatizadas (end-to-end testing).
- Scraping de datos en aplicaciones modernas.
- Extracción de información en entornos headless.
- Monitoreo y análisis de aplicaciones web.
El problema en detalle: ¿por qué shadowRoot devuelve null?
Cuando un Web Component se define con mode: "closed", el navegador establece una barrera de acceso. La propiedad shadowRoot del elemento host devuelve null para cualquier código que no sea el propio componente. Esto es intencional y forma parte del diseño de encapsulamiento.
El componente en sí mismo recibe la referencia al ShadowRoot en el momento de su creación, pero esa referencia no se expone al exterior. Por lo tanto, cualquier intento de acceder a su contenido desde la consola del navegador o desde Puppeteer (sin instrumentación) falla.
La paradoja
El contenido se ve, pero no se puede leer desde JavaScript externo. ElshadowRootdevuelvenully elinnerHTMLdel host está vacío. Es como una caja fuerte que muestra su interior pero no se deja abrir.
El laboratorio: una demostración funcional y completa
Para demostrar el problema y su solución, he creado un laboratorio completo que puedes ejecutar localmente. Este laboratorio es un sitio web estático que incluye:
- Un Web Component real (
<product-card>) con Shadow DOM cerrado. - Una interfaz visual que muestra el componente y una consola simulada.
- Un script de Puppeteer que intercepta la creación del ShadowRoot.
- Evidencia real de la salida de la consola y de Puppeteer.
- Selector de idioma (ES/EN), tema oscuro/claro y botón “Scroll to top”.
El laboratorio está diseñado para ser reproducible en cualquier entorno con Node.js y Puppeteer. Puedes descargarlo desde este enlace o clonar el repositorio. También te dejo el código clave más abajo.
La solución: interceptar attachShadow antes de que se ejecute la aplicación
La clave está en el timing. Si tu script de Puppeteer se ejecuta después de que el componente haya creado su ShadowRoot, ya es tarde. No hay un método mágico que lo convierta en abierto. Pero si puedes inyectar código antes de que la página cargue sus scripts, puedes interceptar la llamada a Element.prototype.attachShadow y capturar la referencia del ShadowRoot en el momento de su creación.
Qué es evaluateOnNewDocument()
page.evaluateOnNewDocument() es una función de Puppeteer que permite ejecutar un script en el contexto de la página antes de que se cargue cualquier otro script del documento. Esto significa que podemos modificar el comportamiento nativo del navegador (como Element.prototype.attachShadow) antes de que la aplicación tenga la oportunidad de utilizarlo.
Esta función es ideal para:
- Interceptar métodos nativos.
- Inyectar código de depuración.
- Modificar el entorno de ejecución de la página.
- Preparar el terreno para la automatización.
Código de instrumentación
El siguiente código sobrescribe Element.prototype.attachShadow para forzar el modo open y almacenar una referencia al ShadowRoot:
// Instrumentación ejecutada antes de cargar la aplicación
await page.evaluateOnNewDocument(() => {
const original = Element.prototype.attachShadow;
Element.prototype.attachShadow = function (init) {
// Forzamos el modo 'open' para poder capturarlo
const root = original.call(this, { ...init, mode: 'open' });
// Almacenamos la referencia en el elemento
this.__capturedShadowRoot = root;
return root;
};
});
Con este código, cambiamos silenciosamente el modo a open y almacenamos una referencia en el elemento host. Luego, desde Puppeteer podemos recuperarla:
> const root = await page.evaluate(() =>
document.querySelector('product-card').__capturedShadowRoot
);
> root.innerHTML
<div class="card">
<div class="name">Test Product</div>
<div class="price">€47.95</div>
</div>
El resultado es el HTML interno del componente, que de otra forma sería inaccesible.
Distinción técnica importante
evaluateOnNewDocument()no es un “bypass” de Shadow DOM. Es una herramienta que nos permite ejecutar código antes de que la página cargue sus scripts. La técnica real es la intercepción deattachShadow.
Evidencia real: lo que observamos en el laboratorio
Ejecutando el laboratorio, estos son los resultados concretos. La siguiente tabla resume el comportamiento en cada escenario:
| Escenario | Entrada (código) | Salida obtenida |
|---|---|---|
| Consola del navegador (sin instrumentación) | document.querySelector('product-card').shadowRoot |
null |
| Consola del navegador (sin instrumentación) | document.querySelector('product-card').innerHTML |
"" (cadena vacía) |
| Puppeteer (con instrumentación) | element.__capturedShadowRoot |
ShadowRoot { ... } (objeto válido) |
| Puppeteer (contenido extraído) | root.innerHTML |
"<div class="card"><div class="name">Test Product</div><div class="price">€47.95</div></div>" |
Aquí tienes la salida real de la consola del laboratorio, tal como se obtiene al ejecutar los comandos:
# En la consola del navegador (sin instrumentación)
> document.querySelector('product-card').shadowRoot
null
> document.querySelector('product-card').innerHTML
""
# Con instrumentación de Puppeteer
> const root = await page.evaluate(() =>
document.querySelector('product-card').__capturedShadowRoot
);
> root.innerHTML
"<div class="card">
<div class="name">Test Product</div>
<div class="price">€47.95</div>
</div>"
Esta evidencia demuestra que la técnica funciona y que el contenido del componente es accesible cuando se aplica la instrumentación adecuada.
El flujo de ejecución paso a paso
Para que quede claro, te explico el orden exacto de los acontecimientos:
- Puppeteer crea la página – Se abre una nueva instancia del navegador. En este punto, la página está vacía y ningún script se ha ejecutado.
- Se instala la instrumentación – Usamos
evaluateOnNewDocument()para inyectar el código que interceptaráattachShadowantes de que se ejecute cualquier script de la página. Esto garantiza que nuestra modificación esté activa cuando el componente se cree. - La aplicación llama a
attachShadow()– El componente<product-card>se instancia y su constructor llama aattachShadow({ mode: 'closed' }). Nuestra función envolvente captura esta llamada. - El ShadowRoot se captura – Nuestra función cambia el modo a
openy guarda la referencia en el elemento host (this.__capturedShadowRoot = root). El componente recibe el ShadowRoot y funciona con normalidad. - Puppeteer recupera la referencia – Una vez que la página está cargada, nuestro script de Puppeteer puede acceder a
__capturedShadowRooty obtener el contenido del componente.
Este flujo es la base del experimento y demuestra que el timing lo es todo. Si intentas hacer lo mismo después de que el componente se haya creado, no funcionará.
Casos de uso reales
Esta técnica tiene aplicaciones prácticas en varios escenarios:
- Testing end-to-end: Permite verificar el contenido y el estado de componentes encapsulados en aplicaciones como React, Angular o Vue que utilizan Shadow DOM.
- Scraping de datos: Extrae información de componentes que de otra forma serían inaccesibles, como precios, descripciones o metadatos.
- Monitoreo de aplicaciones: Obtén datos de rendimiento o depuración de componentes internos.
- Automatización de tareas: Rellena formularios o interactúa con elementos dentro de Shadow DOM cerrado.
- Análisis de seguridad: Detecta comportamientos sospechosos o verifica la integridad de componentes.
Preguntas frecuentes (FAQ) basadas en el laboratorio
¿Por qué el laboratorio muestra el componente pero la consola devuelve null?
Porque el componente está creado con mode: "closed". El navegador oculta intencionadamente el ShadowRoot. La consola no puede acceder a él porque no se expone al exterior.
¿Por qué la instrumentación con evaluateOnNewDocument funciona en el laboratorio?
Porque se ejecuta antes de que el componente llame a attachShadow. Así podemos sobrescribir el método y forzar el modo open en el momento de la creación.
¿Puedo probar el laboratorio sin tener Puppeteer instalado?
Sí, el laboratorio es una página HTML estática. Puedes abrir index.html en el navegador para ver el componente y la consola simulada. Pero para ejecutar la instrumentación necesitas Puppeteer.
¿Qué ocurre si el componente se crea después de un evento (por ejemplo, un clic)?
La instrumentación sigue activa, por lo que cualquier llamada a attachShadow en cualquier momento será interceptada. Siempre que la inyección se haya hecho al inicio, capturará todas las creaciones.
¿El laboratorio incluye algún ejemplo de Puppeteer funcional?
Sí, el repositorio incluye un archivo test.js con el código de instrumentación y la extracción del contenido. Puedes ejecutarlo con node test.js y verás la salida en la consola.
¿Puedo modificar el laboratorio para probar con mis propios componentes?
Claro. Puedes editar el archivo index.html y app.js para definir tu propio Web Component. La técnica de instrumentación seguirá funcionando siempre que la inyectes antes de la carga.
Conclusión
Los Web Components con Shadow DOM cerrado son una barrera para la automatización, pero no son infranqueables. Con la técnica adecuada y entendiendo el flujo de ejecución, podemos interceptar la creación del ShadowRoot y obtener acceso a su contenido.
El laboratorio que he compartido es una demostración práctica de este concepto. Te animo a que lo descargues, lo ejecutes y experimentes por ti mismo. Verás cómo la teoría se convierte en evidencia real.
Esta técnica te permitirá:
- Superar las limitaciones del Shadow DOM cerrado en tus pruebas automatizadas.
- Extraer datos de componentes que antes eran inaccesibles.
- Mejorar la cobertura de tus tests end-to-end.
- Comprender mejor cómo funcionan los Web Components por dentro.
Recursos adicionales:
- MDN: Web Components
- Puppeteer Documentation
- MDN: Element.attachShadow()
- Puppeteer: Page.evaluateOnNewDocument()
- Enlace al repositorio del laboratorio y descarga del mismo: En proceso
Si te ha gustado este artículo, compártelo con otros desarrolladores y déjame un comentario con tus experiencias. ¿Has tenido problemas similares con Shadow DOM? ¿Cómo los resolviste? Tu opinión es valiosa.
¡Hasta la próxima!
Visitas: 30
Automation & Data Specialist | Web Development | Data Processing & Integration
