page.getByText('Success').isVisible() devuelve un booleano verificando el DOM exactamente una vez: si el elemento todavía no se renderizó, obtienes false aunque aparezca 200ms después. expect(page.getByText('Success')).toBeVisible() reintenta durante hasta 5 segundos antes de fallar. Ese comportamiento de reintento es el núcleo del diseño de aserciones de Playwright, y la mayoría de los errores de principiantes vienen de no saber qué métodos lo tienen. Esta guía cubre cada tipo de aserción, la negación con not, las soft assertions para recolectar todos los fallos, y los patrones que deshabilitan el auto-reintento en silencio.
Cómo funcionan las aserciones de Playwright
Las aserciones de Playwright usan la función expect() de @playwright/test. No son las mismas que el expect de Jest. La versión de Playwright es async y tiene lógica de reintento integrada.
import { test, expect } from '@playwright/test';La diferencia clave respecto a la mayoría de los frameworks de testing: las aserciones de Playwright reintentan automáticamente. Cuando escribes:
await expect(page.getByText('Bienvenido')).toBeVisible();Playwright no solo verifica una vez. Verifica repetidamente durante hasta 5 segundos (el expect timeout por defecto), esperando a que la condición sea verdadera. Eso elimina la necesidad de llamadas manuales a waitFor en el 90% de los casos.
Si la condición nunca se vuelve verdadera dentro del timeout, el test falla con un mensaje claro que muestra qué se esperaba y qué existía realmente.
Aserciones de elementos (basadas en locators)
Estas verifican propiedades de un elemento específico en la página.
toBeVisible / toBeHidden
// El elemento está renderizado y es visible para el usuario
await expect(page.getByText('Dashboard')).toBeVisible();
// El elemento no está presente, o está presente pero oculto (display:none, visibility:hidden, opacity:0)
await expect(page.getByRole('dialog')).toBeHidden();toBeHidden() es verdadero si el elemento no existe O si existe pero es invisible. Usa not.toBeAttached() si específicamente necesitas confirmar que el elemento no está en el DOM.
toHaveText / toContainText
// Coincidencia exacta de texto (recorta espacios automáticamente)
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Mis Ítems de Viaje');
// Coincidencia parcial de texto
await expect(page.getByRole('heading')).toContainText('Viaje');
// Array: verificar texto de múltiples elementos
await expect(page.getByRole('listitem')).toHaveText(['Tokio', 'París', 'Londres']);
// Regex: coincidencia de patrón
await expect(page.getByTestId('price')).toHaveText(/\$\d+\.\d{2}/);toHaveText con un array verifica el texto completo de cada elemento en orden. Muy útil para verificar filas de tablas o listas ordenadas.
toHaveValue
Para elementos , , :
// Input de texto
await expect(page.getByLabel('Email')).toHaveValue('admin@becomeqa.com');
// Input vacío
await expect(page.getByLabel('Buscar')).toHaveValue('');
// Dropdown select
await expect(page.getByLabel('Estado')).toHaveValue('completed');toBeChecked / not.toBeChecked
Para checkboxes y radio buttons:
await expect(page.getByLabel('Recordarme')).toBeChecked();
await expect(page.getByLabel('Suscribirse al boletín')).not.toBeChecked();toBeEnabled / toBeDisabled
// El botón es clickeable
await expect(page.getByRole('button', { name: 'Enviar' })).toBeEnabled();
// El botón está grisado / tiene atributo disabled
await expect(page.getByRole('button', { name: 'Guardar' })).toBeDisabled();Úsalo para verificar que un botón de envío de formulario solo se habilita cuando se completan los campos requeridos.
toBeEditable / toBeReadOnly
await expect(page.getByLabel('Nombre de usuario')).toBeEditable();
await expect(page.getByLabel('Fecha de creación')).toBeReadOnly();toHaveAttribute
Verificar cualquier atributo HTML:
// href en un link
await expect(page.getByRole('link', { name: 'Inicio' })).toHaveAttribute('href', '/');
// data-testid
await expect(page.getByTestId('status-badge')).toHaveAttribute('data-status', 'active');
// aria-label
await expect(page.getByRole('button', { name: 'Cerrar' })).toHaveAttribute('aria-label', 'Cerrar diálogo');
// Valor con regex
await expect(page.getByRole('img')).toHaveAttribute('src', /\/images\/.+\.svg/);toHaveClass
// El elemento tiene esta clase CSS
await expect(page.getByTestId('alert')).toHaveClass(/error/);
// Lista de clases exacta
await expect(page.getByTestId('button')).toHaveClass('btn btn-primary active');toHaveClass verifica si la clase está presente, no si es la única. Usa regex para coincidencias parciales.
toHaveCount
Para colecciones de elementos:
// La tabla tiene header + 5 filas de datos
await expect(page.getByRole('row')).toHaveCount(6);
// El dropdown tiene 4 opciones
await expect(page.getByRole('option')).toHaveCount(4);
// Sin mensajes de error
await expect(page.getByRole('alert')).toHaveCount(0);toHaveCSS
Verificar propiedades CSS computadas:
await expect(page.getByTestId('error-message')).toHaveCSS('color', 'rgb(220, 38, 38)');
await expect(page.getByRole('dialog')).toHaveCSS('display', 'flex');Usa valores computados (rgb(...)) y no variables CSS ni propiedades shorthand.
Aserciones de página
Verifican propiedades de toda la página, no de un elemento específico.
toHaveURL
// URL exacta
await expect(page).toHaveURL('https://lab.becomeqa.com/dashboard');
// Coincidencia parcial con regex
await expect(page).toHaveURL(/\/dashboard/);
// Con baseURL configurada en playwright.config.ts
await expect(page).toHaveURL('/dashboard');Úsalo después de una navegación para confirmar que llegaste donde esperabas.
toHaveTitle
await expect(page).toHaveTitle('Mis Ítems de Viaje | BecomeQA Lab');
await expect(page).toHaveTitle(/BecomeQA/);toHaveScreenshot (regresión visual)
// La primera ejecución crea el screenshot de referencia
// Las ejecuciones siguientes comparan contra él
await expect(page).toHaveScreenshot('dashboard.png');
// Con opciones
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 100, // permitir diferencias menores de píxeles
threshold: 0.2, // escala 0-1 de tolerancia a diferencias de color
});Testing de regresión visual: la primera ejecución guarda una imagen de referencia. Cada ejecución siguiente compara el estado actual con esa referencia. Falla si difieren más allá del umbral.
Aserciones de respuesta de API
Al usar el fixture request para testing de API:
test('GET /api/items devuelve datos válidos', async ({ request }) => {
const response = await request.get('/api/items');
// Código de estado
expect(response.status()).toBe(200);
expect(response.ok()).toBeTruthy(); // true para 200-299
// Cuerpo de la respuesta
const items = await response.json();
expect(items).toHaveLength(5);
expect(items[0]).toHaveProperty('id');
expect(items[0]).toHaveProperty('title');
expect(items[0].status).toBe('planned');
// Headers
expect(response.headers()['content-type']).toContain('application/json');
});response.ok() es un método integrado (no una aserción) que devuelve true para códigos de estado 2xx.
Aserciones de valores genéricos
Para valores que no son elementos: variables, respuestas de API, valores computados:
// Igualdad
expect(items.length).toBe(5);
expect(user.role).toBe('admin');
// No igual
expect(errorCode).not.toBe(0);
// Truthy/falsy
expect(isVisible).toBeTruthy();
expect(errorMessage).toBeFalsy();
// Null/undefined
expect(result).toBeNull();
expect(result).not.toBeNull();
expect(result).toBeDefined();
expect(result).toBeUndefined();
// Números
expect(count).toBeGreaterThan(0);
expect(price).toBeGreaterThanOrEqual(9.99);
expect(discount).toBeLessThan(100);
// Arrays
expect(statuses).toContain('completed');
expect(items).toHaveLength(3);
expect(tags).toEqual(['qa', 'automation', 'playwright']); // coincidencia exacta de array
// Objetos
expect(user).toMatchObject({ email: 'admin@becomeqa.com', role: 'admin' }); // coincidencia parcial
expect(user).toEqual({ id: 1, email: 'admin@becomeqa.com', role: 'admin' }); // coincidencia exacta
// Strings
expect(message).toContain('success');
expect(slug).toMatch(/^[a-z0-9-]+$/);Negación: not
Cada aserción puede negarse con .not:
await expect(page.getByRole('dialog')).not.toBeVisible();
await expect(page.getByText('Error')).not.toBeAttached();
expect(response.status()).not.toBe(404);Soft assertions: recolectar todos los fallos
Por defecto, la primera aserción fallida detiene el test. Las soft assertions continúan ejecutándose incluso después de un fallo y reportan todos los fallos al final:
test('los datos del dashboard son correctos', async ({ page }) => {
await page.goto('/dashboard');
// Estas no se detienen ante el primer fallo
await expect.soft(page.getByRole('heading')).toHaveText('Mis Ítems de Viaje');
await expect.soft(page.getByRole('row')).toHaveCount(6);
await expect.soft(page).toHaveURL('/dashboard');
// Esto lanza si alguna soft assertion anterior falló
expect(test.info().errors).toHaveLength(0);
});Usa soft assertions cuando quieres ver todo lo que está roto en una sola ejecución del test, no solo el primer fallo.
Mensajes de aserción personalizados
Cuando una aserción falla, Playwright muestra qué se esperaba vs. qué se encontró. Puedes agregar un mensaje personalizado para hacer los fallos más legibles:
await expect(page.getByRole('heading'), 'La página debería mostrar el dashboard después del login')
.toHaveText('Mis Ítems de Viaje');
expect(items.length, `Se esperaban 5 ítems pero se obtuvieron ${items.length}`)
.toBe(5);Configurar timeouts
El timeout de aserción por defecto es 5 segundos. Puedes sobreescribirlo por aserción o de forma global:
// Por aserción (10 segundos para una operación lenta)
await expect(page.getByText('Reporte listo')).toBeVisible({ timeout: 10000 });
// Global en playwright.config.ts
export default defineConfig({
expect: {
timeout: 10000, // todas las aserciones esperan hasta 10 segundos
},
});No aumentes los timeouts para corregir tests inestables. Un test inestable con un timeout más largo sigue siendo inestable. Solo falla más despacio. Corrige la causa raíz.
Errores comunes
Usar page.locator().isVisible() en lugar de expect().toBeVisible()
// Mal: verifica una vez, sin reintento, devuelve booleano
const visible = await page.getByText('Éxito').isVisible();
expect(visible).toBe(true);
// Bien: reintenta hasta que sea visible o timeout
await expect(page.getByText('Éxito')).toBeVisible();La primera versión puede fallar de forma intermitente porque verifica exactamente una vez. La segunda reintenta.
Asertar sobre locators desactualizados
// No guardes locators antes de una navegación y los asertes después
const heading = page.getByRole('heading');
await page.goto('/nueva-pagina');
await expect(heading).toHaveText('Nueva Página'); // puede estar desactualizado
// Mejor: crear el locator cerca de la aserción
await page.goto('/nueva-pagina');
await expect(page.getByRole('heading')).toHaveText('Nueva Página');Usar expect(await locator.textContent()).toBe(...) en lugar de toHaveText
// Mal: evalúa una vez, sin reintento
expect(await page.getByRole('heading').textContent()).toBe('Dashboard');
// Bien: reintenta con espera automática
await expect(page.getByRole('heading')).toHaveText('Dashboard');Asertar count en listas dinámicas
Si una lista carga de forma asíncrona, aserta el count después de confirmar que la lista es visible:
await expect(page.getByRole('list')).toBeVisible(); // esperar a que aparezca la lista
await expect(page.getByRole('listitem')).toHaveCount(5); // luego contar los ítemsUn test completo usando múltiples tipos de aserciones
import { test, expect } from '@playwright/test';
test('el usuario puede agregar y ver un ítem de viaje', async ({ page }) => {
await page.goto('/');
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();
// Aserción de página: verificar navegación
await expect(page).toHaveURL('/dashboard');
// Aserción de visibilidad
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
// Aserción de texto
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Mis Ítems de Viaje');
// Agregar un nuevo ítem
await page.getByRole('button', { name: 'Add Item' }).click();
await page.getByLabel('Destination').fill('Tokio');
await page.getByRole('button', { name: 'Save' }).click();
// Aserción de count: la tabla debería tener una fila más
const rows = page.getByRole('row');
await expect(rows).toHaveCount(7); // header + 6 ítems
// Aserción de contenido de texto en la nueva fila
await expect(page.getByRole('cell', { name: 'Tokio' })).toBeVisible();
// Aserción de estado en el nuevo ítem
const tokyoRow = page.getByRole('row').filter({ hasText: 'Tokio' });
await expect(tokyoRow.getByRole('cell').last()).toHaveText('Planned');
});FAQ
¿Por qué mi aserción pasa en local pero falla en CI?Timing. Las máquinas de CI son más lentas. El elemento existe pero tarda más en aparecer. Aumenta el timeout de aserciones en playwright.config.ts o investiga por qué el elemento carga más lento en los entornos de CI.
toEqual y toBe?
toBe verifica igualdad por referencia (mismo objeto en memoria, o primitivos idénticos). toEqual verifica igualdad profunda (misma estructura y valores, funciona para objetos y arrays). Para comparar objetos y arrays, usa toEqual. Para strings, números y booleanos, usa toBe.
¿Cuándo usar toMatchObject vs toEqual?
toMatchObject es una coincidencia parcial: el objeto real puede tener más propiedades de las que especificas. toEqual requiere una coincidencia exacta. Para respuestas de API donde quieres verificar campos clave sin listar cada campo, usa toMatchObject.
Mi toHaveText falla porque el texto real tiene espacios adicionales. ¿Cómo lo corrijo?
toHaveText recorta automáticamente los espacios al inicio y al final. Para espacios internos (múltiples espacios, saltos de línea), usá regex: toHaveText(/destino:\s+Tokio/i).
→ See also: Fixtures Personalizados en Playwright: El Patrón que Hace los Tests Legibles | Depurando Tests Inestables: Una Guía Práctica