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
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:
- user lands on the website
- clicks on a button
- sees a permission prompt
- clicks “Allow” inside the prompt
- User gets subscribed and an API request is made to the backend
to…
- User lands on the website
- clicks on a button
- mock browser APIs to behave as if the user clicked “Allow”
- 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);
})
});
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')
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: () => {} },
};
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)
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)
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!
Top comments (0)