Widgeon

Widgeon provides implementation of PageObject pattern for Capybara 2.0.3 to be compatible with Ruby 1.8.7. Later it will support latest versions of Capybara and Ruby and the compatibility with older versions will be left in the correspondent branch of the gem.

Widgeon is based on the tooth gem (https://github.com/kliuchnikau/tooth/) and extends its abilities in context of constructing PageObjects based on self-opened (optionally) widgets ('components' in tooth) and their collections. Self-opened widgets was inspired by Selenium's LoadableComponent pattern, but applied to complex html elements (or components/widgets) and implemented in the less tightly coupled way. There is also a common implementation of PageFactory for widgeon PageObjects. In addition you can find a common implementation of "waiting" for AJAX helpers, which are not needed in real life if you use Capybara, but was needed at least in building Widgeon itself.

Widgeon was created recently and it's still pretty raw. Its DSL may be enhanced in future.

Installation

Add this line to your application's Gemfile:

gem 'widgeon'

And then execute:

$ bundle

Or install it yourself as:

$ gem install widgeon

Usage

Basic example

require 'widgeon'
include Widgeon

class MainPage
  include PageObject                           

  def open
    visit '/main'
  end

  def init                                     
    e :some_element, '#some_element_locator'   
    ee :some_list_elements, '#some_list_of_elements'
    ww :articles, Article, '[id^="article"]'   
    e :open_side_panel, '#open_side_panel'
    w :side_panel, SidePanel, '#side_panel', :open => lambda {   
      open_side_panel.click                    
    }
  end

  # Assuming side panel exists only on main page its class defined inside the MainPage class
  class  SidePanel                             
    include Widget 

    def init
      w :sign_in_form, SignInForm, '#sign_in_form'
      e :other_element, '#other_element'
    end

    # Assuming SignInForm exists only inside side panel...
    class SignInForm                           
      include Widget

      def init
        e :mail, '#mail'
        e :password, '#password'
        e :signin, '#sign_in'
      end

      def  mail_and_password = {}
        fill_with mail_and_password
        .click
      end
    end
  end
end

# Assuming articles may exist on several pages, the class is defined globally
class Article                                
  include Widget                            

  def init
    e :heading, 'heading'
    e :text, 'article'
    e :mark_as_read, '#mark_as_read'
  end
end

# -- -- -- 

require 'widgeon/page_factory'
include Widgeon::PageFactory

visit_page MainPage do |main|
  main.side_panel.. :mail => '[email protected]', :password => 'supersecret'
end

Example Explained

Make your class a 'widgeon' Pageobject:

class MainPage
  include PageObject  

Define elements inside the init method:

  def init                                     
    element :some_element, '#some_element_locator'   
    #...
  end

Define element with '#some_element_locator' and accessible from the page object by some_element name:

    element :some_element, '#some_element_locator' 

Define a collection of elements:

    elements :some_list_elements, '#some_list_of_elements'

Define a widget object (complex element/component containing other elements):

      widget :sign_in_form, SignInForm, '#sign_in_form'

The additional second parameter (SignInForm) should be specified in order to tell the class where the widget is defined.

Define a collection of widgets:

    widgets :articles, Article, '[id^="article"]' 

Define a widget that should be opened automatically if it's not visible:

    widget :side_panel, SidePanel, '#side_panel', :open => lambda {   
      open_side_panel.click                    
    }

Use Aliases if needed:

    e :some_element, '#some_element_locator'           # i.e. element
    ee :some_list_elements, '#some_list_of_elements'   # i.e. elements
    ww :articles, Article, '[id^="article"]'           # i.e. widgets
    e :open_side_panel, '#open_side_panel'
    w :side_panel, SidePanel, '#side_panel', :open => lambda { #i.e. widget
      open_side_panel.click                    
    }

Use factories to create and use page objects:

visit_page MainPage do |main|
  main.side_panel.. :mail => '[email protected]', :password => 'supersecret'
end

AJAX context handling with PageObject#within or PageObject#ajaxed_at block

Assuming some scope/list of elements will appear on the page only after some 'ajax loading', you can make the Widgeon wait for these elements by putting them into the within block:


    def init

      ajaxed_at '#section_that_will_appear_after_some_loading_finished' do
        :ee :items, 'li#some_item'
      end

    end

ajaxed_at is just an alias to the PageObject#within in order to emphasize the goal of putting elements into the scope.

More examples

See /spec files for more examples of usage.

TODO list

  • move to ruby 2.0 (update docs and comments correspondingly)
  • refactor javascript code for the test dummy app: switch to "events delegation" instead of "putting callback directly on elements"
  • add spec steps for testing a list of items of different type
  • add examples for locators as lambdas
  • consider removing 'loading widgets via Widget#open' in one of next major versions by removing the "owner" field
  • resolve all 'todos'
  • refactor all comments to be of rdoc style, etc.
  • consider enhancing DSL for element definition with an ability to define blocks with additional action, like:


    e :login, '#login-btn' { |it| it.click}
    #or
    e :login, '#login-btn', :action => :click

    # - in order to write just:

    page.login

    # - instead of 

    page.login.click

    #or even like this:
    w :sign_in, SignInForm, '#sign-in', :do => { |it, mail_and_password| it.fill_with mail_and_password; it.submit }

    # - in order to:

    page.sign_in :mail => '...', :password => '...'

    # - instead of:

    page.sign_in.do_sign_in :mail => '...', :password => '...'
  • consider enhancing DSL to be able to define page elements and widgets outside of the PageObject#init method.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/widgeon/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request