Hay una diferencia enorme entre tener tests E2E y tener una arquitectura de testing E2E.

La primera te da una carpeta llena de specs.

La segunda te da una suite que podés mantener, leer, diagnosticar y escalar sin querer prender fuego el pipeline cada dos semanas.

Y esa diferencia no la define Playwright, Cypress ni Selenium.

La define cómo pensás la estructura.

Porque automatizar un flujo no es difícil. Lo difícil es que ese flujo siga siendo entendible cuando el producto cambia, cuando falla en CI, cuando entra otra persona al equipo o cuando tenés que explicar qué evidencia dejó el test.

Ahí aparece el problema de fondo: mucha gente escribe E2E como si estuviera grabando pasos.

Ir a login.
Completar email.
Completar password.
Click en ingresar.
Validar dashboard.

Eso puede funcionar para una demo. Pero en un proyecto real, esa forma de escribir tests se convierte rápido en deuda técnica.

Un test E2E no debería ser una coreografía de clicks. Debería ser una verificación de comportamiento con capas claras.

El error: meter todo en el spec

El síntoma más común de una suite inmadura es el spec gigante.

El archivo de test tiene selectors, navegación, datos, asserts, waits, lógica condicional, screenshots, llamadas a APIs, helpers improvisados y comentarios que intentan explicar el quilombo.

Algo así:

test("login", async ({ page }) => {
  await page.goto("/login")
  await page.fill("#email", "user@test.com")
  await page.fill("#password", "123456")
  await page.click("button[type='submit']")
  await expect(page.locator(".dashboard-title")).toBeVisible()
})

¿Está mal porque no funciona?

No necesariamente.

Está mal porque mezcla responsabilidades.

El spec sabe demasiado. Sabe cómo se navega, qué selectors usa la UI, cómo se ejecuta el flujo y qué debería validar. Eso al principio parece práctico, pero después cada cambio visual rompe veinte tests y nadie sabe si falló el producto, el selector, el dato o el diseño del test.

Ese es el momento donde el equipo empieza a decir: “los E2E son frágiles”.

Y no. Muchas veces lo frágil no es E2E.

Lo frágil es la arquitectura que escribimos alrededor.

La idea: separar responsabilidades

Una suite E2E mantenible necesita capas. No por moda. No para hacerla “enterprise”. Para que cada pieza tenga un motivo claro para cambiar.

Una separación simple y efectiva puede ser esta:

selectors -> pages -> steps -> specs

Cada capa responde una pregunta distinta:

  • Selectors: ¿cómo encuentro los elementos?
  • Pages: ¿qué acciones puedo hacer en esta pantalla?
  • Steps: ¿qué comportamiento de negocio ejecuto o valido?
  • Specs: ¿qué escenario quiero cubrir?

La clave está en no saltearse capas cuando el proyecto empieza a crecer.

Porque si el spec importa selectors directamente, rompiste el aislamiento. Si el step empieza a tener veinte locators sueltos, mezclaste comportamiento con detalles de UI. Si la page decide escenarios de negocio, la convertiste en un dios escondido.

Y los dioses escondidos en testing siempre terminan cobrando intereses.

Selectors: el contrato con la interfaz

Los selectors deberían ser lo más aburrido del mundo.

Eso es bueno.

Su trabajo no es testear. No es navegar. No es decidir. Solo exponer una forma estable de encontrar elementos.

Cuando los selectors están desperdigados por todos lados, cualquier cambio de markup se vuelve una cacería.

Hoy cambió #submit por [data-testid="login-submit"] y tenés que buscar en quince archivos. Mañana el botón cambia de texto por una decisión de UX y se rompen tests que ni siquiera deberían depender de esa copia exacta.

Por eso prefiero que los selectors vivan centralizados y con intención:

export const loginSelectors = {
  emailInput: "[data-testid='login-email']",
  passwordInput: "[data-testid='login-password']",
  submitButton: "[data-testid='login-submit']",
  errorMessage: "[data-testid='login-error']",
}

No hay magia. Hay orden.

Y el orden, en suites grandes, vale más que un truco elegante.

Pages: acciones de pantalla, no casos de negocio

La page object debería representar capacidades de una pantalla.

Completar un campo. Hacer click. Leer un mensaje. Esperar que una vista esté lista.

No debería decidir si el login es válido, si el usuario tiene permisos o si el flujo completo de compra terminó bien.

Una page sana tiene métodos chicos y explícitos:

export class LoginPage {
  constructor(page) {
    this.page = page
  }

  async fillEmail(email) {
    await this.page.locator(loginSelectors.emailInput).fill(email)
  }

  async fillPassword(password) {
    await this.page.locator(loginSelectors.passwordInput).fill(password)
  }

  async submit() {
    await this.page.locator(loginSelectors.submitButton).click()
  }
}

Esto no es burocracia.

Es poner cada ladrillo donde corresponde.

Si mañana cambia el selector, tocás selectors. Si cambia la interacción con la pantalla, tocás page. Si cambia la regla de negocio, tocás steps o specs. Esa separación te permite entender el impacto del cambio.

Sin eso, todo cambio parece una cirugía a corazón abierto.

Steps: donde aparece el lenguaje de negocio

Los steps son la capa que más valor suele aportar y la que más se subestima.

Un step no debería ser “click en botón”. Para eso ya tenés pages.

Un step debería expresar una acción o validación con sentido funcional:

export async function loginWithCredentials(page, credentials) {
  const loginPage = new LoginPage(page)

  await loginPage.fillEmail(credentials.email)
  await loginPage.fillPassword(credentials.password)
  await loginPage.submit()
}

Y en proyectos donde la evidencia importa, el step también debería envolver reporting:

