Por qué se rompen los tests E2E cuando cambia la interfaz

Por

Los tests E2E se rompen al cambiar la interfaz porque están anclados a detalles frágiles: clases CSS generadas, posiciones como nth(4) o rutas XPath completas. La solución es anclar por lo que el usuario percibe —el rol accesible y el texto del elemento— y reservar los atributos data-testid para lo que no tiene texto ni rol claro. Un selector bueno describe qué es el elemento, no dónde está.

Rediseñan el checkout un martes. El miércoles la suite está en rojo entera y nadie tocó la lógica.

La reacción típica es culpar al rediseño. Pero los tests no se rompieron porque la interfaz cambió: se rompieron porque estaban agarrados de cosas que iban a cambiar.

Un selector es una promesa

Cuando escribes un selector estás afirmando: este elemento se va a poder encontrar así en el futuro. La pregunta es qué tan probable es que esa promesa siga siendo cierta.

await page.click('.css-1x7f9k2');

Esa clase la generó una herramienta de estilos. Va a cambiar sola en el próximo build, sin que nadie lo decida.

await page.click('div > div:nth-child(3) > button');

Esto promete que la estructura del HTML no va a cambiar. Es la promesa más frágil que existe: alguien envuelve un contenedor y se rompe.

await page.getByRole('button', { name: 'Confirmar compra' });

Esta promete que va a seguir habiendo un botón que dice “Confirmar compra”. Si eso cambia, cambió el producto, y entonces sí corresponde actualizar el test.

Esa es la diferencia. Un buen selector se rompe cuando cambia el comportamiento; uno malo se rompe cuando cambia el aspecto.

El orden de preferencia

De más estable a más frágil:

1. Rol accesible y texto

page.getByRole('button', { name: 'Confirmar compra' })
page.getByLabel('Número de tarjeta')
page.getByRole('heading', { name: 'Tu pedido' })

Es lo que percibe el usuario y lo que percibe un lector de pantalla. Si esto cambia, cambió algo real.

2. Atributo de prueba

page.getByTestId('checkout-submit')

Un contrato explícito entre quien escribe la interfaz y quien escribe el test. Estable, pero exige que alguien mantenga los data-testid en el código de producción.

Úsalo para lo que no tiene texto ni rol claro: un contenedor, una fila de tabla, un icono suelto.

3. Texto visible

page.getByText('Gracias por tu compra')

Bien, hasta que el equipo de contenido decide que ahora dice “¡Listo!”.

4. CSS con clases propias

page.locator('.checkout-submit-button')

Aceptable si son clases escritas a mano y con intención. Frágil si las genera una herramienta.

5. Posición

page.locator('.producto').nth(4)

Evítalo. Y no solo porque se rompe: a veces no se rompe y hace clic en el elemento equivocado. El test sigue, pasa, y probó otra cosa. Es una fábrica de falsos verdes.

La trampa de los grabadores

Casi todos los grabadores —incluido el codegen de Playwright— generan selectores del nivel 4 o 5 cuando el elemento no tiene texto ni rol claro. Es entendible: el grabador ve el DOM, no la intención.

Si grabas un flujo y publicas el resultado sin revisarlo, estás heredando esa fragilidad completa.

Lo que hay que revisar después de grabar:

  • Todo nth(...) que aparezca.
  • Toda clase que parezca generada: css-1x7f9k2, _button_1a2b3, sc-bdVaJa.
  • Todo XPath largo.
  • Todo selector que apunte a un contenedor en vez de al control.

Cinco minutos de revisión ahorran semanas de mantenimiento.

Cuando el elemento no tiene de dónde agarrarse

A veces el HTML sencillamente no da opciones: un icono sin etiqueta, un div clicable, una celda sin encabezado.

Ahí hay dos caminos honestos:

Pedir un data-testid. Es una línea en el código de producción y convierte un selector frágil en uno estable para siempre.

Anclar desde un vecino estable.

page.getByRole('row', { name: 'Lámpara de mesa Fjord' })
    .getByRole('button', { name: 'Eliminar' })

En vez de buscar el botón por posición global, lo buscas dentro de la fila que sí puedes identificar. Este patrón resuelve la mayoría de los casos difíciles.

Una prueba de resistencia

Antes de dar por bueno un test, pregúntate qué pasaría si:

  • Cambian el color y el tamaño de todos los botones. (Si se rompe, dependes de estilos.)
  • Agregan un banner arriba de la lista. (Si se rompe, dependes de posiciones.)
  • Cambian “Confirmar compra” por “Pagar ahora”. (Si se rompe, es correcto: cambió el producto.)
  • Envuelven la sección en un contenedor nuevo. (Si se rompe, dependes de la estructura.)

Solo el tercer caso justifica que un test falle.

Y si ya tienes cientos de selectores frágiles

No los reescribas todos de golpe. Lo que funciona:

  1. Cada vez que un test se rompa por un cambio de interfaz, arregla el selector bien, no con otro parche frágil.
  2. Empieza por los flujos que más se rompen. Un registro de qué falló el último mes te dice cuáles son.
  3. Negocia data-testid para los diez elementos que más problemas dan, no para toda la aplicación.

En tres meses la suite es otra, sin haber parado a reescribirla.


Si estás montando la suite desde cero, la guía de automatización E2E tiene el orden completo. Y si tu problema es que los tests fallan de forma intermitente en vez de siempre, eso es otra cosa: tests flaky.

Oráculo graba los flujos desde el navegador y guarda el spec de Playwright resultante para que puedas revisar y corregir cada selector: el código es tuyo y es legible, no una caja negra.

Sigue leyendo