Selenium and Ruby element mapping - ruby

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.

Related

How to implicitly refer to a variable

My code has a lot of this: driver.blahblahblah. Consider the following code sample, taken from http://www.browserstack.com/automate/ruby.
require 'rubygems'
require 'selenium-webdriver'
driver = Selenium::WebDriver.for(:remote,
:url => "http://USERNAME:ACCESS_KEY#hub.browserstack.com/wd/hub")
driver.navigate.to "http://www.google.com/ncr"
element = driver.find_element(:name, 'q')
element.submit
puts driver.title
driver.quit
How can I make driver implicit? For example there's a method called driver.save_screenshot(). I want to say save_screenshot("a.png") because only the driver variable/object has this method.
You can use delegate from ActiveSupport like the example below:
require 'active_support/core_ext/module/delegation'
class MyClass
delegate :find_element, :save_screenshot, to: :driver
def foo
find_element
save_screenshot
end
def driver
#driver ||= Driver.new
end
end
class Driver
def find_element
puts "find_element"
end
def save_screenshot
puts "save_screenshot"
end
end
MyClass.new.foo
Or decorate the driver using SimpleDelegator (but I don't recommend that).
If you have lots of methods whose receiver is driver, then a way to make the receivers implicit is:
driver.instance_eval do
method_1...
method_2...
...
end
but notice that this slows down a little. If you are just finding a way to be lazy, then the best way is to make the local variable as short as a single letter, and do not bother making it implicit.
d = .... # instead of `drive`
d.method_1...
d.method_2...
...

Create a ruby object from a supertype object

I am using the Selenium Webdriver libraries in Ruby. A typical piece of code looks like this:
require 'rubygems'
require 'selenium-webdriver'
driver = Selenium::WebDriver.for :firefox
# driver is an instance of Selenium::WebDriver::Driver
url = 'http://www.google.com/'
wait = Selenium::WebDriver::Wait.new(:timeout => 10)
driver.get(url)
wait.until { driver.title.start_with? "Google" }
I would like to create a subclass of Selenium::WebDriver::Driver called Selenium::WebDriver::Driver::MyClass that will contain some new methods and instance variables.
As the above code illustrates, the way that instances of Selenium::WebDriver::Driver are created is with Selenium::WebDriver.for.
Without wholesale copying of code, how can I create a version of Selenium::WebDriver.for that does the same thing as Selenium::WebDriver.for but creates instances of Selenium::WebDriver::Driver::MyClass?
Why not just override the Selenium::WebDriver.for ? let me show you that my an example
# selenium code
module Selenium
class WebDriver
def self.for
puts "creating oldclass"
end
end
end
# your code
class Selenium::WebDriver
def self.for
puts "creating myclass"
end
end
Selenium::WebDriver.for
output:
creating myclass
Safe alternative is to derive class from Selenium::WebDriver and use that in your code, or to the extreme you can just open Driver class and add your behavior to it.
Check the source code. Selenium::WebDriver.for simply delegate the method call to Selenium::WebDriver::Driver.for.
If you don't have listener attached, you can simple create your own bridge MyClass::Bridge.new and then pass that to Selenium::WebDriver::Driver.new.
If you insist override the for method, here is some code snippet that might help.
module Selenium
module WebDriver
class Driver
class << self
alias_method :old_for, :for
def for(browser, opts = {})
if browser == :myclass
# create your MyClass::Bridge instance and pass that to new()
else
old_for(browser, opts)
end
end
end
end
end
end
If you just want to define some extra methods on your driver, you do not need to override WebDriver.for.
The following worked well for me:
First, in file customdriver.rb
require 'selenium-webdriver'
class CustomDriver < Selenium::WebDriver::Driver
#a custom method..
def click_on (_id)
element = find_element :id => _id
element.click
end
#add other custom methods here
#....
end
Then, in file main.rb
require-relative 'customdriver'
driver = CustomDriver.for :chrome
driver.click_on("buttonID")
Regards,

Adding #to_yaml to DataMapper models

I am using DataMapper for Database access. My goal is to send the models to an webservice as read-only object. This is my current try:
class User
include DataMapper::Resource
def to_yaml(opts = {})
mini_me = OpenStruct.new
instance_variables.each do |var|
next if /^#_/ =~ var.to_s
mini_me.send("#{var.to_s.gsub(/^#/, '')}=", instance_variable_get(var))
end
mini_me.to_yaml(opts)
end
....
end
YAML::ENGINE.yamler = 'psych'
u = User.get("hulk")
p u.to_yaml
# => "--- !ruby/object:OpenStruct\ntable:\n :uid: hulk\n :uidNumber: 1000\n :gidNumber: 1001\n :email: hulk#example.com\n :dn: uid=hulk,ou=People,o=example\n :name: Hulk\n :displayName: Hulk\n :description: Hulk\n :homeDirectory: /home/hulk\n :accountFlags: ! '[U ]'\n :sambaSID: S-1-5-21-......\nmodifiable: true\n"
p [ u ].to_yaml # TypeError: can't dump anonymous class Class
Any ideas how to make this work and get rid of the exception?
Thanks,
krissi
Using to_yaml is deprecated in Psych, and from my testing it seems to be actually broken in cases like this.
When you call to_yaml directly on your object, your method gets called and you get the result you expect. When you call it on the array containing your object, Psych serializes it but doesn’t correctly handle your to_yaml method, and ends up falling back onto the default serialization. In your case this results in an attempt to serialize an anonymous Class which causes the error.
To fix this, you should use the encode_with method instead. If it’s important that the serialized form is tagged as an OpenStruct object in the generated yaml you can use the represent_object (that first nil parameter doesn’t seem to be used):
def encode_with(coder)
mini_me = OpenStruct.new
instance_variables.each do |var|
next if /^#_/ =~ var.to_s
mini_me.send("#{var.to_s.gsub(/^#/, '')}=", instance_variable_get(var))
end
coder.represent_object(nil, mini_me)
end
If you were just using OpenStruct for convenience, an alternative could be something like:
def encode_with(coder)
instance_variables.each do |var|
next if /^#_/ =~ var.to_s
coder[var.to_s.gsub(/^#/, '')]= instance_variable_get(var)
end
end
Note that Datamapper has its own serializer plugin that provides yaml serialization for models, it might be worth looking into.

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.

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

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

Resources