Cypress: reportes con Mochawesome y Allure (y por qué se peleaban entre sí)

Sumo Mochawesome y Allure a mi suite Cypress + TS: el choque en after:run, cypress-on-fix, un run script que no se corta y failed vs broken.

Overview del reporte de Allure 3 en un archivo: dona 96.82%, Roto 2 / Aprobado 61, bloque Metadatos (framework, navegador, AUT, API) y el árbol de suites.
Allure 3 en un solo archivo: la vista Resultados con el desglose Roto/Aprobado, los metadatos cargados desde la config y el árbol de suites.

Hasta este punto de la serie corría la suite y miraba el resultado en la terminal. Los cuadritos verdes, el 63 tests, 2 failing, y listo. Eso me sirve a mí, en mi máquina, mientras estoy mirando.

Resumen de Cypress en la terminal al terminar: 8 specs, 63 tests, 61 passing y 2 failing en clientes-data.cy.ts, con tiempos por spec y el total "1 of 8 failed".
El punto de partida: el resumen en la terminal. Sirve mientras mirás, pero se pierde al cerrar la consola. De ahí la necesidad de un reporte.

El problema es que ahí se termina: cierro la consola y no queda nada. Nadie más ve qué falló, con qué captura, ni hace cuánto que ese test viene fallando.

Un reporte es justamente eso: la forma de mostrar el trabajo a otra persona —o a un yo del futuro que abre el repo dentro de tres meses—. Y para una suite que quiero mostrar como portfolio, el reporte no es un extra: es la mitad del punto.

En este post sumo dos reporters a la suite de Cypress + TypeScript: Mochawesome y Allure. No fue "instalar y listo": los dos se peleaban por el mismo evento de Cypress, mi comando para correrlos se cortaba a la mitad, y conseguir un reporte de un solo archivo para presentar me hizo pasar por tres versiones de Allure hasta que anduvo. Esos tropiezos —y cómo los resolví— son el centro de esto.

Por qué dos reporters y no uno

Podría haber elegido uno. Puse los dos a propósito, porque resuelven cosas distintas:

  • Mochawesome genera un HTML autocontenido: un solo archivo, con las capturas embebidas adentro. Es liviano, se abre con doble clic y se adjunta a un mail o se sube como artifact de CI sin drama. Es el reporte "de bolsillo".
Reporte de Mochawesome con panel lateral de filtros (Show Passed/Failed), navegación por specs y la suite de API testing con sus tests en verde y sus tiempos.
Mochawesome: un HTML autocontenido con filtros por estado y navegación por specs. El reporte "de bolsillo" para compartir rápido.
  • Allure es un dashboard más completo: agrupa por suites, tiene categorías de fallas, timeline, severidad, y —lo más interesante— historial entre corridas (la pestaña Trends), que te muestra la tendencia de tests que pasan y fallan a lo largo del tiempo.
Overview del reporte de Allure 3 en un archivo: dona 96.82%, Roto 2 / Aprobado 61, bloque Metadatos (framework, navegador, AUT, API) y el árbol de suites.
Allure 3 en un solo archivo: la vista Resultados con el desglose Roto/Aprobado, los metadatos cargados desde la config y el árbol de suites.

No es "uno o el otro". La forma más simple que encontré de pensarlo: Mochawesome es el que compartís, Allure es el que presentás. Mochawesome es un archivo que le mandás a alguien por mail o pegás en un PR —"mirá cómo quedó esta corrida"— y lo abre con doble clic, sin instalar nada. Allure es el dashboard que ponés adelante cuando lo que querés mostrar es evolución: la tendencia, las categorías de fallas, las métricas que hablan el idioma de management.

Hay un matiz importante acá, porque es fácil creer que "Allure = servidor" y "Mochawesome = archivo". No es tan así: Allure clásico (la versión 2) genera una carpeta multi-archivo que necesita servirse por HTTP (allure open), pero Allure 3 puede generar directamente un HTML único que se abre con doble clic, igual que Mochawesome. Ese archivo único de Allure es el que sirve para presentar resultados sin que nadie del otro lado tenga que levantar un servidor —en mi trabajo hago exactamente eso—. Tener los dos reporters me obligó, además, a entender cómo Cypress deja (y no deja) engancharlos.

