Having confidence in your application helps with the aid of great interation tests. For Rails, that means utilizing the Capybara gem for testing features from end-to-end.

While Capybara is a great gem for accomplishing this goal, it can be at time difficult, frustrating, and nuanced in its implementation. I’ve been keeping track of all the tricks I use reguarly and compiled a list of the best ones. Hopefully, this helps give some clarity to a few best practices I use on a daily basis.

Find element attribute

You can use the hash symbol syntax to return the full value of any attributes from a captured element.

have_css(".PathContentOptions .NewButton[disabled]")

The additional syntax of find(".PathContentOptions .NewButton")[:disabled] is also available but not recommended due to its inability to wait for the element to load fully with the specific attribute.

Find the parent of an element

Sometimes it might be nearly impossible to grab a specific container element. Whether that is due to multiple existing on the page or insufficient selector specificity, it can be difficult to grab ambigous elements.

However, if you can find an element that is a child of the element you want to write expectations against you can use xpath to find its parent.

<div>
  <p class="target">
</div>
<div>
  <p>Excluded</p>
</div>
node = page.find(".target")
parent = node.find(:xpath, "..")
parent["innerHTML"] #=> <p>Excluded</p>

Set element value without an ID or name

Using the keyword set you can mimic the fill_in shortcut for elements that are difficult to find by id or name.

find(".AssignModal-recurringAmount").set(2)

You can also select options from a drop down in a similiar fashion with the select_option keyword.

find(".AssignModal-recurringUnit select").first(:option, "Weeks").select_option

Mimic user typing directly with send_keys

While the previous example gives you a way to directly set what value is within an element, sometimes you want to actually mimic the user’s interaction. This is especially important with things like autocomplete or fuzzy searches.

find(".someElement").native.send_keys("User typing each character")

Native returns the element directly from the driver allowing for more control of elements that is closer to the real implementation of the browser.

Send_keys mimics a user typing each character one at a time into whatever element it is pointed at.

Assert that the button is disabled

This allows you to match only disabled buttons on the page. Without this line Capybara defaults to marking elements that are disabled as not being part of the content.

expect(page).to have_button("Review and Assign", disabled: true)

Furthermore, you can also check for visible buttons or elements with the visible attribute.

expect(page).to have_button("Review and Assign", visible: true)

Switch your test context to a popup

By switching to the latest browser window as the current page window you can seamlessly test a popup’s interactions and styles.

popup = page.driver.browser.window_handles.last
page.driver.browser.switch_to_window(popup)

Ensure specific counts for element matches

Allows for matching and expecting a certain number of elements. Fails if there are more or less than the specified amount.

expect(page).to have_selector(".Icon", count: 4)

Expect selector with text

Matching a css class or ID can sometimes not be specific enough. Especially, in cases where a class appears more than once further matching the selector based on the text within it can help increase expectation specificity.

expect(page).to have_selector(".LearningContent-actionButton", text: "Start Lesson")

Additionally, text can accept regexp syntax so you can use the /i case insensitive matcher

expect(page).to have_selector(".LearningContent-actionButton", text: /Start Lesson/i)

Waiting for ajax? Sleep no more!

Waiting for ajax can be a pain. How long should sleep be set for? A quarter-second? 1 second? Luckily, there’s a way to properly wait for ajax to complete without using sleep.

# Put this in your javascript helpers or other helper location you use for
# your feature specs
def wait_for_ajax
  Timeout.timeout(Capybara.default_max_wait_time) do
    loop until page.evaluate_script("jQuery.active").zero?
  end
end

# Then use in your test like
it "performs an ajax request" do
  some_ajax_event

  wait_for_ajax

  more_expectations
end

The original solution (which the above is based on) was proposed by Thoughtbot and can be found here on Coderwall.

Poltergeist web driver clicking elements

Error message: Capybara::Poltergeist::MouseEventFailed: The element you are trying to interact with might be overlapping another element. If you don’t care about this, using node.trigger(‘click’) can ignore this situation.

find("some element").trigger("click")

Search within particular element context

You probably know that you can change the page context with the within(".selector") do block. But did you know you can also select a specific element based on where it appears?

within(first(".selector", match: :first))
within(first(".selector", text: "Some text here"))

Note Make sure you use an option or element waiting approach when using first() since first() doesn’t wait until all elements load.

Use current_scope to debug the current context

If you wanted to debug the html from within a within block you unfortunately can’t do something like:

