Históricamente, la automatización de navegadores web se articuló en torno al estándar W3C WebDriver (Selenium). Dicha arquitectura exigía un binario de driver HTTP independiente (ChromeDriver, geckodriver) para cada motor de navegador. Cada clic, pulsación de tecla o lectura del DOM se traducía en una secuencia de peticiones HTTP sincrónicas serializadas mediante el JSON Wire Protocol. Este enfoque adolecía de tres desventajas fundamentales:
StaleElementReferenceException.+-----------------------------------------------------------------------+
| Arquitectura Selenium WebDriver |
| |
| +--------------+ HTTP / REST +--------------+ Custom Protocol |
| | Node.js Test | ---------------> | ChromeDriver | ----------------> | Browser |
| +--------------+ (JSON Wire) +--------------+ | |
+-----------------------------------------------------------------------+
+-----------------------------------------------------------------------+
| Arquitectura Playwright 2026 |
| |
| +--------------+ WebSocket Bidireccional |
| | Node.js / TS | <=================================================> | Browser |
| | Test Runner | (CDP / WebDriver BiDi / LibWebSockets) | Process |
| +--------------+ +---------+
+-----------------------------------------------------------------------+Playwright erradica estas limitaciones mediante una conexión multiplexada y bidireccional persistente sobre WebSocket directamente con los procesos del motor del navegador (Chromium, Firefox, WebKit). En el caso de Chromium, opera mediante el Chrome DevTools Protocol (CDP) y el estándar interoperable WebDriver BiDi.
Esta integración nativa proporciona:
networkidle) y creación de contextos de ejecución JS;La arquitectura de Playwright descansa sobre una jerarquía rigurosa de cuatro entidades: Browser, BrowserContext, Page y Locator.
+-----------------------------------------------------------------------------+
| Browser Instance |
| (Proceso de SO único: chromium.exe / firefox.exe / webkit.exe) |
| |
| +-----------------------------------+ +---------------------------------+ |
| | BrowserContext 1 | | BrowserContext 2 | |
| | - Cookies aisladas | | - localStorage independiente | |
| | - IndexedDB aislado | | - Cache Storage independiente | |
| | - Permisos propios (Geo, Notif) | | - Perfil de red independiente | |
| | | | | |
| | +-----------+ +-----------+ | | +-----------+ | |
| | | Page 1 | | Page 2 | | | | Page 3 | | |
| | | (Tab / | | (Popup) | | | | (Tab) | | |
| | | Frame) | | | | | | | | |
| | +-----------+ +-----------+ | | +-----------+ | |
| +-----------------------------------+ +---------------------------------+ |
+-----------------------------------------------------------------------------+La causa primordial de inestabilidad (flakiness) en pruebas web radica en interactuar con elementos que atraviesan estados transitorios (animaciones CSS, espera de respuestas API o superposición de modales).
En Playwright, toda acción sobre un elemento (como locator.click() o locator.fill()) desencadena un pipeline determinista de Actionability Checks:
+-------------------------------------------+
| Llamada a locator.click() |
+-------------------------------------------+
│
▼
+-------------------------------------------+
| 1. Attached? (Nodo presente en el DOM) |
+-------------------------------------------+
│ Sí
▼
+-------------------------------------------+
| 2. Visible? (No hidden, caja > 0x0) |
+-------------------------------------------+
│ Sí
▼
+-------------------------------------------+
| 3. Stable? (Animaciones concluidas, |
| coordenadas constantes en dos frames) |
+-------------------------------------------+
│ Sí
▼
+-------------------------------------------+
| 4. Enabled? (Sin atributo disabled) |
+-------------------------------------------+
│ Sí
▼
+-------------------------------------------+
| 5. Editable? (Solo para fill / clear) |
+-------------------------------------------+
│ Sí
▼
+-------------------------------------------+
| 6. Receives Events? (Hit-test: punto |
| central no cubierto por modales) |
+-------------------------------------------+
│ Sí
▼
+-------------------------------------------+
| Despacho físico del evento vía CDP/BiDi |
+-------------------------------------------+Si alguna de estas comprobaciones no se satisface, Playwright no falla de inmediato, sino que activa el ciclo de Auto-waiting: reintenta las comprobaciones continuamente hasta que se cumplan todas o venza el timeout configurado (por defecto, 30 segundos).
Playwright fomenta interactuar con la interfaz desde la perspectiva del usuario final y las directrices W3C Accessible Rich Internet Applications (ARIA).
import { test, expect } from '@playwright/test';
test('demuestra localizadores accesibles de alta prioridad', async ({ page }) => {
await page.goto('/checkout');
// 1. Búsqueda por rol accesible (W3C ARIA role) y nombre
const submitButton = page.getByRole('button', { name: /completar pedido/i });
// 2. Búsqueda de campo de formulario por su elemento <label> asociado
const emailInput = page.getByLabel('Correo Electrónico Corporativo');
// 3. Búsqueda por texto visible renderizado
const banner = page.getByText('Descuento del 20% aplicado');
// 4. Búsqueda por atributo placeholder
const promoInput = page.getByPlaceholder('Introduzca código promocional');
// 5. Alternativa para nodos dinámicos no semánticos (data-testid)
const orderSummary = page.getByTestId('order-summary-container');
// Encadenamiento y filtros
const premiumCard = page.getByRole('listitem').filter({
has: page.getByRole('heading', { name: 'Plan Enterprise' }),
});
await emailInput.fill('[email protected]');
await submitButton.click();
});Los métodos de acción admiten un objeto de configuración para calibrar el comportamiento de Auto-waiting:
interface ActionOptions {
/** Tiempo máximo de espera para superar las actionability checks en milisegundos */
timeout?: number;
/** Forzar ejecución omitiendo las actionability checks (no recomendado) */
force?: boolean;
/** Omitir la espera de navegaciones desencadenadas por la acción */
noWaitAfter?: boolean;
/** Modo de ensayo: evalúa las actionability checks sin emitir el evento real */
trial?: boolean;
/** Coordenadas de clic relativas a la esquina superior izquierda del bounding box */
position?: { x: number; y: number };
}Trace Viewer registra evidencia forense detallada: instantáneas del DOM en cada paso, cascada de peticiones de red, registros de consola y filmstrip de vídeo. Registrar trazas en cada prueba exitosa sobrecarga el pipeline; las configuraciones profesionales adoptan la estrategia 'on-first-retry'.
import { defineConfig, devices } from '@playwright/test';
import path from 'path';
export default defineConfig({
testDir: './e2e',
/* Ejecución paralela entre archivos */
fullyParallel: true,
/* Prohibición de test.only en entornos de integración continua */
forbidOnly: !!process.env.CI,
/* Reintentos para pruebas fallidas */
retries: process.env.CI ? 2 : 0,
/* Hilos de trabajo concurrentes */
workers: process.env.CI ? '100%' : undefined,
/* Informes: consola y reporte HTML estático */
reporter: [
['list'],
['html', { outputFolder: 'playwright-report', open: 'never' }],
],
use: {
/* URL base para simplificar page.goto('/') */
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
/* Registro de traza únicamente al reintentar pruebas fallidas */
trace: 'on-first-retry',
/* Captura de pantalla solo ante fallos */
screenshot: 'only-on-failure',
/* Retención de vídeo únicamente si la prueba falla */
video: 'retain-on-failure',
/* Timeout por defecto para acciones (click, fill) */
actionTimeout: 10_000,
/* Timeout por defecto para navegaciones */
navigationTimeout: 15_000,
},
/* Matriz de motores de navegador soportados */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chromium',
use: { ...devices['Pixel 7'] },
},
],
});sleep()#Nunca recurra a pausas fijas como page.waitForTimeout(3000). Los tiempos de espera estáticos dilatan la duración de los pipelines o generan fallos intermitentes cuando los agentes de CI se saturan.
import { test, expect } from '@playwright/test';
test('gestión asíncrona de actualización de tablas mediante Auto-waiting', async ({ page }) => {
await page.goto('/analytics/reports');
const filterDropdown = page.getByRole('combobox', { name: 'Periodo' });
const tableRows = page.getByRole('table').getByRole('row');
const skeletonLoader = page.getByTestId('table-skeleton-loader');
// Seleccionamos el filtro
await filterDropdown.selectOption('last-30-days');
// Paso 1: Confirmamos que el skeleton loader ha desaparecido del DOM
await expect(skeletonLoader).not.toBeAttached({ timeout: 10_000 });
// Paso 2: Las aserciones expect(locator).to* incorporan auto-waiting nativo
// Evalúan periódicamente la condición hasta satisfacerla o agotar el tiempo
await expect(tableRows).toHaveCount(31); // 1 encabezado + 30 filas de datos
// Paso 3: Comprobamos el valor específico en la cuadrícula
const firstDataRow = tableRows.nth(1);
await expect(firstDataRow.getByRole('cell').first()).toHaveText(/2026-08/);
});Validar interacciones colaborativas (como un chat de soporte en tiempo real sobre WebSockets) no requiere inicializar dos instancias pesadas de Browser. Dos sesiones aisladas de BrowserContext pueden coexistir en un mismo proceso de navegador.
import { test, expect } from '@playwright/test';
test('sincronización en tiempo real entre dos usuarios aislados', async ({ browser }) => {
// Contexto 1: Cliente
const clientContext = await browser.newContext({
locale: 'es-ES',
permissions: ['notifications'],
});
const clientPage = await clientContext.newPage();
// Contexto 2: Agente de Soporte
const agentContext = await browser.newContext({
locale: 'en-US',
});
const agentPage = await agentContext.newPage();
try {
// El cliente accede al chat de soporte y envía un mensaje
await clientPage.goto('/support/chat');
await clientPage.getByRole('textbox', { name: 'Su mensaje' }).fill('Fallo en el pago con tarjeta');
await clientPage.getByRole('button', { name: 'Enviar' }).click();
// El agente abre el panel de tickets
await agentPage.goto('/admin/tickets');
// Auto-waiting: el mensaje del cliente debe recibirse vía WebSocket
const ticketItem = agentPage.getByRole('listitem').filter({
hasText: 'Fallo en el pago con tarjeta',
});
await expect(ticketItem).toBeVisible({ timeout: 8_000 });
// El agente toma el ticket y responde
await ticketItem.getByRole('button', { name: 'Responder' }).click();
await agentPage.getByRole('textbox', { name: 'Respuesta al cliente' }).fill('¡Hola! Estamos revisando la pasarela.');
await agentPage.getByRole('button', { name: 'Enviar respuesta' }).click();
// El cliente visualiza la respuesta en su propio contexto
const incomingMessage = clientPage.getByRole('article').filter({
hasText: '¡Hola! Estamos revisando la pasarela.',
});
await expect(incomingMessage).toBeVisible();
} finally {
// Liberación garantizada de los recursos de ambos contextos
await clientContext.close();
await agentContext.close();
}
});Las pausas fijas (sleep) congelan la ejecución durante milisegundos ciegos. En máquinas rápidas desperdician tiempo valioso; en nodos de CI congestionados el tiempo suele ser insuficiente, causando falsos positivos.
// ❌ INCORRECTO: Pausa estática esperando a que la tabla renderice
await page.getByRole('button', { name: 'Cargar' }).click();
await page.waitForTimeout(5000);
expect(await page.locator('.row').count()).toBe(10);
// ✅ CORRECTO: Aserción asíncrona con auto-waiting
await page.getByRole('button', { name: 'Cargar' }).click();
await expect(page.locator('.row')).toHaveCount(10);El modificador force: true desactiva las comprobaciones de visibilidad, estabilidad y superposición. El clic se dispara directamente a las coordenadas del centro del elemento aunque un modal de error, un spinner o un menú lo tapen por completo. Esto enmascara defectos graves de interfaz.
// ❌ INCORRECTO: El botón está cubierto por un spinner, pero force hace clic igual
await page.getByRole('button', { name: 'Confirmar' }).click({ force: true });
// ✅ CORRECTO: Aguardar a que el elemento bloqueador desaparezca de forma natural
await expect(page.getByTestId('global-spinner')).toBeHidden();
await page.getByRole('button', { name: 'Confirmar' }).click();Localizadores del tipo div > div.flex > button.btn-primary:nth-child(2) se rompen ante el menor refactor de estructura (como sustituir un div por section o envolver en un flex-container), aun cuando el comportamiento para el usuario no varíe.
// ❌ INCORRECTO: Acoplado a clases CSS y jerarquía estructural
await page.locator('div.header-container > ul > li:nth-child(3) > a').click();
// ✅ CORRECTO: Búsqueda por rol accesible y nombre
await page.getByRole('navigation').getByRole('link', { name: 'Documentación' }).click();Invocar chromium.launch() en hooks beforeEach para cada caso eleva los tiempos de ejecución de 10 a 20 veces debido a la inicialización de procesos del sistema operativo. Confíe en el modelo de fixtures de Playwright, donde la instancia de Browser se reutiliza por worker y las pruebas se ejecutan en sesiones ligeras de BrowserContext.
Playwright busca el selector en el documento activo. Si coincide con múltiples nodos, lanza un Strict Mode Violation.
Comprueba que el elemento pertenezca al DOM activo y presente dimensiones mayores a cero (sin display: none o visibility: hidden).
Compara el BoundingBox del elemento a lo largo de frames de animación sucesivos para garantizar el cese de transiciones CSS.
Desplaza automáticamente el elemento a la vista y ejecuta document.elementFromPoint para verificar que el centro no esté cubierto.
Emite eventos físicos de hardware (mousemove, mousedown, mouseup, click) directamente mediante protocolos WebSocket.