Migrating from Capybara-Webkit to Poltergeist-PhantomJs
Motivation
Today I migrated a medium size test suite from capybara-webkit to Poltergeist with PhantomJS. I had two main motivations for switching:
- PhantomJS is more sensitive to avoiding false positives. For example, in the past, one could click on non-visible DOM elements with capybara-webkit. While this may not currently be true with the latest Capybara, I've had good luck with PhantomJS in the past.
- Speed. When I last checked, PhantomJS was faster. Speed is critical for slow feature tests.
Here's one reason that Poltergeist is more accurate and sensitive to failure:
When Poltergeist clicks on an element, rather than generating a DOM click event, it actually generates a "proper" click. This is much closer to what happens when a real user clicks on the page - but it means that Poltergeist must scroll the page to where the element is, and work out the correct co-ordinates to click. If the element is covered up by another element, the click will fail (this is a good thing - because your user won't be able to click a covered up element either).
Tips for Migrating
Upgrade Gems First
At first, I lost time due to timing issues where I was clicking on elements of a hidden dialog that was not finished showing. Capybara-webkit was not bothered by the fact that the dialog was actually hidden and being loaded. PhantomJS bombed out. However, after I worked around the issue, I realized that my gems were outdated. Since you're going to be fixing a bunch of tests anyway, it makes sense to get on the latest versions of the testing gems. The gems you want to upgrade are: rspec, rspec-rails, Capybara, and poltergeist.
Visible Option
After upgrading the gems, my workarounds were no longer necessary. However, the change from Capybara 2.0 to 2.1 had a big change in the way that it handles finding dom elements that are not visible. Previously, Capybara would not care if the dom element was hidden. For my tests, this resulted in breaking any tests that queried any non-visible DOM elements, such as scripts, meta tags, and links.
The key thing to be aware of is that you might get this obscure error
message, and the fix is to add the visible: false
optional parameter
so that Capybara is not filtering by visible: true
. The visible
parameter is available to most finder methods in Capybara.
The obscure error you might see is something like this:
#=> Capybara::ExpectationNotMet Exception: expected to find xpath "//title" with text "Title Text." but there were no matches. Also found "", which matched the selector but not all filters.
The reason is the title element is not visible, and "visible" is the "not all filters" part of the error message.
Debugging Capybara Tests
The main reasons that previously passing feature tests will fail when migrating to Poltergeist is due to timing and visibility. The two main techniques for debugging Capybara tests are:
- Using screen shots (
render_page
below) - Using HTML dumps (=page! below)
Keep in mind that these methods will not wait for elements to load.
Thus, you should either have a Capybara statement that will wait for
some DOM element to load or you might want to put in a sleep 10
to
sleep for 10 seconds before capturing the screen shot or dumping the
HTML.
If you use the helper methods specified below, and you should be able to work through why Poltergeist is not doing what you think it should be doing. So far, I haven't yet run into a case where I have not found out that it's been my fault rather than a bug in Poltergeist that's caused a failure due to the migration. In many cases, you'll be somewhat pleasantly surprised that you'll be fixing a false positive.
Capybara's Wait Strategy
Be sure to carefully read the Capybara
documentation, especially the
part titled "Asynchronous JavaScript". That section explains how
Capybara cleverly will wait until the page or ajax call finished so that
the element expected appears. There's a configurable timeout
(Capybara.default_wait_time
) for changing the default wait time before
a test bombs out.
Xpath Tip
Be sure to understand the difference between //something
and
.//something
. The later can be used inside a within
block. The
former will find the tag anywhere on the page, even when used inside of
a within
block!
Setup and Utility Debugging Methods
Here's the setup and a couple utility methods that I use. Put these in a file in your helpers directory, such spec/helpers/capybara.rb.
Capybara.default_wait_time = 8 # Seconds to wait before timeout error. Default is 2
# Register slightly larger than default window size...
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, { debug: false, # change this to true to troubleshoot
window_size: [1300, 1000] # this can affect dynamic layout
})
end
Capybara.javascript_driver = :poltergeist
# Saves page to place specfied at name inside of
# test.rb definition of:
# config.integration_test_render_dir = Rails.root.join("spec", "render")
# NOTE: you must pass "js:" for the scenario definition (or else you'll see that render doesn't exist!)
def render_page(name)
png_name = name.strip.gsub(/\W+/, '-')
path = File.join(Rails.application.config.integration_test_render_dir, "#{png_name}.png")
page.driver.render(path)
end
# shortcut for typing save_and_open_page
def page!
save_and_open_page
end