Collective Idea

Collective Idea Logo

Ryan Glover

Mocking HTML5 API's Using PhantomJS Extensions

By Ryan Glover on January 21, 2014 in capybara, geolocation, javascript, phantomjs, poltergeist, rspec, ruby, and testing

Recently one of our projects called for using the browser’s Geolocation API. We were excited about this project. However, we had an immediate concern about how to test a feature that interacts with one of the browser’s built in APIs. Luckily, PhantomJS has extension support which, along with Poltergeist’s options, makes mocking these built in APIs even easier.

Let’s assume we have a simple registration form with an acceptance spec as follows.


feature "Registration" do
  scenario "Normal Registration" do
    visit new_user_path
    
    fill_in :name, with: "John Doe"
    fill_in :email, with: "john@fake.com"
    
    click_button "Create"
    
    expect(page).to have_content "Welcome, John Doe!"

    user = User.last
    expect(user.name).to eq("John Doe")
    expect(user.email).to eq("john@fake.com")
    expect(user.location).to eq("")
  end
end

Now our project manager asks if we can make the location fill in automatically while they are registering.

(As responsible engineers we would tell him it is possible, but we do not see the cost to benefit ratio, right?)

The code part is pretty straightforward, but how do we test it?  If Developer A in Holland, MI hardcodes his lat/long coordinates into the spec, the test will fail on Developer B’s machine in Washington, DC, not to mention Travis-CI.

We would usually mock the request to the external service using something like VCR, but this is not an external service this is part of the browser.

This is where the PhantomJS extensions become so helpful.

We first write our new acceptance spec.


feature "Registration" do
  scenario "Normal Registration" do
    …
  end
  
  scenario "Geolocation Registration" do
    visit new_user_path
    
    fill_in :name, with: "Jane Doe"
    fill_in :email, with: "jane@fake.com"
    
    click_button "Create"
    
    user = User.last
    expect(user.name).to eq("Jane Doe")
    expect(user.email).to eq("jane@fake.com")
    expect(user.location).to eq("Brooklyn, NY")
  end
end

Now that we have a red test, we can begin working towards an implementation to make it pass.

In order to mock the browser’s Geolocation API we need to create the extension file to include in PhantomJS. Let’s put it at spec/support/phantomjs_ext/geolocation.js


navigator.geolocation = 
{
  getCurrentPosition: function(callback) {
    callback({ coords: { latitude: "40.714224", longitude: "-73.961452" } });
  }
}

The built in Geolocation API getCurrentPosition function prompts the user for access and then calls the callback provided. Our implementation simply returns a predetermined set of coordinates.

Now we just have to let PhantomJS know about this extension. In spec_helper.rb add


Capybara.register_driver :poltergeist do |app|
  Capybara::Poltergeist::Driver.new(
    app,
    extensions: [File.expand_path("../support/phantomjs_ext/geolocation.js", __FILE__)]
  )
end
Capybara.default_driver = :poltergeist

Then it is a matter of writing the js (or coffeescript in this case) code to make the test pass.


if navigator.geolocation
  navigator.geolocation.getCurrentPosition (geoData) ->
    $.getJSON("http://maps.googleapis.com/maps/api/geocode/json?latlng=#{geoData.coords.latitude},#{geoData.coords.longitude}").done (json) -> 
    components = json["results"][0]["address_components"]
    city = (item["short_name"] for item in components when item["types"][0] is "administrative_area_level_3")
    state = (item["short_name"] for item in components when item["types"][0] is "administrative_area_level_1")
    $("#user_location").val("#{city}, #{state}")

Our geolocation test is green, but our first test is failing. Since we do not want the geolocation to kick in on the first test, like a user denying access, we can use the same technique to disable the geolocation.

Instead of adding the extension by default we can add it based on RSpec metadata.

Change our spec_helper.rb from above to


Capybara.register_driver :poltergeist do |app|
  Capybara::Poltergeist::Driver.new(app, extensions: [])
end
Capybara.default_driver = :poltergeist

Then we will add spec/support/phantomjs_ext.rb to define when to use the extension and when not to use it.


RSpec.configure do |config|
  config.before(:each, type: :feature) do
    page.driver.browser.extensions = [File.expand_path("../phantomjs_ext/disable_geolocation.js", __FILE__)]
  end

  config.before(:each, geolocation: true) do
    page.driver.browser.extensions = [File.expand_path("../phantomjs_ext/geolocation.js", __FILE__)]
  end
end

We will add the spec/support/phantomjs_ext/disable_geolocation.js for all non-geolocation features.


navigator.geolocation = false

And finally, we add the metadata to our test that requires geolocation.


feature "Registration" do
  scenario "Normal Registration" do
    …
  end
  
  scenario "Geolocation Registration", geolocation: true do
    visit new_user_path
    
    fill_in :name, with: "Jane Doe"
    fill_in :email, with: "jane@fake.com"
    
    click_button "Create"
    
    user = User.last
    expect(user.name).to eq("Jane Doe")
    expect(user.email).to eq("jane@fake.com")
    expect(user.location).to eq("Brooklyn, NY")
  end
end

Now that we know how we can mock the browser’s default Geolocation API, we can use this knowledge to mock other things such as HTML5 FileAPI, Web Storage, etc.

By Ryan Glover on January 21, 2014 in capybara, geolocation, javascript, phantomjs, poltergeist, rspec, ruby, and testing

Post a Comment

Contact Us

Find us on Google Maps
Collective Idea
44 East 8th Street, Suite 410
Holland, Michigan 49423 USA 42.790334-86.105251

Follow us on the Interwebs

We are currently available for medium and long term projects. Please get in touch if we can be of service.