Migration Guide

Migrating to Cypress version 12.0

This guide details the changes and how to change your code to migrate to Cypress version 12.0. See the full changelog for version 12.0.

The experimentalSessionAndOrigin flag has been removed and all functionality associated with the Session and Origin experiments are now available. The cy.origin() and cy.session() commands are generally available and the concept of Test Isolation has been introduced.

Test Isolation

The testIsolation config option defaults to on. This means Cypress resets the browser context before each test by:

Test suites that relied on the application to persist between tests may have to be updated to revisit their application and rebuild the browser state for each test that needs it.

Before this change, it was possible to write tests such that you could rely on the application (i.e. DOM state) to persist between tests. For example you could log in to a CMS in the first test, change some content in the second test, verify the new version is displayed on a different URL in the third, and log out in the fourth.

Here's a simplified example of such a test strategy.

Before
Multiple small tests against different origins
it('logs in', () => {
  cy.visit('https://supersecurelogons.com')
  cy.get('input#password').type('Password123!')
  cy.get('button#submit').click()
})

it('updates the content', () => {
  // already on page redirect from clicking button#submit
  cy.get('#current-user').contains('logged in')
  cy.get('button#edit-1').click()
  cy.get('input#title').type('Updated title')
  cy.get('button#submit').click()
  cy.get('.toast').type('Changes saved!')
})

it('validates the change', () => {
  cy.visit('/items/1')
  cy.get('h1').contains('Updated title')
})

After migrating, when testIsolation='on', this flow would need to be contained within a single test. While the above practice has always been discouraged we know some users have historically written tests this way, often to get around the same-origin restrictions. But with cy.origin() you no longer need these kind of brittle hacks, as your multi-origin logic can all reside in a single test, like the following.

After
One big test using cy.origin()
it('securely edits content', () => {
  cy.origin('supersecurelogons.com', () => {
    cy.visit('https://supersecurelogons.com')
    cy.get('input#password').type('Password123!')
    cy.get('button#submit').click()
  })

  cy.origin('mycms.com', () => {
    cy.url().should('contain', 'cms')
    cy.get('#current-user').contains('logged in')
    cy.get('button#edit-1').click()
    cy.get('input#title').type('Updated title')
    cy.get('button#submit').click()
    cy.get('.toast').type('Changes saved!')
  })

  cy.visit('/items/1')
  cy.get('h1').contains('Updated title')
})

The just-release cy.session() command can be used to setup and cache cookies, local storage and session storage between tests to easily re-establish the previous (or common) browser contexts needed in a suite. This command will run setup on the initial execution and will restore the saved browser state on each sequential command execution. This command reduces the need for repeated application logins, while users also benefit from the test isolation guardrails to write independent, reliable and deterministic tests from the start.

If for whatever reason you still need to persist the dom and browser context between tests, you can set testIsolation='off on the root configuration or at the suite-level. For example:

describe('workflow', { testIsolation: 'off' }, () => {
  ...
})

It is important to note that while turning test isolation off may improve the overall performance of end-to-end tests, previous tests could be impact the browser state. It is important to be extremely mindful of how test are written when using this mode and ensure tests continue to run independent from one other.

For example
the following tests are not independent nor deterministic:
describe('workflow', { testIsolation: 'off' }, () => {
  it('logs in', () => {
    cy.visit('my-app.com/log-in)
    cy.get('username').type('User1')
    cy.get('password').type(Cypress.env('User1_password'))
    cy.get('button#login').click()
    cy.contains('User1')
  })

  it('clicks user profile', () => {
    cy.get('User1').find('#profile_avatar).click()
    cy.contains('Email Preferences')
  })

  it('updates profile', () => {
    cy.get('button#edit')
    cy.get('email').type('user1@email.com')
    cy.get('button#save').click()
  })
})

In the above example, each test is relying on the previous test to be successful to correctly execute. If at any point, the first or second test fails, the sequential test(s) will automatically fail and provided un-reliable debugging errors since the errors are representative of the previous test.

The best way to ensure your tests are independent is to add a .only() to your test and verify it can run successfully without the test before it.

Alias Behaviors Changes

Cypress always re-queries aliases when they are referenced. This can result in certain tests that use to pass failing. For example,

cy.findByTestId('popover')
  .findByRole('button', { expanded: true })
  .as('button')
  .click()

cy.get('@button').should('have.attr', 'aria-expanded', 'false')

previously passed, because the initial button was collapsed when first queried, and then later expanded. However, in Cypress 12, this test fails because the alias is always re-queried from the DOM, effectively resulting in the following execution:

cy.findByTestId('popover').findByRole('button', { expanded: true }).click()

cy.findByTestId('popover')
  .findByRole('button', { expanded: true }) // A button which matches here (is expanded)...
  .should('have.attr', 'aria-expanded', 'false') // ...will never pass this assertion.

You can rewrite tests like this to be more specific; in our case, we changed the alias to be the first button rather than the unexpanded button.

cy.findByTestId('popover').findAllByRole('button').first().as('button')

Command / Cypress API Changes

Cypress.Cookies.defaults and Cypress.Cookies.preserveOnce

The Cypress.Cookies.defaults and CypressCookies.preserveOnce APIs been removed. Use the cy.session() command to preserve cookies (and local and session storage) between tests.

describe('Dashboard', () => {
  beforeEach(() => {
-    cy.login()
-    Cypress.Cookies.preserveOnce('session_id', 'remember_token')
+    cy.session('unique_identifier', cy.login, {
+       validate () {
+        cy.getCookies().should('have.length', 2)
+       },
+       cacheAcrossSpecs: true
+    })
  })

cy.server(), cy.route() and Cypress.Server.defaults

The cy.server() and cy.route() commands and the Cypress.server.defaults API has been removed. Use the [cy.intercept()(/api/commands/intercept) command instead.

  it('can encode + decode headers', () => {
-   Cypress.Server.defaults({
-     delay: 500,
-     method: 'GET',
-   })
-   cy.server()
-   cy.route(/api/, () => {
-      return {
-        'test': 'We’ll',
-      }
-    }).as('getApi')
+   cy.intercept('GET', /api/, (req) => {
+      req.on('response', (res) => {
+        res.setDelay(500)
+      })
+      req.body.'test': 'We’ll'
+    }).as('getApi')
    cy.visit('/index.html')
    cy.window().then((win) => {
      const xhr = new win.XMLHttpRequest
      xhr.open('GET', '/api/v1/foo/bar?a=42')
      xhr.send()
    })

    cy.wait('@getApi')
-   .its('url').should('include', 'api/v1')
+   .its('request.url').should('include', 'api/v1')
  })

.invoke()

.invoke() now throws an error if the function returns a promise. If you wish to call a method that returns a promise and wait for it to resolve, use .then() instead of .invoke().

cy.wrap(myAPI)
-  .invoke('makeARequest', 'http://example.com')
+  .then(api => api.makeARequest('http://example.com'))
   .then(res => { ...handle response... })

If .invoke() is followed by additional commands or assertions, it will call the named function multiple times. This has the benefit that the chained assertions can more reliably use the function's return value.

If this behavior is undesirable because you expect the function to invoked only once, break the command chain and move the chains commands and/or assertions to their own chain. For example, rewrite

- cy.get('input').invoke('val', 'text').type('newText')
+ cy.get('input').invoke('val', 'text')
+ cy.get('input').type('newText')

.should()

.should() now throws an error if Cypress commands are invoked from inside a .should() callback. This previously resulted in unusual and undefined behavior. If you wish to execute series of commands on the yield value, use.then() instead.

cy.get('button')
-  .should(($button) => {

    })
+  .then(api => api.makeARequest('http://example.com'))
   .then(res => { ...handle response... })

If .invoke() is followed by additional commands or assertions, it will call the named function multiple times. This has the benefit that the chained assertions can more reliably use the function's return value.

If this behavior is undesirable because you expect the function to invoked only once, break the command chain and move the chains commands and/or assertions to their own chain. For example, rewrite

- .invoke('val', 'text').type('newText')
+ cy.get('input').invoke('val', 'text')
+ cy.get('input').type('newText')

Cypress.Commands.override()

The follow commands can no longer be overridden:

  • .as()
  • .children()
  • .closest()
  • .contains()
  • cy.debug()
  • cy.document()
  • .eq()
  • .filter()
  • .find()
  • .first()
  • .focused()
  • .get()
  • .hash()
  • .its()
  • .last()
  • cy.location()
  • .next()
  • .nextAll()
  • .not()
  • .parent()
  • .parents()
  • .parentsUntil()
  • .prev()
  • .prevUntil()
  • cy.root()
  • .shadow()
  • .siblings()
  • cy.title()
  • cy.url()
  • cy.window()

Migrating to Cypress version 11.0

This guide details the changes and how to change your code to migrate to Cypress version 11.0. See the full changelog for version 11.0.

Component Testing Updates

As of Cypress 11, Component Testing is now generally available. There are some minor breaking changes. Most projects should be able to migrate without any code modifications.

Changes to Mounting Options

Each major library we support has a mount function with two arguments:

  1. The component
  2. Mounting Options

Mounting options previously had several properties that are now removed:

  • cssFile, cssFiles
  • style, styles
  • stylesheet, stylesheets

Read more about the rationale here. We recommend writing test-specific styles in a separate css file you import in your test, or in your supportFile.

Before (Cypress 10)

import { mount } from 'cypress/react'
import { Card } from './Card'

it('renders some content', () => {
  cy.mount(<Card title="title" />, {
    styles: `
      .card { width: 100px; }
    `,
    stylesheets: [
      'https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css',
    ],
  })
})

After (Cypress 11)

/** style.css */
@import "https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css";
.card { width: 100px }

/** Card.cy.jsx */
import { mount } from 'cypress/react'
import { Card } from './Card'
import './styles.css' // contains CDN link and custom styling.

it('renders some content', () => {
  cy.mount(<Card title="title" />)
})

React - mountHook Removed

mountHook from cypress/react has been removed. Read more about the rationale here.

We recommend simply replacing it with mount and a component.

Consider the following useCounter hook:

import { useState, useCallback } from 'react'

function useCounter() {
  const [count, setCount] = useState(0)
  const increment = useCallback(() => setCount((x) => x + 1), [])

  return { count, increment }
}

Before - Cypress 10 and mountHook

import { mountHook } from 'cypress/react'
import { useCounter } from './useCounter'

it('increments the count', () => {
  mountHook(() => useCounter()).then((result) => {
    expect(result.current.count).to.equal(0)
    result.current.increment()
    expect(result.current.count).to.equal(1)
    result.current.increment()
    expect(result.current.count).to.equal(2)
  })
})

After - Cypress 11 and mount

import { useCounter } from './useCounter'

it('increments the count', () => {
  function Counter() {
    const { count, increment } = useCounter()
    return (
      <>
        <h1 name="count">Count is {{ count }}</h1>
        <button onClick={increment}>Increment</button>
      </>
    )
  }

  cy.mount(<Counter />).then(() => {
    cy.get('[name="count"]')
      .should('contain', 0)
      .get('button')
      .click()
      .get('[name="count"]')
      .should('contain', 1)
  })
})

React - unmount Removed

unmount from cypress/react has been removed. Read more about the rationale here. We recommend using the API React provides for unmounting components, unmountComponentAtNode.

Before - Cypress 10 and unmount

import { unmount } from 'cypress/react'

it('calls the prop', () => {
  cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />)
  cy.contains('My component')

  unmount()

  // the component is gone from the DOM
  cy.contains('My component').should('not.exist')
  cy.get('@onUnmount').should('have.been.calledOnce')
})

After - Cypress 11 and unmountComponentAtNode

import { getContainerEl } from 'cypress/react'
import ReactDom from 'react-dom'

it('calls the prop', () => {
  cy.mount(<Comp onUnmount={cy.stub().as('onUnmount')} />)
  cy.contains('My component')

  cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl()))

  // the component is gone from the DOM
  cy.contains('My component').should('not.exist')
  cy.get('@onUnmount').should('have.been.calledOnce')
})

Vue - mountCallback Removed

mountCallback from cypress/vue has been removed. Read more about the rationale here. We recommend using mount.

Before - Cypress 10 and mountCallback

import { mountCallback } from 'cypress/vue'

beforeEach(mountCallback(MessageList))

it('shows no messages', () => {
  getItems().should('not.exist')
})

After - Cypress 11 and mount

beforeEach(() => cy.mount(MessageList))

it('shows no messages', () => {
  getItems().should('not.exist')
})

Angular - Providers Mounting Options Change

There is one breaking change for Angular users in regards to providers. In Cypress 10, we took any providers passed as part of the Mounting Options and overrode the component providers via the TestBed.overrideComponent API.

In Cypress 11, providers passed as part of the Mounting Options will be assigned at the module level using the TestBed.configureTestingModule API.

This means that module-level providers (resolved from imports or @Injectable({ providedIn: 'root' }) can be overridden, but providers specified in @Component({ providers: [...] }) will not be overridden when using cy.mount(MyComponent, { providers: [...] }).

To override component-level providers, use the TestBed.overrideComponent API.

See a concrete example here.

Vite Dev Server (cypress/vite-dev-server)

When providing an inline viteConfig inside of cypress.config, any vite.config.js file is not automatically merged.

Before - Cypress 10 and viteConfig

import { defineConfig } from 'cypress'

export default defineConfig({
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite',
      viteConfig: {
        // ... custom vite config ...
        // result merged with `vite.config` file if present
      },
    },
  },
})

After - Cypress 11 and viteConfig

import { defineConfig } from 'cypress'
import viteConfig from './vite.config'

export default defineConfig({
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite',
      viteConfig: {
        ...viteConfig,
        // ... other overrides ...
      },
    },
  },
})