export async function loginWithCredentials(page, testInfo, credentials) {
  await businessStep(testInfo, "Iniciar sesión con credenciales válidas", async () => {
    const loginPage = new LoginPage(page)

    await loginPage.fillEmail(credentials.email)
    await loginPage.fillPassword(credentials.password)
    await loginPage.submit()
  })
}

Ahí el test deja de ser una lista muda de instrucciones y empieza a dejar evidencia entendible.

Eso importa más de lo que parece.

Cuando un test falla, no querés leer veinte líneas de código para entender qué estaba intentando validar. Querés abrir el reporte y ver:

1. Navegar al login
2. Iniciar sesión con credenciales válidas
3. Validar acceso al dashboard

Eso le habla al QA, al dev, al líder técnico y a cualquier persona que tenga que diagnosticar la falla.

El reporting no es decoración. Es trazabilidad.

Specs: escenarios, no implementación

El spec debería ser la capa más fácil de leer.

Si está bien diseñado, una persona debería entender el escenario sin conocer los selectors ni la estructura interna de la pantalla.

test("permite iniciar sesión con credenciales válidas", async ({ page }, testInfo) => {
  await openLogin(page, testInfo)
  await loginWithCredentials(page, testInfo, validUser)
  await expectDashboardVisible(page, testInfo)
})

Eso es mucho más mantenible que un spec lleno de locator, fill, click, waitForTimeout y asserts mezclados.

El spec debería responder:

  • qué escenario cubro,
  • con qué datos,
  • qué comportamiento espero,
  • y qué tags o metadata ayudan a ejecutarlo y reportarlo.

Nada más.

Si el spec necesita explicar cómo está construida la UI, ya está sabiendo demasiado.

El reporting es parte de la arquitectura

Hay equipos que agregan reporting al final, como si fuera una capa cosmética.

Error.

El reporting define cómo vas a entender la suite cuando falle. Y una suite que no se puede diagnosticar rápido es una suite cara.

No alcanza con saber que falló login.spec.js:34.

Necesitás saber:

  • qué flujo estaba ejecutando,
  • qué paso funcional falló,
  • qué datos usó,
  • qué evidencia dejó,
  • si falló el producto, el ambiente o el test.

Por eso me gusta que el reporting viva en steps y no desperdigado en specs.

El spec declara escenarios. El step deja evidencia. La page ejecuta interacciones. El selector conoce la UI.

Cada cosa en su lugar.

Qué pasa cuando no separás capas

Cuando todo vive en el spec, los problemas aparecen rápido:

  • Cambia un selector y rompés tests que no tienen nada que ver con ese cambio.
  • Cambia el copy de un botón y falla media suite.
  • Repetís el mismo flujo de login en veinte archivos.
  • Los reportes muestran pasos técnicos que nadie de negocio entiende.
  • Los tests pasan, pero no queda claro qué validaron.
  • Los errores son difíciles de diagnosticar.
  • Agregar un nuevo escenario implica copiar y pegar medio archivo viejo.

Eso no es velocidad. Es deuda.

Y ya escribí sobre esto antes: un test automatizado puede mentir si valida lo incorrecto, aunque esté verde. También puede amplificar basura si primero no hay criterio, como conté en por qué QA manual sigue siendo clave antes de automatizar.

La arquitectura de testing no resuelve la falta de criterio, pero ayuda a que el criterio tenga una forma mantenible.

Cómo sé si mi arquitectura E2E está sana

Hay algunas preguntas incómodas que sirven como termómetro:

¿Puedo leer el spec sin mirar selectors?

Si para entender el escenario tengo que leer locators, la abstracción está floja.

¿Puedo cambiar un selector tocando un solo lugar?

Si un cambio visual te obliga a editar diez specs, la UI está demasiado acoplada al test.

¿Los steps hablan en lenguaje funcional?

“Completar email” es una acción técnica. “Iniciar sesión con credenciales válidas” es comportamiento.

¿El reporte explica qué pasó?

Si el reporte solo dice que falló un locator, todavía te falta evidencia funcional.

¿Puedo reutilizar flujos sin copiar y pegar?

Si cada spec reescribe login, setup o navegación, la suite está creciendo mal.

¿El test falla por el motivo correcto?

Una suite confiable no es la que nunca falla. Es la que falla cuando tiene que fallar y deja claro por qué.

No conviertas la arquitectura en religión

También hay que decir esto: separar capas no significa llenar el proyecto de abstracciones inútiles.

No todo flujo necesita una mega estructura. No toda pantalla necesita una clase enorme. No todo helper merece existir.

La arquitectura sirve si reduce costo de cambio.

Si solo agrega ceremonia, es teatro técnico.

La pregunta no es “¿estoy usando Page Object Model?”.

La pregunta es:

¿mi suite es más fácil de entender, modificar y diagnosticar gracias a esta estructura?

Si la respuesta es no, estás diseñando para tu ego, no para el equipo.

Lo que me quedó claro

Una buena suite E2E no se mide por cuántos tests tiene.

Se mide por cuánta confianza genera y cuánto cuesta mantenerla.

Selectors, pages, steps y specs no son carpetas para parecer prolijo. Son límites de responsabilidad.

El selector conoce la interfaz.

La page conoce la pantalla.

El step conoce el comportamiento.

El spec conoce el escenario.

Y el reporting cuenta la historia cuando algo falla.

Cuando esas piezas están mezcladas, la suite se vuelve frágil, ruidosa y cara. Cuando están bien separadas, la automatización deja de ser una colección de clicks y empieza a parecerse a un sistema de calidad.

Ese es el punto.

No automatizar más.

Automatizar mejor.