Simulate geolocation with Capybara and Headless Chrome
I recently added a “Locate me” button to Film Chase, which uses the Geolocation API (specifically getCurrentPosition
) to get the current geolocation of a users device. Clicking the “Locate me” button triggers some Javascript like this
getPosition() {
navigator.geolocation.getCurrentPosition((position) => {
this.filter(position.coords.latitude, position.coords.longitude);
});
}
I was keen to add a feature specification to test this functionality
it 'allows a user to share their current geolocation', :js do
visit root_path
click_button 'Locate me'
expect(page).to have_text 'Location: Oxford'
end
This test won’t work because thankfully headless Chrome isn’t configured by default to share geolocation. In Chrome DevTools, a geolocation can be simulated using geolocation override. This is also possible within headless Chrome using Emulation.setGeolocationOverride
.
Film Chase uses a pretty vanilla Rails 7 testing stack
gem "capybara"
gem "selenium-webdriver"
gem "webdrivers"
Selenium helpfully provides a method to execute Chrome DevTools Protocol commands, succinctly named execute_cdp
so within the context of a js
enabled feature specification Capybara’s driver can be accessed via page.driver
and Selenium’s driver via page.driver.browser
. Meaning headless Chrome geolocation override can be enabled by appending the feature specification with
page.driver.browser.execute_cdp(
'Emulation.setGeolocationOverride',
latitude: 51.7,
longitude: -1.3,
accuracy: 50
)
Attempting to run that test still won’t work because we also need to grant the browser permission to access the geolocation, and we can achieve this with
page.driver.browser.execute_cdp(
'Browser.grantPermissions',
origin: page.server_url,
permissions: ['geolocation']
)
So putting this all together, we have
it 'allows a user to share their current geolocation', :js do
page.driver.browser.execute_cdp(
'Emulation.setGeolocationOverride',
latitude: 51.7,
longitude: -1.3,
accuracy: 50
)
page.driver.browser.execute_cdp(
'Browser.grantPermissions',
origin: page.server_url,
permissions: ['geolocation']
)
visit root_path
click_button 'Locate me'
expect(page).to have_text 'Location: Oxford'
end
The test is now working so we can extract this behaviour into a helper method
module HeadlessChromeHelpers
def geolocation_override(latitude:, longitude:, accuracy: 100)
grant_permissions('geolocation')
set_geolocation_override(
latitude: latitude,
longitude: longitude,
accuracy: accuracy
)
end
private
def grant_permissions(*permissions, origin: nil)
origin ||= page.server_url
page.driver.browser.execute_cdp(
'Browser.grantPermissions',
origin: origin,
permissions: permissions
)
end
def set_geolocation_override(coordinates)
page.driver.browser.execute_cdp(
'Emulation.setGeolocationOverride',
**coordinates
)
end
end
RSpec.configure do |config|
config.include HeadlessChromeHelpers
end
and now the feature specification can be refactored to be
it 'allows a user to share their current geolocation', :js do
geolocation_override(latitude: 51.7, longitude: -1.3)
visit root_path
click_button 'Locate me'
expect(page).to have_text 'Location: Oxford'
end
I hope this is helpful!