Skip to content

Extending PageObjects

December 1, 2012

When testing web applications in Ruby with Cucumber, I typically use the PageObject gem to simplify the creation and maintenance of my Page Objects.  While these are useful tools for working with simple web pages, I often find it necessary to build test widgets to interact with web application widgets or GXT controls.

In order to simplify the use of test widgets, and maintain a consistent pattern in my Page Object classes, I create widgets which extend the capabilities of the PageObject gem.  In order to avoid forking PageObject, and ultimately getting out of sync with updates, I write the extension in several modules, and use Ruby’s ‘send’ method to include the widgets in the appropriate modules of PageObject.

The example below shows a simple extension of PageObject for working with a table in a GXT application.  The tables created by GXT were not typical html tables, rather, each row is itself a table element containing a single row, the tables are organized within divs, and the topmost element of the control is itself a div.  This structure can make GXT applications difficult to test, since the methods for interacting with standard elements do not apply.

The code below extends the PageObject::Elements::Table class to override the child_xpath method which PageObject uses to collect rows into an array.  The Table class gets the immediate child rows, the GxtTable extension gets all descendant rows.  This change aggregates all the rows from each table element in the GXT Table into a single object and allows us to interact with it using the methods on the Table class.  By creating classes such as this, I am able to create page object classes which treat controls as isolated objects regardless of how complex the html used to create them actually is.

Although I’ve been happy with this solution as a template, I believe that I can simplify the creation of Element classes by dynamically creating the methods which need to be included.  To that end, in the coming weeks, I’ll be working on modifying this solution so that Widget Elements can be added to PageObject by defining and registering each class.

The following line are placed in the env.rb file to add the extension to PageObject on load.


PageObject.send(:include,Widgets)

The code below is the modules and the class created so that the GxtTable class behaves as any other PageObject Element.

require 'watir-webdriver/extensions/alerts'
require 'page-object/elements'
require 'page-object/platforms/selenium_webdriver/page_object'
require 'page-object/platforms/watir_webdriver/page_object'

module Widgets
  def self.included(base)
        base::Elements.send(:include, WidgetElements)
        base.send(:include, WidgetElementLocators)
        base::Platforms::SeleniumWebDriver::PageObject.send(:include, WidgetSeleniumPageObject)
        base::Platforms::WatirWebDriver::PageObject.send(:include, WidgetWatirPageObject)
        base::Accessors.send(:include, WidgetAccessors)
        base::Elements::Element.send(:include, WidgetNestedElements)
  end
end

module WidgetElements
  class GxtTable < PageObject::Elements::Table     protected     def child_xpath       ".//descendant::tr"     end   end   ::PageObject::Elements.tag_to_class[:gxt_table] = ::Widgets::WidgetElements::GxtTable end module WidgetAccessors   #   # adds two methods - one to retrieve the table element, and another to   # check the table's existence.  The existence method does not work   # on Selenium so it should not be called.   #   # @example   #   gxt_table(:cart, :id => 'shopping_cart')
  #   # will generate a 'cart_element' and 'cart?' method
  #
  # @param [Symbol] the name used for the generated methods
  # @param [Hash] identifier how we find a table.  You can use a multiple paramaters
  #   by combining of any of the following except xpath.  The valid keys are:
  #   * :class => Watir and Selenium
  #   * :id => Watir and Selenium
  #   * :index => Watir and Selenium
  #   * :name => Watir and Selenium
  #   * :xpath => Watir and Selenium
  # @param optional block to be invoked when element method is called
  #
  def gxt_table(name, identifier={:index => 0}, &block)
    define_method("#{name}_element") do
      return call_block(&block) if block_given?
      platform.gxt_table_for(identifier.clone)
    end
    define_method("#{name}?") do
      return call_block(&block).exists? if block_given?
      platform.gxt_table_for(identifier.clone).exists?
    end
    alias_method "#{name}_table".to_sym, "#{name}_element".to_sym
  end
end

module WidgetElementLocators
  #
  # Finds a table
  #
  # @param [Hash] identifier how we find a table.  You can use a multiple paramaters
  #   by combining of any of the following except xpath.  It defaults to {:index => 0}
  #   which will return the first table.  The valid keys are:
  #   * :class => Watir and Selenium
  #   * :id => Watir and Selenium
  #   * :index => Watir and Selenium
  #   * :name => Watir and Selenium
  #   * :xpath => Watir and Selenium
  #
  def gxt_table_element(identifier={:index => 0})
    platform.gxt_table_for(identifier.clone)
  end

  #
  # Finds all tables that match the provided identifier
  #
  # @param [Hash] identifier how we find a table.  You can use a multiple paramaters
  #   by combining of any of the following except xpath.  It defaults to an empty Hash
  #   which will return all tables.  The valid keys are:
  #   * :class => Watir and Selenium
  #   * :id => Watir and Selenium
  #   * :index => Watir and Selenium
  #   * :name => Watir and Selenium
  #   * :xpath => Watir and Selenium
  #
  def gxt_table_elements(identifier={})
    platform.gxt_tables_for(identifier.clone)
  end
end

module WidgetNestedElements
  def gxt_table_element(identifier={:index => 0})
    @platform.gxt_table_for(identifier)
  end

  def gxt_table_elements(identifier={:index => 0})
    @platform.gxt_tables_for(identifier)
  end
end

module WidgetSeleniumPageObject
  #
  # platform method to retrieve a table element
  # See WidgetAccessors#table
  #
  def gxt_table_for(identifier)
    find_selenium_element(identifier, PageObject::Elements::GxtTable, 'div')
  end

  #
  # platform method to retrieve all table elements
  #
  def gxt_tables_for(identifier)
    find_selenium_elements(identifier, PageObject::Elements::GxtTable, 'div')
  end
end

module WidgetWatirPageObject
  #
  # platform method to retrieve a table element
  # See WidgetAccessors#table
  #
  def gxt_table_for(identifier)
    find_watir_element("div(identifier)", PageObject::Elements::GxtTable, identifier, 'div')
  end

  #
  # platform method to retrieve an array of table elements
  #
  def gxt_tables_for(identifier)
    find_watir_elements("div(identifier)", PageObject::Elements::GxtTable, identifier, 'div')
  end
end
Leave a Comment

Leave a comment