Recently I have made two large-scale transitions from Cypress v9.7.0 to the latest version 12.7.0. The first project was the example tests I used for my Cypress Network Testing Exercises course. You can find the example source code before the transition set up for v9 in the repo bahmutov/fastify-example-tests and after the transition in the repo bahmutov/fastify-example-tests-new. The second transition was for my day job at Mercari US. Altogether, both projects had about 400 spec files with 800 end-to-end tests.
I had several reasons for upgrading:
- I wanted to use the new query commands from my plugin cypress-map
- we started hitting the error #22291 a lot, both running Cypress v9.7.0 locally and on CI
1 | TypeError: Cannot read properties of undefined (reading 'isServer') |
- I was sick to my stomach of hitting the error #21428
1 | TypeError: ErrorConstructor is not a constructor |
There were several obstacles why we postponed upgrading for so long
- we use a lot of Cypress plugins (almost all plugins described in my course Cypress Plugins!) and some of them did not support Cypress v12 yet. For example, my own cypress-if plugin does not support v12 still. The plugins I could control, like cyclope I could upgrade to make work with Cypress v12.
- Cypress team changed how cy.log works for some reason
- upgrade requires time and effort. All code using
cy.if
andcy.then(cy.log)
has to be updated
Strategy
Ok, here is how the transition worked. We first renamed cypress/integration
folder to cypress/integration-all
. We took a few simple specs and moved them to the old cypress/integration
folder. This way we could migrate and run only a few specs at a time, and even work in parallel as a team. We split up and each engineer ported one subfolder at a time
1 | cypress/ |
Here I am starting by porting a single cypress/integration/feature-a
folder with a few specs. I installed Cypress v12.7.0 and opened it for the very first time. The upgrade wizard moved my plugins file and renamed the cypress/integration
folder to cypress/e2e
. We kept the original *.js
spec pattern.
We have went through the list of plugins and checked if there was a newly released version compatible with Cypress v12. Tip: you can use available-versions to quickly find out all versions of an NPM package.
Code changes
First, read the Cypres migration guides. They cover each major Cypress version. Here are some common code changes we had to do to move from Cypress v9 to v12.
CI changes
We changed our CircleCI and GitHub Actions workflows to new versions that support Cypress v12 and let the CI tell us all failing specs.
1 | # CircleCI config file |
1 | # GitHub Actions |
Replace cy.then(cy.log)
Previously code cy.then(cy.log)
yielded whatever the original subject of cy.then
was. In Cypress v10, cy.log
started yielding null
, "breaking" the subject. The solution was to wrap the original subject passed to cy.then
like this:
1 | // 🔻 Cypress v9 |
If we just want to print the subject value to the Command Log, we could replace .then(cy.log)
with an assertion. The assertion both checks the value and prints it to the Command Log.
1 | // ✅ all Cypress versions |
Even better was to use A Better Cypress Log Command, for example from cypress-map.
1 | // ✅ Cypress v12 |
Another example of changes due to cy.log
1 | // 🔻 Cypress v9 |
There were even hidden ways for cy.log
to trick you. If cy.then
callback function returns undefined, the yielded value is the the result of the last internal command. Which caused problems if you had cy.log
at the end of cy.then(callback)
1 | // 🔻 Cypress v9 |
The above code quietly changes the subject from the addresses
array to null
in Cypress v12 and had to be rewritten.
1 | // ✅ Cypress v12 |
Disable cy.invoke retries
I said many times that Cypress V12 Is A Big Deal, but cy.invoke
switching by default to retries is ... weird. For example, this code only runs once in Cypress v9, but multiple times in v10+ causing problems.
1 | // 🔻 Cypress v9 |
The trick to disable retries is to put them after or inside cy.then
command
1 | // ✅ Cypress v12 |
I wish Cypress team added an option to cy.invoke
command to skip retries, like .invoke({ retries: false }, 'json')
Of course, I got you, buddy. You can use cypress-map cy.invokeOnce
1 | // ✅ Cypress v12 |
Another example where I am using the application window's fetch
method (which I can intercept using cy.intercept
command, unlike cy.request
network calls)
1 | // 🔻 Cypress v9 |
Disable cy.as retries
Cypress v12 has changed how cy.as command works. If the previous commands are queries, accessing the aliased value would re-run the queries, which would suddenly show you a different value
1 | // 🔻 Cypress v9 |
In Cypress v12, if the .item
element changed, you would suddenly see "new item name", even if the value of the alias was "old item name" before. The solution is to save it with the "static" option
1 | // ✅ Cypress v12 |
Remove cypress-if commands
Removing cypress-if plugin with its cy.if
command was very sad. It is a powerful plugin, even if it relies on internals of Cypress command chain implementation.
Existence condition
The element existence assertion is built into Cypress querying commands, like cy.get
, cy.find
, and cy.contains
. If we wanted to conditionally do operations if the element exists or not, we had to disable the built-in assertion using cy.should(Cypress._.noop)
assertion and check inside the cy.then(callback)
1 | // 🔻 Cypress v9 |
Visibility condition
1 | // 🔻 Cypress v9 |
Checked box condition
If a checkbox might be checked or not, and we wanted to have it checked:
1 | // 🔻 Cypress v9 |
Location pathname
Imagine you are buying an item, but sometimes the system does extra security checks, and redirects you to verify you credit card. We used to handle it quite easily by checking the location pathname after the "Purchase" click.
1 | // 🔻 Cypress v9 |
Without cypress-if
the simplest check waits 5 seconds, then checks the URL
1 | // ✅ Cypress v12 |
I think this was it. Now smooth sailing with Cypress v12.