Re-Usable Components in Cheezy Page Object - ruby

I'm using cheezy page-object gem and cucumber.
I have page objects for an angular website and many pages contain angular ng-select element which is a dropdown. All of the ng-select elements are the same format for each page. The only thing that changes is the data and the id of the ng-select. I'd like to build some re-usable ng-select component that I can put in my page-objects as I have quite a few methods I use on the element.
class NGSelectComponent
include PageObject
def wrapper(id)
element(:element, tag_name: 'ng-select', id: id)
end
def wrapper_text_field
wrapper.text_field_element
end
def wrapper_span
wrapper.span_element(class: ['ng-value-label','ng-star-inserted'])
end
def wrapper_value
wrapper_span.text
end
def wrapper_values
wrapper.div_elements
end
end
As you can see the wrapper method is the ng-select element and it takes an id for the locator hash. This is as far as I got. I saw something like this but it looks like it only works for HTML elements.
How can I turn this into a re-usable component using page-object gem? As a sidenote I call my page objects using the on() method in my step definitions. So for example on(SomePage). I felt like that matters for however the solution turns out.

Widgets and page sections are the 2 options for re-usable components. As you will likely want to setup getters/setters for the field, widgets are the better choice.
The widget could be defined like:
class NGSelectComponent < PageObject::Elements::Element
def self.accessor_methods(accessor, name)
#
# Define a getter
#
accessor.send(:define_method, "#{name}") do
self.send("#{name}_element").value
end
#
# Define a setter. Use "#{name}=" so that the widget can be used
# in #populate_page_with
#
#
accessor.send(:define_method, "#{name}=") do |value|
self.send("#{name}_element").set(value)
end
end
def set(value)
text_field_element.set(value)
end
def value
text_field_element.value
end
def wrapper_text_field
text_field_element
end
def wrapper_span
span_element(class: ['ng-value-label','ng-star-inserted'])
end
def wrapper_value
wrapper_span.text
end
def wrapper_values
div_elements
end
PageObject.register_widget :ng_select, self, :element
end
Page objects would define the ng-select elements like any other accessor:
class TestPage
include PageObject
ng_select(:name, id: 'name')
end
Giving the page a getter/setter for the field - eg:
page = TestPage.new(browser)
page.populate_page_with(name: 'My Name')
p page.name
#=> "My Name"

Related

Is it possible to use the page function inside a class?

I'm writing some tests in calabash, and trying to use the page function inside a helper class.
I have my steps file
Given /^I am on my page$/ do
mypage = page(MyPage)
MyPageHelper.DoMultiActionStep()
end
And my page file
class MyPage < Calabash::ABase
def my_element_exists
element_exists(MY_ELEMENT_QUERY)
end
end
And my helper file
class MyPageHelper
def self.DoMultiActionStep
mypage = page(MyPage)
mypage.do_action_one
mypage.my_element_exists
end
end
When I run this though I get the error
undefined method 'page' for MyPageHelper:Class (NoMethodError)
The page function works fine in the steps file, but it just seems to have a problem being called from the MyPageHelper class. Is it possible to do this? Is there a using statement that I need to add in?
Thanks!
I am afraid that I don't know how to answer your question directly.
At the risk of being flamed, I recommend an alternative approach.
Option 1: If you don't need the helper class, don't bother with it.
I realize that your actual code is probably more complex, but do you need the helper here? Why not implement do_multi_action_step in the MyPage class as a method?
def do_multi_action_step
my_element_exists
my_other_method
end
Option 2: Pass an instance of MyPage
In your step you created an instance of MyPage. You should use that instance instead of creating a new one in MyPageHelper.do_multi_action_step.
def self.do_multi_action_step(my_page)
my_page.my_element_exists
my_page.my_other_method
end
Example:
# my_page_steps.rb
Given /^I am on my page$/ do
# use the await method to wait for your page
my_page = page(MyPage).await
# pass an instance instead of creating a new one
MyPageHelper.do_multi_action_step(my_page)
# or just use a method on the MyPage instance
my_page.do_multi_action_step
end
# my_page_helper.rb
class MyPageHelper
# pass the page as an object
def self.do_multi_action_step(my_page)
my_page.my_element_exists
my_page.my_other_method
end
end
# my_page.rb
require 'calabash-cucumber/ibase'
class MyPage < Calabash::IBase
# some view that is unique to this page
def trait
"view marked:'some mark'"
end
def my_element_exists
element_exists("view marked:'foo'")
end
def my_other_method
puts 'do something else'
end
# why not do this instead?
def do_multi_action_step
my_element_exists
my_other_method
end
end

