如何动态地将部分添加到SitePrism页面对象?

我正在使用SitePrism来测试我的Web应用程序。 我有许多扩展SitePrism::Page的类,许多经常使用的HTML片段由扩展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,其中可以通过基于预定义内容选择模板然后将任意数量的可用组件拖放到页面上来组装页面。

初始开发人员创建了一个页面对象来镜像每个可用的模板 。 只要测试数量很少并且我们不得不在我们的function文件中测试的页面变体太多,这就没问题了。

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

虽然我们可以通过为CMS中可用的每个组件定义Sections并在页面对象中重用它们来轻松减轻代码重复,但是很多属性很少被使用。

 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选择器泄漏到步骤定义中,这通常是一种不好的做法。

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

无论如何,问题的技术部分代表着。 无论它的概念有多好或多坏,我如何动态扩展页面对象的附加部分? 我只是好奇。

我曾经一度想要做出你正在谈论的事情,原因几乎相同。 我们的网页可能会有新的内容部分被拖入其中; 使他们非常有活力。 我尝试过这样做的方法,从来没有发现任何我特别喜欢的东西。

像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 

另一方面,为了在测试中使用,创建页面对象的一些不同变体可能更容易。 (你真的要测试那么多变化吗?也许......不可能。)

您可以通过修改单个类来为页面对象实例添加一个部分。

 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)