Cypress tests don’t just fail; they often fail spectacularly, leaving you with a cryptic error message and no idea what happened. This is where capturing screenshots and videos becomes indispensable.

Imagine a scenario where your UI test, which has been green for weeks, suddenly starts failing in your CI environment. You pull the logs, and it’s a generic "Timed out retrying after 30000ms: cy.get('button.submit')". What button? Where? Was it disabled? Did it not appear? This is precisely the kind of ambiguity screenshots and videos resolve.

Common Causes of Failing Cypress Tests (and how to fix them)

  1. Element Not Found/Visible/Interactable: This is the most frequent culprit. Cypress timed out because the element it was looking for (e.g., cy.get('button.submit')) simply wasn’t present in the DOM, wasn’t visible, or was obscured by another element.

    • Diagnosis: Run the test locally and use Cypress’s built-in DevTools to inspect the DOM at the point of failure. You’ll see the DOM state exactly as Cypress did.
    • Fix:
      • Wait for Element: If the element appears after a short delay (e.g., due to an AJAX call), increase the default command timeout:
        # cypress.config.js
        export default defineConfig({
          defaultCommandTimeout: 10000, // Increased from 4000ms to 10000ms
        })
        
        This gives Cypress more time to wait for the element to appear.
      • Explicit Waits/Assertions: Use cy.get('selector').should('be.visible') or cy.get('selector').should('exist') before interacting with the element. This makes the wait explicit and provides better error messages.
      • Element State: If the element is present but disabled or hidden, use cy.get('selector').should('not.be.disabled') or cy.get('selector').should('be.visible') before clicking.
    • Why it Works: Cypress commands have default timeouts. If the DOM doesn’t match the expectation within that time, the command fails. Increasing the timeout or adding explicit assertions provides the necessary breathing room or checks the element’s state more precisely.
  2. Incorrect Selector: A typo, a change in the HTML structure, or a dynamic class name can render your selector useless.

    • Diagnosis: Use Cypress’s selector playground or manually inspect the DOM in your browser’s DevTools to verify the selector matches the intended element.
    • Fix: Update the selector to accurately target the element. Prioritize stable attributes like data-cy, id, or name over volatile ones like class names or text content.
      // Bad: Relies on potentially changing class
      cy.get('.user-profile-header__name')
      
      // Good: Uses a stable data attribute
      cy.get('[data-cy="user-profile-name"]')
      
    • Why it Works: A correct selector ensures Cypress can find the intended DOM node to interact with.
  3. Application State Issues: The application might be in an unexpected state, preventing an element from appearing or behaving as expected. This is common with complex forms or asynchronous operations.

    • Diagnosis: Use cy.log() statements before critical actions to print application state or relevant data to the Cypress command log. Examine the screenshot/video at the point of failure to understand the UI state.
    • Fix: Introduce explicit waits for asynchronous operations or application states. Use cy.intercept() to mock or assert network requests, ensuring the application is in a known state before proceeding.
      // Wait for a specific network request to complete
      cy.intercept('/api/users').as('getUsers')
      cy.wait('@getUsers')
      
    • Why it Works: Cypress executes commands synchronously. If your application’s UI depends on asynchronous processes (like API calls), Cypress might try to interact with elements before they are ready. Explicit waits or intercepting network requests synchronize Cypress with your application’s state.
  4. Cross-Origin Issues: If your application navigates to a different domain (e.g., a third-party login page) and you try to interact with elements there, Cypress will encounter security restrictions.

    • Diagnosis: Cypress will typically throw an error related to cross-origin restrictions. The screenshot will show the new domain loaded.
    • Fix: If you need to interact with cross-origin pages, you must explicitly enable it in your cypress.config.js:
      // cypress.config.js
      export default defineConfig({
        e2e: {
          chromeWebSecurity: false, // Disable Chrome security for cross-origin
        },
      })
      
      Caution: Disabling web security can have implications for test security and might not be suitable for all scenarios.
    • Why it Works: Browsers enforce the Same-Origin Policy to prevent malicious websites from accessing data from other origins. Cypress, running within a browser context, adheres to this. Disabling chromeWebSecurity tells Cypress to bypass these restrictions for your test runs.
  5. Browser/Environment Differences: Tests might pass on your local machine but fail in CI due to differences in browser versions, operating system, or headless execution.

    • Diagnosis: Run the test in the exact same browser and version as your CI environment locally. Use npx cypress run --browser chrome (or your CI browser) to simulate CI conditions.
    • Fix: Ensure consistent browser versions between local development and CI. Use Docker containers for CI environments to guarantee a consistent OS and software setup.
    • Why it Works: Minor rendering differences, CSS interpretations, or JavaScript engine behaviors can vary across browser versions and environments, leading to subtle UI changes that break tests.
  6. Typo in cypress.config.js or cypress.config.ts: A simple mistake in your configuration file can lead to unexpected behavior.

    • Diagnosis: Cypress will often fail to start or report errors related to loading the configuration. Check the output of npx cypress open or npx cypress run.
    • Fix: Carefully review your cypress.config.js file for syntax errors, incorrect property names, or missing exports.
      // cypress.config.js - Example of a common typo
      export default defineConfig({
        e2e: {
          setupNodeEvents(on, config) {
            // ...
          },
          baseUrl: 'http://localhost:3000', // Corrected from 'baseurl'
        },
      })
      
    • Why it Works: The configuration file dictates how Cypress runs. Any errors in it prevent Cypress from initializing correctly, leading to test failures or the inability to run tests at all.