Namespacing methods within a Ruby class

I'm using Ruby's metaprogramming methods creating a bunch of methods within a class. Within the class OmekaItem there are a bunch of methods of this form dc_title and dc_subject, and there are a bunch of methods of this form itm_field1 and itm_field2. I'd like to group those methods better. Ideally, given an instance of the class named item, I'd like call the methods this way:
item.dublin_core.title
item.item_type_metadata.field
and so on. Is there a way to do this?
This question has the code I'm working with.
Would something like the following work for you?
class OmekaItem
class DublinCore
def initialize(omeka_item)
#omeka_item = omeka_item
end
def title
#omeka_item.dc_title
end
def subject
#omeka_item.dc_subject
end
end
class ItemTypeMetadata
def initialize(omeka_item)
#omeka_item = omeka_item
end
def field1
#omeka_item.itm_field1
end
def field2
#omeka_item.itm_field2
end
end
def dublin_core
#dublin_core ||= DublinCore.new(self)
end
def item_type_metadata
#item_type_metadata ||= ItemTypeMetadata.new(self)
end
end
The methods on DublinCore and ItemTypeMetadata could be dynamically generated using define_method as appropriate.

Ruby inheritance and advised approach?

What I want is a single API which determines the class to delegate methods to based on a parameter passed through initializer. Here is a basic example:
module MyApp
class Uploader
def initialize(id)
# stuck here
# extend, etc. "include Uploader#{id}"
end
end
end
# elsewhere
module MyApp
class UploaderGoogle
def upload(file)
# provider-specific uploader
end
end
end
My desired outcome:
MyApp::Uploader('Google').upload(file)
# calls MyApp::UploaderGoogle.upload method
Please be aware the above is for demonstration purposes only. I will actually be passing an object which contains an attribute with the uploader id. Is there a better way to handle this?
Haven't tested it, but if you want to include a module:
module MyApp
class Uploader
def initialize(id)
mod = ("Uploader"+id).constantize
self.send(:include, mod)
end
end
end
If you want to extend your class with a module:
module MyApp
class Uploader
def initialize(id)
mod = ("Uploader"+id).constantize
self.class.send(:extend, mod)
end
end
end
Sounds like you want a simple subclass. UploaderGoogle < Uploader Uploader defines the basic interface and then the subclasses define the provider specific methods, calling super as necessary to perform the upload. Untested code OTTOMH below…
module MyApp
class Uploader
def initialize(id)
#id = id
end
def upload
#perform upload operation based on configuration of self. Destination, filename, whatever
end
end
class GoogleUploader < Uploader
def initialize(id)
super
#google-specific stuff
end
def upload
#final configuration/preparation
super
end
end
end
Something along those lines. To base this on a passed parameter, I'd use a case statement.
klass = case paramObject.identifierString
when 'Google'
MyApp::GoogleUploader
else
MyApp::Uploader
end
Two things: If you do this in several places, probably extract it into a method. Second, if you're getting the input from the user, you've got a lot of anti-injection work to do as well if you, for instance, create a class name directly from a provided string.

Ruby Sunspot: how to refactor common code between search do block?

