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.log
is 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
require
caches 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.