Capybara is good when it's good, and it's bad when it's bad. When you don't know how to use it, it can be tragic. When you use it for the wrong job, it is a pain.
Here us a quick, and absolutely subjective guide to using it correctly, which mostly means adapting it to the needs of your project, handling it limitations, and connecting it to RSpec.
Main problem with asserting in RSpec and Capybara for me is that there is too many options. However RSpec makes it relatively easy to build your own. So instead of always googling and checking documentation while making sure you are looking at the right version of it, you can adapt those matchers to the needs of your projects.
This can mean making aliases to simplify the naming and always use the same, limited number of matchers, or creating your own from scratch.
I will go over a few examples.
module Support::CapybaraMatchers
extend RSpec::Matchers::DSL
class HaveFieldsWithValuesMatcher
def initialize(prefix, fields)
@prefix = prefix
@fields = fields
end
def matches?(page)
@fields.each do |field, expected_value|
@field = "#{@prefix}#{field}"
@expected_value = expected_value
@value = nil
begin
@value = page.find_field(@field).value
return false unless @expected_value == @value
rescue Capybara::ElementNotFound
return false
end
end
end
def failure_message
if @value
"Excepted field #{@field} to have value '#{@expected_value}' but it had " \
"'#{@value}'"
else
"Excepted field #{@field} to have value '#{@expected_value}' but the " \
'field was not found'
end
end
def failure_message_when_negated
if @value
"Excepted field #{@field} to not have value #{@expected_value}"
else
"Excepted field #{@field} to not have value '#{@expected_value}' but the " \
'field was not found'
end
end
end
def have_fields_with_values(prefix, fields)
HaveFieldsWithValuesMatcher.new(prefix, fields)
end
def have_field_with_value(field, value)
HaveFieldsWithValuesMatcher.new('', field.to_sym => value)
end
matcher :have_selected_values do |prefix, fields|
match do |actual|
fields.each do |field, value|
return false unless actual.has_select? "#{prefix}#{field}", selected: value
end
true
end
end
matcher :have_hidden_field_with_value do |field, value|
match do |actual|
actual.find("##{field}", visible: false).value == value
end
end
matcher :have_hidden_fields_with_values do |prefix, fields|
match do |actual|
fields.each do |field, value|
return false unless actual.find("##{prefix}#{field}", visible: false).value == value
end
true
end
end
end
In general we can define all matchers in one support file, that groups them together. This way we can include them in RSpec configuration and use them in our tests. Below (after the examples) you also have a simple automatic inclusion in RSpec configuration. As for the example itself, this matcher was created due to often repetition of checking for the field existence and value. It is used in the following way:
it 'has the correct values' do
expect(page).to have_fields_with_values('search_', {
start_date: '2018-01-01', end_date: '2018-01-31'
})
end
Without the matcher, the code would look like this:
it 'has the correct values' do
expect(page).to have_field('search_start_date', with: '2018-01-01')
expect(page).to have_field('search_end_date', with: '2018-01-31')
end
So you have to repeat the prefix for each field of the given object/form (search) as many times as there are fields. This is not only more readable, but also more maintainable, as you can change the prefix in one place, and not in every test, so if the field changes, the change is also in one place.
If more attributes come, you do not need to add another line, but just another key-value pair to the hash. This was very useful in the project that has multiple big forms, less expects and simpler code structure thanks to simple matcher.
There are also additions in the form of hidden fields matcher, select matcher which is always a pain. Those two cases are often a big of a problem in capybara since it is easy to forget how to handle them, and there are a few ways to do it. Those matcher give us a standardized way to do it, and also a way to make it more readable.
We get a controllable and clear failure messages, that we adapt to the way we want to debug the spec.
Another example:
class HaveLabelsWithValuesMatcher
def initialize(instance, fields)
@instance = instance
@fields = Array(fields)
end
def matches?(page)
@fields.each do |field|
@label = @instance.class.human_attribute_name(field)
@value = @instance.send(field)
return false unless page.has_content? "#{@label}: #{@value}"
end
end
def failure_message
"Could not find label '#{@label}' with content '#{@value}'"
end
def failure_message_when_negated
"Expected to not find label '#{@label}' with content '#{@value}'"
end
end
def have_labels_with_values(instance, fields)
HaveLabelsWithValuesMatcher.new(instance, fields)
end
class HaveLabelsMatcher
def initialize(klass, fields)
@klass = klass
@fields = Array(fields)
end
def matches?(page)
@fields.each do |field|
@label = @klass.human_attribute_name(field)
return false unless page.has_content? @label
end
end
def failure_message
"Could not find label '#{@label}'"
end
end
class HaveNoLabelsMatcher
def initialize(klass, fields)
@klass = klass
@fields = Array(fields)
end
def matches?(page)
@fields.each do |field|
@label = @klass.human_attribute_name(field)
return false if page.has_content? @label
end
end
def failure_message
"Expected to not find label '#{@label}'"
end
end
def have_labels(klass, fields)
HaveLabelsMatcher.new(klass, fields)
end
def have_no_labels(klass, fields)
HaveNoLabelsMatcher.new(klass, fields)
end
This is more of a syntax sugar, for a test that was often repeated in the project. It is a simple way to check if the labels are present on the page, and if they have the correct values. It is used in the following way:
it 'has the correct labels' do
expect(page).to have_labels_with_values(@user, %i[name email])
end
Once again, instead of having an expect
statement for every label, we have one line that checks all of them. This is a simple example, but it can be very useful in a project with a lot of forms and a lot of fields.
Of course not always you need to check for labels in a spec, but when you do, it is a good way to do it.
Another example:
matcher :have_content_order do
match do |actual|
document = actual.try(:document)
if document.respond_to? :synchronize
document.synchronize { try_match! }
else
try_match!
end
true
rescue Capybara::ElementNotFound
false
end
def failure_message
"Expected to find #{expected_as_array.map(&:inspect).join(', ')} in order, but their indexes were #{@pos.join(', ')}"
end
private
def try_match!
@pos = expected_as_array.map { |text| actual.text.index(text) }
raise Capybara::ElementNotFound unless @pos.all? && (@pos == @pos.sort)
end
end
matcher :have_meta_tag do |property, content, match: :exact|
match do |actual|
if content.empty?
actual.has_css?("meta[property=\"#{property}\"]", visible: false)
elsif match == :contains
actual.has_css?("meta[property=\"#{property}\"][content*=\"#{content}\"]", visible: false)
else
actual.has_css?("meta[property=\"#{property}\"][content=\"#{content}\"]", visible: false)
end
end
end
The have_content_order
example is very specific to a project, but also a good example of creating a more understandable matcher. It is used in the following way:
it 'has the correct order' do
expect(page).to have_content_order(*company_info.map(&:name))
end
Checking order in which things are displayed is an often case in specs, for query of some index pages. There is no one standardized way of doing it, and it is usually a very "raw" test, that maps and orders in the expectation. This is often confusing and the setup also has its own problems and ordering, sorting. This matcher uses a more hidden DSL and less understandable code underneath, to give us a very clear matcher that "just works".
The other example is also a good example of hiding a very unreadable matcher that is often useful. Those deeply nested tags searches are a pain in capybara specs. Having a standardized way of testing it, also enforces a standardized way of writing the HTML itself, along with its tags. If you write the test first, and then adapt the code to meet its demands, your Capybara tests will not only be easier to write, but will also help you create a more concise code
And here is the config to automatically include those matchers in RSpec (you also need the support folder to be required in the rspec/rails helper):
RSpec.configure do |rspec|
rspec.include self, type: :feature
rspec.include self, type: :component
end
Alongside matchers, we can also create helpers that will make it easier to create concise, consistent, readable specs, instead of the usual bloated mess that capybara specs can be:
module Support
module CapybaraHelpers
#
# Check visibility of page elements
#
def visible?(id)
page.has_selector?(id, visible: true)
end
def invisible?(id)
page.has_selector?(id, visible: :hidden)
end
def check_visibility_becomes(visible: nil, invisible: nil)
visible = Array.wrap(visible)
invisible = Array.wrap(invisible)
visible.each do |item|
expect(invisible?(item)).to eq(true), "#{item} was not already invisible"
end
invisible.each do |item|
expect(visible?(item)).to eq(true), "#{item} was not already visible"
end
yield
visible.each do |item|
check_visibility(item, true)
end
invisible.each do |item|
check_visibility(item, false)
end
end
def check_visibility(selector, visibility)
if page.document.respond_to?(:synchronize)
page.document.synchronize do
check_visibility_raw(selector, visibility)
end
else
check_visibility_raw(selector, visibility)
end
end
def check_visibility_raw(selector, visibility)
find(selector, visible: false).synchronize do
return if visible?(selector) == visibility
raise Capybara::ElementNotFound, "#{selector} is not #{:in unless visibility}visible"
end
end
private :check_visibility_raw
def attach_file(locator, path, **options)
super locator, File.absolute_path(path), **options
end
def fill_in_form_text(prefix, attributes)
attributes.each do |name, value|
fill_in "#{prefix}_#{name}", with: value
end
end
def select_values(prefix, fields)
fields.each do |field, value|
select value, from: "#{prefix}#{field}", selectize: false
end
end
def selectize_has_option?(key, value)
selectize_div = page.find(:xpath, %(//input[@id="#{key}"]/../div[1]), visible: false)
begin
selectize_div.find(:xpath, %(.//div[@data-value="#{value}"]), visible: false)
true
rescue Capybara::ElementNotFound
false
end
end
def selectize_has_no_option?(key, value)
selectize_div = page.find(:xpath, %(//input[@id="#{key}"]/../div[1]), visible: false)
selectize_div.has_no_xpath?(%(.//div[@data-value="#{value}"]), visible: false)
end
def get_selectize(field_id)
# This might need additional code for select boxes
# field = page.find_field(key, visible: false)
page.evaluate_script(%{
$('##{field_id}').selectize()[0].selectize.getValue()
})
end
def has_image?(src)
has_xpath?("//img[contains(@src,\"#{src}\")]")
end
def has_link_with_text?(text, href:)
has_xpath? "//a[@href='#{href}'][contains(text(), #{text}']"
end
end
module HeadingSteps
extend RSpec::Matchers::DSL
def have_heading(text)
have_selector('h2', text: text)
end
def have_subheading(text)
have_selector('h3', text: text)
end
end
end
So those are basically wrappers for some capybara dsl steps that tests usually have to take to either create a context for the assertions, or a page state or scope in which assertions need to happen. In big feature specs, this can be cumbersome, bloated and hard to read and understand what is happening visually on the page. Specially if there are many ways of achieving that page state.
An example of this, are helpers like fill_in_form_text
or attach_file
. Many ways to achieve that, so we create one that is as concise and clear about its intentions as possible.
We can also create blocks that take in actions and do an test on their result:
check_visibility_becoms(invisible: '#loading', visible: '#content') do
choose "option"
end
We wrap an action in block, and then we check if the visibility of the elements changes as expected. This is a very common pattern in feature specs, and it is often repeated, so it is a good idea to create a helper for it.
Other wise, you have to get many lines of code, here you can pass array and be done in 3 lines of code if the option
element controls many other elements visibility (like changing options on forms that shows/hides many other field). Very useful in big views, specially given how common pattern is hiding/showing something on click, including doing it to many things at once.
There are also wrappers for select fields, which are a common issue in capybara. This adapts wrappers and helpers to work with the select types used in this particular project (it used selectize
). Dealing with those fields is often a problem in Capybara, since most projects use some sort of select plugin and testing it becomes unclear with the default option capybara gives you. Those helpers aim to use the complicated, ugly xpaths and method calls underneath a clean DSL.
select_values('search_', { start_date: '2018-01-01', end_date: '2018-01-31' })