Capybara’s very good about waiting for AJAX. For example, this code
will keep checking the page for the element for Capybara.default_max_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_max_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 require
d. 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.