🔎 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.
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.
Let's write a test to perform the same search. We will enter "Boston" and confirm there are search suggestions shown.
1 | /// <reference types="cypress" /> |
The test does nothing - it just shows "Loading ..." text.
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.
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.
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.
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.
The function getLocation
is interesting - it calls the browser API method navigator.geolocation.getCurrentPosition
.
Does it work? Let's find out. After setting the breakpoints, I re-run the test.
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.
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 | it('finds me', () => { |
The test works.
We can improve our test a little. For example, we could use Sinon utility methods to stub the method.
1 | it('finds me', () => { |
We can also move this code into beforeEach
hook to always register this logic:
1 | beforeEach(() => { |
The successful test shows the expected call happens
Tip: you can try the plugin cypress-browser-permissions to specify browser permission in Cypress tests.