Cypress Reports: How to Generate, Configure, and Share Them

By Alex Gandy May 20, 2026

The shortest path to a useful Cypress report is two packages, a five-line config block, and a CI step that uploads the artifact somewhere your team can actually open it. Skip the “Cypress is a popular framework” intro. Here’s the config:

cypress.config.js
const { defineConfig } = require("cypress");
module.exports = defineConfig({
reporter: "cypress-multi-reporters",
reporterOptions: {
reporterEnabled: "cypress-mochawesome-reporter, mocha-junit-reporter",
cypressMochawesomeReporterReporterOptions: {
reportDir: "cypress/reports/mocha",
embeddedScreenshots: true,
inlineAssets: true,
},
mochaJunitReporterReporterOptions: {
mochaFile: "cypress/reports/junit/results-[hash].xml",
toConsole: false,
},
},
e2e: {
setupNodeEvents(on, config) {
require("cypress-mochawesome-reporter/plugin")(on);
},
},
});

That runs every spec through two reporters in one pass: an HTML report (with screenshots and a failure timeline) and a JUnit XML file that any CI dashboard, GitHub Actions check, or test-analytics platform can ingest. The rest of this guide explains each piece, then walks through what to do with the resulting files once CI starts generating one per push, per spec, per shard.

What is a Cypress test report?

A Cypress test report is the structured output of a Cypress run: which specs ran, which assertions passed and failed, screenshots and videos for failures, durations, and any captured logs. The default Cypress CLI prints a human-readable summary to stdout. Adding a reporter writes that information to disk in a format another tool can parse: HTML for humans, JUnit XML or JSON for CI systems and dashboards.

There are two layers worth keeping separate:

  1. The reporter decides the format. Cypress ships with a few built-ins (spec, min, dot, nyan, json, junit) and accepts any reporter Mocha understands as a plugin.
  2. The hosting/analysis layer decides what happens to that file once it exists. The reporter writes results.xml to cypress/reports/junit/. Whether your team ever sees it is a separate problem.

Most Cypress-report tutorials stop at layer one. The interesting part is layer two, which this guide gets to below.

How to get a report in Cypress?

The fastest path to a Cypress report is the --reporter CLI flag with a built-in:

Terminal window
npx cypress run --reporter junit \
--reporter-options "mochaFile=cypress/reports/junit/results.xml,toConsole=false"

That writes cypress/reports/junit/results.xml after the run. For HTML reports, install cypress-mochawesome-reporter and configure it in cypress.config.js as shown above. For multiple reporters in one pass, use cypress-multi-reporters.

The CLI flag and the config-file approach behave identically. CI workflows usually pin the config in cypress.config.js so the reporter setup lives in version control, then call npx cypress run with no extra flags.

Built-in Cypress reporters

Cypress inherits its reporter system from Mocha. The built-ins are useful for local terminal output and the simplest possible CI integration, but they’re not what you reach for once you want HTML, screenshots, or stable cross-run analytics.

Spec reporter (default)

The default. Prints each spec file as it runs, with a green check or red X per test. Good for local development, useless as a CI artifact because it only exists in the terminal scrollback.

Terminal window
npx cypress run
# Same as: npx cypress run --reporter spec

Mocha-based reporters (min, dot, nyan)

min, dot, and nyan are progress-style reporters inherited from Mocha. min shows just the final summary, dot prints one character per test, nyan is a cat. They’re terminal-only and don’t write any files, so they’re irrelevant for CI but handy when you want quieter local output:

Terminal window
npx cypress run --reporter dot

JUnit XML reporter

The most useful built-in. Cypress includes a JUnit reporter that emits the XML format every major CI provider knows how to display:

Terminal window
npx cypress run --reporter junit \
--reporter-options "mochaFile=cypress/reports/junit/results-[hash].xml"

The [hash] placeholder is critical when running specs in parallel. Without it, every parallel spec overwrites the same file. With it, each spec writes its own file and you merge them later.

JUnit XML is the lowest-common-denominator format for test results. GitHub Actions can render it inline with dorny/test-reporter, GitLab CI has a built-in junit: artifact type, and test-analytics platforms (Gaffer included) treat JUnit as a first-class input. For a deeper look at the schema and where it shows up in CI pipelines, see the JUnit XML format guide.

Built-ins cover the terminal and CI summary cases. Once you want a shareable HTML report with screenshots, or a richer JSON for analytics, you reach for a plugin.

cypress-mochawesome-reporter (HTML reports with screenshots)

