Automatically wait for AJAX with Capybara

Capybara’s very good about waiting for AJAX. For example, this code will keep checking the page for the element for Capybara.default_wait_time seconds, allowing AJAX calls to finish:

expect(page).to have_css('.username', text: 'Gabe B-W')

But there are times when that’s not enough. For example, in this code:

visit users_path
click_link 'Add Gabe as friend via AJAX'
reload_page
expect(page).to have_css('.favorite', text: 'Gabe')

We have a race condition between click_link and reload_page. Sometimes the AJAX call will go through before Capybara reloads the page, and sometimes it won’t. This kind of nondeterministic test can be very difficult to debug, so I added a little helper.

Capybara’s Little Helper

Here’s the helper, via Coderwall:

# spec/support/wait_for_ajax.rb
module WaitForAjax
  def wait_for_ajax
    Timeout.timeout(Capybara.default_wait_time) do
      loop until finished_all_ajax_requests?
    end
  end

  def finished_all_ajax_requests?
    page.evaluate_script('jQuery.active').zero?
  end
end

RSpec.configure do |config|
  config.include WaitForAjax, type: :feature
end

We automatically include every file in spec/support/**/*.rb in our spec_helper.rb, so this file is automatically required. Since only feature specs can interact with the page via JavaScript, I’ve scoped the wait_for_ajax method to feature specs using the type: :feature option.

The helper uses the jQuery.active variable, which tracks the number of active AJAX requests. When it’s 0, there are no active AJAX requests, meaning all of the requests have completed.

Usage

Here’s how I use it:

visit users_path
click_link 'Add Gabe as friend via AJAX'
wait_for_ajax # This is new!
reload_page
expect(page).to have_css('.favorite', text: 'Gabe')

Now there’s no race condition: Capybara will wait for the AJAX friend request to complete before reloading the page.

Change we can believe in (and see)

This solution can hide a bad user experience. We’re not making any DOM changes on AJAX success, meaning Capybara can’t automatically detect when the AJAX completes. If Capybara can’t see it, neither can our users. Depending on your application, this might be OK.

One solution might be to have an AJAX spinner in a standard location that gets shown when AJAX requests start and hidden when AJAX requests complete. To do this globally in jQuery:

jQuery.ajaxSetup({
  beforeSend: function(xhr) {
    $('#spinner').show();
  },
  // runs after AJAX requests complete, successfully or not
  complete: function(xhr, status){
    $('#spinner').hide();
  }
});

What’s next?

There is no official documentation on jQuery.active, since it’s an internal variable, but this Stack Overflow answer is helpful. To see how we require all files in spec/support, read through our spec_helper template.

Credits

Thanks to Jorge Dias and Ancor Cruz on Coderwall for the original and refactored helper implementations.