Viniendo de otros frameworks la diferencia se nota. En Playwright los reporters son nativos: le pasás un array en la config (reporter: [['html'], ['list']]) y listo, conviven sin que tengas que hacer nada. En la serie de Selenium con Java usaba Allure a través de un listener de TestNG, que es el mecanismo estándar de ese mundo. En Cypress no hay nada de eso incluido: los reporters son piezas de terceros que uno arma, y ahí aparece el trabajo real.

La instalación

Cuatro paquetes de desarrollo:

npm install -D cypress-mochawesome-reporter allure-cypress allure cypress-on-fix
  • cypress-mochawesome-reporter: el wrapper zero-config de Mochawesome que ya embebe las capturas.
  • allure-cypress: el plugin de Allure para Cypress (del repo allure-framework/allure-js). Es el que va escribiendo los resultados durante la corrida.
  • allure: el CLI de Allure 3 que toma esos resultados y arma el HTML. Ojo con esto: es la versión nueva de Allure (no allure-commandline, que es la 2). Allure 3 está reescrito en JS —no necesita Java— y genera el reporte de un solo archivo de forma nativa, sin herramientas extra. Cómo llegué a Allure 3 lo cuento al final, porque fue un camino con varias piedras.
  • cypress-on-fix: el que resuelve el choque entre los dos reporters. Ya llego a por qué.

Lo primero que aprendí es que cada reporter se engancha por un mecanismo distinto, y esa diferencia explica casi todo lo que vino después.

Mochawesome es un reporter de Mocha. Cypress corre sobre Mocha, así que Mochawesome se declara en el campo reporter de la raíz de la config, con sus opciones:

export default defineConfig({
  reporter: 'cypress-mochawesome-reporter',
  reporterOptions: {
    reportDir: 'cypress/reports/mochawesome',
    reportPageTitle: 'Cypress + TypeScript — Reporte de ejecución',
    charts: true,
    embeddedScreenshots: true,  // capturas en base64, dentro del HTML
    inlineAssets: true,         // un único index.html, sin carpeta de assets
    saveAllAttempts: false,     // con retries: 2, guardo solo el último intento
    quiet: true,
  },
  // ...
})

Allure no toca el campo reporter. Se engancha por setupNodeEvents, escuchando el ciclo de vida de Cypress, y va escribiendo un archivo JSON por cada test en allure-results/. Después el CLI toma esos JSON y arma el HTML.

Dos reporters, dos formas completamente distintas de meterse en la corrida. Y ahí está la trampa.

El problema real: los dos quieren el mismo evento

Cypress deja registrar handlers para eventos del ciclo de vida —before:run, after:run, after:spec—. Mochawesome necesita after:run para armar su HTML cuando termina todo. Allure necesita after:spec y after:run para cerrar sus resultados.

El detalle que no sabía: Cypress permite un solo handler por evento. Si registrás after:run dos veces, el segundo pisa al primero. No tira error, no avisa nada: simplemente uno de los dos reportes deja de generarse. Corrés la suite, ves que Allure salió bien, y te falta el de Mochawesome (o al revés). Silencioso, que es lo peor.

La solución es cypress-on-fix. Es un paquetito que envuelve el objeto on de Cypress y lo hace acumular handlers en vez de pisarlos. Con eso, los dos reporters pueden registrarse en after:run sin problema:

setupNodeEvents(on, config) {
  // Cypress permite UN handler por evento. Mochawesome y Allure
  // quieren los mismos → el segundo pisa al primero. cypress-on-fix
  // envuelve `on` para permitir varios handlers por evento.
  on = cypressOnFix(on)

  mochawesome(on)
  allureCypress(on, config, {
    resultsDir: 'allure-results',
    environmentInfo: {
      Framework: 'Cypress + TypeScript',
      Navegador: 'Edge',
      AUT: 'demo.serenity.is',
      API: 'restful-booker.herokuapp.com',
    },
  })

  // ...el resto de mis tasks (leer Excel, etc.)
  return config
}

Esto es lo que más me gustó de armar el post: no es un truco raro, es entender una limitación real de Cypress y usar la herramienta que existe para eso. Si alguien me pregunta "¿por qué el cypress-on-fix?", tengo una respuesta concreta: porque sin él, uno de los dos reportes se pierde en silencio.