The most widely-used Cypress HTML reporter. Generates a self-contained HTML file with collapsible test trees, embedded screenshots, video links, and a pass/fail summary. The cypress-mochawesome-reporter package is the Cypress-aware fork of the original mochawesome plugin and handles Cypress-specific quirks like the before:run and after:spec event hooks.

Install:

Terminal window
npm install --save-dev cypress-mochawesome-reporter

Configure:

cypress.config.js
const { defineConfig } = require("cypress");
module.exports = defineConfig({
reporter: "cypress-mochawesome-reporter",
reporterOptions: {
reportDir: "cypress/reports",
charts: true,
reportPageTitle: "E2E Test Results",
embeddedScreenshots: true,
inlineAssets: true,
},
e2e: {
setupNodeEvents(on, config) {
require("cypress-mochawesome-reporter/plugin")(on);
},
},
});

After a run, open cypress/reports/index.html in a browser. inlineAssets: true produces a single-file HTML report you can email, attach to a Slack message, or upload as a CI artifact without losing styling.

Allure Cypress reporter

Allure is the heavier-weight alternative to Mochawesome. It generates richer reports with test history, attachments, severity tagging, and step-by-step execution traces. The trade-off is setup complexity and bundle size: Allure reports require both a result-generation step (during the Cypress run) and a separate report-build step using the Allure CLI.

Terminal window
npm install --save-dev @shelex/cypress-allure-plugin allure-commandline
cypress.config.js
const { defineConfig } = require("cypress");
const allureWriter = require("@shelex/cypress-allure-plugin/writer");
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
allureWriter(on, config);
return config;
},
env: {
allure: true,
},
},
});

Run, then build the report:

Terminal window
npx cypress run
npx allure generate ./allure-results --clean -o ./allure-report
npx allure open ./allure-report

Pick Allure when you need test-history charts and step-level traces. Pick Mochawesome when you want one HTML file you can share without a CLI.

CTRF reporter (portable JSON format)

CTRF (Common Test Report Format) is a standardized JSON schema designed to be framework-agnostic. The point of CTRF is that the same JSON shape comes out of Cypress, Playwright, Jest, Vitest, and pytest, so downstream tools (CI dashboards, Slack notifiers, test-analytics platforms) only need to parse one format.

Terminal window
npm install --save-dev cypress-ctrf-json-reporter
cypress.config.js
const { defineConfig } = require("cypress");
const { GenerateCtrfReport } = require("cypress-ctrf-json-reporter");
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
new GenerateCtrfReport({ on });
},
},
});

CTRF is the right choice if you’re standardizing report formats across multiple frameworks in the same org. If you only run Cypress, JUnit XML covers the same ground with broader tooling support today.

How to configure a custom reporter in cypress.config.js

All Cypress reporter configuration lives in cypress.config.js (or cypress.config.ts for TypeScript projects). There are three patterns to know.

Single reporter setup

The simplest case. Set reporter and reporterOptions at the top level:

const { defineConfig } = require("cypress");
module.exports = defineConfig({
reporter: "cypress-mochawesome-reporter",
reporterOptions: {
reportDir: "cypress/reports",
charts: true,
inlineAssets: true,
},
e2e: {
setupNodeEvents(on, config) {
require("cypress-mochawesome-reporter/plugin")(on);
},
},
});

Multiple reporters at once

cypress-multi-reporters lets you run two or more reporters in a single pass. Useful when you want a JUnit XML for CI annotations and an HTML report for humans:

Terminal window
npm install --save-dev cypress-multi-reporters mocha-junit-reporter cypress-mochawesome-reporter
const { defineConfig } = require("cypress");
module.exports = defineConfig({
reporter: "cypress-multi-reporters",
reporterOptions: {
reporterEnabled: "cypress-mochawesome-reporter, mocha-junit-reporter",
cypressMochawesomeReporterReporterOptions: {
reportDir: "cypress/reports/mocha",
embeddedScreenshots: true,
inlineAssets: true,
},
mochaJunitReporterReporterOptions: {
mochaFile: "cypress/reports/junit/results-[hash].xml",
toConsole: false,
},
},
e2e: {
setupNodeEvents(on, config) {
require("cypress-mochawesome-reporter/plugin")(on);
},
},
});

The naming convention is strict: each reporter’s options key is <reporterNameInCamelCase>ReporterOptions. cypress-mochawesome-reporter becomes cypressMochawesomeReporterReporterOptions.

Merging reports across spec files (parallel / sharded runs)

This is the part no tutorial covers in depth, and it’s where most teams trip the first time they shard Cypress across multiple CI runners.

