Mocking Web Push notification in Cypress
Tags: cypress, testing, automation, javascript
Published: 2022-06-28 09:03:24

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iar3ke06ujfvmou8h7st.png)
  
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](https://github.com/puppeteer/puppeteer/issues/3461), 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:

```jsx
Cypress.Commands.add('setPermission', (permission = 'default') => {
  cy.window().then(win => {
    cy.stub(win.Notification, 'permission', permission);
  })
});
```

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:

```jsx
cy.stub(win.Notification, 'requestPermission').resolves('granted')
```

### 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:

```jsx
{
  active: { state: 'activated' },
  pushManager: { subscribe: () => {} },
};
```

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!

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

### 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:

```jsx
// 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)
```

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](https://twitter.com/jamesmarcusbach)**
> 

Keep testing! Until next time!