Control The Application Through PubSub From Cypress

How to use the PubSub instance inside the application to publish events during Cypress tests.

A user has recently asked me about using Cypress tests not delivering events into the application. The application is using pubsub-js from the "Publisher" component to publish events. Other components, like the "Subscriber" component can subscribe to PubSub events and then updates the page UI.

Publisher
1
2
3
4
5
6
7
8
<button type="button" @click.prevent="notifyNavbar" data-cy="publisherbutton">

import PubSub from "pubsub-js";
function notifyNavbar() {
console.log("Sending Notification");
PubSub.publish('notification-update', 1);
console.log("Notification Sent");
}
Subscriber
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<span data-cy="notficationcounter">{{ notificationCount }}</span>

import PubSub from 'pubsub-js';

const notificationCount = ref(0);

PubSub.subscribe(
'notification-update',
(message: string, data: number) => {
console.log('notification received.');
console.log(data);
notificationCount.value += data;
},
);

The first test works correctly confirming the application is working

cypress/integration/test.js
1
2
3
4
5
it('click the button', function() {
cy.visit('http://localhost:8080/');
cy.get('[data-cy=publisherbutton]').click();
cy.get('[data-cy=notficationcounter]').contains('1');
})

The application is working

Tip: move the http://localhost:8080/ URL into the cypress.json file. This will avoid the test reload, watch the video How to correctly use the baseUrl to visit a site in Cypress.

📺 If you would rather watch the explanation from this blog post, watch my video How To Use PubSub From Cypress Test To Publish Events To Application.

The broken test

The user has written the second test, trying to publish the tests from the test and then check if the "Subscriber" component is updated.

cypress/integration/test.js
1
2
3
4
5
6
7
import PubSub from "pubsub-js";

it('publish the event', function() {
cy.visit('http://localhost:8080/')
PubSub.publish('notification-update', 1);
cy.get('[data-cy=notficationcounter]').contains('1');
})

Unfortunately the test fails.

The application does not update after the dispatch

The application does not see the updated count - it never receives the "notification-update" event. The reason is that Cypress test file is placed in a separate iframe from the application.

The application and the spec iframes

Each iframe has its own JavaScript environment, its own window object, its own ... PubSub instance. It is as if two applications were loaded:

1
2
3
4
5
6
7
8
9
10
<iframe>
<script>
import PubSub from "pubsub-js";
PubSub.subscribe('hey', ...)
</script>
<script>
import PubSub from "pubsub-js";
PubSub.publish('hey', ...)
</script>
</iframe>

While the code looks similar, the two PubSub instances are completely separate. The events published in one iframe are invisible and never mix with the events published in the second one. Our test file published the event - but on its own PubSub.

The solution

We need to access the application's PubSub instance from the spec file. The simplest way is for the application to share it by adding it as a property to the window object.

Publisher
1
2
3
4
5
6
import PubSub from "pubsub-js";

// @ts-ignore
if (window.Cypress) {
window.PubSub = PubSub
}

From the spec file we can access the application's window object using the cy.window command, and then wait for the property PubSub to exist, see The window.property recipe.

cypress/integration/test.js
1
2
3
4
5
6
7
it('publish the event', function () {
cy.visit('http://localhost:8080/')
cy.window().its('PubSub').then(PubSub => {
PubSub.publish('notification-update', 1);
})
cy.get('[data-cy=notficationcounter]').contains('1');
})

Now the test works correctly.

The test published the event using the right PubSub

We can simplify the above test a little. We need the window instance after the cy.visit command. The cy.visit command yields the application's window object, thus we can directly chain the visit and the its commands.

1
2
3
4
5
6
7
it('publish the event', function () {
cy.visit('http://localhost:8080/')
.its('PubSub').then(PubSub => {
PubSub.publish('notification-update', 1);
})
cy.get('[data-cy=notficationcounter]').contains('1');
})

We can simplify the above test even more. We are getting the PubSub object and then immediate invoke its method publish. We can use the Cypress command .invoke for this.

1
2
3
4
5
6
it('publish the event', function () {
cy.visit('http://localhost:8080/')
.its('PubSub')
.invoke('publish', 'notification-update', 1);
cy.get('[data-cy=notficationcounter]').contains('1');
})

The nice thing about Cypress .invoke command: if the method returns a Promise, the command will wait for the promise to resolve before continuing with the next Cypress command.