【问题标题】:How to add a section to a SitePrism page object dynamically?如何动态地将部分添加到 SitePrism 页面对象?
【发布时间】:2015-11-19 08:57:49
【问题描述】:

我正在使用 SitePrism 测试我的 Web 应用程序。我有许多扩展 SitePrism::Page 的类,并且许多常用的 HTML sn-ps 由扩展 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

问题是,我正在开发的应用程序是基于 CMS,其中可以通过基于预定义内容选择 模板 来组装页面,然后拖放 -将任意数量的可用组件拖放到页面上。

最初的开发者创建了一个页面对象来镜像每个可用的模板。这很好,只要测试数量少,并且我们必须在功能文件中测试的页面变体不多。

随着多个测试用例的加入,页面对象开始以惊人的速度增长。

虽然我们可以通过为 CMS 中的每个可用组件定义 Sections 并在 Page Objects 中重用它们来轻松减少代码重复,但只有很多属性很少得到用过。

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

SitePrism 中是否有一种方法可以将部分动态添加到 页面对象 的实例中,而不是整个类?

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

我还担心,这样做可能会导致 CSS 选择器泄漏到步骤定义中,这通常是一种不好的做法。

解决此问题的另一种方法是为特定的页面示例构建页面对象,而不是通用模板。 模板页面对象 可以只包含嵌入到模板中的任何内容,并由镜像特定页面的其他页面对象 扩展,以处理差异。这听起来像是一种更简洁的方法,所以我可能会这样写我的测试

无论如何,问题的技术部分仍然存在。不管它是好是坏,我怎样才能动态地扩展一个带有附加部分的页面对象?我只是好奇。

【问题讨论】:

    标签: ruby cucumber capybara automated-tests site-prism


    【解决方案1】:

    您可以通过修改其单例类将部分添加到页面对象实例中。

    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
    

    这可能会导致在多个步骤中重复部分定义。为了解决这个问题,您可以在BlogPost 中创建一个方法来动态添加指定的部分。

    在下面的BlogPost 类中,创建了一个可用组件的字典。该类有一个根据字典定义添加组件的方法。

    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
    

    作为使用示例:

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

    【讨论】:

    • 很好,明天去试试。
    • 贾斯汀,你在我还在打字的时候发帖了,但看起来我们的想法非常相似:-)
    【解决方案2】:

    出于几乎相同的原因,我曾一度想做您所说的事情。我们的页面可以拖入新的内容部分;使它们非常有活力。我尝试了多种方法来做到这一点,但从未找到我特别喜欢的方法。

    site-prism 中的elementsections 等方法都为该类定义了许多方法。您可以在测试中调用 MyPage.section 或添加调用 self.class.section 的方法并使用它来添加新部分。但是这些将存在于该页面的所有实例中;可能不是你想要的。

    您也可以通过 singleton_class 附加它们:

    my_page = MyPage.new
    my_page.singleton_class.section(:new_section, NewSection, '#foo')
    

    但这在你的测试中有点难看,对吧?

    我一直认为 Sections 应该有一个 default_locator(但很难让补丁被接受)
    有了这个,我们可以概括一下:

    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
    

    然后您可以将这些用作父母。

    class FooSection < DynamicSection
      set_default_locator '#foo'
      element :username, "#username"
    end
    
    class BlogPostPage < DynamicPage
      # elements that exist on every BlogPost
    end
    

    在测试中:

    @page = BlogPostPage.new
    @page.include_sections(:foo_section, :bar_section)
    expect(@page.foo_section).to be_visible
    

    另一方面,创建一些不同的页面对象变体用于测试可能更容易。 (你真的要测试这么多变体吗?也许……也许不是。)

    【讨论】:

    • 如问题本身所述。我更倾向于遵循 page object per page 模式,而不是 page object per template。我只是将模板保留为基类或 mixin。我只是好奇如何在 Ruby 中的元编程方面做到这一点。
    • 重新审视这个问题原来是有趣和有趣的;所以我很高兴你问它:-)我可能会在我的叉子上加入这样的东西。
    【解决方案3】:

    为此目的使用 page.find

    class MyPage < SitePrism::Page
      element :static_selector_element, "#some-static-id"
      def dynamic_element(id)
        find "label[for=\"dynamic-value-#{id}\"]"
      end
    end
    

    在你的测试中:

    RSpec.feature 'My Feature' do
      scenario 'Success' do
        p = MyPage.new 
        p.visit '/'
        p.static_selector_element.click
        p.dynamic_element(SomeObject.fist.id).click
      end
    end
    

    【讨论】:

    • 谢谢 - 这正是我所需要的。
    猜你喜欢
    • 1970-01-01
    • 2012-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-02
    相关资源
    最近更新 更多