Vite 3+ users could make use of the mergeConfig API.

Migrating to Cypress version 10.0

This guide details the changes and how to change your code to migrate to Cypress version 10.0. See the full changelog for version 10.0.

Cypress Changes

  • The "Run all specs" and "Run filtered specs" functionality have been removed.
  • The experimental "Cypress Studio" has been removed and will be rethought/revisited in a later release.
  • Unsupported browser versions can no longer be run via cypress run or cypress open. Instead, an error will display.
  • In 9.x and earlier versions, cypress open would bring you directly to the project specs list. In 10.0.0, you must pass --browser and --e2e or --component as well to launch Cypress directly to the specs list.

Configuration File Changes

Cypress now supports JavaScript and TypeScript configuration files. By default, Cypress will automatically load a cypress.config.js or cypress.config.ts file in the project root if one exists. The Configuration guide has been updated to reflect these changes, and explains them in greater detail.

Because of this, support for cypress.json has been removed. Documentation for cypress.json has been moved to the Legacy Configuration guide.

Related notes:

  • If no config file exists when you open Cypress, the automatic set up process will begin and either a JavaScript or TypeScript config file will be created depending on what your project uses.
  • You may use the --config-file command line flag or the configFile module API option to specify a .js or .ts file. JSON config files are no longer supported.
  • Cypress now requires a config file, so specifying --config-file false on the command line or a configFile value of false in the module API is no longer valid.
  • You can't have both cypress.config.js and cypress.config.ts files. This will result in an error when Cypress loads.
  • A defineConfig() helper function is now exported by Cypress, which provides automatic code completion for configuration in many popular code editors. For TypeScript users, the defineConfig function will ensure the configuration object passed into it satisfies the type definition of the configuration file.
  • Many pages and examples throughout the documentation have been updated to show configuration using cypress.config.js and cypress.config.ts vs the older cypress.json. For example:
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:1234'
  }
})
import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:1234'
  }
})
{
  "e2e": {
    "baseUrl": "http://localhost:1234"
  }
}

Plugins File Removed

Because Cypress now supports JavaScript and TypeScript configuration files, a separate "plugins file" (which used to default to cypress/plugins/index.js) is no longer needed.

Support for the plugins file has been removed, and it has been replaced with the new setupNodeEvents() and devServer config options.

Related notes:

  • The cypress/plugins/index.js plugins file is no longer automatically loaded by Cypress.
  • The setupNodeEvents() config option is functionally equivalent to the function exported from the plugins file; it takes the same on and config arguments, and should return the same value. See the Config option changes section of this migration guide for more details.
  • The devServer config option is specific to component testing, and offers a much more streamlined and consistent way to configure a component testing dev server than using the plugins file. See the Config option changes section of this migration guide for more details.
  • Many pages and examples throughout the documentation have been updated to show configuration in setupNodeEvents as well as the legacy plugins file. For example:
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  // setupNodeEvents can be defined in either
  // the e2e or component configuration
  e2e: {
    setupNodeEvents(on, config) {
      // bind to the event we care about
      on('<event>', (arg1, arg2) => {
        // plugin stuff here
      })
    }
  }
})
import { defineConfig } from 'cypress'

export default defineConfig({
  // setupNodeEvents can be defined in either
  // the e2e or component configuration
  e2e: {
    setupNodeEvents(on, config) {
      // bind to the event we care about
      on('<event>', (arg1, arg2) => {
        // plugin stuff here
      })
    }
  }
})
// cypress/plugins/index.js

module.exports = (on, config) => {
  // bind to the event we care about
  on('<event>', (arg1, arg2) => {
    // plugin stuff here
  })
}

Config Option Changes

baseUrl

The baseUrl config option is no longer valid at the top level of the configuration, and may only be defined inside the e2e configuration object.

componentFolder

The componentFolder config option is no longer used, as it has been replaced by the specPattern testing-type specific option.

devServer

All functionality related to starting a component testing dev server previously in the pluginsFile has moved here. These options are not valid at the top-level, and may only be defined in the component configuration object.

Related notes:

  • Do not configure your dev server inside setupNodeEvents(), use the devServer config option instead.

Variant 1 (webpack & vite dev servers)

Before
const { startDevServer } = require('@cypress/webpack-dev-server')
const webpackConfig = require('../../webpack.config.js')

module.exports = (on, config) => {
  if (config.testingType === 'component') {
    on('dev-server:start', async (options) =>
      startDevServer({ options, webpackConfig })
    )
  }
}
After
const { defineConfig } = require('cypress')
const webpackConfig = require('./webpack.config.js')

module.exports = defineConfig({
  component: {
    devServer: {
      framework: 'react', // or vue
      bundler: 'webpack',
      webpackConfig,
    },
  },
})
const { defineConfig } = require('cypress')
const webpackConfig = require('./webpack.config.js')

module.exports = defineConfig({
  component: {
    devServer(cypressConfig) {
      return devServer({
        framework: 'react', // or vue
        cypressConfig,
        webpackConfig,
      })
    },
  },
})
import { defineConfig } from 'cypress'
import webpackConfig from './webpack.config'

