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.
Join the conversation