Olvidarse del await en una aserción de Playwright es un bug silencioso: expect(page.getByText('Error')).toBeVisible() sin await siempre pasa porque evalúa un objeto Promise en lugar del estado real de la página. JavaScript no espera que las operaciones del navegador terminen por defecto, así que cada método de Playwright que toca el navegador devuelve una Promise que debe ser esperada antes de que el resultado sea usable. Este artículo explica la mecánica, muestra qué se rompe cuando olvidás el await y cubre los patrones para funciones async, valores almacenados y operaciones en paralelo.

El problema que async/await resuelve

JavaScript es no bloqueante por defecto. Cuando le dices al navegador que haga algo (navegar a una URL, hacer clic en un botón, cargar una página), JavaScript no espera que termine antes de pasar a la siguiente línea. Inicia la operación y continúa de inmediato.

Esto es un problema para los tests. Necesitás navegar a la página antes de hacer clic en el botón. Necesitás hacer clic antes de verificar el resultado.

Antes de que existiera async/await, la solución eran los callbacks y las Promises. Funcionan, pero producen código enredado difícil de leer y depurar. async/await es la solución moderna: hace que las operaciones asincrónicas se vean y se comporten como secuenciales.

La explicación más simple

await significa: "espera aquí hasta que esta operación termine, luego continúa."

// Sin await: no espera, falla o se comporta de forma inesperada
test('ejemplo roto', async ({ page }) => {
  page.goto('https://lab.becomeqa.com'); // inicia la navegación y continúa de inmediato
  page.getByRole('button', { name: 'Login' }).click(); // intenta hacer clic antes de que cargue la página
});

// Con await: espera que cada paso se complete
test('ejemplo correcto', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com'); // esperar que la navegación complete
  await page.getByRole('button', { name: 'Login' }).click(); // luego hacer clic
});

El segundo test funciona porque cada await pausa la ejecución hasta que la operación se completa.

Qué significa async

async antes de una función significa que esa función puede usar await dentro de ella y devolverá una Promise:

// Esta función es async: usa await
test('test de login', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  // ...
});

En los tests de Playwright, todas las funciones de test son async automáticamente porque todas involucran operaciones del navegador. El test runner de Playwright se encarga de esto. Solo necesitás recordar poner await antes de las operaciones.

La regla: cuándo usar await en Playwright

Usa await antes de cualquier método de Playwright que interactúe con el navegador (navegación, clics, rellenos, selecciones), devuelva información de la página (contenido de texto, valores de atributos, conteos) o haga aserciones (expect(...).toBeVisible()).

test('demuestra el uso de await', async ({ page }) => {
  // Navegación: await
  await page.goto('https://lab.becomeqa.com');

  // Acciones: await
  await page.getByRole('button', { name: 'Login' }).click();
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Submit' }).click();

  // Aserciones: await
  await expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible();

  // Obtener valores de la página: await
  const heading = await page.getByRole('heading').textContent();
  console.log(heading); // 'My Travel Items'

  // Verificar visibilidad: await
  const isVisible = await page.getByRole('button', { name: 'Add Item' }).isVisible();
  expect(isVisible).toBe(true);
});

Qué pasa si olvidás await

Las consecuencias dependen de dónde lo olvidaste:

Olvidaste await en una acción

// Esto rellena el campo y continúa de inmediato: a veces funciona de casualidad
// pero fallará bajo cualquier carga o red lenta
page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByRole('button', { name: 'Submit' }).click(); // puede hacer clic antes de que termine el relleno

Olvidaste await en una aserción

// Error muy común: la aserción evalúa un objeto Promise
// que siempre es truthy, así que el test siempre pasa aunque debería fallar
expect(page.getByText('Error')).toBeVisible(); // MAL: falta await, el test siempre pasa
await expect(page.getByText('Error')).toBeVisible(); // BIEN

Este último es particularmente peligroso: los tests que siempre pasan son peores que los que siempre fallan. TypeScript ayuda a detectarlo. En modo estricto, TypeScript te avisa cuando tienes una Promise sin await en una aserción.

Almacenar valores con await

Cuando necesitás capturar lo que devuelve Playwright:

test('verificar valores', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');

  // Almacenar el contenido de texto
  const title = await page.getByRole('heading').textContent();
  // title es ahora un string: 'My Travel Items'

  // Almacenar un conteo
  const rowCount = await page.getByRole('row').count();
  // rowCount es un número: 5

  // Almacenar el estado de visibilidad
  const isButtonVisible = await page.getByRole('button', { name: 'Add Item' }).isVisible();
  // isButtonVisible es un booleano: true o false

  // Usar los valores
  expect(title).toBe('My Travel Items');
  expect(rowCount).toBeGreaterThan(0);
  expect(isButtonVisible).toBe(true);
});