export default defineConfig({
  component: {
    devServer: {
      framework: 'react', // or vue
      bundler: 'webpack',
      webpackConfig,
    },
  },
})
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/webpack-dev-server'
import webpackConfig from './webpack.config'

export default defineConfig({
  component: {
    devServer(cypressConfig) {
      return devServer({
        framework: 'react', // or vue
        cypressConfig,
        webpackConfig,
      })
    },
  },
})

Variant 2 (react plugin dev servers)

Before
const devServer = require('@cypress/react/plugins/react-scripts')

module.exports = (on, config) => {
  if (config.testingType === 'component') {
    injectDevServer(on, config, {})
  }
}
After
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  component: {
    devServer: {
      framework: 'react', // or vue
      bundler: 'webpack',
    },
  },
})
const { defineConfig } = require('cypress')
const webpackConfig = require('./webpack.config.js')

module.exports = defineConfig({
  component: {
    devServer(cypressConfig) {
      return devServer({
        framework: 'react', // or vue
        cypressConfig,
        webpackConfig,
      })
    },
  },
})

experimentalStudio

This option is no longer used. The experimental "Cypress Studio" has been removed and will be rethought/revisited in a later release.

ignoreTestFilesexcludeSpecPattern

The ignoreTestFiles option is no longer used, and has been replaced with the excludeSpecPattern testing-type specific option.

Default values

  • e2e.excludeSpecPattern default value is *.hot-update.js (same as pervious ignore value)
  • component.excludeSpecPattern default value is ['/snapshots/*', '/image_snapshots/*'] updated from *.hot-update.js
  • The **/node_modules/** pattern is automatically added to both e2e.specExcludePattern and component.specExcludePattern, and does not need to be specified (and can't be overridden).
Before
{
  "ignoreTestFiles": "path/to/**/*.js"
}
After
{
  component: {
    excludeSpecPattern: "path/to/**/*.js"
  },
  e2e: {
    excludeSpecPattern: "other/path/to/**/*.js"
  }
}

integrationFolder

This option is no longer used, as it has been replaced by the specPattern testing-type specific option.

pluginsFile

This option is no longer used, and all plugin file functionality has moved into the setupNodeEvents() and devServer options. See the Plugins file removed section of this migration guide for more details.

setupNodeEvents()

All functionality related to setting up events or modifying the config, previously done in the plugins file, has moved into the setupNodeEvents() config options. This option is not valid at the top level of the config, and may only be defined inside the component or e2e configuration objects.

More information can be found in the Plugins API documentation and the Configuration API documentation.

Before
cypress/plugins/index.js
module.exports = (on, config) => {
  if (config.testingType === 'component') {
    // component testing dev server setup code
    // component testing node events setup code
  } else {
    // e2e testing node events setup code
  }
}
After
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  component: {
    devServer(cypressConfig) {
      // component testing dev server setup code
    },
    setupNodeEvents(on, config) {
      // component testing node events setup code
    },
  },
  e2e: {
    setupNodeEvents(on, config) {
      // e2e testing node events setup code
    },
  },
})
import { defineConfig } from 'cypress'

export default defineConfig({
  component: {
    devServer(cypressConfig) {
      // component testing dev server setup code
    },
    setupNodeEvents(on, config) {
      // component testing node events setup code
    },
  },
  e2e: {
    setupNodeEvents(on, config) {
      // e2e testing node events setup code
    },
  },
})

Alternately, you can continue to use an external plugins file, but you will need to load that file explicitly, and also update it to move any component testing dev server code into the devServer config option.

const { defineConfig } = require('cypress')
const setupNodeEvents = require('./cypress/plugins/index.js')

module.exports = defineConfig({
  component: {
    devServer(cypressConfig) {
      // component testing dev server setup code
    },
    setupNodeEvents,
  },
  e2e: {
    setupNodeEvents,
  },
})
import { defineConfig } from 'cypress'
import setupNodeEvents from './cypress/plugins/index.js'

export default defineConfig({
  component: {
    devServer(cypressConfig) {
      // component testing dev server setup code
    },
    setupNodeEvents,
  },
  e2e: {
    setupNodeEvents,
  },
})

slowTestThreshold

The slowTestThreshold configuration option is no longer valid at the top level of the configuration, and is now a testing-type specific option.

Note that the default values are unchanged (10000 for e2e and 250 for component).

supportFile

The supportFile configuration option is no longer valid at the top level of the configuration, and is now a testing-type specific option. More information can be found in the support file docs.

Before
{
  "supportFile": "cypress/support/index.js"
}
After
{
  component: {
    supportFile: 'cypress/support/component.js'
  },
  e2e: {
    supportFile: 'cypress/support/e2e.js'
  }
}

testFilesspecPattern

The testFiles option is no longer used, and has been replaced with the specPattern option, which must be defined inside the component and e2e configuration objects.

Default values:

  • No longer matches with .coffee or .cjsx.
  • e2e.specPattern default value is cypress/e2e/**/*.cy.{js,jsx,ts,tsx}.
  • component.specPattern default value is **/*.cy.{js,jsx,ts,tsx}.

Important note about matching:

  • E2E tests will be found using the e2e.specPattern value.
  • Component tests will be found using the component.specPattern value but any tests found matching the e2e.specPattern value will be automatically excluded.

Updated Test File Locations

Previously, you could specify the locations of test files and folders using the configuration options: componentFolder, or integrationFolder, and testFiles. These options have been replaced with specPattern, which is not valid at the top-level, but within the component or e2e configuration objects. For example:

Before
{
  "componentFolder": "src",
  "integrationFolder": "cypress/integration",
  "testFiles": "**/*.cy.js"
}
After
{
  component: {
    specPattern: 'src/**/*.cy.js'
  },
  e2e: {
    specPattern: 'cypress/integration/**/*.cy.js'
  }
}

Generated Files

Generated screenshots and videos will still be created inside their respective folders (screenshotsFolder, videosFolder). However, the paths of generated files inside those folders will be stripped of any common ancestor paths shared between all spec files found by the specPattern option (or via the --spec command line option or spec module API option, if specified).

Here are a few examples, assuming the value of videosFolder is cypress/videos, screenshotsFolder is cypress/screenshots and cy.screenshot('my-screenshot') is called once per spec file:

Example 1

  • Spec file found
    • cypress/e2e/path/to/file/one.cy.js
  • Common ancestor paths (calculated at runtime)
    • cypress/e2e/path/to/file
  • Generated screenshot file
    • cypress/screenshots/one.cy.js/my-screenshot.png
  • Generated video file
    • cypress/videos/one.cy.js.mp4

Example 2

  • Spec files found
    • cypress/e2e/path/to/file/one.cy.js
    • cypress/e2e/path/to/two.cy.js
  • Common ancestor paths (calculated at runtime)
    • cypress/e2e/path/to
  • Generated screenshot files
    • cypress/screenshots/file/one.cy.js/my-screenshot.png
    • cypress/screenshots/two.cy.js/my-screenshot.png
  • Generated video files
    • cypress/videos/file/one.cy.js.mp4
    • cypress/videos/two.cy.js.mp4

Command / Cypress API Changes

cy.mount()

If you set up your app using the automatic configuration wizard, a basic cy.mount() command will be imported for you in your support file from one our supported frameworks.

Cypress.Commands.add()

Cypress.Commands.add() has been updated to allow the built-in "placeholder" custom mount and hover commands to be overwritten without needing to use Cypress.Commands.overwrite().

Component Testing Changes

Component Testing has moved from experimental to beta status in 10.0.0.

Component Testing can now be ran from the main app, and launching into component testing via the command cypress open-ct is now deprecated. To launch directly into component testing, use the cypress open --component command instead.

All the Component Testing dev servers are now included in the main cypress npm package. Configuring them is done via specifying a framework and bundler in the devServer config option, and the packages are no longer directly importable. See Framework Configuration for more info.

The mount libraries for React and Vue have also been included in the main cypress package and can be imported from cypress/react and cypress/vue respectively.

Any previous dev servers or mounting libraries from the @cypress namespace should be uninstalled in Cypress 10.

Code Coverage Plugin

The Cypress Code Coverage plugin will need to be updated to version >= 3.10 to work with Cypress 10. Using a previous version will result in an error when tests are ran with code coverage enabled.

Migrating from cypress-file-upload to .selectFile()

Selecting files with input elements or dropping them over the page is available in Cypress 9.3. Read the .selectFile() API docs for more information on how this works and how to use it. This guide details how to change your test code to migrate from the cypress-file-upload plugin to .selectFile().

Quick guide

The argument signature is different for Cypress' builtin .selectFile() command than the .attachFile command the cypress-file-upload plugin provided. You can follow the steps below for each argument in order to migrate:

When the first argument is a file path:

  • Prefix the path with cypress/fixtures/.

When the first argument is an object:

  • filePath: Rename the property to contents. Prefix the value with cypress/fixtures/.
  • fileContent: Rename the property to contents. Use Cypress.Buffer.from() or other Buffer methods, rather than Cypress.Blob.
  • encoding: Remove this property. It is no longer needed due to improved binary file handling in Cypress 9.0.
  • mimeType: No change necessary. In most cases you do not need to give a mimeType explicitly. Cypress will attempt to infer the MIME type based on the extension of the fileName if none is provided.

