Estructura de Paquetes y Red de Dependencias de Playwright
Como marco de automatización web contemporáneo, Playwright adopta un diseño de paquetes altamente modular. Mediante una red de dependencias explícita, logra el desacoplamiento funcional y la extensibilidad. Analizaremos la organización de sus paquetes fundamentales y cómo interactúan entre sí.
Visión General de la Arquitectura de Paquetes
Empleando un esquema monorepo, Playwright centraliza sus módulos en el directorio packages/, gestionándolos mediante workspaces. La arquitectura se segmenta en las siguientes capas:
Capa Base (Núcleo)
playwright-core: Motor fundamental, expone las API de base y la capacidad de manipulación del navegador.protocol: Definición y gestión del protocolo de comunicación con el navegador.injected: Administración de scripts inyectados en el contexto de la página.
Capa de Adaptación de Navegadores
playwright-browser-chromium: Adaptador para Chromium.playwright-browser-firefox: Adaptador para Firefox.playwright-browser-webkit: Adaptador para WebKit.
Capa de Enlace de Lenguaje
playwright: Paquete principal para JavaScript/TypeScript.playwright-chromium,playwright-firefox,playwright-webkit: Paquetes específicos por motor.
Capa de Framework de Testing
playwright-test: Ejecutor de pruebas y librería de aserciones.html-reporter: Generador de reportes HTML.trace-viewer: Visor de trazas de ejecución.
Capa de Testing de Componentes
playwright-ct-core: Núcleo para pruebas de componentes.playwright-ct-react,playwright-ct-vue,playwright-ct-svelte: Soportes específicos para frameworks UI.
Análisis Detallado de Dependencias
1. Núcleo de playwright-core
Como cimiento del framework, sus dependencias son minimalistas pero potentes:
{
"requires": {
"node_types": "^20.x",
"logger_util": "^5.1.0",
"event_emitter": "^3.2.0",
"web_sockets": "^8.5.0",
"yaml_parser": "^2.1.0"
}
}
2. Dependencias de Adaptadores de Navegadores
Cada adaptador depende de su respectivo binario:
| Adaptador | Dependencia Binaria | Función Principal |
|---|---|---|
| playwright-chromium | playwright-browser-chromium | Control de Chromium |
| playwright-firefox | playwright-browser-firefox | Control de Firefox |
| playwright-webkit | playwright-browser-webkit | Control de WebKit |
3. Matriz de Dependencias del Framework de Testing
El paquete playwright-test posee una matriz de dependencias más intrincada, requiriendo módulos de reportes, ejecución y aserciones.
Mecanismos de Comunicación Entre Paquetes
1. Exportación de API
// El núcleo exporta las interfaces principales
export interface Navegador {
crearContexto(opciones?: ConfigContexto): Promise<ContextoNavegador>;
terminar(): Promise<void>;
}
// Consumo desde otros módulos
import { Navegador } from 'motor-playwright';
2. Extensión vía Plugins
interface ExtensionPruebaComponentes {
registrarComponente(id: string, mod: any): void;
montar(mod: any, opts: OpcionesMontaje): Promise<ManejadorElemento>;
}
3. Mensajería de Protocolo
interface MensajeProtocolo {
seq: number;
accion: string;
argumentos: any;
respuesta?: any;
}
Gestión de Versiones y Empaquetado
Playwright aplica versionado unificado; todos los paquetes sincronizan su número de versión para garantizar compatibilidad estricta. El sistema de construcción utiliza workspaces para enlazar los paquetes localmente:
{
"espacios_trabajo": [
"modulos/*"
],
"comandos": {
"compilar": "node herramientas/compilar/compilar.js",
"ejecutar_pruebas": "pw test --config=pruebas/config/pw.config.ts"
}
}
Este diseño permite el desarrollo aislado, dependencias transparentes, extensiones modulares y consistencia de versiones.
Mecanismo de Aislamiento del Contexto de Navegación y sus Fundamentos
El Contexto de Navegación (Browser Context) constituye la unidad primordial de aislamiento en Playwright, garantizando la segregación total a nivel de sesión y facilitando escenarios multiusuario complejos.
Conceptos y Arquitectura del Contexto
Un Contexto encapsula un entorno de sesión independiente, provisto de sus propios:
- Cookies y Almacenamiento Local: Datos de sesión segregados.
- Caché y Permisos: Configuraciones de caché y privilegios independientes.
- Configuración de Red: Proxies y autenticación HTTP aislada.
- Entorno de Ejecución JS: DOM y contexto de ejecución exclusivos.
Implementación del Aislamiento
1. Aislamiento a Nivel de Proceso
Playwright instancia procesos de navegador distintos para cada Contexto, asegurando la separación de recursos del sistema operativo.
const nav = await motor.chromium.iniciar();
const ctxA = await nav.crearContexto();
const ctxB = await nav.crearContexto();
const pestanaA = await ctxA.nuevaPestana();
const pestanaB = await ctxB.nuevaPestana();
2. Segregación de Almacenamiento
Cada Contexto maneja sus particiones de almacenamiento internamente:
export abstract class ContextoNavegador extends ObjetoSdk {
private _dominios = new Set<string>();
readonly _privilegios = new Map<string, string[]>();
readonly _archivosDesc = new Set<Descarga>();
protected abstract obtenerCookies(dom: string[]): Promise<CookieRed[]>;
protected abstract limpiarCookies(): Promise<void>;
}
3. Aislamiento de Capa de Red
Cada Contexto opera con su propia pila de red, incluyendo caché HTTP exclusiva, interceptores de petición, almacenamiento de cookies y configuración de proxy.
Características Clave de Aislamiento
1. Gestión de Cookies y Almacenamiento
El aislamiento se materializa a través de la clase AlmacenCookies:
export class AlmacenCookies {
private _galletas = new Map<string, CookieRed[]>();
async recuperar(dom: string[]): Promise<CookieRed[]> {
// Devuelve cookies exclusivas de este contexto
}
async guardar(galletas: CookieNueva[]): Promise<void> {
// Persiste solo en el ámbito actual
}
}
2. Administración de Permisos
const ctx = await nav.crearContexto({
privilegios: ['ubicacion', 'alertas'],
ubicacion: { longitud: 12.49, latitud: 41.88 },
agenteUsuario: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
});
3. Aislamiento de Solicitudes de Red
ctxA.interceptar('**/api/*', ruta => {
ruta.procesar(); // Solo afecta a ctxA
});
ctxB.interceptar('**/api/*', ruta => {
ruta.cancelar(); // ctxB mantiene su propio interceptor
});
Funcionalidades Avanzadas
1. Reutilización de Contextos
Para mitigar la sobrecarga de creación, es posible inyectar estados previos:
const ctx = await nav.crearContexto({ estadoAlmacen: 'sesion.json' });
await ctx.restablecerParaReuso();
2. Restricciones de Comunicación Inter-Contexto
Los contextos son mutuamente excluyentes por defecto. El intento de lectura cruzada fallará:
await pestanaA.ejecutar(() => {
localStorage.setItem('info', JSON.stringify({ val: 1 }));
});
await pestanaB.ejecutar(() => {
const d = localStorage.getItem('info'); // Resultado: null
});
Optimización y Casos de Uso
1. Agrupación de Contextos
class PilaContextos {
private disponibles: ContextoNavegador[] = [];
private ocupados = new Set<ContextoNavegador>();
async obtener(): Promise<ContextoNavegador> { /*...*/ }
liberar(ctx: ContextoNavegador) { /*...*/ }
}
2. Limpieza de Recursos
despuesDeCada(async () => {
await ctx.cerrar();
});
prueba.usar({ contexto: async ({}, uso) => {
const ctx = await nav.crearContexto();
await uso(ctx);
await ctx.cerrar();
}});
3. Simulación Multiusuario
const contextosUsuarios = await Promise.all(
usuarios.map(usr => nav.crearContexto({ estadoAlmacen: usr.sesion }))
);
await Promise.all(contextosUsuarios.map(async (ctx, i) => {
const p = await ctx.nuevaPestana();
await p.ir(urlApp);
await ejecutarAcciones(p, usuarios[i]);
}));
4. Pruebas A/B
const ctxVariante = await nav.crearContexto({
cabecerasHTTPExtra: { 'X-Prueba': 'variante-b' }
});
const ctxControl = await nav.crearContexto({
cabecerasHTTPExtra: { 'X-Prueba': 'control' }
});
const resVar = await probarVariante(ctxVariante);
const resCtrl = await probarVariante(ctxControl);
Arquitectura Multiproceso y Mecanismos de Comunicación del Navegador
Playwright implementa una arquitectura multiproceso que se alinea de forma nativa con el modelo de procesos de los navegadores modernos, proporcionando aislamiento robusto y gestión eficiente de recursos.
Visión General de la Arquitectura de Procesos
El diseño se fundamenta en un modelo primario-secundario, donde el proceso de Node.js actúa como orquestador y los procesos del navegador como ejecutores aislados.
Mecanismos de Transporte y Comunicación
1. Transporte por Tuberías (PipeTransport)
Canal de comunicación binaria eficiente basado en los flujos estándar de entrada/salida de Node.js:
export class TransportePipe implements TransporteConexion {
private _lectura: NodeJS.ReadableStream;
private _escritura: NodeJS.WritableStream;
private _buffPendientes: Buffer[] = [];
constructor(escritura: NodeJS.WritableStream, lectura: NodeJS.ReadableStream) {
this._lectura = lectura;
this._escritura = escritura;
lectura.on('data', buf => this._procesar(buf));
}
enviar(msg: PeticionProtocolo) {
this._escritura.write(JSON.stringify(msg));
this._escritura.write('\0'); // Delimitador de fin de mensaje
}
_procesar(buf: Buffer) {
let fin = buf.indexOf('\0');
if (fin === -1) {
this._buffPendientes.push(buf);
return;
}
// Lógica de ensamblaje y despacho del mensaje
}
}
2. Transporte por WebSocket (WebSocketTransport)
Utilizado para conexiones remotas o comunicación mediante el protocolo CDP:
export class TransporteWebSocket implements TransporteConexion {
private _socket: WebSocket;
static async conectar(prog: Progreso, url: string, opts: OpcionesWS = {}) {
const trans = new TransporteWebSocket(prog, url, opts);
// Lógica de establecimiento de conexión y manejo de errores
}
enviar(msg: PeticionProtocolo) {
this._socket.send(JSON.stringify(msg));
}
}
Flujo de Inicio y Conexión del Navegador
El lanzamiento sigue una secuencia estricta para garantizar la disponibilidad del proceso antes de establecer el canal de control:
async _iniciarProceso(prog: Progreso, opts: OpcionesInicio) {
const { procesoIniciado } = await iniciarProceso({
cmd: ejecutablePrep,
argumentos: argsPrep,
stdio: 'pipe', // Habilita tuberías para IPC
});
const { puntoFinalWS } = await this.esperarListo(opts);
let trans: TransporteConexion;
if (opts.puertoCdp !== undefined || !this.soportaPipe()) {
trans = await TransporteWebSocket.conectar(prog, puntoFinalWS!);
} else {
const pipes = procesoIniciado.stdio;
trans = new TransportePipe(pipes[3], pipes[4]); // Tuberías dedicadas 3 y 4
}
return { procesoNavegador: procesoIniciado, transporte: trans };
}