page.waitForTimeout(2000) antes de una aserción casi siempre está mal: las aserciones de Playwright ya hacen polling hasta que pasan, así que el sleep es redundante en ejecuciones rápidas y sigue siendo demasiado corto en las lentas. El patrón correcto es dejar que la aserción espere, o usar waitForResponse dentro de Promise.all para capturar una llamada específica a la API antes de verificar su resultado. Este artículo cubre las condiciones de auto-espera de Playwright, cuándo no son suficientes, y las cinco APIs de espera explícita: cuándo usar cada una y cuáles tratar como señal de alerta.
Cómo funciona la auto-espera de Playwright
Cuando escribes await page.getByRole('button', { name: 'Submit' }).click(), Playwright no hace clic de inmediato. Espera a que el botón esté:
- Adjunto al DOM
- Visible (no oculto, no con
display: none) - Estable (sin animarse)
- Habilitado (no deshabilitado)
- Recibiendo eventos de puntero (no tapado por otro elemento)
Esto ocurre automáticamente, hasta el actionTimeout (por defecto: 30 segundos). No escribes ningún código de espera; Playwright lo gestiona.
Por eso Playwright es más rápido de escribir que Selenium. En Selenium, escribirías WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ROLE, 'button'))). En Playwright: solo haces clic.
Cuándo la auto-espera no es suficiente
La auto-espera funciona para las interacciones con elementos. No gestiona todo:
Navegación de página
Después de enviar un formulario o hacer clic en un enlace, la URL cambia. La auto-espera no esperará a que la nueva página cargue completamente antes de tu próxima acción.
Carga de datos
Un elemento existe y es visible, pero muestra un spinner mientras se cargan los datos. Playwright podría interactuar con él antes de que lleguen los datos.
Múltiples peticiones de red
Una carga de página dispara tres llamadas a la API. Playwright ve que el DOM está listo, pero la tercera llamada todavía no terminó.
Finalización de animaciones
Un elemento es técnicamente visible pero está en medio de una animación. La auto-espera maneja transiciones CSS simples pero no todos los estados de animación.
expect como herramienta de espera
El patrón de espera más subutilizado en Playwright: las aserciones esperan.
// Espera hasta que la URL coincida
await expect(page).toHaveURL('/dashboard');
// Espera a que aparezca el texto
await expect(page.getByRole('heading')).toHaveText('Pedido confirmado');
// Espera a que el conteo de elementos llegue a 5
await expect(page.getByRole('listitem')).toHaveCount(5);Cada aserción expect en Playwright hace polling hasta que pasa o se agota el tiempo. Esto hace que las aserciones sean la forma más limpia de esperar el estado de la aplicación.
Anti-patrón
await page.waitForTimeout(2000); // nunca hacer esto
await expect(page.getByRole('heading')).toHaveText('Pedido confirmado');Correcto
await expect(page.getByRole('heading')).toHaveText('Pedido confirmado');El expect hace la espera. El waitForTimeout es una señal de que no sabes qué esperar.
Herramientas de espera explícita
Cuando necesitas esperas explícitas, Playwright provee opciones específicas:
waitForURL
await page.getByRole('button', { name: 'Iniciar sesión' }).click();
await page.waitForURL('/dashboard');
// Ahora es seguro verificar el contenido del dashboardwaitForResponse: esperar una llamada específica a la API
const [response] = await Promise.all([
page.waitForResponse(resp => resp.url().includes('/api/orders') && resp.status() === 200),
page.getByRole('button', { name: 'Realizar pedido' }).click(),
]);
const orderData = await response.json();
expect(orderData.status).toBe('created');Inicia la espera antes de la acción que dispara la petición. Promise.all garantiza que no te pierdes una respuesta rápida.
waitForRequest: verificar que se hizo una petición
const [request] = await Promise.all([
page.waitForRequest(req => req.url().includes('/api/track') && req.method() === 'POST'),
page.getByRole('button', { name: 'Comprar' }).click(),
]);
// Verificar que se disparó el evento de analytics
expect(request.postDataJSON()).toMatchObject({ event: 'purchase' });waitForSelector: esperar el estado de un elemento
// Esperar a que desaparezca el spinner de carga
await page.waitForSelector('.spinner', { state: 'detached' });
// Esperar a que el elemento se vuelva visible
await page.waitForSelector('[data-testid="results-table"]', { state: 'visible' });Opciones de state: 'attached', 'detached', 'visible', 'hidden'.
Prefiere expect(locator).toBeVisible() sobre waitForSelector; el enfoque de aserción es más legible.
waitForLoadState
await page.goto('/pagina-pesada');
await page.waitForLoadState('networkidle'); // Esperar hasta que no haya actividad de red durante 500msLas opciones de loadState son 'load' (el evento window.load se disparó, valor por defecto de goto), 'domcontentloaded' (DOM parseado, antes de imágenes/scripts) y 'networkidle' (sin peticiones de red durante 500ms).
'networkidle' es lento y frágil. Evítalo a menos que la página genuinamente no tenga otra forma de señalar "está lista". Prefiere esperar un elemento específico.
Configuración de timeouts
Los timeouts son configurables en tres niveles:
// playwright.config.ts: aplica a todos los tests
export default defineConfig({
timeout: 30000, // Timeout del test (test completo)
expect: {
timeout: 5000, // Timeout de las aserciones
},
use: {
actionTimeout: 15000, // Timeout de acciones individuales (click, fill, etc.)
navigationTimeout: 30000,
},
});Sobreescribir por test:
test('carga de datos lenta', async ({ page }) => {
test.setTimeout(60000); // Este test tiene 60 segundos
// ...
});Sobreescribir por aserción:
await expect(page.getByText('Reporte generado')).toBeVisible({ timeout: 30000 });La regla del waitForTimeout
page.waitForTimeout(ms) es un sleep. A veces es necesario como último recurso (por ejemplo, esperar un script de terceros que no puedes observar). Pero trata cada uso como un TODO: ¿qué deberías esperar realmente aquí?
Si te encuentras escribiendo await page.waitForTimeout(1000) en más de uno o dos tests, la suite tiene un problema estructural de esperas que vale la pena resolver.