In the second argument:

  • subjectType: Rename this property to action. Change the value from drag-n-drop to drag-drop or from input to select.
  • allowEmpty: Remove this property. .selectFile() does not check the length of a file read from disk, only its existence.
  • force: Works the same with .selectFile() as it did in cypress-file-upload. No change necessary.

Examples

Below are several examples of migrating various commands from cypress-file-upload to the builtin .selectFile() command.

Read and attach a fixture

Before
Attaching a fixture from disk with cypress-file-upload
cy.get('[data-cy="file-input"]').attachFile('myfixture.json')
After
Selecting a fixture from disk with .selectFile() . Cypress follows paths from your project root (same as cy.readFile() ).
cy.get('[data-cy="file-input"]').selectFile('cypress/fixtures/myfixture.json')

// Or

cy.fixture('myfixture.json', { encoding: null }).as('myfixture')
cy.get('[data-cy="file-input"]').selectFile('@myfixture')

Using drag-n-drop

Before
Dragging and dropping a file with cypress-file-upload
cy.get('[data-cy="dropzone"]').attachFile('myfixture.json', {
  subjectType: 'drag-n-drop',
})
After
Selecting a fixture from disk with .selectFile() . Cypress follows paths from the root of your test folder (same as cy.readFile() ).
cy.get('[data-cy="dropzone"]').selectFile('fixtures/myfixture.json', {
  action: 'drag-drop',
})

Overriding the file name

Before
Dragging and dropping a file with cypress-file-upload
cy.get('[data-cy="dropzone"]').attachFile({
  filePath: 'myfixture.json',
  fileName: 'customFileName.json',
})
After
Selecting a fixture from disk with .selectFile() . Cypress follows paths from the root of your test folder (same as cy.readFile() ).
cy.get('[data-cy="dropzone"]').selectFile({
  contents: 'fixtures/myfixture.json',
  fileName: 'customFileName.json',
})

Working with file contents

Before
Working with file contents before using using cypress-file-upload
const special = 'file.spss'

cy.fixture(special, 'binary')
  .then(Cypress.Blob.binaryStringToBlob)
  .then((fileContent) => {
    // ...process file contents
    cy.get('[data-cy="file-input"]').attachFile({
      fileContent,
      filePath: special,
      encoding: 'utf-8',
      lastModified: new Date().getTime(),
    })
  })
After
Working with file contents before using with .selectFile() . The null encoding introduced in Cypress 9.0 makes working with binary data simpler, and is the preferred encoding for use with .selectFile() .
const special = 'file.spss'

cy.fixture(special, { encoding: null }).then((contents) => {
  // ...process file contents
  cy.get('[data-cy="file-input"]').selectFile({
    contents,
    fileName: special,
    lastModified: new Date().getTime(),
  })
})

// Or

cy.fixture(special, { encoding: null })
  .then((contents) => {
    // ...process file contents
  })
  .as('special')

cy.get('[data-cy="file-input"]').selectFile('@special')

Specifying a custom mimeType

Before
Specifying the MIME type with cypress-file-upload
cy.get('[data-cy="dropzone"]').attachFile({
  filePath: 'myfixture.json',
  fileName: 'customFileName.json',
})
After
Specifying a MIME type with .selectFile() .
cy.get('[data-cy="dropzone"]').selectFile({
  contents: 'fixtures/myfixture.json',
  mimeType: 'text/plain',
})

Migrating to Cypress 8.0

This guide details the changes and how to change your code to migrate to Cypress 8.0. See the full changelog for 8.0.

cypress run runs all browsers --headless

When running cypress run previous to 8.0, some browsers would launch headed while others were launched headless by default. In 8.0, we've normalized all browsers to launch as headless by default.

This could cause a couple of changes to your existing runs:

  • You may see the screenshot or video resolution of runs during cypress run change to the default of 1280x720. This is because headless browsers use the set screen size as opposed to the browser's size when opening headed.
  • Chrome extensions will not load during a --headless run. If your run depends on a Chrome extension being loaded during cypress run, you should explicitly pass the --headed flag.

You can now remove the use of the --headless flag during cypress run as this is the default for all browsers.

You should also update any use of the isHeaded or isHeadless property on Cypress.browser or the browser launch API accordingly.

Before
run headless browser
cypress run --browser=chrome --headless
cypress run --browser=firefox --headless
After
All browsers headless by default, so you can remove the --headless flag during cypress run .
cypress run --browser=chrome
cypress run --browser=firefox

Default screen size during --headless

The default screen size when running a headless browser has been reverted back to 1280x720 pixels. If you have any code in the browser launch API to set the screen size to 1280x720, this can be removed.

Before
set screen size to 1280x720
// cypress/plugins/index.js
module.exports = (on, config) => {
  on('before:browser:launch', (browser, launchOptions) => {
    if (browser.name === 'chrome' && browser.isHeadless) {
      launchOptions.args.push('--window-size=1280,720')
    }

    if (browser.name === 'electron' && browser.isHeadless) {
      launchOptions.preferences.width = 1280
      launchOptions.preferences.height = 720
    }

    if (browser.name === 'firefox' && browser.isHeadless) {
      launchOptions.args.push('--width=1280')
      launchOptions.args.push('--height=720')
    }

    return launchOptions
  })
}
After
no longer need overrides
// cypress/plugins/index.js
module.exports = (on, config) => {
  // the default screen size is 1280x720 in all headless browsers
}

Migrating to Cypress 7.0

This guide details the changes and how to change your code to migrate to Cypress 7.0. See the full changelog for 7.0.

cy.intercept() changes

Cypress 7.0 comes with some breaking changes to cy.intercept():

Handler ordering is reversed

Previous to Cypress 7.0, cy.intercept() handlers were run in the order that they are defined, stopping after the first handler to call req.reply(), or once all handlers are complete.

With Cypress 7.0, cy.intercept() handlers are now run in reverse order of definition, stopping after the first handler to call req.reply(), or once all handlers are complete.

This change was done so that users can override previously declared cy.intercept() handlers by calling cy.intercept() again. See #9302 for more details.

Before
cy.intercept(url, (req) => {
  /* This will be called first! */
})
cy.intercept(url, (req) => {
  /* This will be called second! */
})
After
cy.intercept(url, (req) => {
  /* This will be called second! */
})
cy.intercept(url, (req) => {
  /* This will be called first! */
})

Read more about the cy.intercept() interception lifecycle.

URL matching is stricter

Before Cypress 7.0, cy.intercept() would match URLs against strings by using minimatch, substring match, or by equality.

With Cypress 7.0, this behavior is being tightened - URLs are matched against strings only by minimatch or by equality. The substring match has been removed.

This more closely matches the URL matching behavior shown by cy.route(). However, some intercepts will not match, even though they did before.

For example, requests with querystrings may no longer match:

// will this intercept match a request for `/items?page=1`?
cy.intercept('/items')
// ✅ before 7.0.0, this will match, because it is a substring
// ❌ after 7.0.0, this will not match, because of the querystring
// solution: update the intercept to match the querystring with a wildcard:
cy.intercept('/items?*')

Also, requests for paths in nested directories may be affected:

// will this intercept match a request for `/some/items`?
cy.intercept('/items')
// ✅ before 7.0.0, this will match, because it is a substring
// ❌ after 7.0.0, this will not match, because of the leading directory
// solution: update the intercept to include the directory:
cy.intercept('/some/items')

Additionally, the matchUrlAgainstPath RouteMatcher option that was added in Cypress 6.2.0 has been removed in Cypress 7.0. It can be safely removed from tests.

Deprecated cy.route2() command removed

cy.route2() was the original name for cy.intercept() during the experimental phase of the feature. It was deprecated in Cypress 6.0. In Cypress 7.0, it has been removed entirely. Please update existing usages of cy.route2() to call cy.intercept() instead.

Before
cy.route2('/widgets/*', { fixture: 'widget.json' }).as('widget')
After
cy.intercept('/widgets/*', { fixture: 'widget.json' }).as('widget')

res.delay() and res.throttle() have been renamed

The res.delay() and res.throttle() functions that exist on responses yielded to response handlers have been renamed.

The new names are res.setDelay() and res.setThrottle(), respectively.

Before
cy.intercept('/slow', (req) => {
  req.continue((res) => {
    // apply a delay of 1 second and a throttle of 56kbps
    res.delay(1000).throttle(56)
  })
})
After
cy.intercept('/slow', (req) => {
  req.continue((res) => {
    // apply a delay of 1 second and a throttle of 56kbps
    res.setDelay(1000).setThrottle(56)
  })
})

Read more about available functions on res.

Falsy values are no longer dropped in StaticResponse bodies

Previously, falsy values supplied as the body of a StaticResponse would get dropped (the same as if no body was supplied). Now, the bodies are properly encoded in the response.

Before
cy.intercept('/does-it-exist', { body: false })
// Requests to `/does-it-exist` receive an empty response body
After
cy.intercept('/does-it-exist', { body: false })
// Requests to `/does-it-exist` receive a response body of `false`

Errors thrown by request and response handlers are no longer wrapped

Previously, errors thrown inside of req and res handlers would be wrapped by a CypressError. In 7.0.0, errors thrown inside of these handlers are not wrapped before failing the test.

This should only affect users who are explicitly asserting on global errors. See #15189 for more details.

Component Testing

In 7.0, component testing is no longer experimental. Cypress now ships with a dedicated component test runner with a new UI and dedicated commands to launch it.

