Test Polyfills

How a Cypress test can confirm the polyfill is working.

Imagine you are using all the modern ES6 methods in your application. Most modern evergreen browsers support everything, right? But once in a while you see in your crash reporting system errors like "X.findLast is not a function". Ughh, what is going on? Why is this ES6 Array method missing? And most importantly, how do we test to prevent this?

The application

Let's take an application that uses Array.findLast method. The page has a button and once the user clicks on it, it finds the last element in the array that satisfies a predicate function.

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<h1>I use ES6</h1>
<p>This page uses some ES6 Array methods, like <code>findLast</code>.</p>
<code>[5, 12, 50, 130, 44]</code>
<br />
<button id="find">Find the last number &gt; 45</button>
<br />
<output id="output" />
<script>
const array1 = [5, 12, 50, 130, 44]
document.getElementById('find').addEventListener('click', () => {
const found = array1.findLast((element) => element > 45)
document.getElementById('output').innerText = String(found)
})
</script>
</body>

🎁 You can find the source code for this application and for the tests used in this blog post in the repo bahmutov/cypress-polyfill-example.

The spec

We can write a test for this app using Cypress. Click on the button, check the output.

spec.cy.js
1
2
3
4
5
it('finds the last element', () => {
cy.visit('index.html')
cy.contains('button', 'Find').click()
cy.contains('output', '130')
})

The passing test

The simulated browser without findLast method

How do we test our application in an older browser that does not support ES6 Array methods? It is hard (and unsafe) to keep a local old browser installed on your machine; most browsers try to update themselves. You could use a Docker image with an older browser installed, but that is a pain. Instead, we can do the following trick: simulate an older browser by deleting the ES6 methods. Cypress cy.visit command allows us to access the application window object before the application loads. We can "adjust" the Array.prototype methods and see if the application still works.

spec.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
it('finds the last element (older browser)', () => {
cy.visit('index.html', {
onBeforeLoad(win) {
// simulate a browser that does not have
// some ES6 Array methods
// If there is no polyfill, the app
// will throw an error trying to use it
delete win.Array.prototype.findLast
},
})
cy.contains('button', 'Find').click()
cy.contains('output', '130')
})

Now we are talking!

The failing test

The polyfill

We need to support browsers that do not have the methods the application code uses. The best way is to use a polyfill. For example, we could include the core-js from CDN.

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
...
<script>
const array1 = [5, 12, 50, 130, 44]
document.getElementById('find').addEventListener('click', () => {
const found = array1.findLast((element) => element > 45)
document.getElementById('output').innerText = String(found)
})
</script>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/index.min.js"
async
></script>
</body>

Let's run the test now. Beautiful, it works. The core-js-bundle JavaScript "fills" the Array.prototype.findLast method we have deleted, and our application is happy again.

Confirm the polyfill loads

Because we are great at Cypress Network Testing and every day study a free lesson during Cypress Training Advent Calendar 2022, we can spy on the core-js library and confirm it loads correctly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
it('finds the last element (older browser, observe polyfill loads)', () => {
cy.intercept({
method: 'GET',
hostname: 'cdn.jsdelivr.net',
pathname: '/npm/core-js-bundle*/*',
}).as('core-js')
cy.visit('index.html', {
onBeforeLoad(win) {
// simulate a browser that does not have
// some ES6 Array methods
// If there is no polyfill, the app
// will throw an error trying to use it
delete win.Array.prototype.findLast
},
})
// confirm the polyfill loads
cy.wait('@core-js').its('response.statusCode').should('be.below', 400)
cy.contains('button', 'Find').click()
cy.contains('output', '130')
})

The polyfill loads and works test

Tip: the browser caches the polyfill, so to see it load 100% of times, you need to strip the caching headers.

Strategy

I don't think you need to recreate every possible older browser environment and run every test. A single test that specifically checks that the polyfill is loaded and applied would be enough. You can add more regression tests as needed if you find a bug.