The next error you’ll likely encounter after fixing these is a "Timed out retrying after 30000ms: cy.visit('/some/page')" if your application’s server isn’t running or accessible at the baseUrl.


Capturing Screenshots and Videos

Cypress has built-in capabilities for this. You enable them in your cypress.config.js.

When you run npx cypress open or npx cypress run, Cypress automatically takes a screenshot on failure. Videos are recorded for entire test suites when run via cypress run.

Here’s how you configure it:

// cypress.config.js
import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
    screenshotsFolder: 'cypress/screenshots', // Default location
    videosFolder: 'cypress/videos',         // Default location
    screenshotOnRunFailure: true, // Default is true
    video: true,                  // Default is true for cypress run
  },
})
  • screenshotsFolder: Specifies where screenshots are saved. By default, it’s cypress/screenshots.
  • videosFolder: Specifies where videos are saved. By default, it’s cypress/videos.
  • screenshotOnRunFailure: If true (which is the default), Cypress will automatically take a screenshot when a test fails.
  • video: If true (the default for cypress run), Cypress will record a video of the entire test run.

Running Locally:

When you run npx cypress open, you’ll see the test runner UI. If a test fails, a screenshot will be saved in your cypress/screenshots folder, named after the spec file and the test.

Running in CI:

When you run npx cypress run, Cypress will execute all tests in headless mode. It will automatically generate:

  1. Screenshots: One for each failed test, saved in cypress/screenshots/.
  2. Video: A single video file recording the entire suite run, saved in cypress/videos/. This video is invaluable for understanding the flow leading up to a failure across multiple tests.

Example Workflow:

  1. Your CI pipeline runs npx cypress run.
  2. A test fails.
  3. The CI job logs will show the test output, including the failure.
  4. Cypress automatically saves a screenshot of the failing test and a video of the whole run in cypress/screenshots and cypress/videos, respectively.
  5. You can then configure your CI to upload these artifacts. For example, in GitHub Actions, you’d use an actions/upload-artifact step.
# Example GitHub Actions snippet
- name: Upload Cypress Artifacts
  uses: actions/upload-artifact@v3
  if: failure() # Only upload if the job failed
  with:
    name: cypress-artifacts
    path: |
      cypress/screenshots
      cypress/videos

This makes the screenshots and videos directories available for download from the GitHub Actions run.

The Most Surprising Thing About Screenshots and Videos

The most surprising thing is how much context Cypress captures beyond just the DOM state at failure: it records the entire sequence of commands, the network requests, the console logs, and the browser’s state leading up to that point. It’s not just a snapshot; it’s a full diagnostic playback.

When you view a Cypress video, you’re not just seeing a GIF of your test; you’re seeing a timeline of its execution. You can pause it, scrub through it, and inspect the DOM at any given frame using the associated screenshot. This level of detail is often more informative than stepping through a debugger because it shows the actual flow in the browser, including timing and rendering delays, which are often the root cause of flaky tests.

The screenshotOnRunFailure: true setting is a default for a reason, but understanding what it captures and how to leverage the video recording for suite-level debugging is where the real power lies. Many teams only enable screenshots on failure but miss out on the comprehensive suite-level video that can reveal subtle race conditions or inter-test dependencies.

Customizing Screenshots

You can also trigger screenshots manually within your tests, which is incredibly useful for debugging specific states or actions that might not always cause a failure but are interesting to inspect.

it('should handle user login', () => {
  cy.visit('/login')
  cy.get('[data-cy="username"]').type('testuser')
  cy.get('[data-cy="password"]').type('password123')

  // Take a screenshot before clicking login
  cy.screenshot('before-login-click')

  cy.get('[data-cy="login-button"]').click()

  // Assert that login was successful
  cy.url().should('include', '/dashboard')
})

This command cy.screenshot('before-login-click') will save a screenshot named before-login-click.png in your cypress/screenshots folder, alongside the automatic failure screenshots. This allows you to capture the UI state at any point you deem critical for understanding test behavior.

If you’re running your tests locally with npx cypress open, you can even trigger a screenshot on any command by clicking the camera icon next to it in the command log. This is a fantastic interactive debugging tool.

The next step after mastering screenshots and videos is often integrating them with your test reporting tools or building custom dashboards to analyze failure trends.

Want structured learning?

Take the full Cypress course →