El patrón es siempre: const result = await page.someMethod().

Funciones async fuera de los tests

Cuando escribes funciones auxiliares o métodos de Page Object que usan Playwright, también necesitan ser async:

// Método de Page Object: debe ser async
class LoginPage {
  constructor(private page: Page) {}

  async login(email: string, password: string) {
    await this.page.getByRole('button', { name: 'Login' }).click();
    await this.page.getByLabel('Username').fill(email);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: 'Submit' }).click();
  }
}

// Cuando llamas a un método async, necesitas await
test('test de login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login('admin@becomeqa.com', 'testpass123'); // await al método async
});

La regla: si una función usa await dentro, declárala async. Si llamás a una función async, poné await antes de la llamada.

Operaciones en paralelo con Promise.all

A veces querés que dos cosas ocurran simultáneamente. Promise.all ejecuta múltiples operaciones async en paralelo y espera que todas terminen:

// Esperar la navegación Y que aparezca un elemento específico al mismo tiempo
// Esto es más rápido que esperarlos secuencialmente
await Promise.all([
  page.waitForURL('/dashboard'),
  expect(page.getByRole('heading', { name: 'My Travel Items' })).toBeVisible(),
]);

En Playwright, el uso más común es esperar una navegación y una aserción juntas después de hacer clic en algo. Playwright maneja la mayoría de esto automáticamente a través del auto-waiting, pero vas a ver Promise.all en código más antiguo y en escenarios de timing específicos.

Qué son las Promises (breve)

Cuando ves algo como:

const textContent = page.getByRole('heading').textContent();
// textContent es ahora una Promise<string>, no un string

Una Promise es un marcador de posición para un valor que todavía no existe, porque la operación sigue corriendo. await desenvuelve la Promise, pausando la ejecución hasta que el valor esté listo:

const textContent = await page.getByRole('heading').textContent();
// Ahora textContent es un string

No necesitás entender las Promises en profundidad para escribir tests de Playwright. El modelo mental "await hace que espere" es suficiente para el 95% del código de tests. Solo recordá que cada método de Playwright devuelve una Promise, así que necesitás await.

TypeScript te ayuda a no olvidarlo

TypeScript sabe qué métodos devuelven Promises. Si olvidás await, TypeScript te avisa:

Type 'Promise<string>' is not assignable to type 'string'

Esta es una de las razones para usar TypeScript en los tests de Playwright: el compilador detecta el await faltante antes de que corran tus tests.

Referencia rápida

| Qué estás haciendo | Patrón |

|---|---|

| Navegar | await page.goto(url) |

| Hacer clic | await element.click() |

| Rellenar input | await element.fill('texto') |

| Verificar visibilidad | await expect(element).toBeVisible() |

| Obtener texto | const text = await element.textContent() |

| Obtener conteo | const n = await elements.count() |

| Llamar función async | await myAsyncFunction() |

| Ejecutar en paralelo | await Promise.all([op1, op2]) |

FAQ

¿Necesito entender cómo funciona el event loop de JavaScript?

No, no para escribir tests de Playwright. "await hace que espere el resultado" es suficiente. El event loop es el mecanismo por debajo, pero no necesitás entender el mecanismo para usar la herramienta.

¿Por qué mi test pasa aunque olvidé await en expect?

Porque expect(element).toBeVisible() sin await devuelve un objeto Promise (que es truthy), y el test runner no lo evalúa como una aserción. El test no ve ningún fallo. Es un bug silencioso. El modo estricto de TypeScript lo detecta.

¿Puedo usar .then() en lugar de await?

Sí, .then() es la sintaxis antigua de Promise. Funciona, pero produce código más difícil de leer:

// Sintaxis antigua con Promise
page.goto('https://lab.becomeqa.com').then(() => {
  return page.getByRole('button', { name: 'Login' }).click();
}).then(() => {
  // ...
});

// Equivalente con async/await: mucho más claro
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();

Usa async/await. Evita las cadenas .then() en código de tests nuevo.

Veo Promise en TypeScript. ¿Qué significa? void significa que la función no devuelve un valor significativo. Solo hace algo (como navegar o hacer clic). Igual necesitas await, pero no almacenas el resultado en una variable. → See also: TypeScript para QA: Por Qué los Tipos Estáticos Mejoran tus Tests | Empezando con Playwright: Tus Primeros Tests en 30 Minutos