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

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)

Use page.find for that purpose
class MyPage < SitePrism::Page
element :static_selector_element, "#some-static-id"
def dynamic_element(id)
find "label[for=\"dynamic-value-#{id}\"]"
end
end
in your test:
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

Related

Class methods vs Instances when using Page object model in ruby

I hope you are having a great day!
I'm wandering something, when using the POM (Page object model) we always create new instances of our classes, like the following simple example:
class LoginPage
def initialize(driver)
#driver = driver
end
def click_button
#driver.find_element(xpath: "//button[#title='Login']").click
end
end
# We create a new instance and we click the button
login_page = LoginPage.new(driver)
login_page.click_button
This makes sense for me, we can create multiple pages if we need to, we can update the state of whatever we need.
However, if we only would have one page open at the time, and we know nothing changes, doesn't it make more sense to take a class method based approach like?:
class LoginPage
class << self
def click_button
#driver.find_element(xpath: "//button[#title='Login']").click
end
end
end
# We create a new instance and we click the button
LoginPage.click_button
Please let me know your ideas, and if you have tried this approach before

Using shove to add a string to an array in ruby

As part of an online Ruby tutorial, I must create a text-based game. One requirement is that I use require to pull in another file. I've done that as well as include the module that holds a method. However, I cannot produce the result I want. Here's my file with the module:
module Inventory
def Inventory.inventory(item)
items = Array.new
if item == "show"
items.inspect
else
items << item
end
end
end
I want the parameter (item) to be added to the array items as a string that can be inspected when I pass the "show" argument to it.
So for example, I want to add a 'bat' to the inventory so I call Inventory.inventory("bat"). Later I'd like to add other things. But when I call Inventory.inventory("show") it doesn't show anything.
I've spent days looking through many other tutorials and hundreds of questions here but still can't get it work. I'm probably not understanding something really fundamental so please be gracious to me as I'm still learning.
Is it the way I'm adding to an array? The way I'm trying to get it to show? Or do I not understand how to use methods and arguments?
you can Dylan's answer if you want to go with instance approach or you can use class variables.
The problem with your code is that you initialize items local variable every time you call inventory.
Here is a version that will persist items in a class variable:
module Inventory
def Inventory.inventory(item)
##items ||= Array.new
if item == "show"
##items.inspect
else
##items << item
end
end
end
Inventory.inventory 1
Inventory.inventory 2
p Inventory.inventory 'show'
this is producing
"[1, 2]"
This would make a lot more sense as a class. This way, you can store the items in an instance variable that will persist during multiple calls to add, show, etc. You can of course put this class into a separate file and still include it.
class Inventory
def initialize
#items = []
end
def add(item)
#items << item
end
def show
#items.inspect
end
end
# To use the class:
inventory = Inventory.new
inventory.add('bat')
inventory.show
# => ["bat"]
The issue is that your items array is being recreated every time this method is called, so there is no persistence between method calls for what is passed into the array. Dylan Markow's answer shows how you can use instance variables to persist values between method calls.

Is this Ruby class really badly designed?

