I just finished up migrating all the Capybara feature tests in my Rails/React app from Poltergeist to Headless Chrome. I ran into a few issues I didn’t see covered in other write-ups, so I thought I’d pull together what I learned and what I found useful.
This pull request shows all the changes I had to make.
Poltergeist is based on the no-longer-maintained PhantomJS headless browser. When we started using PhantomJS a few years ago, it was the only good way to run headless browser tests, but today there’s a good alternative in Selenium with Chrome’s headless mode. (Firefox has a headless mode now as well.) I briefly tried out a switch to headless Chrome a while ago, but I ran into too many problems early on and gave up.
This time, I decided to try it again after running into a weird bug — which I still don’t know the cause of. (Skip on down if you just want the migration guide.) This was a classic yak shave…
Using R in production was fun and interesting, but I definitely won’t be doing it again any time soon.
If you want to see that Vega plot that started it all, this is a good place to look. Just click ‘Change in Structural Completeness’. (Special thanks to Amit Joki, who added the interactive controls.)
Setting up Capybara
The basic setup is pretty simple: put
chromedriver-helper in the Gemfile, and then register the driver in the test setup file. For me it looked like this:
Capybara.register_driver :selenium do |app|
options = Selenium::WebDriver::Chrome::Options.new(
args: %w[headless no-sandbox disable-gpu --window-size=1024,1024]
Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
Adding or removing the
headless option makes it easy to switch between modes, so you can pop up a real browser to watch your tests run when you need to debug something.
chrome: stable addon in .travis.yml got it working on CI as well.
Dealing with the differences between Poltergeist and Selenium Chromedriver
You can run Capybara feature tests with a handful of different drivers, and the core features of the framework will work with any driver. But around the edges, there are some pretty big differences in behavior and capabilities between them. For Poltergeist vs. Selenium and Chrome, these are the main ones that I had to address during the migration:
More accurate rendering in Chrome
PhantomJS has some significant holes in CSS support, which is especially a problem when it comes to misrendering elements as overlapping when they should not be. Chrome does much more accurate rendering, closely matching what you’ll see using Chrome normally. Relatedly, Poltergeist implements
.trigger('click'), which unlike the normal Capybara
.click , can work even if the target element is underneath another one. A common error message with Poltergeist points you to try
.trigger('click') when the normal
.click fails, and I had to swap a lot of those back to
Working with forms and inputs
The biggest problem I hit was interacting with date fields. In Poltergeist, I was using text input to set date fields, and this worked fine. Things started blowing up in Chrome, and it took me a while to figure out that I needed provide Capybara with
Date objects instead of strings to make it work. Capybara maintainer Thomas Walpole (who is incredibly helpful) explained it to me:
fill_in with a string will send those keystrokes to the input — that works fine with poltergeist because it doesn’t actually support date inputs so they’re treated as standard text inputs so the with parameter is just entered into the field – Chrome however supports date inputs with it’s own UI.
By passing a date object, Capybara’s selenium driver will use JS to correctly set the date to the input across all locales the browser is run in. If you do want to send keystrokes to the input you’d need to send the keystrokes a user would have to type on the keyboard in the locale the browser is running in —In US English locale that would mean
fill_in(‘campaign_start’, with: ’01/10/2016’)
Chromedriver is also pickier about which elements you can send input to. In particular, it must be focusable. With some trial and error, I was able to find focusable elements for all the inputs I was interacting with.
The biggest shortcoming with Selenium + Chrome is the lack of support for the
Fortunately, there’s a fairly easy way to hack this feature back in with Chrome, as suggested by Alessandro Rodi. I modified Rodi’s version a bit, adding in the option to disable the error catching on individual tests — since a few of my tests involve testing error behavior. Here’s what it looks like, in my
config.after(:each, type: :feature, js: true) do |example|
errors = page.driver.browser.manage.logs.get(:browser)
# pass `js_error_expected: true` to skip JS error checking
next if example.metadata[:js_error_expected]
errors.each do |error|
# some specs test behavior for 4xx responses and other errors.
# Don't fail on these.
next if error.message =~ /Failed to load resource/
expect(error.level).not_to eq('SEVERE'), error.message
next unless error.level == 'WARNING'
Different behavior using matchers to interact with specific parts of the page
Much of the Capybara API is driven with HTML/CSS selectors for isolating the part of the page you want to interact with.
I found a number of cases where these behaved differently between drivers, most often in the form of Chrome reporting an ambiguous selector that matches multiple elements when the same selector worked fine with Poltergeist. These were mostly cases where it was easy enough to write a more precise selector to get the intended element.
In a few cases with some of the intermittently failing specs, Selenium + Chrome also provided more precise and detailed error messages when a target element couldn’t be found — giving me enough information to fix the badly-specified selectors that were causing the occasional failures.
Blocking external urls
With Poltergeist, you can use the
url_blacklist option to prevent loading specific domains. That’s not available with Chromedriver. We were using it just to reduce unnecessary network traffic and speed things up a bit, so I didn’t bother trying out the alternatives, the most popular of which seems to be to use Webmock to mock responses from the domains you want to block.
In Poltergeist, you can easily see what the HTTP status code for a webpage is:
page.status_code. This feature is missing altogether in Selenium. I read about a few convoluted ways to get the status code, but for my test suite I decided to just do without explicit tests of status codes.
Other useful migration guides and resources
There are a bunch of blog posts and forum threads on this topic, but the two I found really useful are: