Here is a basic primer for getting your head around the hard parts of writing tested code using Capybara. You can use Capybara with either Minitest or Rspec.

WaitsDoes not Wait
find(“#foo”)Calling an attribute (like .text on an element after it has been found)

The most important Capybara feature you must know is this:

Capybara automatically waits for elements to appear or disappear on the page, except when it doesn’t.

Try a command like:


This will block (or wait) until an element with id “foo” appears on the page.

Other Capybara selectors that wait are  has_content?("bar")click_link("baz") and most other things you can do with Capybara.

Let’s look at a more complicated case:


Even if #foo is originally on the page and then removed and replaced with a #foo which contains baz after a short wait, Capybara will still figure this out. Let’s make that really clear, Capybara is very good at waiting for content.

Generally speaking, this behavior is completely transparent. You don’t even really have to think about it, because Capybara just does it for you. What trips a lot of people up is that they try to do something like this:

page.find("foo").text.should contain("login failed")

And now they have introduced potential timing issues. text, being just a regular method, which returns a regular string, isn’t going to sit around and wait for anything. It will simply return the text as it appears when Capybara gets to this line and calls it a day.


Keep your Tests Fast *IMPORTANT*

Always Test Behavior, Never Test Implementation Details *IMPORTANT*

Don’t Prefer XPath

You might love Xpath and if you do more power to you. I personally cannot stand it, nor can I stand having to think about syntax that is specifically very foreign to all the other tools I work with. I generally find myself using CSS selectors, button selectors, and within blocks a lot.

Only Test The UI, Except When You Can’t

Don’t Find Based on Presentational Elements, Use Content Instead

But if You Can’t Use Content For Assertions, Add a “Testid”

Don’t Find the First (or last), It’s Sloooooooooow

Capybara and Animations

Capybara and Sticky Footers

Capybara and Animate on Scroll

Watch out for the Animate-on-Scroll library, which has become more popular with modern marketing-driven websites.

If you use it, you’ll find yourself wanting to scroll to the element on the page to make sure it appears.

But there’s a terrible catch: You can’t scroll to an element if that element itself is hidden by AOS, or if it is inside something that is hidden by AOS.

That means you must wrap the things that will as appear-on-scroll inside of empty wrapping elements which are 1) targetable using CSS or other selectors, etc, and 2) are not themselves hidden when the page loads.

You’ll want to scroll to that thing first, then use one of the waiting-style finders (see the left column in the list above).

Watch out for your window size

Selenium Chrome vs. Selenium Chrome Headless

Sometimes you need to debug the same spec in both the headless driver and the selenium driver. This is extremely tedious. A naive approach might think, why not just stick to one or the other. The problem, is without seeing the UI, it’s often very difficult to debug the test at all, so you find yourself switching back and forth between the headless driver and the selenium driver.

But, sometimes, the test itself behaves differently between the two drivers, making this task the most complicated part of Ruby development.

Date & Time Mysteries

iFrame & Stripe with Capybara

• Find the frame using find()

• Use send_keys to access it

      frame = find(:css, '#card-element > div > iframe')
      within_frame(frame) do
        card.to_s.chars.each do |piece|

        find_field('exp-date').send_keys expiry
        find_field('cvc').send_keys cvc
        find_field('postal').send_keys postal

• Because the payment intent must match the actual transaction, it’s tricky if not impossible to do this with VCR cassettes. I, myself, could not get it working and just solved this by ignoring these requests. TO do this creatively, I suggest the VCR around block, which goes outside of the it statement (but inside the describe)

around do |example|
VCR.turned_off { }

Then all requests within this describe block will be ignored (passed through to the Stripe API).

Race Conditions and What To Do About Them