# How to test in Chrome using cuprite

> Cuprite drives a real Chrome over the DevTools Protocol, so your Rails system tests are faster, steadier and closer to what users see.

- Author: Ash Gaikwad (https://ashgaikwad.com)
- Published: 2025-01-18
- Topics: testing, rails, chrome, cdp
- Canonical: https://ashgaikwad.com/blogs/how-to-test-in-chrome-using-cuprite

Unit tests tell you that your methods work. They do not tell you that a person can click a button, see a form close and read a thank you message. Between your code and that person sits a browser, and the browser is where a lot of bugs like to hide.

I run a Rails app with a good number of pages, a few small widgets and some JavaScript sprinkled on top. The tests that save me the most trouble are the ones that open a real Chrome and behave like a visitor. In this article I will explain how Cuprite makes that pleasant, why the Chrome DevTools Protocol (CDP) is the reason it works so well, and how to decide what deserves a browser test and what does not.

![Ruby, Cuprite and Chrome connected by arrows: Ruby tests drive Chrome through the Cuprite driver.](/images/blogs/how-to-test-in-chrome-using-cuprite-social.png)

## What is Cuprite?

[Cuprite](https://github.com/rubycdp/cuprite) is a driver for Capybara, the Ruby library that lets tests say things like "visit this page, click that button, expect this text". Capybara does not care how the browser is controlled. It hands the work to a driver. The most famous driver is Selenium. Cuprite is another one, and it talks to Chrome directly with no extra server in between.

Under Cuprite sits [Ferrum](https://github.com/rubycdp/ferrum), a small Ruby library that launches Chrome and speaks CDP. So the stack is Capybara on top, Cuprite as the driver, Ferrum as the CDP client and Chrome at the bottom. Your test code stays plain Capybara. If you already know how to write a system test, you already know how to write one for Cuprite.

## Why the Chrome DevTools Protocol matters

CDP is the protocol that Chrome's own developer tools use. When you open the Network tab or inspect an element, the panel is sending JSON messages to the browser over a WebSocket and reading JSON messages back. The protocol is [documented publicly](https://chromedevtools.github.io/devtools-protocol/), and anyone can use it. That includes a Ruby test suite.

This has a few practical benefits for testing.

The first is fewer moving parts. The older Selenium setup needs a separate driver binary that must match your browser version, and it speaks the WebDriver protocol, which is a request and response style over HTTP. With CDP there is just Chrome and a socket. When something breaks, there are fewer places to look.

The second is that the browser can tell you things as they happen. With CDP, Chrome pushes events to the client, such as "the page finished loading" or "a network request is in flight". A driver can use these to know when the page is really ready, instead of sleeping for a fixed time and hoping. In my experience this removes a large share of flaky tests, because most flakiness is a test racing ahead of the page.

The third is power. CDP can set headers, capture screenshots, run JavaScript, read console messages and emulate devices. You get what a developer has in the DevTools panel, but from code, in a repeatable way.

## A setup that fits on one screen

You add the gem to the test group, make sure Chrome or Chromium is installed, and tell Rails to use it. This is close to what I use in a real app.

```ruby
require "capybara/cuprite"

Capybara.javascript_driver = :cuprite
Capybara.register_driver(:cuprite) do |app|
  Capybara::Cuprite::Driver.new(app, window_size: [ 1400, 1400 ], process_timeout: 10)
end

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :cuprite
end
```

The window size gives you a predictable desktop layout. The process timeout stops a stuck browser from hanging your whole run. Everything else is a sensible default.

One small detail has bitten me. Chrome inherits the preferred languages of the machine it runs on. My app picks a language from the browser's Accept-Language header, so the same test could behave differently on my laptop and on a colleague's laptop. Because Cuprite goes through CDP, I can pin that header for every request in one line inside a setup block.

```ruby
setup do
  page.driver.add_header("Accept-Language", "en", permanent: true)
end
```

Anything your app reads from the browser (language, time zone, cookies, user agent) is worth pinning like this. A test that passes only on one machine is worse than no test, because it teaches you to ignore red builds.

## What a good browser test looks like

Here is a test from the same app. A signed-in visitor sends feedback from a floating widget, and the page they were on is captured automatically.

```ruby
test "a signed-in surfer can send feedback from the floating widget" do
  sign_in_as users(:two)

  visit root_path
  click_button "Give feedback"
  fill_in "ticket_content", with: "Loved the new dashboard, but the search is slow."
  click_button "Send feedback"

  assert_text "Thanks for the feedback!"
  assert_equal root_url, Ticket.last.page_url
end
```

It reads like a story. There are no sleeps and no waiting code. Capybara keeps retrying `assert_text` for a short time, and Cuprite knows when the page is busy. The final line checks the database, so the test proves that the click in the browser really ended up as a saved record.

## When should you write browser tests?

Browser tests are slower than unit tests, so they should earn their place. I write one when the feature depends on the browser doing something that a plain request test cannot see.

The clearest case is JavaScript behaviour. A dialog that opens, a form that resets, a list that filters when you tick a box, a page that updates without a reload. A controller test sees none of that, because no JavaScript runs in it.

The second case is a flow that crosses several pages and several pieces of state. Signing up, filling a multi-step form, paying and landing on a confirmation page is the classic example. Each step may be tested alone, yet the bug often lives in the handoff between them.

The third case is anything that follows a bug. When a real user finds a problem in a flow, write the browser test that would have caught it before you fix it. Those tests tend to be the most valuable ones in the suite, because they describe things that actually went wrong.

I do not write browser tests for validations, formatting helpers or business rules that can be checked in a fast model or controller test. A good rule is to push each check down to the cheapest level that can still catch the bug.

## What is important to cover

If you are starting from zero, do not try to cover every page. Cover the paths where a failure costs you money, trust or sleep.

Start with sign in and sign out, because if people cannot get in, nothing else matters. Then cover whatever turns a visitor into a customer, such as signup, checkout or a contact form. After that, cover the permission boundaries. Check that a signed-out visitor does not see an admin button, and that an admin does not see the visitor-only widget. These tests are cheap and catch embarrassing leaks.

Then cover the interactive pieces that have their own logic, like filters, wizards and modals, and make sure they behave on a second use as well as the first. One of my favourite tests simply opens a widget, submits it, closes it and opens it again to check that the form is blank. State that leaks between uses is a very common bug.

Finally, add a quick smoke test that each important public page renders and shows its main heading. It will tell you fast if a deploy broke a whole page.

## Habits that keep the suite healthy

Use stable hooks. Find elements by visible text, labels and ids that you control, rather than by deep CSS paths that change with every redesign. If a test breaks every time someone moves a div, people will stop trusting it.

Never sleep. If you need to wait, assert on the thing you are waiting for. Capybara will retry until it sees it or times out. A negative check such as `assert_no_text` works the same way, and it is the right tool for "this should disappear".

Accept the places where a real browser is strict. Cuprite clicks by coordinates, the way a person does. If an element is visually hidden, for example a checkbox tucked behind a styled label, the click will miss it, which is a faithful result because a real user could not click it either. In the rare case where you need to bypass that, you can run a line of JavaScript on the page. Treat that as a last resort and leave a comment explaining why.

Run the suite before you deploy. I have a hook that runs the system tests before every build. There is no separate continuous integration service in that project, so the hook is the only gate, and it has stopped bad releases more than once.

## What about other browsers?

Cuprite only drives Chrome, which is its main limitation. If a good part of your audience is on Safari or Firefox, a Chrome-only suite will miss engine-specific rendering bugs.

That is a trade-off to check against your own numbers. For my app, the large majority of visitors use Chrome-based desktop browsers, so Chrome tests already represent most real usage. I decided to accept that gap until Safari traffic grows or an incident points at a Safari-only bug. Look at your analytics before you decide, and revisit the decision when the audience changes.

## A first week plan

1. On day one, add the gem, install Chrome and run a single test that visits your home page.
1. On day two, write the sign in test.
1. On day three, test the one flow that makes you money.
1. On day four, add the permission checks.
1. On day five, hook the system tests into your deploy so they run without anyone remembering.

After that, add a browser test whenever a user-facing bug shows up. In a few months you will have a suite that is small, quick and shaped by real problems.

The best test suites feel boring. You push a change, the browser opens somewhere in the background, a few clicks happen and everything stays green. Boring is the goal. Go let a robot click your buttons so you do not have to.