Changes are required for all existing projects. The required changes are limited to configuration and there are no breaking changes to the mount API. The migration guide contains the following steps:

  1. Update your Cypress configuration to remove experimentalComponentTesting
  2. Install updated dependencies
  3. Update the plugins file
  4. Use CLI commands to launch
  5. Update the support file (optionally)

1. Remove experimentalComponentTesting config

The experimentalComponentTesting configuration is no longer needed to run component tests. Remove this flag in order to run Cypress tests without erroring.

Before
experimentalComponentTesting flag is required for component testing
{
  "experimentalComponentTesting": true,
  "componentFolder": "src",
  "testFiles": "**/*spec.{js,jsx,ts,tsx}"
}
After
experimentalComponentTesting flag must be removed
{
  "componentFolder": "src",
  "testFiles": "**/*spec.{js,jsx,ts,tsx}"
}

2. Install component testing dependencies

The Component Test Runner requires the following dependencies:

Install React dependencies

  1. Upgrade to @cypress/react 5.X.
  2. Install @cypress/webpack-dev-server.
  3. (Optional) Install cypress-react-selector if any tests use cy.react().
  4. (Optional) Install code coverage, see installation steps).
npm i cypress @cypress/react @cypress/webpack-dev-server -D

Install Vue 3 dependencies

  1. Upgrade to @cypress/vue@next (3.X and above).
  2. Install @cypress/webpack-dev-server.
npm i cypress @cypress/vue@next @cypress/webpack-dev-server -D

Install Vue 2 dependencies

  1. Upgrade to @cypress/vue@2 (2.X only).
  2. Install @cypress/webpack-dev-server.
npm i cypress @cypress/vue @cypress/webpack-dev-server -D

3. Update plugins file to use dev-server:start

Re-using a project's local development server instead of file preprocessors

In 7.0 Cypress component tests require that code is bundled with your local development server, via a new dev-server:start event. This event replaces the previous file:preprocessor event.

Before
Plugins file registers the file:preprocessor event
const webpackPreprocessor = require('@cypress/webpack-preprocessor')
const webpackConfig = require('../webpack.config.js')

module.exports = (on, config) => {
  on('file:preprocessor', webpackPreprocessor(options))
}
After
Plugins file registers the dev-server:start event
// The @cypress/webpack-dev-server package replaces @cypress/webpack-preprocessor
const { startDevServer } = require('@cypress/webpack-dev-server')
const webpackConfig = require('../webpack.config.js')

module.exports = (on, config) => {
  // You must use the dev-server:start event instead of the file:preprocessor event

  on('dev-server:start', (options) => {
    return startDevServer({ options, webpackConfig })
  })
}

Configure plugins.js for React projects

Projects using React may not need to update their plugins file. If your project is using a webpack scaffold or boilerplate, it is recommended to use a preset plugin imported from @cypress/react/plugins.

Preset Plugins for React

If you are using a preset plugin within @cypress/react, you should not need to update your plugins file. To check if you are using a preset, check to see if your plugins file contains an import to a file inside of @cypress/react/plugins.

After
An example plugins file to configure component testing in a React Scripts project
// The @cypress/react project exposes preset plugin configurations
// These presets automatically register the events to bundle the project properly
const injectReactScriptsDevServer = require('@cypress/react/plugins/react-scripts')

module.exports = (on, config) => {
  // Internally, this method registers `dev-server:start` with the proper webpack configuration
  // Previously, it registered the `file:preprocessor` event.
  injectReactScriptsDevServer(on, config)

  return config
}

Configure plugins.js for Vue

Projects using Vue will likely be using either @vue/cli or manually defining webpack configuration. These steps are identical to the manual setup steps, with the exception of how you resolve the webpack configuration. To access the resolved webpack configuration that contains any vue.config.js setup or the default @vue/cli webpack setup, you must import the configuration and pass it into @cypress/webpack-dev-server.

After
An example plugins file to configure component testing in a Vue CLI project
const { startDevServer } = require('@cypress/webpack-dev-server')

// The resolved configuration, which contains any `vue.config.js` setup
const webpackConfig = require('@vue/cli-service/webpack.config.js')

module.exports = (on, config) => {
  on('dev-server:start', (options) => {
    return startDevServer({ options, webpackConfig })
  })
}

Configuring a project with vanilla webpack

For projects with manually defined or ejected webpack configurations, the webpack configuration must be passed in.

After
An example plugins file to configure component testing in a project with vanilla webpack
const { startDevServer } = require('@cypress/webpack-dev-server')
const webpackConfig = require('../webpack.config.js')

module.exports = (on, config) => {
  on('dev-server:start', (options) => {
    return startDevServer({ options, webpackConfig })
  })
}

4. Use CLI commands to launch

To run your component tests you must use the dedicated component testing subcommands.

  • cypress open-ct
  • cypress run-ct
Before
Commands launches both end-to-end and component tests.
cypress run
After
Command launches Cypress Component Test Runner and executes component tests. End-to-end tests are run separately.
# open component testing runner
cypress open-ct

# run all component tests
cypress run-ct

# e2e tests
cypress open
cypress run

5. Update the support file (optionally)

Previously, a support file was required to set up the component testing target node. This is no longer necessary.

Specifically for React users, if the support file contains the following line, please remove it. The import will fail in the future. We have left it in to avoid a breaking change, but the file does nothing.

Before
The support file was required to import a script from @cypress/react
// support.js

// This import should be removed, it will error in a future update
import '@cypress/react/hooks'

Expanded stylesheet support

Stylesheets are now bundled and imported within spec and support files. Previously, many of mount's mounting options such as stylesheets, cssFiles, and styles were required to import stylesheets into your component tests. This often involved pre-compiling the stylesheets before launching the component tests, which affected performance. Migrating to imports for these styles is optional, but recommended.

Now, stylesheets should be loaded into the document the same way they are in your application. It is recommended you update your code like so:

Before
Stylesheets were loaded using the filesystem
const { mount } = require('@cypress/react')
const Button = require('./Button')

it('renders a Button', () => {
  // Mounting a button and loading the Tailwind CSS library
  mount(<Button />, {
    stylesheets: [
      // Paths are relative to the project root directory and must be pre-compiled
      // Because they are static, they do not watch for file updates
      '/dist/index.css',
      '/node_modules/tailwindcss/dist/tailwind.min.css',
    ],
  })
})
After
Stylesheets are supported via an import and mountingOptions.stylesheets is not recommended
// In the majority of modern style-loaders,
// these styles will be injected into document.head when they're imported below
require('./index.scss')
require('tailwindcss/dist/tailwind.min.css')

const { mount } = require('@cypress/react')
const Button = require('./Button')

it('renders a Button', () => {
  // This button will render with the Tailwind CSS styles
  // as well as the application's index.scss styles
  mount(<Button />)
})

Desktop GUI no longer displays component tests

Previously, the Desktop GUI displayed both end-to-end and component tests. Now, component tests are only displayed when launching via the component testing-specific subcommands. cypress open-ct (or run-ct in CI)

Executing all or some component tests

In 6.X, the Desktop GUI had support for finding and executing a subset of component tests. In 7.0, this is possible with the --headed command and a spec glob, like so:

