Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add a section to a SitePrism page object dynamically?

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.

like image 322
toniedzwiedz Avatar asked Feb 09 '23 05:02

toniedzwiedz


2 Answers

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.)

like image 137
tgf Avatar answered Mar 07 '23 23:03

tgf


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)
like image 20
Justin Ko Avatar answered Mar 07 '23 23:03

Justin Ko