DEV Community

Kushagra Gour
Kushagra Gour

Posted on • Originally published at engineering.pushowl.com

Mocking Web Push notification in Cypress

PushOwl has a JavaScript library that runs on merchant (our customers) websites and handles things like showing UI widgets to request visitor permissions, passing data to the backend, and subscribing the user to the Web Push Service.

It’s one of the most critical pieces of our system because a slight issue in our script can cause unexpected behavior on our customer websites causing a loss for them. So we take utmost care to test the library rigorously before deploying anything to production.

The Roadblock

We use Cypress to test our JavaScript library for end-to-end cases - just like how a website visitor would interact with the website and hence our script. As mentioned, one of the most important parts of our library is requesting permission from the user to send them notifications and then subscribe them to the Web Push Service. Permissions on the website are requested by triggering a prompt like so:

A native prompt asking for permission to show notifications

A native prompt asking for permission to show notifications

Current testing frameworks do not have very great support to test these native permission prompts. You could trigger them in headed mode, but then you can trigger a click on the “Allow” or “Block” buttons programmatically.

But tests don’t run in the headed mode in the CI environment, they run in headless browsers. In a headless environment, it becomes more difficult because the prompt doesn’t show up at all and just fails!

There are a few open issues in these libraries, but they still have to see the light of day!

How do we even test these prompts then? Let’s see how!

Going around these permission prompts

Since we can’t interact with these permission prompts, the next best thing we could do was to mock them!

By mocking, we simply mean converting the following test scenario:

  1. user lands on the website
  2. clicks on a button
  3. sees a permission prompt
  4. clicks “Allow” inside the prompt
  5. User gets subscribed and an API request is made to the backend

to…

  1. User lands on the website
  2. clicks on a button
  3. mock browser APIs to behave as if the user clicked “Allow”
  4. User gets subscribed and an API request is made to the backend

Let’s see how we do this in the specific context of Notification permission.

Mocking browser APIs

Several properties and methods form the complete Web Push Notification subscription flow in the browser. We’ll look into each one individually.

Notification.permission

This property on the global Notification object gives the current status of the website visitor w.r.t. the “Show Notification” permission.

Notification.permission would have the value as default in the default case. And it would be granted or denied in case you allow or deny it respectively.

Mocking this property is simple, we use the Cypress’ stub method like so:

Cypress.Commands.add('setPermission', (permission = 'default') => {
  cy.window().then(win => {
    cy.stub(win.Notification, 'permission', permission);
  })
});
Enter fullscreen mode Exit fullscreen mode

And of course, we have this inside a utility function or a Cypress Command so that we can pass in any permission value and have it set to that.

Notification.requestPermission()

This is the method that triggers the permission prompt i.e. we request permission. This is an async function that resolves to a string value - granted when “Allow” is clicked and denied otherwise.

Cypress gives a method to mock async functions. For a successful permission scenario, it would look like so:

cy.stub(win.Notification, 'requestPermission').resolves('granted')
Enter fullscreen mode Exit fullscreen mode

navigator.serviceWorker.register()

According to the Web Push Subscription flow, once we get the granted permission from the visitor, we need to install a service worker which later handles receiving the push notification and displaying it.

To install a service worker script, our library would at some point calls navigator.serviceWorker.register() method and would await for a serviceWorkerRegistration object on successful resolution of this async method.

Mocking this method would mean providing a serviceWorkerRegistration object with the correct keys on it, which looks something like this:

{
  active: { state: 'activated' },
  pushManager: { subscribe: () => {} },
};
Enter fullscreen mode Exit fullscreen mode

Note that

installing a service worker works fine in headless browsers. We’ll even get a serviceWorkerRegistration object on success. But the reason why we still mock it is the pushManager.subscribe method in that serviceWorkerRegistration object above — once the service worker is registered, we call the pushManager.subscriber method to subscribe the visitor to the remote Web Push notification service and that fails in headless browser environments. Hence, we don’t want our JavaScript library to be calling the actual pushManager.subscribe method on an actual serviceWorkerRegistration method 😄

So let’s mock it too!

const swRegistration = {
  active: { state: 'activated' },
  pushManager: { subscribe: () => {} },
};
cy.stub(win.navigator.serviceWorker, 'register').resolves(swRegistration)
Enter fullscreen mode Exit fullscreen mode

pushManager.subscribe() - The final step!

In the end, we now also want to stub our own pushManager.subscribe method in the serviceWorkerRegistration object we created above. pushManager.subscribe is also an async function which resolves to a subscription object. So let’s make it do that:

// from above
const swRegistration = {
  active: { state: 'activated' },
  pushManager: { subscribe: () => {} },
};
// our mocked subscription object
const subscription = {
  endpoint:
    'https://fcm.googleapis.com/fcm/send/f.....u0LL',
  expirationTime: null,
  keys: {
    p256dh:
      'BGBn2Lco....ZUY',
    auth: 'y8....JWQ',
  },
};

cy.stub(swRegistration.pushManager, 'subscribe').resolves(subscription)
Enter fullscreen mode Exit fullscreen mode

And done!

Now, all we have to do is set these mocks (which we have as commands) at the right time and trick our JavaScript library into believing that the user is subscribing, or not!

In the end, I’ll leave you with a nice quote:

“Testing is an infinite process of comparing the invisible to the ambiguous in order to avoid the unthinkable happening to the anonymous.”— James Bach

Keep testing! Until next time!

Discussion (0)