Headless tests are necessary for CI environments and very useful for unobtrusive local development. No need to ditch the driver that directly controls the browser though, there’s lots of debugging value in being able to switch between both modes.

You’ll first need both drivers installed and configured, which will give you headless tests by default. Running your tests with a HEADLESS=0 environment variable will give you visual feedback from the test suite as it executes.

This visual mode used in combination with breakpoints has proved to be an excellent addition to my toolbelt when debugging. I’ve tested this to work on Rails 5.2.x & 6.0.0

Installation

I’m an RSpec type of guy, these are my Gemfile dependencies (don’t forget to bundle install).

group :development, :test do

  ...

  gem 'rspec-rails', '~> 3.8'
  gem 'capybara', '~> 3.16'
  gem 'selenium-webdriver', '~> 3.141'
end

Download chromedriver from here, make it executable, and send it somewhere cozy. For example on macOS after the executable is downloaded & unzipped:

$ cd ~/Downloads
$ chmod +x chromedriver
$ mv chromedriver /usr/local/bin/

Capybara setup

Uncomment or add the following line on spec/rails_helper.rb

# spec/rails_helper.rb
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

Now the key config for this to work will live in spec/support/capybara.rb and will look something similar to:

# spec/support/capybara.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
  config.include Capybara::DSL

  Capybara.server = :puma, { Silent: true }

  # Chrome non-headless driver
  Capybara.register_driver :chrome do |app|
    Capybara::Selenium::Driver.new(app, browser: :chrome)
  end

  # Chrome headless driver
  Capybara.register_driver :headless_chrome do |app|
    caps = Selenium::WebDriver::Remote::Capabilities.chrome(loggingPrefs: { browser: 'ALL' })
    opts = Selenium::WebDriver::Chrome::Options.new

    chrome_args = %w[--headless --no-sandbox --disable-gpu --window-size=1920,1080 --remote-debugging-port=9222]
    chrome_args.each { |arg| opts.add_argument(arg) }
    Capybara::Selenium::Driver.new(app, browser: :chrome, options: opts, desired_capabilities: caps)
  end

  # Switch between :chrome / :headless_chrome to see tests run in chrome
  case ENV['HEADLESS']
  when 'true', 1, nil
    Capybara.javascript_driver = :headless_chrome
  else
    Capybara.javascript_driver = :chrome
  end
end

As you can see two Selenium Drivers were configured (:chrome & :headless_chrome). But only one of the Drivers is used at a time, based on the HEADLESS environment variable.

Usage

The default behavior is Headless. The usual clean, quick, and simple.

$ bundle exec rspec

Add the environment variable and a Chrome window will be summoned and controlled remotely.

$ HEADLESS=0 bundle exec rspec

Like I said, debugging is the main motivation for this configuration. Breakpoints when tests aren’t passing because an element is not visible is one the best examples I can think of. Being able to inspect on a Chrome window directly has saved me lots of time.

Hope it helps anyone, Pura Vida!