Assertions en Playwright: auto-retry, soft assertions y lo que en Selenium armé a mano

4 tests, 5 errores reales. Auto-retry, soft assertions, .not, y por qué en Selenium necesitaba esperas explícitas que acá no existen.

UI Mode de Playwright mostrando 12 de 12 tests passed en Chromium Firefox y WebKit con 4 tests de assertions contra demo.serenity.is
12/12. 4 tests × 3 browsers. Assertions de navegación, elementos, soft assertions y negativas.

El problema con assertions clásicas

En mi framework de Selenium uso Assert.assertEquals de TestNG:

Assert.assertEquals(driver.getTitle(), "Google");

Si la página tarda en cargar y el título todavía no está, falla. Así nomás. Una oportunidad, un resultado. Para que funcione, armé esperas explícitas antes de cada assertion:

wait.until(ExpectedConditions.titleIs("Google"));
Assert.assertEquals(driver.getTitle(), "Google");

Dos líneas para verificar un título. Y si me olvido la espera, el test es flaky.

Playwright resuelve esto distinto.


Assertions web-first: qué son y por qué importan

En Playwright, las assertions reintentan automáticamente. No hace falta armar esperas.

await expect(page).toHaveTitle(/Iniciar sesión/);

Playwright hace esto internamente:

  1. Verifica la condición
  2. Si no se cumple, espera un poco
  3. Vuelve a verificar
  4. Repite hasta que pase o se agote el timeout (5 segundos por defecto)

No escribí una sola espera. El framework lo maneja.


Test 1: assertions de navegación (URL y título)

Empecé con lo básico: verificar URL y título de la página.

Código del test assertions de navegación con toHaveURL y toHaveTitle verificando login y redirección post-login
Test 1: verificar URL y título antes y después del login. Tres assertions, cero esperas explícitas.

Error 1: el título no es lo que pensaba

Mi primera versión tenía:

await expect(page).toHaveTitle(/Serenity/);

Falló. El error de Playwright:

Expected pattern: /Serenity/
Received string:  "Iniciar sesión en su cuenta"
Timeout: 5000ms
UI Mode mostrando error de toHaveTitle con Expected pattern Serenity y Received string Iniciar sesión en su cuenta con 8 reintentos en 5 segundos
Primer error: asumí que el título tenía "Serenity". Dice "Iniciar sesión en su cuenta". Y mirá el log: 8 reintentos en 5 segundos.

Asumí que el título tenía "Serenity" porque el sitio se llama así. No. El <title> del HTML dice "Iniciar sesión en su cuenta".

Y mirá lo que dice el log: 8 x unexpected value "Iniciar sesión en su cuenta". Reintentó 8 veces durante 5 segundos. Eso es el auto-retry en acción. En Selenium con TestNG, Assert.assertEquals falla una vez y listo, no reintenta.

Corregí a /Iniciar sesión/. Pasó.


Test 2: assertions de elementos (texto, visibilidad, atributos, conteo)

Código del test assertions de elementos con toHaveText toBeVisible toHaveCount y toHaveTitle verificando botones y login externo
Test 2: texto, visibilidad, conteo de botones y título post-login. toHaveCount con regex en vez de CSS frágil.

Error 2: CSS frágil vs roles

Mi primera versión del conteo usaba un selector CSS:

await expect(page.locator('.external-login-flow button')).toHaveCount(3);

Falló:

Expected: 3
Received: 0
UI Mode mostrando error de toHaveCount con selector CSS external-login-flow button Expected 3 Received 0 y 8 reintentos
Segundo error: inventé el selector CSS. La clase no existe. Expected 3, Received 0.

La clase .external-login-flow no existe. Inventé el selector. La solución: usar getByRole con regex, que no depende de clases CSS:

await expect(page.getByRole('button', { name: /Iniciar sesión con/ })).toHaveCount(3);

Matchea los 3 botones (Google, GitHub, Microsoft) por su nombre accesible. Sin CSS frágil.

Error 3: el <title> del HTML no es lo que dice la UI

Después del login, la UI muestra "Tablero". Pero toHaveTitle(/Tablero/) falló:

Expected pattern: /Tablero/
Received string:  "Dashboard - StartSharp"
UI Mode mostrando error de toHaveTitle con Expected pattern Tablero y Received string Dashboard StartSharp mientras la UI muestra Tablero en español
Tercer error: la UI dice "Tablero" pero el title del HTML dice "Dashboard - StartSharp". Lo que ves no es lo que el HTML dice.

La UI está en español porque tengo locale: 'es-AR' en playwright.config.ts. Pero el <title> del HTML quedó en inglés. toHaveTitle verifica el <title> del documento, no lo que se ve en pantalla.

Es una diferencia sutil pero importante: lo que ves no siempre es lo que el HTML dice.

Corregí a /Dashboard/. Pasó.


Test 3: soft assertions

Esto no existe en TestNG de forma nativa.

En Selenium, si la primera assertion falla, el test se frena:

Assert.assertEquals(title, "Dashboard");  // falla acá
Assert.assertTrue(url.contains("/"));      // nunca corre
Assert.assertEquals(header, "Tablero");    // nunca corre

Si falla la primera, no sabés si las otras dos están bien o mal. Tenés que arreglar, correr de nuevo, y rezar.

En Playwright, expect.soft hace que el test siga corriendo aunque falle una assertion:

Código del test soft assertions con expect.soft verificando título URL heading y una assertion intencional de Texto que no existe
Test 3: soft assertions. La cuarta línea va a fallar a propósito. La quinta se ejecuta igual.

Para demostrar el comportamiento, agregué una assertion que falla a propósito:

// Esta va a fallar a propósito
await expect.soft(page.getByText('Texto que no existe')).toBeVisible();

// Esta se ejecuta AUNQUE la anterior falló
await expect.soft(page.getByText('Northwind')).toBeVisible();
UI Mode mostrando soft assertions con 3 passed y 1 failed en Texto que no existe y Northwind ejecutándose después de la falla
Soft assertions en acción: "Texto que no existe" falló, pero "Northwind" se ejecutó igual y pasó. En TestNG, nunca hubiera corrido.

La assertion de "Texto que no existe" falló. Pero la de "Northwind" se ejecutó igual y pasó. En TestNG, nunca hubiera corrido.

Error 4: getByText('Tablero') matcheó 2 elementos

Mi primera versión usaba:

await expect.soft(page.getByText('Tablero')).toBeVisible();

Falló con:

strict mode violation: getByText('Tablero') resolved to 2 elements:
    1) <span class="s-sidebar-link-text">Tablero</span>
    2) <h1>Tablero</h1>
UI Mode mostrando dos errores en Errors tab con strict mode violation en getByText Tablero resolviendo a 2 elementos y Texto que no existe not found
Cuarto error: getByText('Tablero') matcheó 2 elementos: un span del sidebar y el h1. Mismo problema que ANTON en el Post 3.

Mismo problema que en el Post 3 con getByText('ANTON'). La solución: ser más específico con getByRole:

await expect.soft(page.getByRole('heading', { name: 'Tablero' })).toBeVisible();

Apunta directo al <h1>, sin ambigüedad.


Test 4: assertions negativas con .not

En Selenium, verificar que algo no está visible es un dolor:

// Opción 1: try-catch (feo)
try {
    driver.findElement(By.id("dashboard"));
    Assert.fail("El elemento no debería existir");
} catch (NoSuchElementException e) {
    // OK, no existe
}

// Opción 2: ExpectedConditions (mejor pero verboso)
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("login-btn")));

En Playwright: .not.

Código del test assertions negativas con not.toBeVisible y not.toHaveURL verificando estado pre y post login
Test 4: verificar que algo NO está. Sin try-catch, sin ExpectedConditions. Solo .not.

Una línea. .not.toBeVisible(). Sin try/catch, sin ExpectedConditions, sin verbosidad.

Error 5: una carrera de tiempos

Por accidente dejé dos assertions seguidas sobre el mismo botón:

await expect(page.getByRole('button', { name: 'Iniciar sesión', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: 'Iniciar sesión', exact: true })).not.toBeVisible();

Las dos pasaron. ¿Cómo?

UI Mode mostrando toBeVisible en 105ms y not toBeVisible en 764ms sobre el mismo botón de Iniciar sesión con flechas señalando ambas líneas
Quinto error: las dos pasaron. toBeVisible en 105ms porque el botón seguía ahí durante la navegación. not.toBeVisible en 764ms esperando que desaparezca. Carrera de tiempos, no un test confiable.

Mirá los tiempos:

  • toBeVisible()105ms
  • not.toBeVisible()764ms

El click en "Iniciar sesión" dispara la navegación, pero la página no cambia instantáneamente. Cuando toBeVisible() se ejecuta, el botón todavía está ahí porque la navegación no terminó. Después, not.toBeVisible() reintenta hasta que el botón desaparece.

No es un test confiable. Depende de qué tan rápido cargue la página. Si un día la navegación es más rápida, toBeVisible() fallaría. Eliminé la línea y dejé solo not.toBeVisible().


La comparación directa con Selenium + TestNG

Concepto Selenium + TestNG Playwright
Assertion básica Assert.assertEquals(a, b) await expect(x).toHaveText('y')
Auto-retry No. Falla una vez y listo Sí. Reintenta hasta el timeout
Esperas antes de assertions Obligatorias (WebDriverWait) Innecesarias (built-in)
Soft assertions No nativo. Se puede armar con SoftAssert expect.soft() nativo
Assertions negativas try/catch o invisibilityOf .not.toBeVisible()
Mensaje de error Genérico Detallado: locator, expected, received, timeout, call log

Lo que más cambia no es la sintaxis. Es que en Selenium armé esperas explícitas antes de cada assertion para que no sean flaky. En Playwright, eso viene resuelto.


Estado actual del archivo

serenity-assertions.spec.ts
├── assertions de navegación: URL y título
├── assertions de elementos: texto, visibilidad, atributos
├── soft assertions: múltiples verificaciones sin frenar
└── assertions negativas: verificar que algo NO existe
Playwright Test mostrando 12 de 12 tests passed con los 4 tests expandidos en Chromium Firefox y WebKit y dashboard de demo.serenity.is visible
Versión final: 4 tests, 3 browsers, 12/12 passed. Sin la línea de la carrera de tiempos.

4 tests. 3 browsers. Todo verde.


Próximo paso

Explorar fixtures — el sistema de inyección de dependencias de Playwright que reemplaza los @BeforeEach y @AfterEach de TestNG. Fixtures son lo que hace que el { page } aparezca mágicamente en cada test.

🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/playwright-typescript-framework