Skip to content
Cohen Carlisle edited this page May 27, 2016 · 2 revisions

At the heart of it, page sections make it so that you can narrow the focus of a page object to a specific element on the page and its children. This makes identifying important elements easier because we will never accidentally pick up elements outside the scope we are looking in.

The largest value provided is when working with a page with many behaviors and functionalities. To reason about this, I'm going to use the example of amazon.com.

There are two main ways sections add value. Breaking down large conceptual areas, and representing collections of related elements.

Breaking Down Large Conceptual Areas

On any given page of search results on amazon, you have a few different areas of concern:

  1. Search results
  2. Filters on the results
  3. Menus and Navigation
  4. Search and sort bars

This is more complex than some websites, but still less complex than some websites I've worked on.

Now we could put all these concerns into one page object but the number of methods grows a lot. We can break it up into modules based on these areas, but at the end of the day, including all these modules in one place means the instantiated object has the same number of methods, and we have just as much risk, if not more, of having collisions between method names. We could make separate page objects for each section, but it sounds hacky with the DSL and can lead to some ugly code. For example if I have these page objects for the areas of the page:

class SearchBar
  include PageObject

  text_field(:bar)  # if it's the only text field in the section we don't need to identify it
  button(:go) # if we are not using page sections, the search is within the whole page

  def for search_term
    bar = search_term
    go
  end
end
      
class Filters
  include PageObject
  link(:prime_eligible, id: 'whatever_id')
end

Then I can currently use them together using a page object like this:

class SearchPage
  include PageObject

  # whatever elements needed for reasoning about search results
end

on SearchBar do |page|
  page.search_for 'ruby books'
end
on Filters do |page|
  page.check_prime_eligible
end
on SearchResults do |page|
  # verify results are all prime eligible
end

Or by treating each area as a section, I can put it together on one page but with encapsulation like the separate pages:

class SearchPage
  include PageObject

  page_section(:search, SearchBar, id: 'search_bar_section')
  page_section(:filters, Filters, id: 'filters_section')

  # whatever elements needed for reasoning about search results
end

on SearchPage do |page|
  page.search.for 'ruby books'
  page.filters.check_prime_eligible
  # verify results are all prime eligible
end

Yes, the code for setup is longer, but the code of use is shorter and cleaner. If I could choose to have to do more thinking in my implementation of hitting the page, or in my test that is hitting the page to test something, I would choose in the implementation every time.

Representing Collections of Related Elements

Having a collection of page sections makes dealing with multiple collections of related elements far easier. To demonstrate, I'm going to talk about amazon again. If we are trying to test the search results, we need some way to go through all the results and reason about them. This is a big pain point right now. If I use indexed properties:

class SearchPage
  include PageObject

  indexed_property(:results, [
    [:link, :name, id: 'search_result_name_[%s]'],
    [:image, :prime_icon, id: 'search_result_prime_[%s]']
  ])
end

I can reason about any given result by doing page.results[0].name and page.results[0].prime_icon_element.visible? but I have no way of testing whether all the results are prime eligible without using some element collection to identify how many there are in the first place and building a loop out of the size.

Probably a cleaner approach is using collections of elements:

class SearchPage
  include PageObject
  
  links(:name, id: /^search_result_name_[\d+]$/)
  images(:prime_icon, id: /^search_result_prime_[\d+]$/)
end

Using collections like this, I can find the index of a result with a specific name, and use that index for other collections, but depending on how the prime eligibility images are set up, I might not be able to link it up to results 1 to 1. I can reason about the whole collection of any 1 piece, but if any element happens on some results, but not others, I lose a lot of information.

If I want to use sections:

class SearchPage
  include PageObject

  page_sections(:results, SearchResult, class: 'search_result')
end

class SearchResult
  include PageObject

  link(:name, id: /^search_result_name_[\d+]$/)
  image(:prime_icon, id: /^search_result_prime_[\d+]$/)

  def prime_eligible?
    prime_icon_element.visible?
  end
end

I can reason about the set of results and ask about each one. Further, I can define specific methods to reason about a section further:

on SearchPage do |page|
  page.results.each do |result|
    expect(result.prime_eligible?).to be true
  end
  # or shorter still expect(page.results).to all(be_prime_eligible)
end

Because the collections inherit from Array, we have the full power of Array methods to help us process and reason about the data. This becomes even more important when we have collections within collections. Because each section scopes at a specific element, we can never accidentally identify the elements from another section. For example, if search results also had inline reviews listed, I could ask for page.results[0].reviews, and know I'm only getting reviews from inside the first result.