-
Notifications
You must be signed in to change notification settings - Fork 220
Page Sections
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.
On any given page of search results on amazon, you have a few different areas of concern:
- Search results
- Filters on the results
- Menus and Navigation
- 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.
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.