Skip to content
On this page

Composition and Injection 🧩

As described in Using Test Helpers, it's possible to easily combine test helpers.

In this section, we dive into detail on different use cases and ways to solve them.

Understanding Injection

Let's say that we want to leverage some logic from our TableTestHelper.

class TableTestHelper < BaseTestHelper
  def row_for(*cells)
    find(:table_row, cells)
  end

  def have_row(*cells)
    have(:table_row, cells)
  end
end

We will inject this helper with use_test_helpers.

class UsersTestHelper < BaseTestHelper
  use_test_helpers(:table)

  aliases(
    el: '.users'
  )

  def find_user(name)
    within { table.row_for(name) }
  end

  def click_to_edit
    click_link('Edit')
  end

  def have_user(name)
    within { table.have_row(name) }
    self
  end
end

Every time we call table, an instance of TableTestHelper is returned.

TIP

Injected test helpers will preserve the assertion state of the current helper.

We can use any assertion without having to explicitly call should in the table helper.

users.should.have_user('Jim').should_not.have_user('John')

Wrapping 🎁

Any injected helper can optionally take an element as a parameter, which will be wrapped and used as the initial context for the returned helper.

  def find_user(name)
    table(self).row_for(name) # same as: table.wrap_element(el).row_for(name)
  end

Just as before using within, it will only find rows inside the .user element.

users.find_user('Jim')
# same as
find('.users').find(:table_row, ['Jim'])

Wrapping to Chain

In the examples above, find_user returns an instance of TableTestHelper:

users.find_user('Jim').click_to_edit
# NoMethodError: undefined method `click_to_edit' for #<TableTestHelper tag="tr">

We can workaround this by wrapping the result:

  def find_user(name)
    wrap_element table(self).row_for(name)
  end
users.find_user('Jim').click_to_edit
# #<UsersTestHelper tag="a">

Inheritance

Another way to share functionality between test helpers is to use inheritance.

Just like methods, any aliases defined in ancestors will also be available in the subclass.

Capybara.get_test_helper_class(:table) # or require_relative './table_test_helper'

class UsersTestHelper < TableTestHelper
  aliases(
    el: '.users'
  )

  def find_user(name)
    el.row_for(name)
  end

  def click_to_edit
    click_link('Edit')
  end
end

Because we are using inheritance, we can skip all the wrapping and context switching from the previous examples.

users.find_user('Jim').click_to_edit
# #<UsersTestHelper tag="a">

Simple is better though, and composition should be preferred over inheritance in most cases.

Using the Current Element

You can leverage the current element as needed when creating your own actions or assertions by calling to_capybara_node:

def focus
  to_capybara_node.execute_script('this.focus()')
  self
end

Have in mind that in most cases this is unnecessary, as many actions such as click already use the current element.