Avoid blocking with Capybara
Capybara is an awesome tool for testing and screen-scraping. Combined with poltergeist, it provides a very intuitive interface for driving the headless Webkit driver PhantomJS:
def sign_in username, password
fill_in_login_form username, password
login_succeeded?
end
def fill_in_login_form username, password
page.visit 'http://www.example.com/'
page.fill_in 'Username', with: username
page.fill_in 'Password', with: password
page.click_button 'Login'
end
def login_succeeded?
if page.has_text?('Success!')
true
elsif page.has_text?('Login failed!')
false
else
raise "Unexpected content on page: #{page.text}"
end
end
The method above will try to sign in to our site and return true
if login succeeded, or false
if it failed.
But wait: why does this method seem to take forever to run when login fails?
Synchronization
A big part of Capybara’s magic lies in the synchronize method. It uses this method internally to wait until some condition is met. For instance, when you call:
page.fill_in 'Username', with: username
Capybara will check whether a field named Username
appears on the page. If it doesn’t, it doesn’t fail immediately: instead, it keeps retrying for up to 20 seconds1 until it shows up. That way, if the site uses AJAX to load the login form, Capybara will block until the form is in the DOM. No need for sleep
or explicit retries in your code.
As you may expect, this same method is used when we try to fill_in
the password field, when we try to click_button 'Login'
… and when we test for the presence of Success!
on the page.
So when we encounter a failed login attempt, we end up waiting on page.has_text?('Success!')
for 20 seconds before proceeding to test the condition page.has_text?('Login failed!')
. (And if something went wrong and neither message shows up, we end up waiting another 20 seconds before giving up and raising an error.)
Eliminating unnecessary blocking
One way to avoid unnecessarily blocking in this case is to explicitly use the synchronize
method:
def login_succeeded?
page.document.synchronize do
if page.has_text?('Success!')
true
elsif page.has_text?('Login failed!')
false
else
raise Capybara::ExpectationNotMet
end
end
rescue Capybara::ExpectationNotMet
raise "Unexpected content on page: #{page.text}"
end
If the block passed to synchronize
raises Capyabara::ExpectationNotMet
, it will be retried until the timeout is hit. Thus, this new version of the code will wait up to 20 seconds for either Success!
or Login failed!
to appear before proceeding.
(Note: only the outermost synchronize
block is repeatedly retried. So now that the whole thing is in a synchronize
block, the individual calls to has_text?
will immediately return true or false, rather than blocking until the condition is met.)
Other approaches
Sometimes there’s an even simpler way than explicitly calling synchronize
. For instance, if we were looking for a CSS selector rather than text content on the page:
def login_succeeded?
if page.has_css?('.success_message')
true
elsif page.has_css?('.failure_message')
false
else
raise "Unexpected content on page: #{page.text}"
end
end
then we could rewrite this as:
def login_succeeded?
message = page.find('.success_message,.failure_message')
message[:class].include?('success_message')
rescue Capybara::ElementNotFound
raise "Unexpected content on page: #{page.text}"
end
By passing multiple selectors to page.find
, we can block until exactly one of those selectors appears on the page.
-
The timeout is configurable by setting
Capybara.default_max_wait_time
↩