En el lado del browser cada reporter también pide su registro, en el archivo de soporte:

// cypress/support/e2e.ts
import 'cypress-mochawesome-reporter/register'
import 'allure-cypress'

El && que no alcanzaba

Quería un solo comando: npm run report, que corra la suite y genere los dos reportes de una. Lo primero que probé fue lo obvio:

"report": "cypress run && allure generate allure-results"

Y no funcionó. El motivo es puntual de mi suite: tengo dos fallas intencionales —las "trampas" del Excel de posts anteriores, donde el archivo tiene datos mal a propósito para probar que la validación las detecta—. Eso significa que cypress run siempre termina con exit code 1. Y con &&, si el primer comando falla, el segundo no corre. Resultado: Allure nunca se generaba.

Podría haber usado un ; o un ||, pero eso me ataba a la sintaxis del shell, y yo corro esto en Windows local pero lo voy a correr en Linux en el CI. Así que lo resolví con un scriptcito de Node, que es cross-platform y me deja correr los pasos sin importar cómo salió el anterior:

// scripts/report.js
const { spawnSync } = require('node:child_process')
const { rmSync, existsSync, mkdirSync, copyFileSync } = require('node:fs')

const clean = (dir) => {
  try {
    rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })
  } catch (e) {
    console.warn(`No pude limpiar ${dir} (¿archivo abierto?): ${e.message}`)
  }
}

const run = (command) => spawnSync(command, { stdio: 'inherit', shell: true })

// Limpio lo de esta corrida. OJO: NO borro allure-history.jsonl,
// que es lo que Allure 3 usa para acumular el Trend entre corridas.
clean('allure-results')
clean('allure-report')
clean('cypress/reports')
clean('cypress/screenshots')

run('cypress run --browser edge')  // sale con 1, no importa

// Allure 3 genera el reporte según allurerc.mjs (single file + history)
run('npx allure generate allure-results')

// Copio el HTML único a un nombre claro para compartir
const single = ['allure-report/index.html', 'allure-report/awesome/index.html'].find(existsSync)
if (single) {
  mkdirSync('cypress/reports', { recursive: true })
  copyFileSync(single, 'cypress/reports/allure-single.html')
}

Ese clean() al principio no es un detalle menor: si no borrás allure-results, se acumulan resultados de corridas viejas y te aparecen tests duplicados en el HTML. La única carpeta que no toco es el allure-history.jsonl —enseguida se entiende por qué—.

Fijate que allure generate no lleva ni opciones ni rutas de salida: toda la configuración de Allure 3 vive en un archivo aparte, allurerc.mjs, que la CLI detecta sola:

// allurerc.mjs
import { defineConfig } from 'allure'

export default defineConfig({
  name: 'Cypress + TypeScript — Reporte de ejecución',
  output: './allure-report',
  // El Trend se guarda en un único .jsonl que se va appendeando entre
  // corridas. Reemplaza al copiado manual de la carpeta history que
  // necesitaba Allure 2.
  historyPath: './allure-history.jsonl',
  appendHistory: true,
  plugins: {
    awesome: {
      options: {
        singleFile: true,   // un HTML autocontenido, se abre con doble clic
        reportLanguage: 'en',
      },
    },
  },
})

Dos cosas de esta config valen la pena. La primera: singleFile: true genera el reporte de un solo archivo de forma nativa —esto en Allure 2 no existía y me costó bastante llegar hasta acá (lo cuento al final)—. La segunda: el historyPath es lo que le da vida a las gráficas de tendencia. Allure 3 guarda el historial en un único .jsonl y lo va appendeando solo en cada corrida (con appendHistory: true); no hay que copiar carpetas a mano como en Allure 2. Eso sí, necesita 2 corridas o más para mostrar algo —con una sola no hay nada que comparar—.

Vista Gráficas de Allure 3: Current status (dona 96.83%), Status dynamics con una columna por corrida, Test results by severities y Status transitions.
La vista Gráficas de Allure 3. "Status dynamics" es el equivalente al Trend clásico: una columna por corrida a medida que se acumula el history.

