En las aplicaciones empresariales modernas, la gran mayoría de los escenarios de usuario requieren autenticación. Si cada escenario de prueba repite los pasos de inicio de sesión a través de formularios gráficos dentro de un hook beforeEach, el pipeline de pruebas sufre una degradación catastrófica del rendimiento:
/login, renderizar el árbol del formulario, teclear caracteres, esperar respuestas de la API y gestionar redirecciones a /dashboard añade de 2 a 4 segundos a cada prueba.+-----------------------------------------------------------------------------+
| Antipatrón tradicional: Inicios de sesión repetitivos en UI |
| |
| Test 1: [UI Login: 3s] -> [Lógica de Test: 1s] = 4s |
| Test 2: [UI Login: 3s] -> [Lógica de Test: 0.5s] = 3.5s |
| Test 3: [UI Login: 3s] -> [Lógica de Test: 2s] = 5s |
| TOTAL para 100 tests: ~300 segundos (5 minutos) perdidos escribiendo claves|
+-----------------------------------------------------------------------------+
+-----------------------------------------------------------------------------+
| Playwright 2026: Arquitectura de reutilización Storage State |
| |
| Setup Project (1 vez): [API/UI Login: 1.5s] -> Persistencia en storageState|
| │ |
| ┌──────────────────────────────┴────────────────┐ |
| ▼ ▼ |
| Worker 1 (Contexto con storageState) Worker 2 (Contexto con storageState)
| Test 1: [Lógica de Test: 1s] Test 2: [Lógica de Test: 0.5s] |
| Test 3: [Lógica de Test: 2s] Test 4: [Lógica de Test: 1.2s] |
| TOTAL para 100 tests: ~35 segundos de tiempo de ejecución acumulado |
+-----------------------------------------------------------------------------+Playwright transforma radicalmente este paradigma separando la autenticación de la ejecución de escenarios mediante Storage State combinado con el grafo de Project Dependencies.
En el modelo de pruebas de Playwright, la ejecución autenticada se divide en dos fases operativas independientes:
cookies y localStorage) en disco como un artefacto JSON.BrowserContext, hidratándolas inmediatamente con la instantánea en caché storageState.json. El contexto navega directamente a las rutas protegidas sin pasar por formularios de inicio de sesión.+-----------------------------------------------------------------------------+
| 1. SETUP PROJECT (setup) |
| |
| auth.setup.ts ──► POST /api/auth/login ──► Obtener JWT + Set-Cookie |
| │ |
| ▼ |
| page.context().storageState({ path: '.auth/admin.json' }) |
| page.context().storageState({ path: '.auth/user.json' }) |
+-----------------------------------------------------------------------------+
│
▼ (Guardar instantáneas JSON en disco)
+-----------------------------------------------------------------------------+
| Directorio .auth/ (Ignorado en Git) |
| - admin.json (Cookies + LocalStorage con rol de Administrador) |
| - user.json (Cookies + LocalStorage con rol de Usuario Estándar) |
+-----------------------------------------------------------------------------+
│
┌────────────────────────┴────────────────────────┐
▼ ▼
+------------------------------------+ +------------------------------------+
| Project: "admin-tests" | | Project: "customer-tests" |
| use: { | | use: { |
| storageState: '.auth/admin.json'| | storageState: '.auth/user.json' |
| } | | } |
| | | |
| Worker A: dashboard.spec.ts | | Worker C: profile.spec.ts |
| Worker B: settings.spec.ts | | Worker D: orders.spec.ts |
+------------------------------------+ +------------------------------------+storageState.json#El artefacto de estado cumple un esquema estricto que describe cookies y orígenes de almacenamiento:
{
"cookies": [
{
"name": "session_token",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"domain": "localhost",
"path": "/",
"expires": 1789038400,
"httpOnly": true,
"secure": false,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "http://localhost:3000",
"localStorage": [
{
"name": "user_meta",
"value": "{\"id\":\"usr_01\",\"role\":\"admin\",\"theme\":\"dark\"}"
}
]
}
]
}playwright.config.ts#import { defineConfig, devices } from '@playwright/test';
import path from 'path';
/** Rutas absolutas a los estados serializados por rol */
export const ADMIN_AUTH_FILE = path.join(__dirname, '.auth/admin.json');
export const USER_AUTH_FILE = path.join(__dirname, '.auth/user.json');
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? '100%' : undefined,
projects: [
// 1. Proyecto de inicialización: se ejecuta primero para generar las sesiones
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// 2. Suite de pruebas con permisos de administrador
{
name: 'admin-chromium',
testMatch: /.*admin.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
storageState: ADMIN_AUTH_FILE,
},
dependencies: ['setup'], // Dependencia estricta del proyecto setup
},
// 3. Suite de pruebas con permisos de usuario estándar
{
name: 'user-chromium',
testMatch: /.*user.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
storageState: USER_AUTH_FILE,
},
dependencies: ['setup'],
},
// 4. Pruebas públicas sin autenticación (Invitado)
{
name: 'public-chromium',
testMatch: /.*public.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
// storageState omitido: contexto nuevo de incógnito
},
},
],
});auth.setup.ts#Completar formularios mediante interfaces gráficas es entre 10 y 20 veces más lento que realizar llamadas HTTP directas. El proyecto setup alcanza una velocidad óptima autenticándose directamente contra la API de autenticación del backend utilizando el fixture request de Playwright, capturando cookies y escribiendo en storageState.
import { test as setup, expect } from '@playwright/test';
import { ADMIN_AUTH_FILE, USER_AUTH_FILE } from '../playwright.config';
setup.describe('Inicialización global de estados de sesión', () => {
setup('autentica administrador mediante API y persiste el estado', async ({ request, page }) => {
// 1. Envía petición HTTP POST directa al endpoint de login (sin renderizado de UI)
const loginResponse = await request.post('/api/auth/login', {
data: {
email: process.env.E2E_ADMIN_EMAIL ?? '[email protected]',
password: process.env.E2E_ADMIN_PASSWORD ?? 'SecureEnterprisePassword2026!',
},
});
expect(loginResponse.ok()).toBeTruthy();
const { token, user } = (await loginResponse.json()) as { token: string; user: { id: string; role: string } };
// 2. Navega al dominio base para inicializar localStorage bajo el origen correcto
await page.goto('/favicon.ico');
// 3. Inyecta los tokens en localStorage mediante evaluate
await page.evaluate(
({ authToken, userData }) => {
localStorage.setItem('auth_token', authToken);
localStorage.setItem('user_profile', JSON.stringify(userData));
},
{ authToken: token, userData: user }
);
// 4. Guarda el estado consolidado (cookies de la respuesta + localStorage) en disco
await page.context().storageState({ path: ADMIN_AUTH_FILE });
});
setup('autentica usuario estándar mediante formulario de UI (flujo de respaldo)', async ({ page }) => {
// Flujo alternativo para proveedores que requieren tokens CSRF complejos o redirecciones externas
await page.goto('/login');
await page.getByLabel('Correo electrónico').fill(process.env.E2E_USER_EMAIL ?? '[email protected]');
await page.getByLabel('Contraseña').fill(process.env.E2E_USER_PASSWORD ?? 'StandardPassword2026!');
await page.getByRole('button', { name: 'Iniciar sesión' }).click();
// Espera navegación al panel protegido
await page.waitForURL('**/dashboard');
await expect(page.getByRole('heading', { name: 'Mis cursos' })).toBeVisible();
// Persiste el estado de sesión del usuario
await page.context().storageState({ path: USER_AUTH_FILE });
});
});En las pruebas asignadas al proyecto admin-chromium, el contexto del navegador ya inicia completamente autenticado:
import { test, expect } from '@playwright/test';
test.describe('Panel de control administrativo', () => {
test('administrador puede configurar límites de asignación de tokens', async ({ page }) => {
// La sesión ya está preautenticada: navega directamente a la ruta protegida
await page.goto('/admin/security/token-limits');
// Valida la presencia de controles privilegiados
await expect(page.getByRole('heading', { name: 'Configuración de seguridad' })).toBeVisible();
const limitInput = page.getByLabel('Máximo de tokens por worker');
await limitInput.fill('64');
await page.getByRole('button', { name: 'Guardar cambios' }).click();
await expect(page.getByRole('status')).toHaveText('Configuración actualizada con éxito');
});
});Cuando se valida una matriz de control de acceso basada en roles (RBAC) donde un solo escenario comprueba interacciones entre distintos roles:
import { test as base, type Page } from '@playwright/test';
import { ADMIN_AUTH_FILE, USER_AUTH_FILE } from '../../playwright.config';
export interface RoleFixtures {
adminPage: Page;
userPage: Page;
}
export const test = base.extend<RoleFixtures>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: ADMIN_AUTH_FILE });
const page = await context.newPage();
await use(page);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: USER_AUTH_FILE });
const page = await context.newPage();
await use(page);
await context.close();
},
});
export { expect } from '@playwright/test';Los artefactos de estado contienen tokens JWT activos, hashes de sesión y credenciales sensibles. Subir estos archivos compromete la seguridad y genera fallos a otros desarrolladores cuando los tokens expiran.
# ❌ INCORRECTO: Ausencia de .auth en .gitignore
# ✅ CORRECTO: Añadir al .gitignore raíz del proyecto:
.auth/
playwright/.auth/Si 20 pruebas paralelas comparten una única cuenta de administrador y un test muta preferencias globales (como cambiar el idioma a alemán o eliminar una organización), las otras 19 pruebas fallarán con condiciones de carrera impredecibles. Solución: Utilizar estados compartidos preautenticados estrictamente para escenarios de solo lectura (Read-Only). Para acciones destructivas (borrado de cuentas, mutaciones de facturación), generar usuarios efímeros (Ephemeral Users) mediante fixtures de API.
Si un JWT tiene una duración de 5 minutos y la suite de pruebas en CI tarda 12 minutos en completarse, las pruebas situadas al final de la cola fallarán con código 401 Unauthorized.
Solución: Configurar un endpoint dedicado de pruebas con TTL extendido (por ejemplo, 2 horas) en entornos de no producción, o implementar la renovación de tokens (refresh token) dentro de un fixture.
Si no se indica dependencies: ['setup'], Playwright ejecutará las pruebas funcionales al mismo tiempo que auth.setup.ts. Los tests intentarán leer los archivos .auth/*.json antes de que existan y fallarán inmediatamente con ENOENT: no such file or directory.
Declarar el proyecto setup en playwright.config.ts apuntando a auth.setup.ts.
Enviar petición HTTP a /api/auth/login o completar el formulario gráfico una única vez.
Llamar a page.context().storageState({ path: '.auth/role.json' }) para guardar cookies y almacenamiento.
Configurar dependencies: ['setup'] y las opciones storageState en los proyectos de prueba.
Cientos de workers inician contextos preautenticados sin demoras en formularios de login.