Debugging Cypress Geolocation Problem

An investigation into a hanging application that required geolocation access during Cypress test.

🔎 You can find the source code for this blog post in bahmutov/cypress-geolocation-example repo.

The problem

Recently a user has reported an issue with a small application that showed geographical autocomplete search results. In normal browser the search would display a list of locations for entered text.

The list of locations

An important detail: notice a tiny location marker with an "x" in the URL bar. It appears when we start typing. If we inspect the marker it shows that we are blocking "location:3000" from accessing our location.

Chrome browser is blocking the site from accessing the location

Let's write a test to perform the same search. We will enter "Boston" and confirm there are search suggestions shown.

1
2
3
4
5
6
/// <reference types="cypress" />
it('finds me', () => {
cy.visit('/')
cy.get('#search-location').type('Boston')
cy.get('[data-cy=suggestion]').should('have.length.gt', 1)
})

The test does nothing - it just shows "Loading ..." text.

The hanging test

Let's find what is going wrong.

Investigation

In this example we are very lucky. We have a working application in the browser, and a hanging application inside the Cypress-controlled browser. We can always compare the application's behavior in the two browsers to find where the behaviors diverge. For example, the working application is executing multiple network calls to find the places with the input text.

The network traffic in the working application

During the test, there is no network traffic - the Test Runner fetches the spec files, but the application never makes the Ajax requests in response to the text input.

The application under test does not make network calls

Why are the network calls not happening when running the same application inside Cypress-controlled browser? Who is making those calls? To find out which code is making the Ajax network calls, let's look at the network request happening in the plain browser.

Finding JavaScript that makes network calls

So by going from the Ajax call to the source code we found the source file node_modules/mb-places/dist/utils.js making the network calls. Let's find the function geocodeByAddress in the source code running inside Cypress and put a breakpoint. I will use the source code search to find the function and set the breakpoints before and after calling getLocation function.

Find the relevant source code using DevTools in Cypress

The function getLocation is interesting - it calls the browser API method navigator.geolocation.getCurrentPosition.

Function getLocation returns location using the browser API

Does it work? Let's find out. After setting the breakpoints, I re-run the test.

The second breakpoint is never hit

Notice a curious thing: we end up in the expected place, but the second breakpoint in the callback to navigator.geolocation.getCurrentPosition never executes. The behavior of getCurrentPosition depends on the user - if they have given the web application permission to use the location. We can see it by using Chrome browser instead of Electron to run the same test.

The location permission popup shown during the test in Chrome browser

Our application during test seems to hang at navigator.geolocation.getCurrentPosition step. The application source code never handles the user not answering the block popup, this the code keeps hanging.

Note: the application does not even need the user's location. The user's location is only used to order the search results by proximity.

Solution

Since Cypress tests run in the browser, we can simply stub pretty much any API method. Let's update our test. We will overwrite the problematic native method with our own function that calls the callback argument with an error object. This is the same as if the user clicked "Block" button on the browser's location permission popup.

1
2
3
4
5
6
7
8
9
10
11
12
13
it('finds me', () => {
cy.visit('/', {
onBeforeLoad(win) {
win.navigator.geolocation.getCurrentPosition = (cb) => {
const err = new Error('User denied')
err.code = GeolocationPositionError.PERMISSION_DENIED
cb(err)
}
}
})
cy.get('#search-location').type('Boston')
cy.get('[data-cy=suggestion]').should('have.length.gt', 1)
})

The test works.

The fixed test tells the application it cannot have location

We can improve our test a little. For example, we could use Sinon utility methods to stub the method.

1
2
3
4
5
6
7
8
9
10
11
12
13
it('finds me', () => {
cy.visit('/', {
onBeforeLoad(win) {
const err = new Error('User denied')
err.code = GeolocationPositionError.PERMISSION_DENIED
cy.stub(win.navigator.geolocation, 'getCurrentPosition')
.callsArgWith(0, err).as('getCurrentPosition')
}
})
cy.get('#search-location').type('Boston')
cy.get('@getCurrentPosition').should('have.been.called')
cy.get('[data-cy=suggestion]').should('have.length.gt', 1)
})

We can also move this code into beforeEach hook to always register this logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
beforeEach(() => {
cy.on('window:before:load', (win) => {
const err = new Error('User denied')
err.code = GeolocationPositionError.PERMISSION_DENIED
cy.stub(win.navigator.geolocation, 'getCurrentPosition')
.callsArgWith(0, err).as('getCurrentPosition')
})
})
it('finds me', () => {
cy.visit('/')
cy.get('#search-location').type('Boston')
cy.get('@getCurrentPosition').should('have.been.called')
cy.get('[data-cy=suggestion]').should('have.length.gt', 1)
})

The successful test shows the expected call happens

The final test

Tip: you can try the plugin cypress-browser-permissions to specify browser permission in Cypress tests.