When Cypress runs in parallel (cypress run --parallel), each spec file runs in its own process. Each process writes its own report file. You end up with cypress/reports/mocha/mochawesome.json, mochawesome_001.json, mochawesome_002.json, one per spec. The HTML viewer only knows how to render one report at a time.

The fix is the mochawesome-merge package plus mochawesome-report-generator:

Terminal window
npm install --save-dev mochawesome-merge mochawesome-report-generator

After the Cypress run completes, merge the JSON fragments and rebuild the HTML:

Terminal window
# After cypress run finishes
npx mochawesome-merge "cypress/reports/mocha/*.json" > cypress/reports/mocha/merged.json
npx marge cypress/reports/mocha/merged.json \
--reportDir cypress/reports/mocha \
--inline \
--reportFilename index

The result is cypress/reports/mocha/index.html containing every spec from every shard in a single browsable report.

For JUnit XML, the merge is simpler because most CI systems accept multiple XML files as a glob (cypress/reports/junit/*.xml). GitHub Actions, GitLab CI, and Jenkins all handle this natively without a merge step.

Generating Cypress reports in CI

The reporter configuration is the same across CI providers. What changes is how you upload, archive, and surface the resulting files.

GitHub Actions example

A complete workflow that runs Cypress, generates Mochawesome + JUnit reports, merges shards, and uploads everything as artifacts:

.github/workflows/cypress.yml
name: Cypress
on:
push:
branches: [main]
pull_request:
jobs:
cypress-run:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
containers: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Cypress run
uses: cypress-io/github-action@v6
with:
record: false
parallel: true
group: "E2E"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Merge Mochawesome reports
if: always()
run: |
npx mochawesome-merge "cypress/reports/mocha/*.json" > cypress/reports/merged.json
npx marge cypress/reports/merged.json \
--reportDir cypress/reports \
--inline --reportFilename index
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: cypress-reports-${{ matrix.containers }}
path: |
cypress/reports/
cypress/screenshots/
cypress/videos/

The if: always() on the merge step matters. Without it, a single failing spec aborts the workflow before the merge runs and you lose the report. For the full setup including project tokens and reporting back to GitHub Checks, see the GitHub Actions integration docs.

GitLab CI example

GitLab understands JUnit XML natively via the artifacts:reports:junit key:

cypress:
image: cypress/browsers:node-20.10.0-chrome-119
parallel: 4
script:
- npm ci
- npx cypress run --parallel
- npx mochawesome-merge "cypress/reports/mocha/*.json" > cypress/reports/merged.json
- npx marge cypress/reports/merged.json --reportDir cypress/reports --inline --reportFilename index
artifacts:
when: always
paths:
- cypress/reports/
- cypress/screenshots/
- cypress/videos/
reports:
junit: cypress/reports/junit/*.xml

GitLab will surface failed tests in the merge-request widget directly from the JUnit XML. The HTML report and screenshots are downloadable from the job artifacts page.

Hosting and sharing Cypress reports with your team

This is where most guides stop and most real-world setups break.

Why a local HTML file isn’t enough

Your CI just generated cypress/reports/index.html. It’s sitting on a GitHub Actions runner that gets destroyed in 90 days (or 24 hours, if you didn’t set a retention policy). To open it, a teammate has to:

  1. Navigate to the Actions tab.
  2. Find the right workflow run.
  3. Click into the job.
  4. Scroll to the artifacts section.
  5. Download a zip file.
  6. Unzip it.
  7. Open index.html in a browser.

If they want to compare two runs (“did this fail last week too?”), multiply that by two. If they want to look at pass rates across the last fifty runs, the artifact approach falls apart entirely. CI artifacts are storage for the report, not a viewing surface.

The same is true for blob storage: dumping the HTML into an S3 bucket gets you a URL, but you still don’t have cross-run pass rates, flaky-test detection, or a way to filter failures by error pattern. You have a static file with a permalink. That’s a starting point, not the answer.

A dedicated report-hosting layer is what closes the gap. Gaffer’s Cypress test reporting solution ingests JUnit XML (or Mochawesome JSON, or CTRF) from CI and gives you a persistent URL, cross-run history, flaky-test detection, and pass-rate trends without writing any analytics code yourself.

Tracking pass rates across runs

One Cypress report tells you whether the current build passed. Fifty Cypress reports tell you whether your login.spec.ts has been failing intermittently for three weeks while everyone shrugged and re-ran the job.

The data is sitting in your JUnit XML files. Each file has timestamps, durations, status flags, and stack traces. Treat each upload as a row in a time series, and the questions become trivial: which test failed most often this month, which test got slower since the last release, which specs only fail on PR branches and never on main.

This is the layer that pays for the reporter-config work. Generating the report is plumbing. Knowing what changed across the last fifty runs is the actual return.

Sharing a persistent report URL

Slack a CI artifact link to a teammate and the link breaks in 90 days. Slack a hosted-report URL and it works forever (or for the lifetime of your project). The difference matters when an incident postmortem references a CI run from three months ago and the artifact is gone.

A persistent URL also means a non-technical stakeholder, a customer who reported a bug, or a contractor without GitHub access can open the report without navigating CI internals. The hosting layer should be the canonical link, with the CI artifact as a fallback for raw files (videos, traces, screenshots).

If you’re on Playwright instead of Cypress, the Playwright report-sharing setup covers the same problem with framework-specific config.

Uploading Cypress results to Gaffer

The Gaffer CLI accepts JUnit XML, Mochawesome JSON, CTRF, and several other formats directly. The command surface is the upload subcommand from packages/cli/src/main.rs:

Terminal window
gaffer upload ./cypress/reports/junit \
--token $GAFFER_PROJECT_TOKEN \
--commit-sha $GITHUB_SHA \
--branch $GITHUB_REF_NAME \
--test-framework cypress

The flags map one-to-one with the CLI source: --token accepts a gfr_… project token (or falls back to GAFFER_PROJECT_TOKEN, GAFFER_UPLOAD_TOKEN, or GAFFER_TOKEN), --commit-sha and --branch get recorded as tags on the upload session so you can filter runs by branch in the dashboard later, and --test-framework labels the run. Per-file ceiling is 5 GB (raise --max-file-size-mb to 5000); multipart elevation above 90 MB is automatic.

The full GitHub Actions step:

- name: Upload Cypress results to Gaffer
if: always()
uses: gaffer-sh/gaffer-uploader@v2
with:
gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }}
report_path: ./cypress/reports/junit
commit_sha: ${{ github.sha }}
branch: ${{ github.ref_name }}
test_framework: cypress

The if: always() is again deliberate. The runs you most want to track are the ones that failed; gating the upload on success means you lose exactly the data you need. For deeper CI setup including health-score alerts and flaky-test detection, see the Cypress test reporting page.

What is Cypress used for?

Cypress is an end-to-end testing framework for web applications. It runs in a real browser (Chromium, Firefox, Edge, WebKit via experimental support), drives the application the way a user would (clicks, form fills, navigation), and asserts on the resulting DOM state.

Compared to unit tests, Cypress tests exercise the full stack: real network requests, real browser rendering, real cookies and storage. Compared to manual QA, Cypress tests are reproducible, version-controlled, and run on every commit. The trade-off is speed: a Cypress suite that takes ten seconds of test-author time will take twenty minutes of real-browser time once it has a few hundred specs.

Common use cases: smoke tests for critical user flows (signup, checkout, login), regression tests after a deploy, accessibility checks against the rendered DOM, and visual regression testing when paired with a tool like Percy or Chromatic.

Cypress vs Selenium: what’s different about Cypress reporting?

Selenium runs outside the browser and communicates over the WebDriver protocol. Cypress runs inside the browser, in the same JavaScript context as the application under test. The architectural difference shows up in the reports in three ways.

  1. Direct DOM access. Cypress reports capture failures with exact DOM state at the moment of failure, including computed styles and shadow-DOM contents. Selenium failure reports usually carry only the WebDriver-visible state, which is a thinner snapshot.
  2. Built-in screenshots and videos. Cypress records video of every run by default and takes a screenshot on every failure. Selenium needs extra wiring (typically through Allure or a custom listener) to do the same.
  3. Mocha-native reporter ecosystem. Cypress inherits Mocha’s reporter API, so any Mocha reporter (mochawesome, mocha-junit-reporter, mochawesome-merge) plugs in with one config block. Selenium reporting is more heterogeneous: TestNG reports, JUnit, Allure, ExtentReports, all with their own conventions.

Cypress is not “the same as” Selenium. They solve overlapping problems with different architectures. For most JavaScript-heavy SPAs, Cypress’s in-browser model produces more reliable reports with less setup. For polyglot test suites that include Java, Python, and C# clients, Selenium’s WebDriver model is the only thing that works across all of them.

The hosting problem is identical either way. Generating the report is fifteen minutes of config. Making the report useful across hundreds of CI runs is the part that pays back.

Start Free