Migrating Cypress from v12 to v15
If you've been sitting comfortably on Cypress version 12, I don't blame you. It was a solid, stable release. But recently, I decided it was time to make the jump directly from v12 to the latest v15.
Why make such a huge leap? While there are plenty of under-the-hood performance improvements, my primary motivation was the modern plugin ecosystem. Specifically, I wanted to properly integrate and utilize the @cypress/code-coverage plugin. Many modern Cypress plugins now demand newer Node.js environments and Cypress v13+ as a baseline. To future-proof our test automation framework and get the best out of these tools, holding onto v12 was no longer an option.
However, jumping three major versions is rarely a simple npm install cypress@latest. Here is the step-by-step guide on how to survive the migration, the necessary cleanup, and how to resolve the inevitable TypeScript and Node module errors that follow.
1. The Golden Rule: Cypress Doesn't Upgrade Alone
The most common mistake when upgrading Cypress is only updating the cypress package itself. Cypress interacts deeply with your framework through plugins, preprocessors, and reporters. If you bump Cypress to v15 but leave your plugins in the v12 era, your framework will break immediately.
Before changing any versions, you need to map out your Cypress ecosystem. Run this command in your terminal:
npm ls cypress
This command will print out a tree of every package in your project that depends on Cypress (for example, @badeball/cypress-cucumber-preprocessor, cypress-file-upload, or eslint-plugin-cypress).
Action Item: Make a list of these packages. Go to their respective NPM or GitHub pages and check their compatibility matrices to find out which versions support Cypress 15.
2. The Clean Slate Approach
When doing a major version jump, relying on a standard npm update can sometimes leave stale cached files or conflicting peer dependencies. The safest way to upgrade is to burn it down and build it back up.
Step 1: Delete your existing dependencies and lock file.
# For Mac/Linux
rm -rf node_modules package-lock.json
Step 2: Open your package.json and manually update the versions for cypress and all the related plugins you identified in the previous step.
Step 3: Reinstall everything with a fresh slate.
npm install
3. Fixing the Fallout: Common Migration Errors
Once you install Cypress 15 and try to run your tests, you will likely hit a wall of red text. This is because Cypress v15 aligns with modern Node.js standards (Node 18+) and brings a much stricter TypeScript compiler under the hood.
Here are the two most common errors you will encounter and exactly why they happen.
Error Example 1: Cannot find module... or ESM Import Failures
The Symptom: Your plugins or config files suddenly fail to load, throwing Cannot find module errors, or complaining about import / require syntax.
The Cause: The Node.js ecosystem has largely shifted to ECMAScript Modules (ESM). Older setups often use moduleResolution: "node" in their tsconfig.json, which is actually a legacy setting based on Node.js v10. It completely ignores modern package routing features like the "exports" field in a modern package.json.
The Fix: You need to tell TypeScript to resolve modules the way modern Node.js does. Update your tsconfig.json to use node16 (or nodenext):
{
"compilerOptions": {
"target": "es2022",
"module": "node16",
"moduleResolution": "node16",
// ... other settings
}
}
Error Example 2: Property '...' does not exist on type 'Window'
The Symptom: You have custom code injecting state into the browser window, like window.tseState = ..., and suddenly TypeScript throws a hard error refusing to compile it.
The Cause: Cypress 15 ships with updated, stricter TypeScript definitions. In the past, TS might have been configured loosely enough to let you mutate the global Window object. Now, TS explicitly checks the official web standards: "Wait, tseState isn't a standard browser property!" and halts execution.
The Fix: For a quick, temporary fix to get your tests running, you can bypass the TS checker by casting the window object to any:
// The quick fix
(window as any).tseState = myStateObject;
However, as SDETs, we want robust, autocomplete-friendly code. The permanent fix is to extend the global Window interface in a definition file (like cypress/support/global.d.ts):
TypeScript
// The robust fix
declare global {
interface Window {
tseState: any; // Replace 'any' with your actual state interface if possible!
}
}
Conclusion
Migrating from Cypress 12 to 15 requires a bit of heavy lifting—cleaning out your node_modules, meticulously hunting down plugin versions, and wrestling with stricter TypeScript configurations. But the payoff is worth it. By aligning with modern Node standards (node16) and unlocking powerful plugins like code-coverage, your test automation framework will be faster, safer, and much more capable.
Happy testing!

