Note: you can find the source code for this blog post in the repository bahmutov/cypress-universal-test.
Start
Let's take a piece of JavaScript code
1 | function add(a, b) { |
This JavaScript code is universal - it runs the same way in the browser and in Node. Can we confirm this using just the Cypress test runner? Sure. We can directly import this file from the spec and it runs.
1 | $ npm i -D cypress |
1 | require('../../src/sum') |
The test runs, we can see the correct result printed in the browser's console.

This is far from optimal:
- the code runs immediately in the bundle, not during the test block
- we cannot assert the printed sum (since the
console.logis used before the test begins)
What I would like to do is to load and run src/sum.js on demand from the test itself, capture its console.log, console.warn and console.error output, and compare these output to the expected text.
Run code on demand
Let's load the src/sum.js file without running it. Since Cypress uses Webpack under the hood by default, we can install Webpack raw-loader to return the source as text.
1 | $ npm i -D raw-loader |
Let's change our spec.
1 | describe('Universal code', () => { |
The test passes, and the code is evaluated while running the test

By default, raw-loader returns the loaded source file as ES6 module, thus we have to use sum.default. We can turn the ES6 wrapper off by passing line option to the loader using raw-loader?esModule=false.
1 | describe('Universal code', () => { |
Capture output
The evaluated code printed the result to console.log. Let's buffer that output to assert its value.
1 | const util = require('util') |
The test runs and shows every intercepted console.log call from the evaluated src/sum.js

Compare to Node
Universal code should produce the same result when running in the browser as in Node. Let's run the same src/sum.js in Node using cy.task.
1 | module.exports = (on, config) => { |
From the test
1 | ... |
The plugin file executes the file as can be seen in the terminal output

💡 Node
requirecaches the module. Thus if you want to fully run the code again, you need to remove it from the module cache. Use require-and-forget to do this.
1
2 $ npm i -D require-and-forget
+ [email protected]cypress/plugins/index.js
1
2
3
4 const forget = require('require-and-forget')
...
// instead of require(filename)
forget(filename)
We need to capture console.log output, we can do this using the same log buffering in the plugins file and return it - this will be the value yielded by the cy.task command. Since there are no cy commands in the plugins file, we need to wrap the console.log ourselves.
1 | const util = require('util') |
Once the test runs, click on the task in the Command Log to see the yielded text.

💡 You can further isolate the plugins file from the Node code to run by running it inside a child process and then comparing the output, the exit code, etc. You can use execa-wrap to do this.
Now that we run the same code in the browser and in Node, we can remove the expected text and just compare the results from the two executions.
1 | const util = require('util') |
The test runs and compares the outputs - they are equal.

Multiple files
We only had a single src/sum.js file to run. What if we have a dynamic number of source files we want to run in the browser and in Node, comparing their outputs one by one? Let's add two more files to the src folder.
1 | function reverse(s) { |
1 | function cat(...args) { |
We need to create tests dynamically - every file in the src folder should get its own it function. To look through the src folder on disk, we can use the plugins file - since the plugins code executes in Node at the project's load. To find files I will use globby
1 | $ npm i -D globby |
We can find all src/*.js files and then pass the filenames to the spec running in the browser by saving them in the env object.
1 | const util = require('util') |
The environment object is available outside of individual tests through Cypress.env command. We can create a test by calling it for every source name (array .forEach is a synchronous method).
1 | describe('Universal code', () => { |
For now the above suite of tests just has skipped tests - but it shows that our plugins to spec file coordination is working.

Let's finish the spec code.
1 | const util = require('util') |
The tests pass, and you can see the matching output from the browser and from Node

The src/*.js scripts are truly universal - they output the same console text when running in the browser as they do when running in Node.