---
title: Combine Capybara selectors to avoid the sequential timeout trap
teaser: How I shaved over a minute off a test suite with a one-line change.
tags: capybara,performance,testing
author: Matheus Richard
published_on: 2026-03-06
---

I was working on a codebase with a custom Capybara matcher that looked for a
toggle field using two selectors:

```ruby
page.has_css?("label[for='#{field_id}']", text: name) ||
  page.has_css?("label:has(##{field_id})", text: name)
```

The idea is simple: try to find the label by `for` attribute, and if that
doesn't match, try finding a label that wraps the input.

The "problem" with that is how Capybara works. `has_css?` is a waiting method,
so if the selector doesn't match immediately, Capybara will keep retrying until
`Capybara.default_max_wait_time` is reached. On dev that was 5 seconds, on CI,
30.

So every time the first selector didn't match, Ruby waited the *full* timeout
before even trying the second one. With 4 tests hitting this matcher, that's up
to 2 minutes of CI time spent just... waiting.

The fix is combining both selectors into a single CSS query using a comma:

```ruby
page.has_css?("label[for='#{field_id}'], label:has(##{field_id})", text: name)
```

CSS commas work as an OR and Capybara now checks both selectors in a single
pass. If either matches, it returns immediately. No more burning a full timeout
cycle on the first selector before trying the second.

But even better, we can skip the manual CSS altogether and let Capybara do what
it already knows how to do:

```ruby
page.has_selector?(:label, name, for: field_id)
```

Capybara's [built-in `:label` selector](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Selector#:~:text=%3Alabel%20%2D%20Find%20label%20elements) already handles both cases, labels with a
`for` attribute and labels wrapping the input, in a single query. No custom code
needed!

<aside class="info">
  <p>This last approach also aligns with three rules from our <a href="https://github.com/thoughtbot/guides/blob/11e9e5b2122d932a2084306eb390f22a85a5e58c/testing-rspec/README.md">testing guide</a>:
<a href="https://github.com/thoughtbot/guides/blob/11e9e5b2122d932a2084306eb390f22a85a5e58c/testing-rspec/README.md#L48">use the most specific selectors available</a>,
<a href="https://github.com/thoughtbot/guides/blob/11e9e5b2122d932a2084306eb390f22a85a5e58c/testing-rspec/README.md#L49">don't locate elements with CSS selectors or <code>[id]</code> attributes</a>, and
<a href="https://github.com/thoughtbot/guides/blob/11e9e5b2122d932a2084306eb390f22a85a5e58c/testing-rspec/README.md#L50-L52">use accessible names and descriptions</a> to locate elements. Following these rules would have prevented the problem in the first place!</p>
</aside>