Un cambio de UI a tener en cuenta si venís de Allure 2: en Allure 3 ya no hay una "pestaña Trend" en un sidebar. Los gráficos viven en una vista aparte, Gráficas, que se abre desde el desplegable de arriba a la izquierda (el que dice "Report"). Ahí el equivalente al viejo Trend es el chart "Status dynamics" —una columna por corrida, con pasados y rotos—, más otros como "Durations dynamics" para los tiempos.

Lo que se ve al final

npm run report corre las 8 specs (63 tests), deja los 61 verdes y las 2 trampas del Excel en rojo, y escupe los dos reportes:

Explorador de Windows con dos HTML autocontenidos: allure-single.html (5.359 KB) y mochawesome-index.html (869 KB), ambos del 19/09/2026.
Los dos reportes de un solo archivo, listos para mandar: Allure (5,3 MB) y Mochawesome (869 KB). Doble clic y abren, sin servidor.
  • Mochawesomecypress/reports/mochawesome/index.html: un solo archivo, con el gráfico de estados y las capturas de las fallas embebidas adentro. Lo abrís con doble clic.
  • Allure (un archivo)cypress/reports/allure-single.html: el reporte completo de Allure 3 en un solo HTML, que se abre con doble clic y renderiza en cualquier browser. Es el que mando cuando tengo que presentar resultados sin que nadie levante un servidor.
  • Allure (servido)npm run allure:open levanta un servidor local para navegarlo, útil si querés compartirlo por red. Pero con el single-file ya no es imprescindible: el archivo único hace lo mismo sin servidor.
Detalle en Mochawesome del test fallido "los 91 clientes coinciden con el Excel", con el error de 3 diferencias (ALFKI, ANATR, AROUT) y el stack trace.
La falla del Excel en Mochawesome: el error con las 3 diferencias y el stack. Acá figura como "failing", sin distinguir el tipo.

El overview de Allure 3 muestra el 96.82% de éxito, el desglose Passed/Broken, las suites agrupadas, el environmentInfo que cargué (framework, navegador, AUT, API) y una categoría "Test defects" con las 2 fallas. Cada test tiene sus pasos, y la falla del Excel trae la captura y el detalle de las 3 diferencias encontradas.

Detalle en Allure 3 del test "los 91 clientes coinciden con el Excel" marcado como Roto, con el error de 3 diferencias, pestañas Historial/Reintentos y la captura embebida.
El mismo test en Allure 3: marcado "Roto" (no "Fallido"), con el error, las pestañas Historial/Reintentos/Adjuntos y la captura de la grilla.

El detalle que no esperaba: failed vs broken

Acá apareció algo que no tenía en el radar. En Allure, el filtro de estados de arriba no dice "2 failed". Dice 2 broken (amarillo), con 0 failed (rojo).

Allure distingue dos cosas que Mochawesome mete en la misma bolsa:

  • failed (rojo): falla una aserción esperada, un expect(...).to... que no se cumple.
  • broken (amarillo): salta una excepción no controlada, algo que se rompió.

¿Por qué mis tests del Excel son "broken" y no "failed"? Porque mi validación, cuando encuentra diferencias, hace throw new Error("3 diferencias encontradas..."). Eso es una excepción, no una aserción. Para Allure, un test que lanza un Error está roto, no fallando una comprobación.

En Mochawesome esos mismos dos tests aparecen simplemente como "failing", sin la distinción. No es que uno esté bien y el otro mal: es que cada reporter interpreta distinto qué significa que un test no pase. Es la misma lógica que ya había visto en Allure con Java. Por ahora lo dejo así —la validación por throw es intencional y la explico en su propio post—, pero saber que Allure lee esa diferencia me sirve para el día que quiera que esas fallas figuren en rojo.

Errores reales que aparecieron armando esto

El reporte no solo mostró la suite: mostró problemas que sin reporte se me escapaban.

El más claro: al correr con todo esto armado, un test de la API de Restful-Booker que siempre había pasado, falló. El test pedía un booking y exigía que tuviera la key additionalneeds. Resulta que en Restful-Booker ese campo es opcional: la API no lo devuelve si la reserva se creó sin él. Como el test agarra el primer booking de la lista (cualquiera), a veces le tocaba uno sin ese campo y se caía. Un test flaky clásico, escondido a la vista. El fix fue cambiar have.all.keys(...) (exige exactamente esas keys) por include.keys(...) (exige las obligatorias, permite extras), validando las 5 que sí están siempre.

