Replacing Selenium with Cuprite for Rails system tests
4 min read

Replacing Selenium with Cuprite for Rails system tests

In the world of tests, system tests are as close to real users as it gets: you control a browser and make it use your app as you expect humans to.

To do so, you need two things:

  • a browser : you will see it (safari, chrome…) run on its own on your machine unless you set it to be "headless", a gory way to tell your machine that you actually don't want to see the insides of your tests
  • a driver : the pilot that will take your test instructions and command the browser

By default, Rails ships with Selenium, but there's a new kid in town: Cuprite, a "Headless Chrome driver for Capybara", which is faster and has some nice tricks up its sleeves like options to pause and debug.

I discovered cuprite thanks to Evil Martians blog post: "**System of a test:** Proper browser testing in Ruby on Rails".

The following is just a quick-start version for those who don't care about docker or rspec, with some minor twists.

Basic setup

To get started, you simply need to add the cuprite gem. Selenium will have to stay unless you're on Rails 6.1.

group :test do
  gem 'capybara'
  gem 'selenium-webdriver' # Only for rails <= 6.1
  gem 'cuprite'
end

And then you can edit test/application_system_test_case.rb

require "test_helper"
require "capybara/cuprite" # <- Add this

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  # And replace selenium with cuprite
  driven_by :cuprite, using: :chromium, screen_size: [1400, 1400]
end

And with this you're good to go.

You'll notice that I'm not using :chrome but the open source browser it's based on: Chromium. This is because I just uninstalled Chrome, because https://chromeisbad.com.

https://twitter.com/sowenjub/status/1338958371844726784?s=20

A more elaborate setup

There are a couple more setup files I grabbed from Evil Martians' setup, with a slightly different organisation since I'm not using rspec.

test/
  test_helpers/
    system/
      better_rails_system_tests.rb
      capybara_setup.rb
      cuprite_helpers.rb
      cuprite_setup.rb

And with this, test/application_system_test_case.rb will end up looking like this:

require "test_helper"
require "test_helpers/system/better_rails_system_tests"
require "test_helpers/system/capybara_setup"
require "test_helpers/system/cuprite_helpers"
require "test_helpers/system/cuprite_setup"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :cuprite, using: :chromium, screen_size: [1400, 1400]

  include BetterRailsSystemTests
  include CupriteHelpers
end

And of course you need to create the files, taken from Evil Martians.

# test_helpers/system/better_rails_system_tests

module BetterRailsSystemTests
  # Use our `Capybara.save_path` to store screenshots with other capybara artifacts
  # (Rails screenshots path is not configurable https://github.com/rails/rails/blob/49baf092439fc74fc3377b12e3334c3dd9d0752f/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L79)
  def absolute_image_path
    Rails.root.join("#{Capybara.save_path}/screenshots/#{image_name}.png")
  end

  # Make failure screenshots compatible with multi-session setup.
  # That's where we use Capybara.last_used_session introduced before.
  def take_screenshot
    return super unless Capybara.last_used_session

    Capybara.using_session(Capybara.last_used_session) { super }
  end
end



# test_helpers/system/capybara_setup

# Usually, especially when using Selenium, developers tend to increase the max wait time.
# With Cuprite, there is no need for that.
# We use a Capybara default value here explicitly.
Capybara.default_max_wait_time = 2

# Normalize whitespaces when using `has_text?` and similar matchers,
# i.e., ignore newlines, trailing spaces, etc.
# That makes tests less dependent on slightly UI changes.
Capybara.default_normalize_ws = true

# Where to store system tests artifacts (e.g. screenshots, downloaded files, etc.).
# It could be useful to be able to configure this path from the outside (e.g., on CI).
Capybara.save_path = ENV.fetch("CAPYBARA_ARTIFACTS", "./tmp/capybara")

# The Capybara.using_session allows you to manipulate a different browser session, and thus, multiple independent sessions within a single test scenario. That’s especially useful for testing real-time features, e.g., something with WebSocket.
# This patch tracks the name of the last session used. We’re going to use this information to support taking failure screenshots in multi-session tests.
Capybara.singleton_class.prepend(Module.new do
  attr_accessor :last_used_session

  def using_session(name, &block)
    self.last_used_session = name
    super
  ensure
    self.last_used_session = nil
  end
end)

For this one I created a dedicated file but you could put it at the end of cuprite_setup as well.

# test_helpers/system/cuprite_helpers

module CupriteHelpers
  # Drop #pause anywhere in a test to stop the execution.
  # Useful when you want to checkout the contents of a web page in the middle of a test
  # running in a headful mode.
  def pause
    page.driver.pause
  end

  # Drop #debug anywhere in a test to open a Chrome inspector and pause the execution
  def debug(*args)
    page.driver.debug(*args)
  end
end



# test_helpers/system/cuprite_setup

# First, load Cuprite Capybara integration
require "capybara/cuprite"

# Then, we need to register our driver to be able to use it later
# with #driven_by method.
Capybara.register_driver(:cuprite) do |app|
  Capybara::Cuprite::Driver.new(
    app,
    **{
      window_size: [1200, 800],
      # See additional options for Dockerized environment in the respective section of this article
      browser_options: {},
      # Increase Chrome startup wait time (required for stable CI builds)
      process_timeout: 10,
      # Enable debugging capabilities
      inspector: true,
      # Allow running Chrome in a headful mode by setting HEADLESS env
      # var to a falsey value
      headless: !ENV["HEADLESS"].in?(%w[n 0 no false])
    }
  )
end

# Configure Capybara to use :cuprite driver by default
Capybara.default_driver = Capybara.javascript_driver = :cuprite 

And now we just have to write a simple test:

require "application_system_test_case"

class HomeTest < ApplicationSystemTestCase
  test "open home screen" do
    visit root_url
    save_and_open_page # Will save the html page in /tmp/capybara and open it in your default browser
    take_screenshot # Will save a screenshot in /tmp/capybara/screenshots
    pause # To see the current view, requires HEADLESS=0 (or n, no, false)
    debug() # To see the current view with debug tools
    assert_selector "h1", text: "Hello, world"
  end
end

To launch it, run the following:

# If you want to see the browser or use `pause`
HEADLESS=0 rails test test/system/home_test.rb

# Otherwise this will do
rails test test/system/home_test.rb