Una aserción expect() sin await en Playwright siempre pasa, aunque la condición sea falsa: la aserción se ejecuta pero nunca espera a que la verificación se complete. Ese es el filo más peligroso del async en el código de tests. Esta guía cubre cómo funcionan las Promesas, por qué un await faltante causa tests inestables en lugar de fallos inmediatos, Promise.all para la configuración paralela de fixtures, Promise.allSettled para recolectar resultados parciales, y el patrón de race condition que maneja correctamente la navegación después de un clic en un botón.
Qué es una Promesa
Una Promesa representa un valor que todavía no está listo: es un placeholder para algo que llegará en el futuro.
// Una Promesa en tres estados:
// 1. Pendiente — esperando el resultado
// 2. Cumplida — la operación tuvo éxito, el valor está disponible
// 3. Rechazada — la operación falló, el error está disponible
const promise = fetch('/api/users');
// En este punto, la promesa está PENDIENTE
// Más tarde, es:
// CUMPLIDA — llegó la respuesta
// RECHAZADA — error de red, error del servidor.then(): la forma antigua
Antes de async/await, las Promesas se consumían con .then():
fetch('/api/users')
.then(response => response.json())
.then(users => {
console.log(users); // Usar los datos
})
.catch(error => {
console.error('Falló:', error);
});Esto se encadena bien para casos simples, pero se vuelve confuso con lógica compleja. Async/await fue diseñado para resolver esto.
async/await: la forma moderna
async function getUsers() {
try {
const response = await fetch('/api/users');
const users = await response.json();
return users;
} catch (error) {
console.error('Falló:', error);
throw error;
}
}await pausa la ejecución hasta que la Promesa se resuelve. El código se lee como código síncrono pero se ejecuta de forma asíncrona.
Reglas
await solo puede usarse dentro de funciones async, las funciones async siempre devuelven una Promesa y si una función async devuelve un valor, se envuelve en una Promesa resuelta.
Qué pasa cuando olvidás await
Este es el bug async más común en Playwright:
// BUG: falta await
test('test de login', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@test.com');
page.click('[data-testid="submit"]'); // Sin await
// Esta aserción corre ANTES de que el clic se complete
await expect(page).toHaveURL('/dashboard'); // Puede fallar intermitentemente
});Sin await, page.click() empieza pero no espera a terminar. La siguiente línea corre mientras el clic todavía está en progreso. En Playwright, esto genera tests inestables: a veces el clic termina a tiempo, a veces no.
Solución
await page.click('[data-testid="submit"]');Ejecución paralela vs. secuencial
Por defecto, await ejecuta las cosas secuencialmente:
// Secuencial: tiempo total = suma de todas las esperas
const user = await getUser(1); // Esperar usuario
const orders = await getOrders(1); // Luego esperar órdenes
const profile = await getProfile(1); // Luego esperar perfilSi las operaciones son independientes, ejecútalas en paralelo:
// Paralelo: tiempo total = la espera más larga
const [user, orders, profile] = await Promise.all([
getUser(1),
getOrders(1),
getProfile(1),
]);En los fixtures de Playwright, este patrón se usa para configurar múltiples cosas a la vez:
// Configurar datos de prueba en paralelo
const [adminToken, testUser] = await Promise.all([
loginAsAdmin(request),
createTestUser(request),
]);Promise.all: ejecutar múltiples en paralelo
// Las tres peticiones se disparan simultáneamente
const results = await Promise.all([
request.get('/api/users'),
request.get('/api/products'),
request.get('/api/orders'),
]);
// results es un array de respuestas
const [usersResp, productsResp, ordersResp] = results;Promise.all se rechaza, todo se rechaza:
try {
const [a, b, c] = await Promise.all([
fetch('/api/users'),
fetch('/api/va-a-fallar-con-404'), // Esta falla
fetch('/api/products'),
]);
// Nunca llega acá si alguna falla
} catch (error) {
// Una de ellas falló
console.error(error);
}Promise.allSettled: esperar a todas, incluso las que fallan
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/puede-fallar'),
fetch('/api/products'),
]);
// Cada resultado tiene status + value o reason
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Éxito:', result.value);
} else {
console.log('Falló:', result.reason);
}
});Usa allSettled cuando quieres todos los resultados sin importar los fallos individuales.
Promise.race: el primero en llegar gana
// Patrón de timeout usando Promise.race
const fetchWithTimeout = async (url, timeoutMs) => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
);
return Promise.race([fetch(url), timeout]);
};
try {
const response = await fetchWithTimeout('/api/lento', 5000);
} catch (error) {
if (error.message === 'Timeout') {
console.log('El request tardó demasiado');
}
}Manejo de errores
try/catch con async/await
async function createUser(data: UserData) {
try {
const response = await request.post('/api/users', { data });
if (!response.ok()) {
const body = await response.json();
throw new Error(`Error de API: ${body.message}`);
}
return await response.json();
} catch (error) {
console.error('No se pudo crear el usuario:', error);
throw error; // Re-lanzar para que el llamador sepa que falló
}
}En tests de Playwright
test('maneja el error de API correctamente', async ({ page }) => {
// Mockear la API para que falle
await page.route('/api/users', route => route.fulfill({ status: 500 }));
await page.goto('/users');
await expect(page.getByTestId('error-message')).toBeVisible();
await expect(page.getByTestId('error-message')).toContainText('Algo salió mal');
});Bugs async comunes en tests de Playwright
1. Falta await en las aserciones
// BUG
expect(page.getByTestId('button')).toBeVisible(); // Sin await: siempre pasa
// FIX
await expect(page.getByTestId('button')).toBeVisible();2. Usar await dentro de bucles incorrectamente
// BUG: todas corren en paralelo pero los errores pueden quedar sin manejar
const items = ['a', 'b', 'c'];
items.forEach(async (item) => {
await processItem(item); // Corren en paralelo, no se esperan
});
// FIX: secuencial
for (const item of items) {
await processItem(item);
}
// O paralelo con manejo correcto
await Promise.all(items.map(item => processItem(item)));3. Race conditions
// BUG: el clic y la navegación compiten
await page.click('[data-testid="submit"]');
// El submit puede navegar, o puede mostrar un error de validación
// La siguiente línea puede correr antes de saber qué pasó
// FIX: esperar explícitamente lo que se espera
await Promise.all([
page.waitForURL('/dashboard'),
page.click('[data-testid="submit"]'),
]);
// O esperar la respuesta
const [response] = await Promise.all([
page.waitForResponse('/api/auth/login'),
page.click('[data-testid="submit"]'),
]);Async/await en Page Objects
Los métodos de page object deben ser async cuando esperan algo:
class LoginPage {
constructor(private page: Page) {}
// Async porque navega
async navigate(): Promise<void> {
await this.page.goto('/login');
}
// Async porque realiza acciones
async login(email: string, password: string): Promise<void> {
await this.page.fill('[data-testid="email"]', email);
await this.page.fill('[data-testid="password"]', password);
await this.page.click('[data-testid="submit"]');
}
// Async porque lee de la página
async getErrorMessage(): Promise<string | null> {
return this.page.getByTestId('error').textContent();
}
}
// Uso: todo con await
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('user@test.com', 'PassValida1');
const error = await loginPage.getErrorMessage();Resumen
| Concepto | Qué hace |
|----------|---------|
| Promise | Placeholder para un valor futuro |
| async | Marca una función como asíncrona |
| await | Pausa hasta que la Promesa se resuelve |
| Promise.all() | Ejecuta múltiples en paralelo, espera a todas |
| Promise.allSettled() | Como all(), pero continúa ante fallos |
| Promise.race() | Devuelve la primera en resolverse/rechazarse |
| try/catch | Maneja errores async |
La regla más importante: siempre pon await a las acciones y aserciones de Playwright. Un await faltante es la causa número uno de tests inestables en Playwright. Si un test falla intermitentemente sin razón obvia, busca primero keywords await que falten.