How do you pass method params in Ruby which can be used as the name of existing methods? - ruby

Background: I'm using a DSL for automated UI testing in Ruby called Watir-Webdriver.
I want to write a very re-usable method that passes or fails when a specific HTML element is present. Here is what I have so far:
require 'watir-webdriver'
require 'rspec'
b = Watir::Browser.new
def display_check(element_type,unique_element,expectation)
if expectation == "yes"
b.send(element_type).((:id or :class or :name or :value),/#{Regexp.escape(unique_element)}/).exists?.should == true
else
b.send(element_type).((:id or :class or :name or :value),/#{Regexp.escape(unique_element)}/).exists?.should == false
end
end
I can understand that "div" in this example is a string passed as a method argument. But in the context of the dsl, "div" (minus the quotes) is also a Watir-webdriver method. So I guess I need to somehow convert the string to an eligible watir-webdriver method
I basically want to do the following to determine if an element exists.
display_check("div","captcha","no")
Since I'll be looking for select_lists, divs, radio buttons etc, it would be very useful to specify the element type as an option instead of having it hard coded to the method.

When you use send, the first parameter is the method name and the following parameters are the parameters to pass to the method. See doc.
So your b.send should be more like:
b.send(element_type, :id, /#{Regexp.escape(unique_element)}/).exists?
To find an element where one of the attributes (id, class, etc) is a certain value, you can try the following. Basically it iterates through each of the attributes until an element is found.
def display_check(b, element_type, unique_element, expectation)
element_exists = false
[:id, :class, :name, :value].each do |attribute|
if b.send(element_type, attribute, /#{Regexp.escape(unique_element)}/).exists?
element_exists = true
break
end
end
if expectation == "yes"
element_exists.should == true
else
element_exists.should == false
end
end

Related

Selenium and Ruby element mapping

I am looking to map my elements using pageObject model, although I am facing the following issue:
1:. error is thrown given I have no driver, this is ok since I only map my driver when I instantiate the class
element = #driver.find_element(:id => 'username')
def initialize driver
#driver = driver
#driver.navigate.to "http://www.google.com"
end
def set_username input
element.send_keys input
end
2:. in the below approach, it doesn't complain about missing driver because I am initializing it before and passing it as a global variable. but now it tries to map the element even before opening the page, which fails with "couldn't find element"
element = $driver.find_element(:id => 'username')
def initialize
$driver.navigate.to "http://www.google.com"
end
def set_username input
element.send_keys input
end
The question is: Is there any cheeky way I can map my elements and assign them to objects but only have them compiled/read when I actually need to use them (I only perform some action with it in the set_username, and I would only want to trigger the object mapping when utilizing it in this method for example)... I prefer not to use an existing pageObject framework...
With the Page Object model, you have an object that represents the page you're testing, and objects that represent lower level HTML objects. In this case, you might have the following classes:
# Represents a text input HTML element...
class TextInput
attr_reader :element
def initialize(driver, id)
element = driver.find_element(:id => id)
end
def type(text)
element.send_keys text
end
end
# Represents the page you are testing...
class SomePage
attr_reader :driver, :username
def initialize(driver)
#driver = driver
end
def username
#username ||= TextInput.new(#driver, 'username')
end
end
Once you have initialized your driver, you pass it to SomePage, and use it
to drive what you're doing.
some_page = SomePage.new(driver)
some_page.username.type("Timmy")
I can't vouch for the page-object gem because I've never used it, but it would handle those HTML-layer objects, and give you a domain-specific language for constructing them into a page object class. Worth checking out.

Can you choose watir commands through a variable?

I'm fairly new to programming, and I'm not sure what keywords I should be looking for.
I'm doing something like this right now:
def click(text, type)
b.span(:text=> text).click if type == 'span'
b.button(:name=> text).click if type == 'button'
b.image(:src=>text).click if type == 'image'
b.button(:title=>text).click if type == 'title'
end
I don't like it because it isn't scaling very well. I want to do something like:
def click(text,type)
b.type(:text=> text).click
end
It throws an undefined method error if I try to enter the type without quotes, but it's definitely not a string. How do I tell the script to use watir-webdriver span/button/image/etc?
It's hard to figure out exactly what it is you want to do with this method or why it's even necessary--or why your type parameter would ever be anything other than a string--but here's a way to help you clean up your code that's similar to what orde suggested.
Note that it's unclear what you're implying when you say "it's definitely not a string." If it's not a string, what is it? Where is it coming from that you are sticking it into this method's parameters without knowing what type of Object it is?
So... I'm assuming your type doesn't have to be a String object, so I made it so it takes symbols...
def click(text, type)
types={span: :text, button: :name, image: :src, title: :title }
#b.send(type, {types[type]=>text}).click
end
I'm not sure how you are calling your click method in your scripts, but here is a contrived example that seems to work:
require 'watir-webdriver'
def click_method(element, text)
#b.element(:text => "#{text}").click
end
#b = Watir::Browser.new
#b.goto "http://www.iana.org/domains/reserved"
click_method("link", "Domains")
EDIT:
require 'watir-webdriver'
def method_not_named_click(el, locator, locator_val)
if locator_val.is_a? String
#b.send(el, locator => "#{locator_val}").click
elsif locator_val.is_a? Integer
#b.send(el, locator => locator_val).click
end
end
#b = Watir::Browser.new
#b.goto "http://www.iana.org/domains/reserved"
method_not_named_click(:a, :text, "Domains")
method_not_named_click(:a, :index, 3)

data_mapper, attr_accessor, & serialization only serializing properties not attr_accessor attributes

I'm using data_mapper/sinatra and trying to create some attributes with attr_accessor. The following example code:
require 'json'
class Person
include DataMapper::Resource
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json
produces this output:
"{\"id\":null,\"first_name\":\"Mike\"}"
Obviously I would like for it to give me both the first and last name attributes. Any ideas on how to get this to work in the way one would expect so that my json has all of the attributes?
Also, feel free to also explain why my expectation (that I'd get all of the attributes) is incorrect. I'm guessing some internal list of attributes isn't getting the attr_accessor instance variables added to it or something. But even so, why?
Datamapper has it’s own serialization library, dm-serializer, that provides a to_json method for any Datamapper resource. If you require Datamapper with require 'data_mapper' in your code, you are using the data_mapper meta-gem that requires dm-serializer as part of it’s set up.
The to_json method provided by dm-serializer only serializes the Datamapper properties of your object (i.e. those you’ve specified with property) and not the “normal” properties (that you’ve defined with attr_accessor). This is why you get id and first_name but not last_name.
In order to avoid using dm-serializer you need to explicitly require those libraries you need, rather than rely on data_mapper. You will need at least dm-core and maybe others.
The “normal” json library doesn’t include any attributes in the default to_json call on an object, it just uses the objects to_s method. So in this case, if you replace require 'data_mapper' with require 'dm-core', you will get something like "\"#<Person:0x000001013a0320>\"".
To create json representations of your own objects you need to create your own to_json method. A simple example would be to just hard code the attributes you want in the json:
def to_json
{:id => id, :first_name => first_name, :last_name => last_name}.to_json
end
You could create a method that looks at the attributes and properties of the object and create the appropriate json from that instead of hardcoding them this way.
Note that if you create your own to_json method you could still call require 'data_mapper', your to_json will replace the one provided by dm-serializer. In fact dm-serializer also adds an as_json method that you could use to create the combined to_json method, e.g.:
def to_json
as_json.merge({:last_name => last_name}).to_json
end
Thanks to Matt I did some digging and found the :method param for dm-serializer's to_json method. Their to_json method was pretty decent and was basically just a wrapper for an as_json helper method so I overwrote it by just adding a few lines:
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
The completed method override looks like:
module DataMapper
module Serializer
def to_json(*args)
options = args.first
options = {} unless options.kind_of?(Hash)
if options[:include_attributes]
options[:methods] = [] if options[:methods].nil?
options[:methods].concat(model.attributes).uniq!
end
result = as_json(options)
# default to making JSON
if options.fetch(:to_json, true)
MultiJson.dump(result)
else
result
end
end
end
end
This works along with an attributes method I added to a base module I use with my models. The relevant section is below:
module Base
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def attributes
#attributes || []
end
end
def attributes
self.class.attributes
end
end
now my original example:
require 'json'
class Person
include DataMapper::Resource
include Base
property :id, Serial
property :first_name, String
attr_accessor :last_name
end
ps = Person.new
ps.first_name = "Mike"
ps.last_name = "Smith"
p ps.to_json :include_attributes => true
Works as expected, with the new option parameter.
What I could have done to selectively get the attributes I wanted without having to do the extra work was to just pass the attribute names into the :methods param.
p ps.to_json :methods => [:last_name]
Or, since I already had my Base class:
p ps.to_json :methods => Person.attributes
Now I just need to figure out how I want to support collections.

Unable to set numeric DataMapper field to blank value

I can't seem to find any way to successfully set a decimal field to no value from a form, since the form is returning an empty string. Here's a super-simple test case that manually sets the field to ''
require 'dm-core'
require 'dm-mysql-adapter'
require 'dm-migrations'
DataMapper.setup(:default, 'mysql://localhost/test')
class Product
include DataMapper::Resource
property :id, Serial
property :list_price, Decimal
end
DataMapper.finalize
DataMapper.auto_migrate!
p = Product.new
puts p.save #=> true
p = Product.new(:list_price => '')
puts p.save #=> false
p = Product.new(:list_price => nil)
puts p.save #=> true
As you can see, the list_price field will happily save when it's not set, or when it's set to nil. However, when I use a blank string, it won't save at all -- it's seemingly not being typecast.
It seems like I must be missing something obvious here, since this is a pretty basic use-case for an ORM.
You could create your own setter method for the property to check its type. See the section "Over-riding Accessors" on the Datamapper properties documentation page.
Adding:
def list_price=(new_price)
new_price = nil if new_price == ''
super
end
to your Product class will cause any empty string being set as the value of list_price to be converted to nil, and allow the resource to be saved.

"Here methods" in Ruby?

I'm writing a few helpers to DRY up my tests. I pictured something like:
class ActiveSupport::TestCase
def self.test_presence_validation_of model, attribute
test "should not save #{model.to_s} with null #{attribute.to_s}", <<-"EOM"
#{model.to_s} = Factory.build #{model.to_sym}, #{attribute.to_sym} => nil
assert !#{model.to_s}.save, '#{model.to_s.capitalize} with null #{attribute.to_s} saved to the Database'
EOM
# Another one for blank attribute.
end
end
So that this:
class MemberTest < ActiveSupport::TestCase
test_presence_validation_of :member, :name
end
Executes exactly this at MemberTest class scope:
test 'should not save member with null name' do
member = Factory.build :member, :name => nil
assert !member.save, 'Member with null name saved to the Database'
end
Is it possible to do it this way (with a few adaptations, of course; I doubt my "picture" works), or do I have to use class_eval?
Have you seen Shoulda? It's great for testing common Rails functionality such as validations, relationships etc. https://github.com/thoughtbot/shoulda-matchers
In this case, it seems class_eval is necessary since I want to interpolate variable names into actual code.
Illustrated here.

Resources