rails

Simplify your Capybara selectors

How to use a Ruby-friendly syntax alternative to esoteric CSS and xpath expressions. Plus, a trick for making Capybara automatically aware of data-testid attributes.

Attribute selectors in browser tests

When writing browser tests in Rails with capybara, sometimes you’ll need to find an element based on an HTML attribute. A common use-case is when an element is explicitly marked with data-testid to make it easier to find in automated tests.

Consider this markup:

<div data-testid="bio-card" class="max-w-sm rounded overflow-hidden shadow-lg">
  <div class="px-6 py-4">
    <p class="text-gray-700 text-base">
      Matt Brictson is a software engineer in San Francisco.
    </p>
  </div>
</div>
In this example, there is no obviously unique or semantic CSS class or HTML element that denotes this component as a card. To make it easy to reference within tests, we can explicitly assign a data-testid.

Let’s say we want to write an assertion about the text contained in that card. We can use the data-testid to find it.

expect(find('[data-testid="bio-card"]')).to have_text(/software engineer/)
Capybara’s find method can take a CSS selector. In this case we’re using the square-bracketed attribute selector syntax.

This works, but the syntax is not ideal: it requires a mixture of nested single- and double-quotes, plus square brackets that must be placed just so, and it is not particularly easy to read.

Thankfully Capybara’s find method has a better way to write this, which I’ll explain next.

The :element strategy

The find method in Capybara is a lot more capable than it appears at first glance. By default, it accepts a CSS expression, but other strategies are available:

# css expression (implicit)
find('[data-testid="bio-card"]')

# css expression (explicit)
find(:css, '[data-testid="bio-card"]')

# xpath expression
find(:xpath, "//*[@data-testid='bio-card']")
The optional first argument denotes the selector strategy. Capybara includes over 20 strategies, with :css and :xpath being the most familiar.

The :element strategy is one that works particular well to find HTML attributes like data-testid. Here’s how to use it:

expect(find(:element, "data-testid": "bio-card")).to have_text(/software engineer/)
The :element selector takes an arbitrary hash of attribute names and values. Values can be strings or regular expressions.

This looks more like idiomatic Ruby. It’s easier to read and write than CSS and xpath expressions, which always feel a bit out of place within Ruby code. The syntax is also familiar because it is similar to Rails tag helpers.

tag.div("data-testid": "bio-card")
# => '<div data-testid="bio-card"></div>'

find(:element, "data-testid": "bio-card")
# => Capybara::Node::Element
There is a nice syntactic symmetry at play here.

Of course, the :element selector strategy is not limited to data-testid.

# Password field should be empty
expect(page).to have_selector(:element, "input", type: "password", value: "")
The :element selector also accepts the name of the HTML element to search for, and can take multiple attribute matchers.

Capybara’s test_id magic

The :element strategy is a great way to find elements. But for clicking and interacting with elements, Capybara has one more more trick up its sleeve: the test_id configuration option.

Capybara.configure do |config|
  config.test_id = "data-testid"
end
The test_id option is nil by default. When specified, Capybara will automatically search for HTML elements by that attribute name when performing click, fill_in, select, and other actions.

The test_id option doesn’t affect the default behavior of find, but it does simplify interactions with buttons, links, and form fields.

# Entering text into a form field identified by data-testid="username"
find(:element, "data-testid": "username").fill_in(with: "matt")

# Can be simplified to this
fill_in "username", with: "matt"
Ideally, actions should be taken on human-readable labels (e.g. button text, <label>, or arial-label value). In rare cases where data-testid is the only option, the implicit selector comes in handy.

This might be too much magic for some, but is a powerful option, especially if your frontend code relies heavily on data-testid attributes.

Resources

The Capybara configuration and best practices explained in this blog post are included in nextgen, my interactive Rails application generator. Check it out when starting your next project.

Additional references:

Share this? Copy link

Feedback? Email me!

Hi! 👋 I’m Matt Brictson, a software engineer in San Francisco. This site is my excuse to practice UI design, fuss over CSS, and share my interest in open source. I blog about Rails, design patterns, and other development topics.

Recent articles

RSS
View all posts →

Open source projects

mattbrictson/nextgen

Generate your next Rails app interactively!

11
Updated 1 day ago

mattbrictson/tomo

A friendly CLI for deploying Rails apps ✨

360
Updated 1 day ago

More on GitHub →