rubycucumbercapybaraautomated-testssite-prism

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.


Solution

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