So one could ask: “Why yet another PageObject library?”. After all, there are some very decent gems out there already. This is true. Each of the existing libraries offers certain unique features, but none of them has all. Well, WatirPump - the new kid on the block, has the ambition to make an exception. Let’s take a look at it and find out.

Introduction

It’s never easy describing code using a natural language. Especially if it’s a foreign one ;) For the in-depth explanation about what WatirPump can do and how it does it I highly recommend reading its README.

There is also a series of specs that demonstrate certain features of the library in a form of a tutorial.

For this article, let’s try to use as little English as possible. Lets let the code speak for itself.

Example spec

Let’s test a page with multiple ToDo lists. This example scenario checks if addition and removal of list items work properly.

Example page with multiple ToDo lists

Having properly modeled the ToDoListsPage class the spec will look like this:

RSpec.describe ToDoListsPage do
  it 'Adds and removes items' do
    ToDoListsPage.open do
      todo_lists['Groceries'].add('Pineapple')
      expect(todo_lists['Groceries']).to include 'Pineapple'

      todo_lists['Work'].add('Read RubyWeekly')
      expect(todo_lists['Work']).to include 'Read RubyWeekly'

      todo_lists['Groceries']['Bread'].remove
      expect(todo_lists['Groceries']).not_to include 'Bread'
    end
  end
end

Pretty neat, right?

Let’s dig into the internals and learn how to build such a nice page API with WatirPump.

ToDoList

As we look at the page it is clear that the first candidate to be extracted into a reusable component is the ToDoList. It contains several elements:

  • a title
  • a text field for a name of a new item
  • a button that adds the new item of given name
  • a list of the existing list items

Let’s try to represent this component in a code:

class ToDoList < WatirPump::Component
  # Let's use some WatirPump class macros (explained below)
  div_reader :title, role: 'title'
  text_field_writer :item_name, role: 'new_item'
  button_clicker :submit, role: 'add'
  components :items, ToDoListItem, :lis
  query :values, -> { items.map(&:label) }

  # and some extra methods to make the spec look more natural
  def [](label)
    items.find { |i| i.label == label }
  end

  def add(item_name)
    fill_form!(item_name: item_name)
  end

  def include?(item)
    !self[item].nil?
  end
end

Looks intuitive, however, there are some concepts that might require a few words of explanation.

Concept 0: class macros (some would call it a DSL)

Classes that inherit from WatirPump::Page or WatirPump::Component gain access to a set of powerful class macros that (behind the scenes) generate methods which interact with the webpage. For example, each DOM element can be declared with its own class macro. Just like in plain watir code.

p :summary, role: 'summary'

# is a shorthand for:
def summary
  root.p(role: 'summary')
end

See WatirPump docs to learn more about how one can declare page elements (don’t forget to check out how lambdas could help here). While you read the docs please also grep for root vs browser to learn the difference.

Concept 1: element action macros: reader, writer, clicker

It happens quite often, that there is no point in declaring a “plain” HTML element in the page class. After all an abstract DOM node is rather useless from the perspective of a functional test API. What matters, however, is what the user could do with it. And the user usually needs to perform a certain action:

  • read the value of the element
  • write a new value to the element
  • click the element

This is where element action class macros come in: they generate methods that perform actions on the element with given locator.

div_reader :title, role: 'title'

# is a shorthand for:
def title
  root.div(role: 'title').text
end

text_field_writer :item_name, role: 'new_item'

# is a shorthand for:
def item_name=(val) # mind the '=' !
  root.text_field(role: 'new_item').set(val)
end

button_clicker :submit, role: 'add'

# is a shorthand for
def submit
  root.button(role: 'add').click
end

See the documentation about element action macros for more details.

Concept 2: components class macro

Declares a collection of components of given class that are located within the current component using the given locator.

The line below declares a ToDoListItem component instance at every li element

components :items, ToDoListItem, :lis

Concept 3: fill_form!

Instance method fill_form is a fast way to invoke all writers declared for the given component. In our case there is only one: item_name=fill_form accepts a hash of writer method names and values for them. If our component contains a submit method fill_form! (notice the !) will try to invoke it after all writers are executed.

So to sum it up: fill_form!(item_name: 'Pineapple') will do self.item_name='Pineapple' ; submit

The more writers the component has, the more work can fill_form do for us.

For more information about form helpers please see the docs.

The extra methods: add, [], include?

These are rather self-explanatory. They are here to let us write more expressive code. Like this:

todo_list.add('Pineapple')
todo_list.include? 'Pineapple'
todo_list['Pineapple'].remove

Concept 4: query macro

There is one more concept that has been employed in this example. It’s a query class macro and more information about it can be found here.

ToDoListItem

An item consists of two elements:

  • a label
  • a link to remove the item

When represented as a code they would look like this:

class ToDoListItem < WatirPump::Component
  link_clicker :remove, role: 'rm'
  span_reader :label, role: 'name'
end

Both of the macros used here have been already discussed.

ToDoListsPage

The highest object in the model hierarchy is the ToDoListsPage. Its declaration doesn’t differ much from the declaration of a component. The main change is the presence of the required uri macro invocation.

Another new contstruct (macro) presented in the listing below is decorate. It is used to, well, decorate given method with some additional behavior. Here, the value returned by method todo_lists will be wrapped by an instance of a CollectionIndexedByTitle class. It is required to replace the default behavior of [] operator: from integer based (like in Array) to  string based (like in Hash). This will provide us with access to certain ToDoLists by their title.

class ToDoListPage < WatirPump::Page
  uri '/todo_lists.html'
  components :todo_lists, ToDoList, -> { root.divs(role: 'todo_list') }
  decorate :todo_lists, CollectionIndexedByTitle
end

CollectionIndexedByTitle

Decorator class for the collection of ToDoLists. As described above, all it does is overriding the default behavior of [] method.

class CollectionIndexedByTitle < WatirPump::ComponentCollection
  def [](title)
    find { |l| l.title == title }
  end
end

Execution with rspec

Having the page and component classes in place let’s take another look at API exposed to rspec:

RSpec.describe ToDoListsPage do
  it 'Adds and removes items' do
    # open method navigates to the page
    # then executes the provided block in the scope of page class singleton instance:
    # e.g. todo_lists['A'] == ToDoListPage.instance.todo_lists['A']
    ToDoListsPage.open do
      # todo_lists collection with decoration supports indexing with title
      # add method is invoked on a ToDoList with title 'Groceries'
      todo_lists['Groceries'].add('Pineapple')

      # 'include?' method defined in ToDoList class
      # is used by the rspec matcher 'include'
      expect(todo_lists['Groceries']).to include 'Pineapple'

      todo_lists['Work'].add('Read RubyWeekly')
      expect(todo_lists['Work']).to include 'Read RubyWeekly'

      # ToDoList supports '[]' method that is used below
      # to access ToDoListItem by its label
      todo_lists['Groceries']['Bread'].remove
      expect(todo_lists['Groceries']).not_to include 'Bread'
    end
  end
end

Summary

This article showed only a few of the features that WatirPump offers to build maintainable, reusable and elegant Page Object models. For a complete guide please refer to projects README and a tutorial.

In case of ideas for improvement, found bugs or any other questions don’t hesitate to: