Fix For Cypress Plugin Events

A simple fix for the Cypress bug 22428 that only executes the last registered plugin.

If you register multiple Cypress plugins using on(eventName, callback) then Cypress v12.9.0 notifies only the last registered plugin when the event eventName happens. Seems like this bug was introduced in Cypress v10 and described in the issue #22428. At first I put up with it, even if I use a log of my own plugins but lately is has really been annoying as I combine cypress-split and @bahmutov/cypress-code-coverage plugins. So I decided to write one more plugin to fix on(eventName, callback) registration. I call my plugin cypress-on-fix and it is very easy to use in your projects.

Use

Just install the plugin as a dev dependency

1
2
3
4
# install using NPM
$ npm i -D cypress-on-fix
# install using Yarn
$ yarn add -D cypress-on-fix

Proxy the on argument in your cypress.config.js file like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const { defineConfig } = require('cypress')

module.exports = defineConfig({
e2e: {
// baseUrl, etc
supportFile: false,
fixturesFolder: false,
setupNodeEvents(cypressOn, config) {
const on = require('cypress-on-fix')(cypressOn)
// use "on" to register plugins, for example
// https://github.com/bahmutov/cypress-split
require('cypress-split')(on, config)
// https://github.com/bahmutov/cypress-watch-and-reload
require('cypress-watch-and-reload/plugins')(on, config)
// https://github.com/bahmutov/cypress-code-coverage
require('@bahmutov/cypress-code-coverage/plugin')(on, config)
},
},
})

Boom, Cypress emits an event, the wrapper code in cypress-on-fix receives it (single subscription) and then emits it to every listener subscribed to it (which can be multiple plugins). Why do I have to fix everything in this test runner?

Example

🎁 Find the simple example shown in this blog post in the repository bahmutov/cypress-on-fix-example.

Here is the cypress.config.js showing a typical config. We can have multiple "plugins" that need to do something before the test run, after each spec, and after the run is finished.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
e2e: {
// baseUrl, etc
supportFile: false,
fixturesFolder: false,
video: false,
setupNodeEvents(on, config) {
// implement node event listeners here
// and load any plugins that require the Node environment
on('before:run', () => {
console.log('before run 1')
})
on('before:run', () => {
console.log('before run 2')
})
// each spec
on('after:spec', (a) => {
console.log('after spec 1', a.relative)
})
on('after:spec', (a) => {
console.log('after spec 2', a.relative)
})
on('after:spec', (a) => {
console.log('after spec 3', a.relative)
})
// run finished
on('after:run', () => {
console.log('after run 1')
})
on('after:run', () => {
console.log('after run 2')
})
},
},
})

If you run this project, you will see only the last registered callback for every event:

1
2
3
4
5
6
7
8
9
$ npx cypress run
...
before run 2
...
after spec 3 cypress/e2e/spec1.cy.js
...
after spec 3 cypress/e2e/spec2.cy.js
...
after run 2

The previous callbacks were never called.

Now let's add the fix.

1
2
$ npm i -D cypress-on-fix
+ [email protected]

The new plugin should wrap the on argument passed to the setupNodeEvents(on, config)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// updated cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
e2e: {
// baseUrl, etc
supportFile: false,
fixturesFolder: false,
video: false,
setupNodeEvents(cypressOn, config) {
const on = require('cypress-on-fix')(cypressOn)
// implement node event listeners here
// and load any plugins that require the Node environment
on('before:run', () => {
console.log('before run 1')
})
on('before:run', () => {
console.log('before run 2')
})
// each spec
on('after:spec', (a) => {
console.log('after spec 1', a.relative)
})
on('after:spec', (a) => {
console.log('after spec 2', a.relative)
})
on('after:spec', (a) => {
console.log('after spec 3', a.relative)
})
// run finished
on('after:run', () => {
console.log('after run 1')
})
on('after:run', () => {
console.log('after run 2')
})
},
},
})

To focus on the changed line:

1
2
3
4
5
6
7
// 🚨 BEFORE
setupNodeEvents(on, config) {
...
// ✅ AFTER
setupNodeEvents(cypressOn, config) {
const on = require('cypress-on-fix')(cypressOn)
...

Note: if you are doing component testing, you might have a separate setupNodeEvents(on, config) and must use cypress-on-fix there too.

Running the specs now correctly executes all event callbacks

1
2
3
4
5
6
7
8
9
10
11
$ npx cypress run
...
before run 1
before run 2
...
after spec 1 cypress/e2e/spec1.cy.js
after spec 2 cypress/e2e/spec1.cy.js
after spec 3 cypress/e2e/spec1.cy.js
...
after run 1
after run 2

Note: if you are curious why multiple plugins need to listen to the same event, checkout what my plugins can do together in Testing The Swag Store course. The plugins work together to split all tests to run in parallel and then produce combined code coverage report all without using any dashboards, just by using GitHub Actions, or any CI really.

Cypress split workflow

The @bahmutov/cypress-code-coverage merges code reports and even adds its own summary to the GHA output

Combined code coverage summary

Pretty neat, I will write a future blog post how it all works.