Experimental Fetch Polyfill

June 29, 2020

By Gleb Bahmutov

Let's take an application that makes an Ajax call to the browser to load a list of ... fruits. You can find this awesome and healthy application in Cypress Example Recipes under name "Stubbing window.fetch".

Application

Every time you visit an application you get five new fruits

This application presents two challenges to end-to-end tests. First, the data is random - every time the test visits the page localhost:7080 a new list of fruits is returned, which complicates the assertions. Second, the application shows a loading indicator while the Ajax call continues. How would you test the loading behavior?

The application's code to fetch the list of fruits is short.

fetch('/favorite-fruits')
.then((response) => {
  if (response.ok) {
    return response.json()
  }
  // else throw an Error
})
.then((response) => {
  updateFavoriteFruits(response.length ? response : 'No favorites')
})

Stubbing window.fetch

Let's start by slowing down the window.fetch directly. This way we can confirm the loading element is visible, and once the response arrives, the loading indicator disappears. Because Cypress includes spies and stubs from the excellent Sinon.js library, we can directly reach into the window object and stub its fetch method.

describe('stubbing', function () {
  it('shows no Response message', () => {
    cy.visit('/', {
      onBeforeLoad (win) {
        cy.stub(win, 'fetch').withArgs('/favorite-fruits')
        .resolves({
          ok: true,
          json: () => [],
        })
      },
    })

    cy.contains('No favorites').should('be.visible')
  })
})

The test passes, and we see the stub in the Command Log - the two records are due to the fact that we have created first a general method stub window.fetch and then made created an explicit one with cy.stub(win, 'fetch').withArgs('/favorite-fruits'). I think is a good practice to create targeted stubs by giving argument values or types.

Stubbing window.fetch method

Delaying the response

In the test above we are resolving Sinon method stub with a promise. Because Cypress comes with Bluebird.js under Cypress.Promise we can use its rich API when working with promises. For example, we can delay the promise's resolution, simulating a slow server response. This allows us to test the loading indicator.

it('directly stubs window.fetch to test loading indicator', () => {
  // stub the "fetch(/favorite-fruits)" call from the app
  cy.visit('/', {
    onBeforeLoad (win) {
      cy.stub(win, 'fetch').withArgs('/favorite-fruits')
      .resolves(
        // use Bluebird promise bundled with Cypress
        // to resolve after 2000ms
        Cypress.Promise.resolve({
          ok: true,
          json: () => ['Pineapple 🍍'],
        }).delay(2000)
      )
    },
  })

  // at first, the app is showing the loading indicator
  cy.get('.loader').should('be.visible')
  // once the promise is resolved, the loading indicator goes away
  cy.get('.loader').should('not.exist')
  cy.contains('li', 'Pineapple 🍍')
})

The test is passing, and we can see the loading indicator in action.

Loading indicator test from returning a delayed promise

Using network control

Our dear friend Kent C Dodds has recently published an essay Stop mocking fetch where he argues that what we have done above is tying the test to the implementation and can miss some bugs. Instead he recommends mocking the server outside the application. We agree with Kent - that's why Cypress includes network control right out of the box.

For a long, long, loooong time, the Cypress network control could not "see" window.fetch calls and only understood XMLHttpRequest Ajax calls. We are in the process of updating our entire network control layer to solve this problem (and many, many other limitations). Follow the full network stubbing PR [#4176](https://github.com/cypress-io/cypress/pull/4176 ) expected  to land some time in the summer of 2020 that will replace the need for the temporary workaround.

Meanwhile, we have added a quick fetch polyfill as an experimental feature in Cypress v4.9.0. By turning this feature on, the Cypress Test Runner will automatically replace window.fetch with a unfetch polyfill built on top of XMLHttpRequest object, making these Ajax requests "visible" to the Test Runner. Let us try this, in cypress.json configuration file, turn the feature on

{
  "experimentalFetchPolyfill": true
}

Instead of cy.stub let's use plain cy.route before visiting the application.

it('shows loader while fetching fruits', function () {
  // stub the XHR request from the app
  cy.server()
  cy.route({
    url: '/favorite-fruits',
    response: [],
    delay: 1000,
  })

  cy.visit('/')
  cy.get('.loader').should('be.visible')

  // once the network call finishes, the loader goes away
  cy.get('.loader').should('not.exist')
  cy.contains('.favorite-fruits', 'No favorites')
})

The test passes and we can see all matching Ajax requests in their own table above the test's commands.

Stubbing window.fetch using Cypress cy.route method

By using cy.route we can write tests with network stubs that feel much closer to the real server responses rather than function stubs. For example, let's see how our application handles a server error after a delay.

it('displays error', function () {
  cy.server()
  cy.route({
    url: '/favorite-fruits',
    status: 500,
    response: '',
    delay: 2000,
    headers: {
      'status-text': 'Orchard under maintenance',
    },
  })

  cy.visit('/')

  cy.get('.favorite-fruits')
    .should('have.text', 
      'Failed loading favorite fruits: Orchard under maintenance')
})

The test passes - and we did not even need to add any waits, since Cypress has built-in command retry-ability

Testing server error handling

Limitations

The experimental polyfill is not foolproof.  It does not work with fetch calls made from WebWorkers or ServiceWorker for example. It might not work for streaming responses and canceled XHRs. That's why we felt the opt-in experimental flag is the best path forward to avoid breaking already application under tests.

Happy Testing!