Monday, August 01, 2011

The Problem with PageObjects, why PageParts are needed

Over the past year or so, several big dogs in the automated web testing community have been pushing hard with the concept of PageObjects. This is a great thing. As such we now have at least two different page object frameworks to use with Watir-Webdriver thanks to the efforts of Alister Scott and Cheezy.

However, the page object frameworks have placed an item on the backburner. I'm not sure why it is, but the concept of repeating items inside of a page has been ignored at this point. Let's say you have a website like Google Finance. The list of news items is a great example of a what I'm calling a PagePart.

PageParts are items that repeat themselves on a page in terms of content types. They repeat with different content depending upon the user who is logged in, but they all look the same semantically. Here's what a Google Finance News item looks like:

<div class="cluster">
<div>
<a class="title" href="http://www.smh.com.au/world/us-not-out-of-the-woods-yet-despite-deal-to-avert-default-20110801-1i8bt.html?from=smh_sb" rel="nofollow" id="n-hp-">US not out of the woods yet, despite deal to avert default</a>
</div>
<div class="byline">
<span class="src">Sydney Morning Herald</span>
<span class="date" tm="1312209001"> - 17 minutes ago</span></div>
<!-- google_ad_section_end -->
<div class="snippet">A deal may have been reached but the US still faces some tough economic hurdles. Photo: AFP THE US could still lose its coveted AAA credit rating in coming months despite a last-minute deal struck yesterday between the White House and Republicans that ...</div>
<!-- google_ad_section_start -->
</div>
</div>


There are four of these items wrapped in a div with market-news-stream as it's id.

Without PageParts you might do this:

require 'watir-page-helper'
class GoogleFinance
include WatirPageHelper
div :news_items, :id => "market-news-stream"
def initialize(browser)
@browser = browser
end
def news_item(index)
news_items_div.divs[index]
end
def news_link(index)
news_link_link(index).click
end
def news_link_link(index)
news_item(index).a
end
def news_snippet_div_div(index)
news_item(index).div(:class, "snippet")
end
def news_snippet_div(index)
news_snippet_div_div(index).text
end
end


So how do we identify these items in the tests? When I first started tackling this sort of problem on my current project I had pass through methods inside my PageObject that went to a simple PagePart. This worked, but didn't give me the cool features of the PageObject frameworks, and wasn't that the whole point of adopting the framework?

require 'watir-page-helper'
class GoogleFinance
include WatirPageHelper
div :news_items, :id => "market-news-stream"
def initialize(browser)
@browser = browser
end
def news_item(index)
GoogleFinancePart.new(news_items_div.divs[index])
end
def news_link(index)
news_link_link(index).click
end
def news_link_link(index)
news_item(index).a
end
def news_snippet_div_div(index)
news_item(index).div(:class, "snippet")
end
def news_snippet_div(index)
news_snippet_div_div(index).text
end
end

class GoogleFinancePart
def initialize(div)
@news_item = div
end
def news_link
news_link_link.click
end
def news_link_link
@news_item.a
end
def news_snippet_div_div
@news_item.div(:class, "snippet")
end
def news_snippet_div
news_snippet_div_div.text
end
end


Wow, this is more complex than not using page parts.

So, how exactly is this better? It's better when you convert your page parts to use the PageObject framework. Caveat, I do not know how you would do this in Cheezy's PageObjects gem, but it works just fine in the watir-page-helper gem:

require 'watir-page-helper'
class BasePart
include WatirPageHelper
attr_reader :part_key
def initialize(browser, part_key)
@part_key = part_key
@browser = part_div(browser, part_key)
end
def part_div(browser, part_key)
raise 'this method should be overridden and return the HTML element that holds the page part'
end
end
view raw base_part.rb hosted with ❤ by GitHub


Every PagePart extends the BasePart. You'll select your container div in your PagePart object using the part_div method. Obviously this could be any HTML element, but it's most likely to be a div. Since the BasePart will redefine @browser with the div, we'll be able to use the cool shorthand, rather than define every method we might need:

class GoogleFinancePart < BasePart
link :news_link, :index => 0
div :news_snippet, :class => "snippet"
def initialize(browser, index)
super(browser, index)
end
def part_div(browser, index)
browser.div(:id, "market-news-stream").divs[index]
end
end


The example may seem a bit contrived, but, maybe you're not working with an interface as clean as Google Finance. If you are, you might get some benefit out of using PageParts.

ShareThis