Diseño Arquitectónico y Modular de Playwright: Una Visión Exhaustiva

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 };
}

Etiquetas: Playwright automatización-web arquitectura-software testing-e2e nodejs

Publicado el 10-19 12:15