cypress run-ct --headed --spec **/some-folder/*spec.*

Coverage

Previously, the @cypress/react 4.X package embedded code coverage in your tests automatically.

If you still wish to record code coverage in your tests, you must manually install it. Please see our code coverage guide for the latest steps.

cypress-react-selector

If you use cy.react() in your tests, you must manually install cypress-react-selector with npm i cypress-react-selector -D. You do not need to update your support file.

HTML Side effects

As of 7.0, we only clean up components mounted by Cypress via @cypress/react or @cypress/vue.

We no longer automatically reset the document.body between tests. Any HTML side effects of your component tests will carry over.

Before
All HTML content was cleared between spec files
const { mount } = require('@cypress/react')

describe('Component teardown behavior', () => {
  it('modifies the document and mounts a component', () => {
    // HTML unrelated to the component is mounted
    Cypress.$('body').append('<div data-cy="some-html"/>')

    // A component is mounted
    mount(<Button data-cy="my-button"></Button>)

    cy.get('[data-cy="some-html"]').should('exist')
    cy.get('[data-cy="my-button"]').should('exist')
  })

  it('cleans up any HTML', () => {
    // The component is automatically unmounted by Cypress
    cy.get('[data-cy="my-button"]').should('not.exist')

    // The HTML left over from the previous test has been cleaned up
    // This was done automatically by Cypress
    cy.get('[data-cy="some-html"]').should('not.exist')
  })
})
After
Only the components are cleaned up between spec files
const { mount } = require('@cypress/react')

describe('Component teardown behavior', () => {
  it('modifies the document and mounts a component', () => {
    // HTML unrelated to the component is mounted
    Cypress.$('body').append('<div data-cy="some-html"/>')

    // A component is mounted
    mount(<Button data-cy="my-button"></Button>)

    cy.get('[data-cy="some-html"]').should('exist')
    cy.get('[data-cy="my-button"]').should('exist')
  })

  it('only cleans up *components* between tests', () => {
    // The component is automatically unmounted by Cypress
    cy.get('[data-cy="my-button"]').should('not.exist')

    // The HTML left over from the previous test should be manually cleared
    cy.get('[data-cy="some-html"]').should('not.exist')
  })
})

Legacy cypress-react-unit-test and cypress-vue-unit-test packages

For users upgrading from cypress-react-unit-tests or cypress-vue-unit-tests, please update all references to use @cypress/react or @cypress/vue. These packages have been deprecated and moved to the Cypress scope on npm.

Uncaught exception and unhandled rejections

In 7.0, Cypress now fails tests in more situations where there is an uncaught exception and also if there is an unhandled promise rejection in the application under test.

You can ignore these situations and not fail the Cypress test with the code below.

Turn off all uncaught exception handling

Cypress.on('uncaught:exception', (err, runnable) => {
  // returning false here prevents Cypress from
  // failing the test
  return false
})

Turn off uncaught exception handling unhandled promise rejections

Cypress.on('uncaught:exception', (err, runnable, promise) => {
  // when the exception originated from an unhandled promise
  // rejection, the promise is provided as a third argument
  // you can turn off failing the test in this case
  if (promise) {
    // returning false here prevents Cypress from
    // failing the test
    return false
  }
})

Node.js 12+ support

Cypress comes bundled with its own Node.js version. However, installing the cypress npm package uses the Node.js version installed on your system.

Node.js 10 reached its end of life on Dec 31, 2019 and Node.js 13 reached its end of life on June 1, 2019. See Node's release schedule. These Node.js versions will no longer be supported when installing Cypress. The minimum Node.js version supported to install Cypress is Node.js 12 or Node.js 14+.

Migrating cy.route() to cy.intercept()

This guide details how to change your test code to migrate from cy.route() to cy.intercept(). cy.server() and cy.route() are deprecated in Cypress 6.0.0. In a future release, support for cy.server() and cy.route() will be removed.

Please also refer to the full documentation for cy.intercept().

Match simple route

In many use cases, you can replace cy.route() with cy.intercept() and remove the call to cy.server() (which is no longer necessary).

Before
// Set up XHR listeners using cy.route()
cy.server()
cy.route('/users').as('getUsers')
cy.route('POST', '/project').as('createProject')
cy.route('PATCH', '/projects/*').as('updateProject')
After
// Intercept HTTP requests
cy.intercept('/users').as('getUsers')
cy.intercept('POST', '/project').as('createProject')
cy.intercept('PATCH', '/projects/*').as('updateProject')

Match against url and path

The url argument to cy.intercept() matches against the full url, as opposed to the url or path in cy.route(). If you're using the url argument in cy.intercept(), you may need to update your code depending on the route you're trying to match.

Before
// Match XHRs with a path or url of /users
cy.server()
cy.route({
  method: 'POST',
  url: '/users',
}).as('getUsers')
After
// Match HTTP requests with a path of /users
cy.intercept({
  method: 'POST',
  path: '/users',
}).as('getUsers')

// OR
// Match HTTP requests with an exact url of https://example.cypress.io/users
cy.intercept({
  method: 'POST',
  url: 'https://example.cypress.io/users',
}).as('getUsers')

cy.wait() object

The object returned by cy.wait() is different from intercepted HTTP requests using cy.intercept() than the object returned from an awaited cy.route() XHR.

Before
// Wait for XHR from cy.route()
cy.route('POST', '/users').as('createUser')
// ...
cy.wait('@createUser').then(({ requestBody, responseBody, status }) => {
  expect(status).to.eq(200)
  expect(requestBody.firstName).to.eq('Jane')
  expect(responseBody.firstName).to.eq('Jane')
})
After
// Wait for intercepted HTTP request
cy.intercept('POST', '/users').as('createUser')
// ...
cy.wait('@createUser').then(({ request, response }) => {
  expect(response.statusCode).to.eq(200)
  expect(request.body.name).to.eq('Jane')
  expect(response.body.name).to.eq('Jane')
})

Fixtures

You can stub requests and response with fixture data by defining a fixture property in the routeHandler argument for cy.intercept().

Before
// Stub response with fixture data using cy.route()
cy.route('GET', '/projects', 'fx:projects')
After
// Stub response with fixture data using cy.intercept()
cy.intercept('GET', '/projects', {
  fixture: 'projects',
})

Override intercepts

As of 7.0, newer intercepts are called before older intercepts, allowing users to override intercepts. See "Handler ordering is reversed" for more details.

Before 7.0, intercepts could not be overridden. See #9302 for more details.

Migrating to Cypress 6.0

This guide details the changes and how to change your code to migrate to Cypress 6.0. See the full changelog for 6.0.

Non-existent element assertions

In previous versions of Cypress, there was a possibility for tests to falsely pass when asserting a negative state on non-existent elements.

For example, in the tests below we want to test that the search dropdown is no longer visible when the search input is blurred because we hide the element in CSS styles. Except in this test, we've mistakenly misspelled one of our selectors.

cy.get('input[type=search]').type('Cypress')
cy.get('#dropdown').should('be.visible')
cy.get('input[type=search]').blur()

// below we misspelled "dropdown" in the selector 😞
// the assertions falsely pass in Cypress < 6.0
// and will correctly fail in Cypress 6.0 +
cy.get('#dropdon').should('not.be.visible')
cy.get('#dropdon').should('not.have.class', 'open')
cy.get('#dropdon').should('not.contain', 'Cypress')
non-existent element before 6.0

In 6.0, these assertions will now correctly fail, telling us that the #dropdon element doesn't exist in the DOM.

non-existent element in 6.0

Assertions on non-existent elements

This fix may cause some breaking changes in your tests if you are relying on assertions such as not.be.visible or not.contains to test that the DOM element did not exist in the DOM. This means you'll need to update your test code to be more specific about your assertions on non-existent elements.

Before
Assert that non existent element was not visible
it('test', () => {
  // the modal element is removed from the DOM on click
  cy.get('[data-cy="modal"]').find('.close').click()
  // assertions below pass in < 6.0, but properly fail in 6.0+
  cy.get('[data-cy="modal"]').should('not.be.visible')
  cy.get('[data-cy="modal"]').should('not.contain', 'Upgrade')
})
After
Assert that non existent element does not exist
it('test', () => {
  // the modal element is removed from the DOM on click
  cy.get('data-cy="modal"').find('.close').click()
  // we should instead assert that the element doesn't exist
  cy.get('data-cy="modal"').should('not.exist')
})

Opacity visibility

DOM elements with opacity: 0 style are no longer considered to be visible. This includes elements with an ancestor that has opacity: 0 since a child element can never have a computed opacity greater than that of an ancestor.

Elements where the CSS property (or ancestors) is opacity: 0 are still considered actionable however and any action commands used to interact with the element will perform the action. This matches browser's implementation on how they regard elements with opacity: 0.

Assert visibility of opacity: 0 element

Before
Failed assertion that opacity: 0 element is not visible.
it('test', () => {
  // '.hidden' has 'opacity: 0' style.
  // In < 5.0 this assertion would fail
  cy.get('.hidden').should('not.be.visible')
})
After
Passed assertion that opacity: 0 element is not visible.
it('test', () => {
  // '.hidden' has 'opacity: 0' style.
  // In 6.0 this assertion will pass
  cy.get('.hidden').should('not.be.visible')
})

Perform actions on opacity: 0 element

In all versions of Cypress, you can interact with elements that have opacity: 0 style.

it('test', () => {
  // '.hidden' has 'opacity: 0' style.
  cy.get('.hidden').click() // ✅ clicks on element
  cy.get('.hidden').type('hi') // ✅ types into element
  cy.get('.hidden').check() // ✅ checks element
  cy.get('.hidden').select('yes') // ✅ selects element
})

cy.wait(alias) type

cy.route() is deprecated in 6.0.0. We encourage the use of cy.intercept() instead. Due to this deprecation, the type yielded by cy.wait(alias) has changed.

Before
Before 6.0.0, cy.wait(alias) would yield an object of type WaitXHR .
After
In 6.0.0 and onwards, cy.wait(alias) will yield an object of type Interception . This matches the new interception object type used for cy.intercept() .

Restore old behavior

If you need to restore the type behavior prior to 6.0.0 for cy.wait(alias), you can declare a global override for cy.wait() like so:

declare global {
  namespace Cypress {
    interface Chainable<Subject = any> {
      wait(alias: string): Chainable<Cypress.WaitXHR>
    }
  }
}

—disable-dev-shm-usage

We now pass —disable-dev-shm-usage to the Chrome browser flags by default. If you're passing this flag in your plugins file, you can now remove this code.

Before
Passing flag in plugins file.
// cypress/plugins/index.js
module.exports = (on, config) => {
  on('before:browser:launch', (browser = {}, launchOptions) => {
    if (browser.family === 'chromium' && browser.name !== 'electron') {
      launchOptions.args.push('--disable-dev-shm-usage')
    }

    return launchOptions
  })
}
After
Remove flag from plugins file.
// cypress/plugins/index.js
module.exports = (on, config) => {}

Restore old behavior

If you need to remove the flag in 6.0.0+, you can follow the workaround documented here: #9242.

Migrating to Cypress 5.0

This guide details the changes and how to change your code to migrate to Cypress 5.0. See the full changelog for 5.0.

Tests retries

Test retries are available in Cypress 5.0. This means that tests can be re-run a number of times before potentially being marked as a failed test. Read the Test Retries doc for more information on how this works and how to turn on test retries.

When test retries are turned on, there will now be a screenshot taken for every failed attempt, so there could potentially be more than 1 screenshot per test failure. Read the Test Retries doc for more information on how this works.

The cypress-plugin-retries plugin has been deprecated in favor of test retries built into Cypress. There's guidance below on how to migrate from the cypress-plugin-retries plugin to Cypress's built-in test retries.

Configure test retries via the CLI

Before
Setting retries with cypress-plugin-retries via env vars
CYPRESS_RETRIES=2 cypress run
After
Setting test retries in Cypress 5.0 via env vars
CYPRESS_RETRIES=2 cypress run

Configure test retries in the configuration file

Before
Setting retries with cypress-plugin-retries via configuration
{
  "env": {
    "RETRIES": 2
  }
}
After
Setting test retries in Cypress 5.0 via configuration
{
  "retries": 1
}
  • runMode allows you to define the number of test retries when running cypress run
  • openMode allows you to define the number of test retries when running cypress open
{
  "retries": {
    "runMode": 1,
    "openMode": 3
  }
}

Configure test retries per test

Before
Setting retries with cypress-plugin-retries via the test
it('test', () => {
  Cypress.currentTest.retries(2)
})
After
Setting test retries in Cypress 5.0 via test options
it(
  'allows user to login',
  {
    retries: 2,
  },
  () => {
    // ...
  }
)
  • runMode allows you to define the number of test retries when running cypress run
  • openMode allows you to define the number of test retries when running cypress open
it(
  'allows user to login',
  {
    retries: {
      runMode: 2,
      openMode: 3,
    },
  },
  () => {
    // ...
  }
)

Module API results

To more accurately reflect result data for runs with test retries, the structure of each run's runs array resolved from the Promise returned from cypress.run() of the Module API has changed.

Mainly there is a new attempts Array on each test which will reflect the result of each test retry.

Before
results.runs Module API results
{
  // ...
  "runs": [{
    // ...
    "hooks": [{
      "hookId": "h1",
      "hookName": "before each",
      "title": [ "before each hook" ],
      "body": "function () {\n  expect(true).to.be["true"];\n}"
    }],
    // ...
    "screenshots": [{
      "screenshotId": "8ddmk",
      "name": null,
      "testId": "r2",
      "takenAt": "2020-08-05T08:52:20.432Z",
      "path": "User/janelane/my-app/cypress/screenshots/spec.js/test (failed).png",
      "height": 720,
      "width": 1280
    }],
    "stats": {
      // ...
      "wallClockStartedAt": "2020-08-05T08:38:37.589Z",
      "wallClockEndedAt": "2018-07-11T17:53:35.675Z",
      "wallClockDuration": 1171
    },
    "tests": [{
      "testId": "r2",
      "title": [ "test" ],
      "state": "failed",
      "body": "function () {\n  expect(true).to.be["false"];\n}",
      "stack": "AssertionError: expected true to be false\n' +
        '    at Context.eval (...cypress/integration/spec.js:5:21",
      "error": "expected true to be false",
      "timings": {
        "lifecycle": 16,
        "test": {...}
      },
      "failedFromHookId": null,
      "wallClockStartedAt": "2020-08-05T08:38:37.589Z",
      "wallClockDuration": 1171,
      "videoTimestamp": 4486
    }],
  }],
  // ...
}
After
results.runs Module API results
{
  // ...
  "runs": [{
    // ...
    "hooks": [{
      "hookName": "before each",
      "title": [ "before each hook" ],
      "body": "function () {\n  expect(true).to.be["true"];\n}"
    }],
    // ...
    "stats": {
      // ...
      "startedAt": "2020-08-05T08:38:37.589Z",
      "endedAt": "2018-07-11T17:53:35.675Z",
      "duration": 1171
    },
    "tests": [{
      "title": [ "test" ],
      "state": "failed",
      "body": "function () {\n  expect(true).to.be["false"];\n}",
      "displayError": "AssertionError: expected true to be false\n' +
      '    at Context.eval (...cypress/integration/spec.js:5:21",
      "attempts": [{
        "state": "failed",
        "error": {
          "message": "expected true to be false",
          "name": "AssertionError",
          "stack": "AssertionError: expected true to be false\n' +
      '    at Context.eval (...cypress/integration/spec.js:5:21"
        },
        "screenshots": [{
          "name": null,
          "takenAt": "2020-08-05T08:52:20.432Z",
          "path": "User/janelane/my-app/cypress/screenshots/spec.js/test (failed).png",
          "height": 720,
          "width": 1280
        }],
        "startedAt": "2020-08-05T08:38:37.589Z",
        "duration": 1171,
        "videoTimestamp": 4486
      }]
    }],
  }],
  // ...
}

Cookies whitelist option renamed

The Cypress.Cookies.defaults() whitelist option has been renamed to preserve to more closely reflect its behavior.

Before
whitelist option
Cypress.Cookies.defaults({
  whitelist: 'session_id',
})
After
preserve option
Cypress.Cookies.defaults({
  preserve: 'session_id',
})

blacklistHosts configuration renamed

The blacklistHosts configuration has been renamed to blockHosts to more closely reflect its behavior.

This should be updated in all places where Cypress configuration can be set including via the Cypress configuration file, command line arguments, the pluginsFile, Cypress.config() or environment variables.

Before
blacklistHosts configuration
{
  "blacklistHosts": "www.google-analytics.com"
}
After
blockHosts configuration
{
  "blockHosts": "www.google-analytics.com"
}

Return type of Cypress.Blob changed

We updated the Blob library used behind Cypress.Blob from 1.3.3 to 2.0.2.

The return type of the Cypress.Blob methods arrayBufferToBlob, base64StringToBlob, binaryStringToBlob, and dataURLToBlob have changed from Promise<Blob> to Blob.

Before
Cypress.Blob methods returned a Promise
Cypress.Blob.base64StringToBlob(this.logo, 'image/png').then((blob) => {
  // work with the returned blob
})
After
Cypress.Blob methods return a Blob
const blob = Cypress.Blob.base64StringToBlob(this.logo, 'image/png')

// work with the returned blob

cy.server() whitelist option renamed

The cy.server() whitelist option has been renamed to ignore to more closely reflect its behavior.

Before
whitelist option
cy.server({
  whitelist: (xhr) => {
    return xhr.method === 'GET' && /\.(jsx?|html|css)(\?.*)?$/.test(xhr.url)
  },
})
After
ignore option
cy.server({
  ignore: (xhr) => {
    return xhr.method === 'GET' && /\.(jsx?|html|css)(\?.*)?$/.test(xhr.url)
  },
})

Cookies sameSite property

Values yielded by cy.setCookie(), cy.getCookie(), and cy.getCookies() will now contain the sameSite property if specified.

If you were using the experimentalGetCookiesSameSite configuration to get the sameSite property previously, this should be removed.

Before
Cookies yielded before had no sameSite property.
cy.getCookie('token').then((cookie) => {
  // cy.getCookie() yields a cookie object
  // {
  //   domain: "localhost",
  //   expiry: 1593551644,
  //   httpOnly: false,
  //   name: "token",
  //   path: "/commands",
  //   secure: false,
  //   value: "123ABC"
  // }
})
After
Cookies yielded now have sameSite property if specified.
cy.getCookie('token').then((cookie) => {
  // cy.getCookie() yields a cookie object
  // {
  //   domain: "localhost",
  //   expiry: 1593551644,
  //   httpOnly: false,
  //   name: "token",
  //   path: "/commands",
  //   sameSite: "strict",
  //   secure: false,
  //   value: "123ABC"
  // }
})

dirname / filename

The globals __dirname and __filename no longer include a leading slash.

Before
__dirname / __filename
// cypress/integration/app_spec.js
it('include leading slash < 5.0', () => {
  expect(__dirname).to.equal('/cypress/integration')
  expect(__filename).to.equal('/cypress/integration/app_spec.js')
})
After
__dirname / __filename
// cypress/integration/app_spec.js
it('do not include leading slash >= 5.0', () => {
  expect(__dirname).to.equal('cypress/integration')
  expect(__filename).to.equal('cypress/integration/app_spec.js')
})

Linux dependencies

Running Cypress on Linux now requires the libgbm dependency (on Debian-based systems, this is available as libgbm-dev). To install all required dependencies on Ubuntu/Debian, you can run the script below:

apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb

TypeScript esModuleInterop

Cypress no longer forces the esModuleInterop compiler option for TypeScript to be true for spec, support, and plugins files. We recommend setting it in your project's tsconfig.json instead if you need to.

// tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true
    /* ... other compiler options ... */
  }
}