I'm quite new to OOP and I'm concerned that this class that I've written is really poorly designed. It seems to disobey several principles of OOP:
It doesn't contain its own data, but relies on a yaml file for
values.
Its methods need to be called in a particular order
It has a lot of instance variables and methods
It does work, however. It's robust, but I'll need to modify the source code to add new getter methods every time I add page elements
It's a model of an html document used in an automated test suite. I keep thinking that some of the methods could be put in subclasses, but I'm concerned that I'd have too many classes then.
What do you think?
class BrandFlightsPage < FlightSearchPage
attr_reader :route, :date, :itinerary_type, :no_of_pax,
:no_results_error_container, :submit_button_element
def initialize(browser, page, brand)
super(browser, page)
#Get reference to config file
config_file = File.join(File.dirname(__FILE__), '..', 'config', 'site_config.yml')
#Store hash of config values in local variable
config = YAML.load_file config_file
#brand = brand #brand is specified by the customer in the features file
#Define instance variables from the hash keys
config.each do |k,v|
instance_variable_set("##{k}",v)
end
end
def visit
#browser.goto(#start_url)
end
def set_origin(origin)
self.text_field(#route[:attribute] => #route[:origin]).set origin
end
def set_destination(destination)
self.text_field(#route[:attribute] => #route[:destination]).set destination
end
def set_departure_date(outbound)
self.text_field(#route[:attribute] => #date[:outgoing_date]).set outbound
end
def set_journey_type(type)
if type == "return"
self.radio(#route[:attribute] => #itinerary_type[:single]).set
else
self.radio(#route[:attribute] => #itinerary_type[:return]).set
end
end
def set_return_date(inbound)
self.text_field(#route[:attribute] => #date[:incoming_date]).set inbound
end
def set_number_of_adults(adults)
self.select_list(#route[:attribute] => #no_of_pax[:adults]).select adults
end
def set_no_of_children(children)
self.select_list(#route[:attribute] => #no_of_pax[:children]).select children
end
def set_no_of_seniors(seniors)
self.select_list(#route[:attribute] => #no_of_adults[:seniors]).select seniors
end
def no_flights_found_message
#browser.div(#no_results_error_container[:attribute] => #no_results_error_container[:error_element]).text
raise UserErrorNotDisplayed, "Expected user error message not displayed" unless divFlightResultErrTitle.exists?
end
def submit_search
self.link(#submit_button_element[:attribute] => #submit_button_element[:button_element]).click
end
end
If this class is designed as a Facade, then it's not (too) bad design. It provides a coherent unified way to perform related operations that rely on a variety of un-related behavior holders.
It appears to be poor separation of concerns, in that this class essentially coupling all the various implementation details, which might turn out to be somewhat tricky to maintain.
Finally, the fact methods need to be called in a specific order may hint at the fact you're trying to model a state machine - in which case it probably should be broken down to several classes (one per "state"). I don't think there's a "too many methods" or "too many classes" point you'd reach, the fact is you need the features provided by each class to be coherent and making sense. Where to draw the line is up to you and your specific implementation's domain requirements.

Can't display more than one table model inheriting from the same class on different tables in QtRuby

I've been following this article to display ActiveRecord data in QtRuby. I've copied the BoatTableModel class from there(used my own code for the rest). In the article, BoatTableModel is defined to only support the Boat model, but except for the column definitions the code is quite generic. So, I've changed it so instead of having the columns defined there, I've made it take the columns from a column_names method, and define that methods in subclasses for each model.
Here is my code:
class QtArModel<Qt::AbstractTableModel
def initialize(items)
super()
#items=items
end
def rowCount(parent=nil)
#items.size
end
def columnCount(parent=nil)
column_names.length
end
def data(index,role=Qt::DisplayRole)
invalid=Qt::Variant.new
return invalid unless role==Qt::DisplayRole or role==Qt::EditRole
item=#items[index.row]
return invalid if item.nil?
v=item[column_names[index.column]]||""
return Qt::Variant.new(v)
end
def headerData(section,orientation,role=Qt::DisplayRole)
invalid=Qt::Variant.new
return invalid unless role==Qt::DisplayRole
v=case orientation
when Qt::Horizontal
column_names[section]
else
""
end
return Qt::Variant.new(v.to_s)
end
def flags(index)
return Qt::ItemIsEditable|super(index)
end
def setData(index,variant,role=Qt::EditRole)
if index.valid? and role==Qt::EditRole
s=variant.toString
item=#items[index.row]
if index.column.between?(0,column_names.length-1)
item[column_names[index.column]]=s
else
raise "invalid column #{index.column}"
end
item.save
emit dataChanged(index,index)
else
return false
end
end
end
class QtCoursesTableModel<QtArModel
def column_names
return [
:number,
:name,
:tutor_name,
:site,
:active,
]
end
end
class QtTasksTableModel<QtArModel
def column_names
return [
:course,
:ex_number,
:received,
:due,
:description,
:link,
:completed,
:file,
]
end
end
Now, when I display one model(doesn't matter which) - everything works just fine. However, when I display both models, each in it's own Qt::TableView - only the first one is displayed, and the other table view is blank.
I've tried different ordering, and the table that gets to display it's data is always the one which it's Qt::TableView is created first - the order of the creating the Qt models does not matter. Also, when I create the model object for the first table, but don't actually set it's model property to it, the second table displays it's data.
I've also tried to display the same model twice in two different table views - and it worked - for a split second, and then the second view's data disappeared.
I've also tried to copy-paste the QtArModel, change it's name, and make one of the models inherit from the copy. That did work - but it's obviously a huge code duplication, so I would really like to avoid that.
Now, my guess is that something in QtArModel is defined as a class member instead of instance member, making both model instances share something they shouldn't share. It has to be in QtArModel - because if it was higher in the inheritance tree, the problem would have remained when I've duplicated QtArModel. However, I can't find anything in my QtArModel that's class-scoped instead of instance-scoped.
What am I missing?
OK, I've managed to work this out. Apparently, the problem was not the inheritance, but the GC. Since the only connection to the models was from TableView's model property - which is just a wrapper for C++ getter and setter - ruby thought it lost the reference to my models, and GC'd them.
Solved by keeping the models in ruby variables.

How to have a gem controller handle multiple arbitrary models?

I have four models that I allow commenting on by four separate comment controllers. Those four comment controllers do essentially the same thing and vary only slightly.
In an attempt to remove duplication of the four commenting controllers which are essentially all the same, I've created a Rails Engine as a gem to arbitrarily handle commenting on any arbitrary model that I specify in the routes.rb.
So in my routes.rb file I can now use:
comments_on :articles, :by => :users
with comments_on implemented as follows in my gem:
def comments_on(*resources)
options = resources.extract_options!
[snip of some validation code]
topic_model = resources.first.to_s
user_model = options[:by].to_s
# Insert a nested route
Rails.application.routes.draw do
resources topic_model do
resources "comments"
end
end
end
The routes show up in 'rake routes' and requests correctly get routed to my gem's 'CommentsController' but that's where my gem's functionality ends.
What is the best way detect the context in my gem CommentsController so I can process requests specific to how comments_on was called?
More specifically, how would I implement an index action like the following, having it context aware?
def index
#article = Article.find(params[:article_id])
#comments = ArticleComment.find(:all, :conditions => { :article_id => #article.id })
end
Thanks for the help!
You could specify the topic as an extra parameter in your routes:
Rails.application.routes.draw do
resources topic_model do
resources "comments", :topic_model => topic_model.to_s
end
end
Then your controller could be written like this:
def index
#topic = topic
#comments = topic.comments
end
protected
def topic
m = params[:topic_model]
Kernel.const_get(m).find(params["#{m.underscore}_id"])
end
You could move a lot of the logic out of the controller and into the model as well. topic.comments could be a named scope that all of these models should implement.
I've done similar patterns in the past and there's usually an edge-case that breaks this idea down and you end up doing more 'meta' programming than is wise.
I'd recommend making a base controller, then making simplistic controllers that inherit from that, or try to split these common behaviors into modules.

Resources