I'm using SitePrism to test my web application. I have a number of classes that extend SitePrism::Page
and a number of often-used HTML snippets are represented by matching classes extending SitePrism::Section
class Login < SitePrism::Section
element :username, "#username"
element :password, "#password"
element :sign_in, "button"
end
class Home < SitePrism::Page
section :login, Login, "div.login"
end
The problem is, the application I'm working on is based on a CMS, in which a page can be assembled by selecting a Template based on pre-defined content and then drag-and-dropping any number of available components onto the page.
The initial developers created a Page Object to mirror every available Template. This was fine as long as the number of tests was low and there weren't too many variants of pages that we had to test in our feature files.
With the addition of multiple test cases, the page objects started growing at an alarming rate.
While we can easily mitigate code duplication by defining Sections for every component available in the CMS and reusing them across Page Objects, there's just a lot of properties that rarely get used.
class BlogPost < SitePrism::Page
section :logo, MySite::Components::Logo, '.logo'
section :navigation, MySite::Components::Navigation, '.primary-navigation'
section :header, MySite::Components::BlogHeader, '.header'
section :introduction, MySite::Components::Text, '.text .intro'
# and so on, a lot of dynamic staff that could potentially be dropped onto the page
# but does not neccessarily be there, going in dozens of lines
end
Is there a way in SitePrism to dynamically add a section to an instance of a Page Object as opposed to a whole class?
Then(/^Some step$/) do
@blog = PageObjects::BlogPost.new()
@blog.load("some url")
@blog.somehow_add_a_section_here_dynamically
expect (@blog.some_added_section).to be_visible
end
It also worries me that doing something like this would potentially cause CSS selectors to leak into the step definitions, which is generally a bad practice.
Another way to work around this would be to build Page Objects for specific examples of pages as opposed to the versatile templates. The Template Page Objects could just contain whatever's baked into the templates and be extended by other Page Objects that mirror specific pages, taking care of the differences. It sounds like a much cleaner approach so I'm probably going to write my tests this way
Anyway, the technical part of the question stands. Regardless of how good or bad an idea it is, how could I dynamically extend a page object with an additional section? I'm just curious.
I had at one point wanted to do what you're talking about for pretty much the same reason. We had pages that could have new content-sections dragged into them; making them very dynamic. I experimented with ways to do this and never found any that I particularly liked.
Methods like element
and sections
in site-prism each define a number of methods for the class. You could call MyPage.section
in your test or add a method that calls self.class.section
and use that to add on new sections. But those will exist for all instances of that page; probably not what you want.
You could alternatively tack them on to through the singleton_class:
my_page = MyPage.new
my_page.singleton_class.section(:new_section, NewSection, '#foo')
But that's getting a bit ugly to toss into your tests, right?
I've long thought that Sections should have a default_locator (but tough to get patches accepted)
With that we could generalize this a bit:
class DynamicSection < SitePrism::Section
def self.set_default_locator(locator)
@default_locator = locator
end
def self.default_locator
@default_locator
end
end
class DynamicPage < SitePrism::Page
# add sections (and related methods) to this instance of the page
def include_sections(*syms)
syms.each do |sym|
klass = sym.to_s.camelize.constantize
self.singleton_class.section(sym, klass, klass.default_locator)
end
end
end
And then you can use these as the parents.
class FooSection < DynamicSection
set_default_locator '#foo'
element :username, "#username"
end
class BlogPostPage < DynamicPage
# elements that exist on every BlogPost
end
In the tests:
@page = BlogPostPage.new
@page.include_sections(:foo_section, :bar_section)
expect(@page.foo_section).to be_visible
On the other-hand it really might be easier to just create a few different variations of the page-object for use in tests. (Are you really going to test that many variations? Maybe..maybe not.)
You can add a section to just a page object instance by modifying its singleton class.
Then(/^Some step$/) do
@blog = PageObjects::BlogPost.new
@blog.load("some url")
# You can see that @blog does not have the logo section
expect(@blog).not_to respond_to(:logo)
# Add a section to just the one instance of BlogPost
class << @blog
section(:logo, MySite::Components::Logo, '.logo')
end
# You can now see that #blog has the logo section
expect(@blog).to respond_to(:logo)
end
This will likely result in duplicate the section definition in multiple steps. To address this, you could create a method within the BlogPost
to dynamically add the specified sections.
In the following BlogPost
class, a dictionary of available components is created. The class has a method that adds components based on the dictionary definition.
class BlogPost < SitePrism::Page
COMPONENT_DICTIONARY = {
logo: {class: MySite::Components::Logo, selector: '.logo'},
navigation: {class: MySite::Components::Navigation, selector: '.primary-navigation'},
header: {class: MySite::Components::BlogHeader, selector: '.header'}
}
def add_components(*components)
Array(components).each do |component|
metaclass = class << self; self; end
metaclass.section(component, COMPONENT_DICTIONARY[component][:class], COMPONENT_DICTIONARY[component][:selector])
end
end
end
As an example of the usage:
# Create a blog post that just has the logo section
@blog = BlogPost.new
@blog.add_components(:logo)
# Create a blog post that has the navigation and header section
@blog2 = BlogPost.new
@blog2.add_components(:navigation, :header)
# Notice that each blog only has the added components
expect(@blog).to respond_to(:logo)
expect(@blog).not_to respond_to(:navigation)
expect(@blog).not_to respond_to(:header)
expect(@blog2).not_to respond_to(:logo)
expect(@blog2).to respond_to(:navigation)
expect(@blog2).to respond_to(:header)