Page Object library. Inspired by Ruby's WatirPump
0.2.1 released


This shard is a Page Object Model lib, built on top of selenium-webdriver-crystal. It's a crystal port of ruby's watir_pump.


  1. Add the dependency to your shard.yml:
    github: bwilczek/webdriver_pump
  1. Run shards install

Basic usage

require "webdriver_pump"

# define the page model, for example:
class GreeterPage < WebdriverPump::Page
  url "https://bwilczek.github.io/watir_pump_tutorial/greeter.html"
  element :header, { action: :text, locator: -> { root.find_element(:xpath, "//h1") } }
  element :fill_name, { action: :send_keys, locator: {id: "name"} }
  element :submit, { action: :click, locator: {id: "set_name"} }
  element :greeting, { action: :text, locator: {id: "greeting"} }

# use it in specs, for example:
# where `session` is an instance of Selenium::Session
describe "Page without components" do
  it "operates on Selenium::WebElements" do
    GreeterPage.new(session).open do |p|
      p.header.should eq "Greeter app"
      p.fill_name "Crystal"
      p.greeting.should eq "Hello Crystal!"


Please refer to this chapter to learn how to define your Page Objects, and use it from your tests.


WebdriverPump provides a DSL (implemented as macros) to describe the Page Object Model. It's a very close port of Ruby's WatirPump gem. There are some subtle differences in the implementation, but the core concepts remain the same:

  • Nestable, reusable components, to build elegant APIs
  • Element actions, to automatically generate simple, one liner methods (like wrappers for click)
  • Page scoping, so that it's immediately known what page is currently being tested
  • Form helpers, to operate HTML form elements with ease (WebDriver doesn't deliver here)
  • Decorated collections, to access element/component collections with descriptive keys


Describes the page under test. Inherits from Component, so please familiarize yourself with that class as well to fully understand what Page is capable of. This section covers only the differences from the base Component class.

url macro

Declares the full URL to the page under test.

class GitHubUserPage < WebdriverPump::Page
  url "https://github.com/bwilczek"

describe "Page's URL" do
  it "navigates to given URL" do
    GitHubUserPage.new(session).open do |p|
      session.url.should eq "https://github.com/bwilczek"

URL can be parameterized:

class GitHubUserPage < WebdriverPump::Page
  url "https://github.com/{user}"

describe "Page's URL" do
  it "navigates to given parameterized URL" do
    GitHubUserPage.new(session).open(params: {user: "bwilczek"}, query: {repo: "webdriver_pump""}) do |p|
      session.url.should eq "https://github.com/bwilczek?repo=webdriver_pump"


Navigates to Page's URL and executes given block in the scope of the page.

class GitHubUserPage < WebdriverPump::Page
  url "https://github.com/bwilczek"
  element :user_nickname, { locator: { xpath:, "//span[@itemprop='additionalName']") } }

describe "Page's URL" do
  it "navigates to given URL" do
    GitHubUserPage.new(session).open do |page|
      page.class.should eq GitHubUserPage
      page.user_nickname.class.should eq Selenium::WebElement
      page.user_nickname.text.should eq "bwilczek"


Similar to #open, but does not perform the navigation - assumes that the page is already open. Useful when the navigation is triggered by an action on a different page.

GitHubUserPage.new(session).open do { |p| p.navigate_to_repo("webdriver_pump") }

GitHubRepoPage.new(session).use do |p|
  p.class.should eq GitHubRepoPage


Predicate method denoting if page is ready to be interacted with.

In most cases creation of this method will not be required, since WebDriver itself checks if page's resources have been loaded. Only in case of more complex pages, that heavily rely on parts loaded dynamically over XHR providing of custom loaded? criteria might be necessary.

class GitHubUserPage < WebdriverPump::Page
  url "https://js-heavy.com"
  element :created_by_xhr, { locator: {id: "content"} }

  def loaded?


Components are the foundation of WebdriverPump models. They abstract out certain sub-trees of the page's DOM tree into crystal classes and hide the underlying HTML behind the business oriented API.

Pages are the top-level components, that abstract out the complete page (DOM sub-tree starting at //body).

Components can be nested, and grouped into collections.

They are declared inside their parent components using element macro, with a class parameter, that refers to crystal class, a child of WebdriverPump::Component (NOT a CSS class).

#initialize (constructor)

Usually invoked implicitly by the element(s) macro.

Accepts two parameters:

  • @session : Selenium::Session
  • @root : Selenium::WebElement

Example of explicit usage:

class OrderItemDetails < WebdriverPump::Component
  # omitted for brevity

class OrderPage < WebdriverPump::Page
  # omitted for brevity

  def [](name)
    node = root.find_element(:xpath, ".//div[@class='item' and contains(text(), '#{name}')]")
    OrderItemDetails.new(session, node)

OrderPage.new(session).open do |order|
  order["Rubber hammer, 2kg"].class.should eq OrderItemDetails


Reference to associated Selenium::Session instance.


Mounting point of current component in the DOM tree. Type: Selenium::WebElement.

For Pages it points to //body.


Reference to WebdriverPump::Wait module. Usage:

# with default settings
wait.until { condition_is_met }

# with custom settings
wait.until(timeout: 19, interval: 0.3) { other_condition_is_met }

# global config (optional)
WebdriverPump::Wait.timeout = 10    # default = 15
WebdriverPump::Wait.interval = 0.5  # default = 0.2

element macro

A DSL macro to declare WebElements located inside given component.

class MyPage < WebdriverPump::Page
  url "http://example.org"

  # synopsis:
  # element :name : Symbol, params : NamedTuple

  # examples
  # locate and return Selenium::WebElement
  element :title1, { locator: {xpath: ".//div[@role='title']"} }
  # equivalent of:
  def title1
    root.find_element(:xpath, ".//div[@role='title']")

  # locate Selenium::WebElement and perform action (invoke method) on it at once
  element :title2, { locator: {xpath: ".//div[@role='title']"}, action: :text }
  # equivalent of:
  def title2
    root.find_element(:xpath, ".//div[@role='title']").text

  # locate Selenium::WebElement and use it as a mounting point for another component
  element :title3, { locator: {xpath: ".//div[@class='user_details']"}, class: UserDetails }
  # equivalent of:
  def title3
    node = root.find_element(:xpath, ".//div[@role='title']")
    UserDetails.new(session, node)

Required parameter. Locator of the WebElement in the DOM tree. Allowed formats are:

  • 1 element NamedTuple with key in (:id, :name, :tag_name, :class_name, :css, :link_text, :partial_link_text, :xpath), and a respective value.
  • a Proc returning WebElement, e.g. -> { root.find_element(:id, "user") }, -> { some_wrapper_element.find_element(:id, "user") }

Symbol, name of WebElements method to be executed.


Component class. If provided the WebElement located using locator will be the mounting point for the component of given class.

elements macro

A DSL macro to declare a collection of WebElements inside given component.

class CollectionIndexedByName(T) < WebdriverPump::ComponentCollection(T)
  def [](name)
    ret = find { |el| el.name == name }
    raise "Component with name='#{name}' not found" unless ret

class OrderItem < WebdriverPump::Component
  element :name, { action: :text, locator: {css: ".name"} }

class OrderPage < WebdriverPump::Page
  elements :raw_order_items, { locator: {xpath: ".//li"} }

  elements :order_items, {
    locator: {xpath: ".//li"},
    class: OrderItem,
    collection_class: CollectionIndexedByName(OrderItem)

OrderPage.new(session).open do |page|
  page.raw_order_items.class.should eq Array(Selenium::WebElement)
  page.raw_order_items[0].class.should eq Selenium::WebElement

  page.order_items.class.should eq CollectionIndexedByName(OrderItem)
  page.order_items["Rubber hammer, 2kg"].should eq OrderItem

Required parameter. Same rules as for element macro, but returns Array(WebElement).


Optional Component class to wrap each of the collection's elements.


Optional ComponentCollection class to wrap the whole collection. Useful to introduce more descriptive ways of accessing elements.

Form helper macros

WebDriver API itself does not provide methods to easily set and get values of HTML form elements. This is where WebdriverPump's form helper macros come handy.

element_getter and element_setter

These macros generate methods that set and get values for given form elements. Supported types are:

  • :text_field - expected value type: String
  • :text_area - expected value type: String
  • :radio_group - expected value type: Array(String)
  • :checkbox - expected value type: Bool
  • :checkbox_group - expected value type: Array(String)
  • :select_list - expected value type: String
  • :multi_select_list - expected value type: Array(String)

locator parameter accepts the same values as for previously described macros.


class ProfilePage < WebdriverPump::Page
  element_setter :hobbies, { type: :checkbox_group, locator: {name: "hobbies[]"} }
  element_getter :hobbies, { type: :checkbox_group, locator: {name: "hobbies[]"} }

ProfilePage.new(session).open do |page|
  page.hobbies = ["Gardening", "Knitting"]
  page.hobbies.should eq ["Gardening", "Knitting"]

This macro acts as a wrapper for calling multiple element_setters at once.

Let's consider the following example:

class LoginPage < WebdriverPump::Page
  element_setter :username, { type: :text_field, locator: {name: "username"} }
  element_setter :password, { type: :text_field, locator: {name: "password"} }
  element :submit_form, { locator: {id: "submit"} }

  fill_form :login, { submit: :submit_form, fields: [:username, :password] }
  # equivalent of:
  def login(params)
    self.username = params[:username]
    self.password = params[:password]

# Usage:
LoginPage.new(session).open do |page|
  page.login(username: "bob", password: "secret")

fill_form macro expects the following parameters:

  • Symbol name of the method to be generated
  • NamedTuple with the following parameters
    • fields - (required) Array(Symbol) - list of setters to be invoked
    • submit - (optional) Symbol - name of the method to be executed after all setters

This macro acts as a wrapper for calling multiple element_getters at once. It returns a NamedTuple with keys being the getter method names, and values the results that they return.

Let's consider the following example:

class SummaryPage < WebdriverPump::Page
  element_getter :title, { type: :text_field, locator: {name: "title"} }

  # form_data doesn't require `element_getters` - it will work with all methods that don't require arguments
  element :header { locator: {xpath: "../h1"}, action: :text }

  form_data :summary, { fields: [:title, :header] }
  # equivalent of:
  def summary
      title: self.title,
      header: self.header

# Usage:
SummaryPage.new(session).open do |page|
  summary = page.summary
  summary[:title].should eq page.title
  summary[:header].should eq page.header

form_data macro expects the following parameters:

  • Symbol name of the method to be generated
  • NamedTuple with the following parameters
    • fields - (required) Array(Symbol) - list of methods to be invoked and their results returned

Development roadmap

  • [x] Page without Components
  • [x] Declare raw WebDriver elements with webdriver locators
  • [x] Declare raw WebDriver elements with lambdas
  • [x] Declare actions on WebDriver elements
  • [x] Declare reusable Components
  • [x] Collections of elements
  • [x] Collections of Components
  • [x] Nest Components
  • [x] Wait for AJAX-driven Components to be ready to interact
  • [x] ComponentCollection class
  • [x] Fill in complex forms: RadioGroups, SelectLists
  • [x] Parametrize Page url
  • [x] Support loaded? predicate for Pages
  • [x] ~Add support for base url~ do it on webdriver shard level
  • [x] Port WatirPump's form helpers
  • [ ] Form helper for file upload (?)
  • [x] Introduce Exception classes
  • [x] Update README
  • [ ] Update code documentation


  1. Fork it (https://github.com/bwilczek/webdriver_pump/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Test your changes, add relevant specs
  4. Commit your changes (git commit -am 'Add some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create a new Pull Request


  github: bwilczek/webdriver_pump
  version: ~> 0.2.1
License MIT
Crystal 0.27.0


Dependencies 1

  • selenium af4f608612f9810267fb0253b5dc49793222be90
    {'commit' => 'af4f608612f9810267fb0253b5dc49793222be90', 'github' => 'ysbaddaden/selenium-webdriver-crystal'}

Development Dependencies 0

Dependents 0

Last synced .
search fire star recently