Rails on Maui

Programming in Paradise

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:

  1. 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.
  2. 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:

  1. Using screen shots (render_page below)
  2. 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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