Screenshot and Video Recording

7 min read

When a test fails on CI at 3 a.m. and the only thing in your inbox is "1 failed", screenshots and videos are what turn that one-line message into an actionable bug report. Cypress captures both by default — automatically on every failure in headless mode, and on demand via cy.screenshot() whenever you want documentation. This lesson covers the three capture types, how to configure where they land, and the recipe for "videos only on failed runs" that every CI-mature team eventually adopts.

Automatic screenshots on failure

In headless mode (npx cypress run), Cypress takes a screenshot every time a test fails — no configuration required. The image lands in cypress/screenshots/<spec>/<test name> (failed).png and shows the page exactly as it was at the moment of failure.

You don't have to do anything to opt in. The default config:

// cypress.config.ts
export default defineConfig({
  e2e: {
    screenshotsFolder: "cypress/screenshots",
    screenshotOnRunFailure: true,
  },
});

To turn it off (rare — almost always keep it on):

e2e: { screenshotOnRunFailure: false }

The interactive runner doesn't capture failure screenshots automatically — you'd be looking at the browser anyway. Screenshots are a CI-only artefact by default.

Manual screenshots — cy.screenshot()

For documentation, evidence, or step-by-step debugging, capture an image at any point in a test:

it("documents the checkout flow", () => {
  cy.visit("/products");
  cy.contains("Add to cart").first().click();
  cy.screenshot("01-product-added");
 
  cy.visit("/cart");
  cy.screenshot("02-cart-page");
 
  cy.get("[data-testid='checkout-btn']").click();
  cy.screenshot("03-checkout-shipping");
});

The string argument becomes the filename. Numeric prefixes keep them ordered alphabetically — useful when generating reports or stepping through the flow visually.

The three capture types

cy.screenshot() accepts a capture option that controls how much of the page lands in the image:

// Viewport only — what the user can see right now (default for cy.screenshot)
cy.screenshot("viewport-only", { capture: "viewport" });
 
// Full page — including content below the fold
cy.screenshot("full-page", { capture: "fullPage" });
 
// Specific element — bounded to one component
cy.get("[data-testid='product-card']").screenshot("product-card");

Each is the right tool for a different job:

Video recording

Cypress records a video of every spec in headless mode by default — one mp4 per .cy.ts file showing every test in execution order, with the command log overlaid. The default config:

e2e: {
  video: true,
  videosFolder: "cypress/videos",
  videoCompression: 32,    // lower = better quality, larger files
}

videoCompression is a constant rate factor: 0 is lossless (huge files), 51 is unwatchable (tiny files). The default 32 is a balanced compromise. Bump to 51 to halve the storage cost on a CI run; drop to 22 if you need pixel-quality video for a stakeholder review.

To turn videos off entirely:

e2e: { video: false }

Most teams want a middle ground — videos when something fails, no videos when everything passes — covered next.

Videos only on failure (the production recipe)

Videos are huge. A 200-spec suite generates a couple of gigabytes per run. Most teams adopt the official Cypress recipe: keep videos on, then delete them on success in setupNodeEvents:

// cypress.config.ts
import { defineConfig } from "cypress";
import { rmSync } from "node:fs";
 
export default defineConfig({
  e2e: {
    video: true,
    setupNodeEvents(on, _config) {
      on("after:spec", (_spec, results) => {
        const failures = (results.tests ?? []).some((t) =>
          t.attempts.some((a) => a.state === "failed"),
        );
        if (!failures && results.video) {
          rmSync(results.video, { force: true });
        }
      });
    },
  },
});

Now videos exist only for specs that had at least one failure. CI artifact storage drops by 10–100×; the failing-spec evidence is still complete.

Combining screenshots into reports

Mochawesome (covered in the last lesson of this chapter) and most other Cypress reporters can attach screenshots to test results. The default location-based naming makes it trivial — the reporter walks cypress/screenshots/<spec>/ and links every <test name> (failed).png to its corresponding test entry. Manual screenshots from cy.screenshot("my-name") are also picked up; their filenames just don't include "(failed)".

