Is there a way to "extend" Capybara so that custom finders can be used?

Viewed 246

I'm wondering if there is a way to create custom methods that allow me to "chain" calls against a Capybara object. This may be difficult to explain so here's an example that I'm trying to achieve:

find('#element-id').some_custom_method_here

find('#another-element-id').some_custom_method_here

all('.other-class-id')[3].some_custom_method_here

I'd like to be able to use custom class methods on a Capybara object, so that I might be able to find/manipulate/perform actions within a specific part of the DOM that might allow easy re-usability throughout a page.

The only way I've found myself being able to do this is by creating a function that passes the element first, and then moves on with my code. Like this:

def some_custom_method_here(capybara_obj, options={})
# do stuff with capybara_obj, find, click, etc
end
3 Answers

Another option to achieve what you want, would be to once "wrap" or "decorate" your capybara object.

decorated_node = SomeCustomWrapper.new(find('#element-id'))
decorated_node.some_custom_method

class SomeCustomWrapper < SimpleDelegator
  def some_custom_method
    # do something with self
  end
end

If you need to do this a lot, you could also write yourself a method for finding decorated nodes:

def find_decorated(selector, options={})
  SomeCustomWrapper.new(find(selector, options))
end

def all_decorated(selector, options={})
  all(selector, options).map { |node| SomeCustomWrapper.new(node) }
end

If you really want to add functions to the instances of capybaras objects, you could monkey patch the relevant classes, namley: https://www.rubydoc.info/github/jnicklas/capybara/Capybara/Node/Element

But I would recommend against monkey-patching libraries whenever you can avoid it. In the end it will safe you only a couple of lines of code / add a little bit of convenience when writing, but it makes it much harder for others (or you in the future) to understand your code.

Technically you could add methods to Capybara::Node::Element although you then risk breaking things if Capybara ever adds a method with the same name. A safer solution would be to create a wrapper class that proxies methods through to the capybara element, implements to_capybara_node to allow Capybara expectations to work with the wrapped elements, and adds it's own methods too, along the lines of

class ElementWrapper
  def initialize(element)
    @capybara_element = element
  end

  def find(...)
    ElementWrapper.new(@capybara_element.find(...))
  end

  ... # wrap the rest of Capybara Elements methods to return wrapper objects

  def custom_method(...)
    implement custom method and return wrapped element if necessary
  end

  def to_capybara_node
    @capybara_element # return the native capybara element
  end
end

Then you'd have

ElementWrapper.new(page.find(...)).custom_method(...).find(...) 

You could write your own find method in the namespace to remove the need for the ElementWrapper.new above.

You could try using Capybara Test Helpers to encapsulate your test code.

It will allow you to leverage the entire Capybara DSL, while creating your own helpers and aliases:

For example, with RSpec:

RSpec.describe 'Sample', test_helpers: [:example] do
  scenario 'sample scenario' do
    example.element.some_custom_method_here
    example.another_element.some_custom_method_here
    example.all(:other_element)[3].some_custom_method_here
  end
end

And the implementation could be something like:

# test_helpers/example_test_helper.rb
class ExampleTestHelper < Capybara::TestHelper
  aliases(
    element: '#element-id',
    another_element: '#another-element-id',
    other_element: '.other-class-id',
  )

  def some_custom_method_here
    click # or any other Capybara command
  end
end

Any Capybara command that returns an element will be automatically wrapped, which simplifies chaining your own methods or assertions.

Related