How can I refactor out common code from a Sunspot search do block into a method that can then be called from multiple places? I suspect this is perhaps more of a Ruby metaprogramming question than a Sunspot-specific one, but here goes.
I have a model that uses sunspot like so:
class Book
def self.basic_search(params)
search do
# boilerplate...
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
# bespoke basic_search search code goes here
end
end
def self.curated_search(params)
search do
# boilerplate...
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
# bespoke curated_search code goes here
end
end
end
Then I try to refactor the code like so:
class Book
def self.basic_search(params)
search do
boilerplate params
# bespoke basic_search search code goes here
end
end
def self.curated_search(params)
search do
boilerplate params
# bespoke curated_search code goes here
end
end
def self.boilerplate(params)
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
end
end
Since the boilerplate method is defined as a class method on Book, this unsurprisingly results in:
undefined method 'boilerplate' for #<Sunspot::DSL::Search:0x007f92b4177a98
I suspect that some usage of instance_eval is required, but being new to Ruby I'm not quite sure how to apply that.
Here's what I came up with.
def self.basic_search(params)
search do
boilerplate(self, params) # here, self is a sunspot search instance
# bespoke basic_search search code goes here
end
end
def self.curated_search(params)
search do
boilerplate(self, params) # here, self is a sunspot search instance
# bespoke curated_search code goes here
end
end
def self.boilerplate(sunspot, params)
sunspot.instance_eval do
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
end
end
To fix the specific issue, try scoping it to call Book.boilerplate instead. As the error message suggests, the content inside the search's do...end block is evaluated inside Sunspot::DSL::Search rather than inside Book.
See the following example. Simplified for readability where Foo -> Book and Bar -> Search
Original way w/ duplication
class Foo
def self.bar()
p 1
end
def self.baz()
p 1
end
end
De-duplicated. Note that in the sunspot example, most likely method definition hooks or something comparable is used to define methods on Book rather than using the hard-coded pass through here. The point is that Bar can call quux which is in Foo.
class Foo
# Following two methods inserted via DSL magic. #Simplified for readability.
def self.bar
Bar.bar
end
def self.baz
Bar.baz
end
def self.quux()
p 1
end
end
class Bar
def self.bar
Foo.quux
end
def self.baz
Foo.quux
end
end
In your particular case, however, this still may not be doing what you want in that the params being evaluated should presumably be evaluated in Search and not Book. Depending on how search does param caching, you should consider trying something like this.
def self.boilerplate(params)
search do
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
end
end

Sharing variables across submodules and classes

I am trying to build a simple little template parser for self-learning purposes.
How do I build something "modular" and share data across it? The data doesn't need to be accessible from outside, it's just internal data. Here's what I have:
# template_parser.rb
module TemplateParser
attr_accessor :html
attr_accessor :test_value
class Base
def initialize(html)
#html = html
#test_value = "foo"
end
def parse!
#html.css('a').each do |node|
::TemplateParser::Tag:ATag.substitute! node
end
end
end
end
# template_parser/tag/a_tag.rb
module TemplateParser
module Tag
class ATag
def self.substitute!(node)
# I want to access +test_value+ from +TemplateParser+
node = #test_value # => nil
end
end
end
end
Edit based on Phrogz' comment
I am currently thinking about something like:
p = TemplateParser.new(html, *args) # or TemplateParser::Base.new(html, *args)
p.append_css(file_or_string)
parsed_html = p.parse!
There shouldn't be much exposed methods because the parser should solve a non-general problem and is not portable. At least not at this early stage. What I've tried is to peek a bit from Nokogiri about the structure.
With the example code you've given, I'd recommend using composition to pass in an instance of TemplateParser::Base to the parse! method like so:
# in TemplateParser::Base#parse!
::TemplateParser::Tag::ATag.substitute! node, self
# TemplateParser::Tag::ATag
def self.substitute!(node, obj)
node = obj.test_value
end
You will also need to move the attr_accessor calls into the Base class for this to work.
module TemplateParser
class Base
attr_accessor :html
attr_accessor :test_value
# ...
end
end
Any other way I can think of right now of accessing test_value will be fairly convoluted considering the fact that parse! is a class method trying to access a different class instance's attribute.
The above assumes #test_value needs to be unique per TemplateParser::Base instance. If that's not the case, you could simplify the process by using a class or module instance variable.
module TemplateParser
class Base
#test_value = "foo"
class << self
attr_accessor :test_value
end
# ...
end
end
# OR
module TemplateParser
#test_value = "foo"
class << self
attr_accessor :test_value
end
class Base
# ...
end
end
Then set or retrieve the value with TemplateParser::Base.test_value OR TemplateParser.test_value depending on implementation.
Also, to perhaps state the obvious, I'm assuming your pseudo-code you've included here doesn't accurately reflect your real application code. If it does, then the substitute! method is a very round about way to achieve simple assignment. Just use node = test_value inside TemplateParser::Base#parse! and skip the round trip. I'm sure you know this, but it seemed worth mentioning at least...

Resources