TypeScript 3.4+ support

Cypress 5.0 raises minimum required TypeScript version from 2.9+ to 3.4+. You'll need to have TypeScript 3.4+ installed within your project to have TypeScript support within Cypress.

Node.js 10+ support

Cypress comes bundled with its own Node.js version. However, installing the cypress npm package uses the Node.js version installed on your system.

Node.js 8 reached its end of life on Dec 31, 2019 and Node.js 11 reached its end of life on June 1, 2019. See Node's release schedule. These Node.js versions will no longer be supported when installing Cypress. The minimum Node.js version supported to install Cypress is Node.js 10 or Node.js 12+.

Migrating to Cypress 4.0

This guide details the changes and how to change your code to migrate to Cypress 4.0. See the full changelog for 4.0.

Mocha upgrade

Mocha was upgraded from 2.5.3 to 7.0.1, which includes a number of breaking changes and new features outlined in their changelog. Some changes you might notice are described below.

Breaking Change: invoke done callback and return a promise

Starting with Mocha 3.0.0, invoking a done callback and returning a promise in a test results in an error.

This error originates from Mocha and is discussed at length here and here.

The reason is that using two different ways to signal that a test is finished is usually a mistake and there is always a way to only use one. There is a proposal to handle this situation without erroring that may be released in a future version of Mocha.

