Feature specs are notorious for their potential to flake. It’s possible for the results of feature specs to be inconsistent because they have to deal with asynchronous state. In a typical test environment, there’s a single Ruby process at play, so test code will be executed in order as written – we can reasonably expect one line to complete before the next is executed. But when it comes to feature specs with Capybara, we have three processes to deal with:
- the test runner, which uses a driver (something that can translate Ruby instructions into web browser automation) to step through the test instructions in order
- the server, which runs in a separate process and exposes itself on a port so that it can be accessed
- the browser, which is controlled by the driver to browse the site, and thus make requests to the server
This means we need to think about asynchronous state – after all, if the test runner instructs the browser to make a request to the server, how can we be sure that the page has loaded?
Pardon me, are you Aaron Burr, sir?
The secret to remedying flakes is to…wait for it!
That’s not me stalling for word count on this article, that’s the genuine advice – wait for it. When we call something like this:
visit root_path
…that’s the test runner using the driver to instruct the browser to navigate to /. But how can we tell that the browser has rendered the page?
Typically we tend to do that with an assertion of what’s shown on the page:
expect(page).to have_content("Overview")
The have_content matcher is one of many provided by Capybara. This is important, because the matchers provided by Capybara work a little differently to most. While in a typical test where we’d perhaps call something like:
expect(author).to have_attributes(name: "Simon Fish")
…that matcher will only call attributes against author once because we’re working within a synchronous context, and we can reasonably expect author to have those attributes.
But Capybara is built with this asynchronous context in mind, so have_content will poll the page instead. The matcher will succeed if it finds what it’s looking for, and if not, then it’ll raise an error (and thus fail) after a set maximum wait time.
To bring this back around, visit root_path won’t do any waiting – it’ll tell the browser to visit that path, but it’s up to you to decide what it is that indicates that the browser has loaded the page at /. In the case of FreeAgent, our root path is the Overview page, so we could look for the word Overview (have_content(”Overview”)). Or, if that’s too generic, we might want to do something like put a data-testid attribute on the page title and expect that to appear (have_css(”[data-testid=’overview-title’]”)).
Every action, whether that’s visiting a path, clicking a link or button, or submitting a form, needs to have some form of waiting Capybara expectation to verify that the request has completed and the page has changed in response to it.
But that’s not all there is to it – there’s one scenario where just a single waiting matcher on its own isn’t good enough!
Drop some knowledge!
More recently, some of the remaining flaky specs we’ve had to deal with have involved dropdowns. These take one click to open, and another to select an option. Now, let’s say we were to write:
click_on "Actions" # opens a dropdown marked "Actions click_on "View" # selects the dropdown option titled "View"
Running this could go one of three ways:
- Clicking on “Actions” opens the dropdown, and clicking on “View” activates its behaviour
- Clicking on “Actions” opens the dropdown, but due to a code change, “View” isn’t visible. Capybara tries to find a clickable element with “View” in its content, cannot, and eventually fails.
- Clicking on “Actions” does not open the dropdown. Capybara looks for a “View” option next, though, because the
click_onaction completed successfully.
For each click_on call, the only thing Capybara waits for is for the element to be visible. While an “Actions” button may be visible, it may not yet be interactable – we might be waiting for some JS to mount, for example. This is likely to happen in good time for a customer browsing the site, but the feature spec environment might take a little longer to mount the JS, especially if it’s not running on as much memory. So there are reasons why 3 could happen, and there are also reasons why 3 could stop happening with time – the browser could eventually load the JS and the dropdown could become interactable.
In ordinary use, the two things should be tied together, too – clicking on “Actions” should reveal the “View” button. So if we can’t find the “View” button, it’s probably because the “Actions” button needs clicking again. How do we tell Capybara to retry opening the dropdown if the “View” option isn’t visible?
This is where page.document.synchronize comes in. The synchronize method is responsible for Capybara’s waiting behaviour under the hood – there are helper methods that check if an element is on the page right now, which matchers like have_content and have_css call from within a page.document.synchronize block so that they are retried until they pass or time out.
So in the example above, we might use:
page.document.synchronize do click_on "Actions" click_on "View" end
This would mean that if click_on "View" were to fail, we would also retry click_on "Actions" , thus letting us ensure that the dropdown is open – and retry toggling it if it isn’t – before clicking on the “View” action.
One word of warning – using expectations within a synchronize block will cause it to fail, so lean on Capybara methods like page.find , which will raise the error synchronize swallows, instead.
Capybara’s waiting behaviour is absolutely core to consistent feature specs as I’ve illustrated, and page.document.synchronize is a handy extra trick to securing consistency where two coupled elements are concerned.
Knowing this has helped us to remedy many flaky specs and prevent others from becoming possibilities. We have code that dynamically updates form content depending on selected options, which often reveals places where we might not have accurately accounted for the async behaviour at play. Being mindful of waiting with Capybara enables us to find the right solutions and make our feature specs in these areas more resilient – and might help you to do the same!