within(".selector") do
  puts page["innerHTML"] # => NoMethodError
end

Luckily, there is a keyword that allows you to access the current scope called current_scope

within(".selector") do
  puts current_scope["innerHTML"] #=> Returns the html within the within block
end

Directly debugging the current page

Sometimes using save_and_open_page isn’t enough to determine what is going on within a test. It doesn’t load assets properly or interact well with user input.

However, there is a trick to actually debugging the state of the current page within a test.

puts current_url
binding.pry

You’ll end up with a printed url within your test suite which can then be copy-pasted into your browser where you can interact with the current page. Using binding.pry pauses execution right where you want the state of the page to be currently. I can’t take credit for this trick as the excellent post on QuickLeft directed me to it.

Avoiding flaky tests

Another article by the Thoughtbot team, about all the ways of avoiding race condition like tests. I’ve gleaned some of my information from this article which can be found here

Remove css animations for less flakes and a faster test suite

By default poltergeist, and potentially other js drivers, perform css animations. Depending upon your expectations this might slow your test down in that expectations need to wait until an element is fully loaded. Additionally, this could impact the consistency of your tests by introducing flaky race conditions.

Instead, if we disable animations altogether we should in theory have a faster test suite that is more reliable. I found the basic idea for this on StackOverflow.

# spec_helper.rb or wherever you've configured Capybara
Capybara.register_driver :poltergeist do |app|
  opts = {
    extensions: ["#{Rails.root}/features/support/disable_animations.js"], # Disable animations for capybara
    timeout: 2.minutes,
    js_errors: true
  }

  Capybara::Poltergeist::Driver.new(app, opts)
end
// /features/support/disable_animations.js
/*
 * Disable transition, transform, and animation css rules
 * for all feature specs. This produces faster specs that are less prone
 * to flakes due to race conditions of element loading.
 */
var disableAnimationStyles = '-webkit-transition: none !important;' +
                             '-moz-transition: none !important;' +
                             '-ms-transition: none !important;' +
                             '-o-transition: none !important;' +
                             'transition: none !important;' +
                             '-webkit-transform: none !important;' +
                             '-moz-transform: none !important;' +
                             '-ms-transform: none !important;' +
                             '-o-transform: none !important;' +
                             'transform: none !important;' +
                             '-webkit-animation: none !important;' +
                             '-moz-animation: none !important;' +
                             '-ms-animation: none !important;' +
                             '-o-animation: none !important;' +
                             'animation: none !important;'

window.onload = function() {
  var animationStyles = document.createElement('style');
  animationStyles.type = 'text/css';
  animationStyles.innerHTML = '* {' + disableAnimationStyles + '}';
  document.head.appendChild(animationStyles);
};

Disabling Capybara::Poltergeist::JavascriptError

By default Capybara will fail any spec that produces a javascript error. These can be disabled (though not recommended) from where Capybara::Poltergeist::Driver.new is registered with the following:

# specs/spec_helper.rb
Capybara.register_driver :poltergeist do |app|
  Capybara::Poltergeist::Driver.new(app, timeout: 2.minutes, js_errors: false)
end

Using binding.pry as described above can help track down where errors are occuring in the execution flow and is generally the most useful approach.

Poltergeist Javascript Errors caused by your ISP

If you are constantly receieving errors it is possible that there is a network issue preventing the test from passing. Yes, a local network setting could prevent the test suite from passing in development. Here’s an example of an ISP blocking and redirecting a request being made to the jquery cdn from a feature spec.

  Error: Bootstrap's JavaScript requires jQuery
  Error: Bootstrap's JavaScript requires jQuery
    at http://dnserrorassist.att.net/s/js/bootstrap.min.js:6 in global code

The give away here is that jQuery is being required by Bootstrap but Bootstrap can’t find it. At the end of the error in the console the url returned is suspicious http://dnserrorassist.att.net/s/js/bootstrap.min.js:6. Specifically the highlighted part indicates that the request is be re-routed by the network.

In order to fix these type of issues you’ll need to change your network settings on your router, adjust privacy settings with your ISP, or if you are on public WIFI move to a new location.

Do you have an awesome Capybara trick I missed? Let me know with a comment below.

Thanks for reading.

« Previous Post
Saving Script Output From Heroku to a Local File
Next Post »
Resetting your Elasticsearch indices on heroku staging when you've reached the maximum index count for the current plan

Join the conversation

comments powered by Disqus