In the meantime, you can fix the error by choosing a single way to signal the end of your test's execution.

Example #1
Before
This test has a done callback and a promise
it('uses invokes done and returns promise', (done) => {
  return codeUnderTest.doSomethingThatReturnsPromise().then((result) => {
    // assertions here
    done()
  })
})
After
You can remove the done callback and return the promise instead:
it('uses invokes done and returns promise', () => {
  return codeUnderTest.doSomethingThatReturnsPromise().then((result) => {
    // assertions here
  })
})
Example #2
Before
Sometimes it might make more sense to use the done callback and not return a promise:
it('uses invokes done and returns promise', (done) => {
  eventEmitter.on('change', () => {
    // assertions
    done()
  })

  return eventEmitter.doSomethingThatEmitsChange()
})
After
In this case, you don't need to return the promise:
it('uses invokes done and returns promise', (done) => {
  eventEmitter.on('change', () => {
    // assertions
    done()
  })

  eventEmitter.doSomethingThatEmitsChange()
})
Example #3

Test functions using async/await automatically return a promise, so they need to be refactored to not use a done callback.

Before
This will cause an overspecified error.
it('uses async/await', async (done) => {
  const eventEmitter = await getEventEmitter()
  eventEmitter.on('change', () => done())
  eventEmitter.doSomethingThatEmitsChange()
})
After
Update to the test code below.
it('uses async/await', async () => {
  const eventEmitter = await getEventEmitter()
  return new Promise((resolve) => {
    eventEmitter.on('change', () => resolve())
    eventEmitter.doSomethingThatEmitsChange()
  })
})

Tests require a title

Tests now require a title and will error when not provided one.

// Would show as pending in Cypress 3
// Will throw type error in Cypress 4:
it() // Test argument "title" should be a string. Received type "undefined"

Chai upgrade

Chai was upgraded from 3.5.0 to 4.2.0, which includes a number of breaking changes and new features outlined in Chai's migration guide. Some changes you might notice are described below.

Breaking Change: assertions expecting numbers

Some assertions will now throw an error if the assertion's target or arguments are not numbers, including within, above, least, below, most, increase and decrease.

// These will now throw errors:
expect(null).to.be.within(0, 1)
expect(null).to.be.above(10)
// This will not throw errors:
expect('string').to.have.a.length.of.at.least(3)

Breaking Change: empty assertions

The .empty assertion will now throw when it is passed non-string primitives and functions.

// These will now throw TypeErrors
expect(Symbol()).to.be.empty
expect(() => {}).to.be.empty

Breaking Change: non-existent properties

An error will throw when a non-existent property is read. If there are typos in property assertions, they will now appear as failures.

// Would pass in Cypress 3 but will fail correctly in 4
expect(true).to.be.ture

Breaking Change: include checks strict equality

include now always use strict equality unless the deep property is set.

Before
include would always use deep equality
// Would pass in Cypress 3 but will fail correctly in 4
cy.wrap([
  {
    first: 'Jane',
    last: 'Lane',
  },
]).should('include', {
  first: 'Jane',
  last: 'Lane',
})
After
Need to specificy deep.include for deep equality
// Specifically check for deep.include to pass in Cypress 4
cy.wrap([
  {
    first: 'Jane',
    last: 'Lane',
  },
]).should('deep.include', {
  first: 'Jane',
  last: 'Lane',
})

Sinon.JS upgrade

Sinon.JS was upgraded from 3.2.0 to 8.1.1, which includes a number of breaking changes and new features outlined in Sinon.JS's migration guide. Some changes you might notice are described below.

Breaking Change: stub non-existent properties

An error will throw when trying to stub a non-existent property.

// Would pass in Cypress 3 but will fail in 4
cy.stub(obj, 'nonExistingProperty')

Breaking Change: reset() replaced by resetHistory()

For spies and stubs, the reset() method was replaced by resetHistory().

Before
Spies and stubs using reset() .
const spy = cy.spy()
const stub = cy.stub()

spy.reset()
stub.reset()
After
Update spies and stubs should now use resetHistory() .
const spy = cy.spy()
const stub = cy.stub()

spy.resetHistory()
stub.resetHistory()

Plugin Event before:browser:launch

Since we now support more advanced browser launch options, during before:browser:launch we no longer yield the second argument as an array of browser arguments and instead yield a launchOptions object with an args property.

You can see more examples of the new launchOptions in use in the Browser Launch API doc.

Before
The second argument is no longer an array.
on('before:browser:launch', (browser, args) => {
  // will print a deprecation warning telling you
  // to change your code to the new signature
  args.push('--another-arg')

  return args
})
After
Access the args property off launchOptions
on('before:browser:launch', (browser, launchOptions) => {
  launchOptions.args.push('--another-arg')

  return launchOptions
})

Electron options in before:browser:launch

Previously, you could pass options to the launched Electron BrowserWindow in before:browser:launch by modifying the launchOptions object.

Now, you must pass those options as launchOptions.preferences:

Before
Passing BrowserWindow options on the launchOptions object is no longer supported.
on('before:browser:launch', (browser, args) => {
  args.darkTheme = true

  return args
})
After
Pass BrowserWindow options on the options.preferences object instead.
on('before:browser:launch', (browser, launchOptions) => {
  launchOptions.preferences.darkTheme = true

  return launchOptions
})

Launching Chrome Canary with --browser

Before 4.0, cypress run --browser canary would run tests in Chrome Canary.

Now, you must pass --browser chrome:canary to select Chrome Canary.

See the docs for cypress run --browser for more information.

Before
Passing canary will no longer find a browser
cypress run --browser canary
After
Pass chrome:canary to launch Chrome Canary
cypress run --browser chrome:canary

Chromium-based browser family

We updated the Cypress browser objects of all Chromium-based browsers, including Electron, to have chromium set as their family field.

module.exports = (on, config) => {
  on('before:browser:launch', (browser = {}, launchOptions) => {
    if (browser.family === 'electron') {
      // would match Electron in 3.x
      // will match no browsers in 4.0.0
      return launchOptions
    }

    if (browser.family === 'chromium') {
      // would match no browsers in 3.x
      // will match any Chromium-based browser in 4.0.0
      // ie Chrome, Canary, Chromium, Electron, Edge (Chromium-based)
      return launchOptions
    }
  })
}

Example #1 (Finding Electron)

Before
This will no longer find the Electron browser.
module.exports = (on, config) => {
  on('before:browser:launch', (browser = {}, args) => {
    if (browser.family === 'electron') {
      // run code for Electron browser in 3.x
      return args
    }
  })
}
After
Use browser.name to check for Electron
module.exports = (on, config) => {
  on('before:browser:launch', (browser = {}, launchOptions) => {
    if (browser.name === 'electron') {
      // run code for Electron browser in 4.0.0
      return launchOptions
    }
  })
}

Example #2 (Finding Chromium-based browsers)

Before
This will no longer find any browsers.
module.exports = (on, config) => {
  on('before:browser:launch', (browser = {}, args) => {
    if (browser.family === 'chrome') {
      // in 4.x, `family` was changed to 'chromium' for all Chromium-based browsers
      return args
    }
  })
}
After
Use browser.name and browser.family to select non-Electron Chromium-based browsers
module.exports = (on, config) => {
  on('before:browser:launch', (browser = {}, launchOptions) => {
    if (browser.family === 'chromium' && browser.name !== 'electron') {
      // pass launchOptions to Chromium-based browsers in 4.0
      return launchOptions
    }
  })
}

cy.writeFile() yields null

cy.writeFile() now yields null instead of the contents written to the file. This change was made to more closely align with the behavior of Node.js fs.writeFile.

Before
This assertion will no longer pass
cy.writeFile('path/to/message.txt', 'Hello World').then((text) => {
  // Would pass in Cypress 3 but will fail in 4
  expect(text).to.equal('Hello World') // false
})
After
Instead read the contents of the file
cy.writeFile('path/to/message.txt', 'Hello World')
cy.readFile('path/to/message.txt').then((text) => {
  expect(text).to.equal('Hello World') // true
})

cy.contains() ignores invisible whitespaces

Browsers ignore leading, trailing, duplicate whitespaces. And Cypress now does that, too.

<p>hello world</p>
cy.get('p').contains('hello world') // Fail in 3.x. Pass in 4.0.0.
cy.get('p').contains('hello\nworld') // Pass in 3.x. Fail in 4.0.0.

Node.js 8+ support

Cypress comes bundled with its own Node.js version. However, installing the cypress npm package uses the Node.js version installed on your system.

Node.js 4 reached its end of life on April 30, 2018 and Node.js 6 reached its end of life on April 30, 2019. See Node's release schedule. These Node.js versions will no longer be supported when installing Cypress. The minimum Node.js version supported to install Cypress is Node.js 8.

CJSX is no longer supported

Cypress no longer supports CJSX (CoffeeScript + JSX), because the library used to transpile it is no longer maintained.

If you need CJSX support, you can use a pre-2.x version of the Browserify preprocessor.

npm install @cypress/browserify-preprocessor@1.1.2
// cypress/plugins/index.js
const browserify = require('@cypress/browserify-preprocessor')

module.exports = (on) => {
  on('file:preprocessor', browserify())
}