Cargar extensiones en Puppeteer y Playwright
Una extensión sin empaquetar necesita contexto persistente y dos flags emparejados, y aun así puede no cargar. Una ejecución medida muestra por qué hay que verificar.
BotBrowser Team
Quieres la documentación estructurada de Plataforma?
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.
Cargar una extensión sin empaquetar desde un lanzamiento de automatización parece un problema de dos flags y se comporta como un problema de condiciones de lanzamiento. BotBrowser proporciona un perfil declarado que mantiene coherentes entre sí las superficies que una página puede leer en cada lanzamiento, incluidas las opciones de nivel de lanzamiento, y no puede hacer que un navegador acepte una extensión que su build rechaza cargar, ni garantiza que un service worker siga activo. Las condiciones que necesita una extensión cargada se pueden enumerar, y una ejecución medida mostró todas ellas en apariencia cumplidas sin ninguna extensión presente.
Resumen
- Una extensión sin empaquetar necesita un contexto persistente, porque un contexto no persistente no puede retener el estado de la extensión entre páginas.
- Los dos flags están pensados para pasarse juntos: uno nombra lo que se permite, el otro excluye todo lo demás.
- Pasar solo el flag de carga deja en su sitio el conjunto de extensiones por defecto, lo que cambia lo que ve la página.
- Un service worker MV3 no es un objeto permanente, así que un worker ausente no prueba que la extensión no se cargara.
- En una ejecución medida se pasaron ambos flags con
headless=new, el navegador salió limpiamente y el perfil listó cero extensiones. - Lo mismo ocurrió en el modo headless antiguo y en modo con interfaz en el mismo host, así que el fallo era del entorno y no del modo.
- La respuesta útil es afirmar que la extensión está presente al arrancar y hacer fallar la ejecución cuando no lo está.
- Un test que se ejecuta en silencio sin su extensión pasa por la razón equivocada, lo que es peor que un test que falla.
Contenido
- Las condiciones de lanzamiento y cómo se ve un fallo medido
- El service worker no es permanente, y la lista de comprobación
- La lista de comprobación como fixture
- Cuando headless no es la variable
- Conclusión práctica
Las condiciones de lanzamiento y cómo se ve un fallo medido
Tres condiciones deciden si una extensión sin empaquetar tiene alguna posibilidad, y son independientes. La primera es el tipo de contexto. Una extensión vive en el perfil del navegador, así que un contexto que descarta su perfil al terminar no tiene dónde guardarla. Un contexto persistente con un --user-data-dir explícito es la única forma que se sostiene, y cada herramienta lo expresa a su manera: Playwright recibe el directorio como primer argumento de chromium.launchPersistentContext, mientras que Puppeteer llega al mismo perfil por opciones de lanzamiento y argumentos de línea de comandos. Los argumentos de lanzamiento se tratan en errores de lanzamiento y perfil en Puppeteer, incluido el viewport que prevalece sobre la pantalla del perfil.
persistent context -> profile on disk; extension state can persist
non-persistent -> fresh profile per launch; nothing to hold the extension
La segunda es el par de flags. Un flag nombra el directorio de la extensión que se va a cargar y el otro nombra el único conjunto de extensiones que puede ejecutarse. Pasar solo el flag de carga añade la extensión a lo que ya tenga el perfil, que rara vez es lo que busca un test; pasar ambos hace que el conjunto de extensiones del navegador lanzado sea igual al previsto.
--disable-extensions-except=/path/to/ext
--load-extension=/path/to/ext
La tercera condición es que el build del navegador acepte el directorio. Eso no es una formalidad: un build con la carga de extensiones restringida, un manifest que el build considera inválido y una ruta que el navegador no puede leer producen el mismo resultado observable, que es ninguna extensión.
Las tres condiciones anteriores son fáciles de enunciar y fáciles de creer cumplidas. Se registró una ejecución para comprobar esa creencia, con una extensión MV3 mínima con un service worker de fondo y los dos flags pasados juntos.
persistent_headless_new: headless=true persistent=true extLoaded=false sw_count=0 id=(none)
persistent_headed: headless=false persistent=true extLoaded=false sw_count=0 id=(none)
Después se leyó el perfil cuando el navegador ya había salido, que es una comprobación que no depende del tiempo dentro de un navegador en marcha:
--headless=new: exit=0 prefs_extension_ids=0
--headless: exit=0 prefs_extension_ids=0
(headed): exit=0 prefs_extension_ids=0
Tres modos, tres salidas limpias, cero extensiones en el perfil. Ese resultado merece una lectura cuidadosa, porque dice algo más estrecho que “las extensiones no funcionan”. Dice que en este host, con este build y este directorio, pasar ambos flags documentados no instaló la extensión, y el navegador no informó de ningún error al respecto.
La consecuencia práctica es que un test que supone que los flags funcionaron se ejecutará sin su extensión y aun así pasará. extLoaded=false es el campo que registra la carga, y la lectura del perfil tras la salida informa prefs_extension_ids=0; el conteo de workers es sw_count=0, pero ese valor por sí solo no separa una carga fallida de un worker que nunca arrancó. Toda aserción dentro de la extensión se omite, la página bajo prueba ve el entorno por defecto y la ejecución informa éxito. Es un desenlace peor que un fallo visible, porque nada en la salida invita a una segunda mirada.
La corrección es pequeña y mecánica, pero las dos herramientas exponen pruebas distintas. Puppeteer enumera lo instalado, así que la aserción va contra esa lista. Playwright no tiene un método para el conjunto instalado, de modo que el mismo hecho se lee del perfil después de que el navegador sale, donde extensions.settings está indexado por identificador de extensión.
puppeteer: installed = await browser.extensions() // Map: id -> {name, version}
playwright: JSON.parse(readFileSync(join(userDataDir, 'Default', 'Secure Preferences')))
.extensions.settings // keys are extension ids
assert installed.size > 0, "no extension installed; check flags, manifest and build"
Una comprobación de worker responde otra pregunta y no puede sustituir a esa aserción. Esperar context.serviceWorkers() en Playwright o browser.waitForTarget con un predicado service_worker en Puppeteer confirma que el contexto de fondo arrancó; una espera acotada con un presupuesto como 30_000 ms es correcta siempre que su mensaje de fallo indique que el worker no arrancó, y no que la extensión no se cargó.
El service worker no es permanente, y la lista de comprobación
Hay una segunda razón por la que una comprobación basada en la lista de workers puede inducir a error, y es una propiedad de la plataforma de extensiones y no de la herramienta de automatización. Un service worker de fondo MV3 se inicia cuando algo lo necesita y se detiene cuando está inactivo. Un worker ausente puede ser uno que todavía no se ha iniciado o uno que ya fue suspendido.
absent worker + extension installed -> not started, or suspended
absent worker + extension not present -> load failed
Los dos se ven idénticos en un solo sondeo y significan cosas distintas, y por eso la comprobación de carga corresponde al conjunto instalado y la de worker sigue siendo una aserción de vitalidad aparte con su propio mensaje. Leer el perfil después de la salida, o llamar a browser.extensions() en Puppeteer, distingue los dos casos; sondear la lista de workers durante un número fijo de segundos no lo hace.
El ciclo de vida también cambia cómo debe esperar un test sobre un worker. Un reposo fijo suficientemente largo para una máquina rápida será demasiado corto en una lenta, y un test que espera a que aparezca el worker y se rinde tras un intervalo fijo está midiendo la máquina y no la extensión. Esperar la condición con un presupuesto acotado como 30_000 ms, e informar el presupuesto cuando se agota, produce un fallo que nombra su causa.
Una cuarta condición merece su propio párrafo porque es la que cambia con la herramienta y no con el navegador. Puppeteer y Playwright exponen ambos una lista de argumentos en el momento del lanzamiento y aceptan los mismos dos flags, pero el contexto que construyen por defecto difiere. Puppeteer lanza un navegador y crea una página en su contexto por defecto; Playwright lanza un navegador y deja que quien llama cree los contextos. La extensión vive a nivel del navegador en ambos casos, lo que significa que es visible desde un contexto persistente e invisible desde un contexto creado por la vía no persistente.
puppeteer.launch({ args }) -> browser-level flags; page may still be transient
chromium.launchPersistentContext(dir) -> profile on disk; extension state survives
browser.newContext() -> fresh profile; extension state absent
Esa diferencia es la razón por la que dos equipos pueden informar resultados opuestos con los mismos flags documentados. Uno los pasa a un lanzamiento cuyo perfil persiste, y el otro los pasa a un lanzamiento y después crea un contexto que descarta el perfil. Ninguno se equivoca sobre lo que observó, y el flag por sí solo no explica la diferencia.
Otra fuente de confusión es la versión del manifest. Una extensión MV2 y una MV3 fallan de forma distinta: MV2 tiene una página de fondo persistente que hace la extensión evidente en cuanto se carga, mientras que MV3 inicia un service worker solo cuando algo lo pide. Quien pasó de una extensión antigua a una nueva puede entonces ver un cambio de comportamiento que es una diferencia de manifest y no un fallo de carga, y la comprobación que los distingue tiene que mirar el conjunto instalado y no el worker en ejecución.
MV2 -> persistent background page; visible immediately
MV3 -> service worker; starts on demand, suspends when idle
Ambas diferencias apuntan a la misma disciplina, que es verificar lo que no puede ser ambiguo. El conjunto de extensiones instaladas es esa cosa, porque contiene la extensión o no la contiene, y no depende de que un worker esté ejecutándose en el momento del sondeo.
La lista de comprobación como fixture
Una lista de comprobación trabajada cierra el tema, porque las condiciones se aplican mejor como lista ordenada que como prosa. Cada paso tiene un fallo que evita, y el orden importa: el tipo de contexto decide si el resto puede funcionar, así que va primero.
1. launch a persistent context with an explicit user-data directory
2. pass --disable-extensions-except and --load-extension with the same path
3. validate the manifest before launch
4. read the installed extension set after launch
5. assert the set contains the extension, and fail with the four recorded fields
El primer paso es el que más se omite, porque una llamada chromium.launch() parece que debería funcionar y el fallo es silencioso. Quien añade los flags a un lanzamiento que crea un contexto nuevo está haciendo lo correcto en el nivel equivocado, y el resultado observable es el mismo que pasar ningún flag.
El tercer paso se omite porque un manifest.json mal formado suele producir un mensaje que nombra el navegador y no el archivo. Validar el JSON antes del lanzamiento convierte eso en un error de análisis con un número de línea, que es un arreglo de cinco segundos en lugar de una búsqueda de quince minutos entre opciones de lanzamiento.
Los pasos cuatro y cinco son los que convierten un pase silencioso en un fallo ruidoso. Sin ellos, un test que depende de la extensión ejecutará sus aserciones contra una página que la extensión nunca tocó, y el informe dirá que las aserciones pasaron. Es el desenlace más caro en este tema, porque el trabajo de escribir el test ya está hecho y el resultado no significa nada.
La lista es lo bastante corta para ponerla en un fixture y no en un documento, que es su lugar correcto. Un fixture que ejecuta los cinco pasos una vez y expone el contexto resultante hace que cada test de una suite herede la aserción en lugar de repetirla, y un entorno roto falla en el fixture y no dentro de un test sin relación.
Cuando headless no es la variable
Una medición más merece registrarse porque elimina una hipótesis que quien lee formará de otro modo. El fallo anterior podría atribuirse razonablemente al modo headless, ya que los navegadores headless son los que más a menudo se describen como carentes de soporte de extensiones. Por eso se probaron el mismo directorio y los mismos flags en modo con interfaz en el mismo host.
persistent_headless_new: extLoaded=false
persistent_headed: extLoaded=false
El modo con interfaz falló igual, lo que descarta --headless como causa en este host y apunta al build o al directorio. Es un resultado negativo útil: ahorra a quien lee una tarde cambiando entre modos headless cuando ninguno de los dos es la variable. Un resultado negativo que elimina una de las tres condiciones vale tanto como uno positivo que confirma otra.
La lectura también recuerda cómo deben anotarse estos resultados. “Las extensiones no funcionan en headless” es una afirmación que la medición no sostiene, y es una afirmación sobre la que alguien actuaría. “En este host, con este build y este directorio, los dos modos headless y el modo con interfaz informaron ninguna extensión” es la afirmación que la medición sostiene, y apunta a un paso siguiente distinto.
La misma disciplina se aplica al caso positivo. Una ejecución en la que la extensión carga prueba que las tres condiciones se cumplieron para ese build en ese host, y no prueba que los flags sean correctos en general. Registrar los cuatro campos es lo que permite a un lector posterior saber cuál de las dos afirmaciones estableció realmente una ejecución.
loaded + recorded build -> this build accepted this directory
loaded + no build -> something worked, cause unknown
Una nota final sobre la relación entre una extensión y los valores que informa una página, porque es la razón por la que este tema está junto a la coherencia de perfiles y no en una guía de herramientas aparte. Una extensión puede leer y modificar el documento en el que se ejecuta, y lo hace dentro del mismo navegador que describe el perfil. Eso significa que una extensión que altera una superficie de la página pasa a formar parte de lo que la página informa, y un perfil que no la tiene en cuenta describe un navegador que no es el que está bajo prueba. El mismo razonamiento se aplica a cualquier sustitución que una página pueda observar, que las señales de automatización y su coherencia trata como una cuestión de si las superficies concuerdan y no como una lista de añadidos.
profile without extension -> describes the browser alone
profile with extension -> describes the browser plus what the extension changed
La consecuencia práctica es que la extensión pertenece al mismo registro que el build y el perfil, por la misma razón que ambos. Un valor que cambia al añadir una extensión no es un defecto de ninguno de los dos; es el resultado esperado de meter otro actor dentro del documento, y el registro tiene que decirlo o un lector posterior atribuirá el cambio a la causa equivocada.
Esa es también la razón por la que un entorno de prueba que pierde su extensión en silencio produce un resultado que parece limpio y no lo es. La página informa los valores de un navegador sin la extensión, las aserciones que dependen de la extensión nunca se ejecutan, y el informe es internamente coherente sobre una configuración que nunca se usó. La aserción de la lista anterior existe para evitar exactamente ese desenlace, y es la línea de código que hace accionable el resto del tema.
Conclusión práctica
Construye el lanzamiento en el orden que la plataforma espera: contexto persistente con un directorio de perfil explícito, ambos flags juntos y una aserción inmediatamente después del lanzamiento de que la extensión está realmente presente. Trata la lista de workers como evidencia solo cuando no está vacía, porque un worker ausente es ambiguo y una instalación ausente no lo es. Valida el manifest antes del lanzamiento y no después, para que un manifest.json inválido falle con un mensaje sobre el archivo en lugar de un tiempo de espera agotado sobre un worker.
Cuando la aserción falla, registra lo que se pasó y lo que informó el navegador, porque las tres condiciones anteriores son todo el espacio de búsqueda. Una ejecución que solo registra “la extensión no se cargó” deja al siguiente lector adivinando cuál de las tres falló, y la conjetura suele ser los flags cuando la causa es el build.
Para lo que una extensión puede ver una vez que se ejecuta, lee aislamiento de scripts de contenido en extensiones de navegador; para saber qué build está instalado y cómo fijarlo, lee fijación de versiones de Chrome for Testing.
Una nota sobre dónde vive la extensión corresponde a las condiciones, porque quien traslada un test entre máquinas a menudo traslada la extensión sin notarlo. Una extensión sin empaquetar se carga desde una ruta del sistema de archivos, y esa ruta forma parte del lanzamiento y no del test. Un contenedor que monta la extensión en otra ubicación necesita por tanto otro valor de flag, y una configuración de lanzamiento en caché que fija una ruta falla en silencio en la otra máquina.
host A: --load-extension=/work/ext
host B: --load-extension=/opt/ci/ext (same extension, different path)
El identificador de la extensión se deriva de la ruta en una extensión sin empaquetar, así que un directorio movido produce un identificador distinto además de otro valor de flag. Cualquier cosa que dependa del identificador, como una clave de almacenamiento o una entrada de permisos, deja de coincidir en ese mismo momento, y el fallo parece un problema de permisos en lugar de un problema de ruta.
Por eso los cuatro campos registrados incluyen la cadena de flags y no un booleano como loadExtension=true. Un booleano sobrevive intacto a un traslado de máquina y oculta la diferencia, mientras que la cadena muestra que la ruta cambió y explica de un vistazo tanto la extensión ausente como el identificador cambiado.
El mismo razonamiento se aplica a una extensión empaquetada instalada desde una tienda. El identificador es estable y la ruta no es visible, así que una extensión empaquetada puede moverse entre máquinas sin cambiar nada de lo que registra un lector; el modo de fallo es entonces una discrepancia de versión y no de ruta, y el campo que hay que registrar es la versión de la extensión en lugar del directorio.
Fuentes
Este artículo forma parte de la guía: Superficies de la plataforma web: mapa de revisión
Artículos Relacionados
Chrome headless nuevo y antiguo: shell, modo y diferencias
PlataformaAislamiento de content scripts en extensiones del navegador
PlataformaWidevine en navegadores automatizados: por qué falla EME
PlataformaEjecutar perfiles de navegador Windows en macOS y Linux
ComparaciónNúcleo del navegador vs scripts JavaScript en huellas digitales
Primeros pasosScraping con Puppeteer: opciones y valores observables
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.