For richer attachment in custom reporters, use the addContext helper:

import addContext from "mochawesome/addContext";
 
afterEach(function () {
  if (this.currentTest?.state === "failed") {
    addContext(this, `screenshots/${Cypress.spec.name}/${this.currentTest.title} (failed).png`);
  }
});

The lesson on Mochawesome ties this all together with the full pipeline.

A documentation-driven test

A spec that exists primarily to capture evidence — useful for stakeholder demos, release notes, or design review:

describe("Checkout flow — visual evidence", () => {
  it("captures the four-step checkout for documentation", () => {
    cy.sessionLogin("alice@test.com", "Sup3rS3cret!");
    cy.visit("/cart");
    cy.screenshot("01-cart-with-items", { capture: "fullPage" });
 
    cy.get("[data-testid='checkout-btn']").click();
    cy.get("[data-testid='address']").type("123 Test Street");
    cy.screenshot("02-shipping-form-filled");
 
    cy.get("[data-testid='next-step']").click();
    cy.get("[data-testid='card-number']").type("4242424242424242");
    cy.screenshot("03-payment-form-filled");
 
    cy.get("[data-testid='next-step']").click();
    cy.screenshot("04-review-page", { capture: "fullPage" });
 
    cy.get("[data-testid='place-order-btn']").click();
    cy.contains("Thank you").should("be.visible");
    cy.screenshot("05-confirmation");
  });
});

Five screenshots produced in under fifteen seconds. The product team gets a visual walkthrough of the flow without you having to record anything by hand. Tag the screenshots into release notes and the engineering team has automated visual proof of the feature shipping.

⚠️ Common mistakes

  • Leaving video: true on a hundred-spec CI run with no cleanup. A few hundred megabytes per spec adds up fast — many teams discover their CI artifact bucket has petabytes of green-run videos before they add the failure-only cleanup. Add the after:spec recipe on day one, even on a small project.
  • Capturing a fullPage screenshot of a page with infinite scroll. Cypress will keep scrolling forever and the resulting image is unusable. Use viewport or screenshot a specific element on infinite-scroll pages.
  • Naming screenshots with timestamps or test-run IDs. A name like failure-1730000000.png makes the file unique but breaks every downstream reporter that matches on stable filenames. Use stable, descriptive names; let the reporter handle uniqueness via test path.

🎯 Practice task

Wire screenshots and videos into a CI-mature configuration. 20-25 minutes.

  1. In cypress.config.ts, set video: true, videosFolder: "cypress/videos", and screenshotOnRunFailure: true. Run npm run cy:run on a passing spec and confirm a video is generated.
  2. Implement the after:spec recipe from the lesson — delete the video file when no test in the spec failed. Re-run on a passing spec; confirm cypress/videos/ is empty afterwards.
  3. Force a failure by changing one assertion. Re-run. Confirm the video for that spec is kept and the screenshot is captured. Inspect both files.
  4. Manual capture drill — pick any working spec, add three cy.screenshot() calls at meaningful points (after login, after navigation, before assertion). Use capture: "fullPage" for one of them and cy.get(...).screenshot("...") for an element-only one. Run the spec; confirm three screenshots appear in cypress/screenshots/<spec>/.
  5. Trim CI cost — set videoCompression: 51 (lowest quality). Re-run a failing spec and inspect the video file size. Compare against videoCompression: 22 and decide where the right point on the curve is for your team's storage tolerance.
  6. Stretch: add a cy.visualEvidence(name: string) custom command that wraps cy.screenshot(name, { capture: "fullPage" }). Use it in a documentation-driven spec like the lesson example. The wrapper means later changes (e.g., "always capture full page", "always include a timestamp footer") happen in one place.

The next lesson takes the screenshot pipeline a step further — visual regression testing, where screenshots stop being passive evidence and start being active assertions.

// tip to track lessons you complete and pick up where you left off across devices.