JavaScript Testing in Rails

headless browsers and Ajax

Posted on February 17, 2016

Introduction

While Rails makes testing a breeze, JavaScript testing requires a bit of extra work. This post was written for minitest, but it should be easy to adapt for RSpec.

Initial Setup: Capybara and Poltergeist

For integration testing, I use Capybara, an acceptance test framework that can be used to simulate user interaction. While this works great for testing standard pages, the default driver does not execute JavaScript. The built-in Selenium JavaScript driver launches Firefox and performs testing by clicking around inside the GUI, but this is less performant than a headless browser like PhantomJS. I use Poltergeist, a Capybara driver for PhantomJS.

To set up Capybara and Poltergeist, add them both to your gemfile and add the following lines to your test helper.

require 'capybara/rails'
require 'capybara/poltergeist'

Capybara.javascript_driver = :poltergeist

class ActionDispatch::IntegrationTest
  # Make the Capybara DSL available in all integration tests
  include Capybara::DSL, JavaScriptUtilities
end

I like to create a JavaScriptUtilities module to make it easier to write JavaScript tests. All I have to do is call require_js in the test setup, and it takes care of switching the driver back to default afterwards.

module JavaScriptUtilities
  def require_js
    Capybara.current_driver = Capybara.javascript_driver
  end

  def after_teardown
    super
    Capybara.use_default_driver
  end
end

Ajax Test Failures

I quickly found that Capybara was not always waiting for Ajax requests to complete. Ryan Bigg had a similar problem, but I was unable to get his solution to work. I came up with a kludge using HTML attributes. I set a waiting attribute to be true, wait for the ajaxSuccess event to be fired, and then set the waiting attribute to be false. The loop will not break until this attribute is set to false or it times out.

module JavaScriptUtilities
  ...

  def wait_for_ajax
    waiting_attr = "waiting_#{SecureRandom.hex}"
    page.execute_script("$('body').attr('#{waiting_attr}', true)")
    yield
    page.execute_script("$(document).ajaxSuccess(function( event, xhr, settings ) { $('body').attr('#{waiting_attr}', false); });")
    waiting(waiting_attr)
  end

  private

    def waiting(waiting_attr)
      counter = 0
      while page.has_xpath?("//body[@#{waiting_attr}='true']")
        counter += 1
        sleep(0.1)
        raise "Ajax request took longer than 5 seconds." if counter >= 50
      end
    end
end

Conclusion

I hope this is helpful for those of you trying to write JavaScript tests. If you have any questions or comments, please let me know. You can find me on Twitter.