Y este es exactamente el argumento a favor de tener reportes con historial: un test que falla una de cada cinco veces es invisible en la terminal, pero salta en las gráficas de tendencia de Allure. El reporte no es decoración; es cómo se detecta este tipo de cosas.

Después vinieron los detalles de prolijidad, que también documenté:

  • Cypress tiraba un Warning: We failed to trash... en Windows al intentar vaciar cypress/screenshots (a veces un archivo queda abierto). Como ya limpio esa carpeta desde mi script, apagué esa parte con trashAssetsBeforeRuns: false.
  • El test de "conexión cortada" (que simula caída de red con forceNetworkError) dejaba una captura fantasma. La caída de red genera una excepción no controlada que caía en el hook interno de Allure. Se arregla silenciándola con cy.on('uncaught:exception', () => false), igual que ya hacía el test de error 500.
  • Un DeprecationWarning (DEP0190) de Node por pasarle argumentos a un proceso con shell: true. Se va pasando el comando como un string entero en vez de un array.

Nada grave, pero un reporte que voy a mostrar tiene que salir limpio, sin warnings de fondo.

El camino al single-file de Allure (o cómo me peleé con la versión equivocada)

El reporte de un solo archivo de Allure fue el verdadero pozo del post. Yo quería lo mismo que uso en el trabajo: un HTML único que abro con doble clic. Y con Allure 2 (allure-commandline) eso no existe de fábrica, así que arranqué por el camino largo:

  1. Combinador externo. Sumé allure-single-html-file-js, una herramienta que toma la carpeta del reporte y la embebe en un complete.html. Falló: ERROR: file allure-report/app.js doesnt exists. Resulta que las versiones recientes de Allure 2 (de la 2.40, con la reescritura del "new web") cambiaron la estructura —ya no hay un app.js plano, sino un bundle tipo Vite en assets/— y el combinador no lo entiende.
  2. Fijar una versión vieja. Bajé allure-commandline a la 2.32.0 (la misma que usamos en el trabajo), que todavía usa el formato clásico. El combinador ahora sí corría… pero el reporte quedaba en "Loading…" para siempre, tanto el single-file como el servido. El frontend de la 2.32 tiene dos años y mi Edge actual le rompe algo. Callejón sin salida: estaba atando el proyecto a una versión vieja para sostener un hack, y encima no funcionaba.
  3. Saltar a Allure 3. Acá caí en la cuenta de que estaba peleando contra la herramienta. Allure 3 (el paquete allure) está reescrito, tiene un frontend que renderiza en browsers actuales, y —lo clave— genera el single-file de forma nativa: es una línea en allurerc.mjs (singleFile: true), sin combinadores ni versiones fijadas.
La moraleja me quedó grabada: cuando te encontrás forzando una versión vieja para que un parche siga vivo, muchas veces la salida es al revés —mirar la versión nueva—.

Lo que había intentado resolver con un combinador externo y un downgrade, Allure 3 lo hace con un flag. La contra: la UI de Allure 3 es distinta a la clásica que quizás conozcas del trabajo. Es el mismo Allure, dos generaciones de interfaz.

Lo que queda para el próximo post

Con el allure-history.jsonl ya tengo Trend funcionando localmente: corro dos veces y la tendencia aparece. Pero es una tendencia frágil —ese .jsonl vive en mi carpeta y está gitigneado, así que si lo borro o cambio de máquina, se va—. Es tendencia "de una sola persona en una sola compu".

La tendencia "de verdad" —la que persiste siempre y ve cualquiera que abra el reporte— es la que se arma en el CI, conservando ese historial entre builds. Y ese es el puente natural al próximo post: CI/CD con GitHub Actions (y Bitbucket). Ahí estos dos reportes dejan de ser algo que corro en mi máquina y pasan a generarse en cada push, publicarse como artifacts, y —en el caso de Allure— acumular el historial de forma estable.

Por ahora, la suite ya no solo corre: se documenta a sí misma.


Código en github.com/cesarbeassuarez · el resto de la serie en cesarbeassuarez.dev/tag/cypress