Control Instance of Watir-Webdriver from another file - ruby

How would I control an instance of Watir-Webdriver from another file? For example,
in a file webdriver.rb I have
require 'watir-webdriver'
class Crawler
attr_accessor :browser
def initialize
self.browser = Watir::Browser.new
end
def goto_mypage
browser.goto("http://www.mypage.com")
end
def kill
browser.close
end
end
a = Crawler.new
Now how would I access a from the file "another_file.rb" with content like
a.goto("htttp://www.another_page.com")
a.goto_mypage
I've tried making requiring './webdriver.rb' in another_file.rb. And making the class Crawler part of a module and including that. Getting access to the methods across files is not a problem -- getting access to the webdriver instance is.
I've tried setting an instance variable #a = a in webdriver.rb and then accessing #a from another_file.rb.
I thought this would be simple, but it has me flummoxed.

You may get more help if you include actual results. Are you getting a nil class error?
Is there a reason you cannot have a = Crawler.new in another_file.rb and then just refer to it in there?

Related

poltergeist doesn't seem to wait for phantomjs to load in capybara

I'm trying to get some rspec tests run using a mix of Capybara, Selenium, Capybara/webkit, and Poltergeist. I need it to run headless in certain cases and would rather not use xvfb to get webkit working. I am okay using selenium or poltergeist as the driver for phantomjs. The problem I am having is that my tests run fine with selenium and firefox or chrome but when I try phantomjs the elements always show as not found. After looking into it for a while and using page.save_screenshot in capybara I found out that the phantomjs browser wasn't loaded up when the driver told it to find elements so it wasn't returning anything. I was able to hack a fix to this in by editing the poltergeist source in <gem_path>/capybara/poltergeist/driver.rb as follows
def visit(url)
if #started
sleep_time = 0
else
sleep_time = 2
end
#started = true
browser.visit(url)
sleep sleep_time
end
This is obviously not an ideal solution for the problem and it doesn't work with selenium as the driver for phantomjs. Is there anyway I can tell the driver to wait for phantom to be ready?
UPDATE:
I was able to get it to run by changing where I included the Capybara::DSL. I added it to the RSpec.configure block as shown below.
RSpec.configure do |config|
config.include Capybara::DSL
I then passed the page object to all classes I created for interacting with the webpage ui.
An example class would now look like this
module LoginUI
require_relative 'webpage'
class LoginPage < WebPages::Pages
def initialize(page, values = {})
super(page)
end
def visit
browser.visit(login_url)
end
def login(username, password)
set_username(username)
set_password(password)
sign_in_button
end
def set_username(username)
edit = browser.find_element(#selectors[:login_edit])
edit.send_keys(username)
end
def set_password(password)
edit = browser.find_element(#selectors[:password_edit])
edit.send_keys(password)
end
def sign_in_button
browser.find_element(#selectors[:sign_in_button]).click
end
end
end
Webpage module looks like this
module WebPages
require_relative 'browser'
class Pages
def initialize(page)
#page = page
#browser = Browser::Browser.new
end
def browser
#browser
end
def sign_out
browser.visit(sign_out_url)
end
end
end
The Browser module looks like this
module Browser
class Browser
include Capybara::DSL
def refresh_page
page.evaluate_script("window.location.reload()")
end
def submit(locator)
find_element(locator).click
end
def find_element(hash)
page.find(hash.keys.first, hash.values.first)
end
def find_elements(hash)
page.find(hash.keys.first, hash.values.first, match: :first)
page.all(hash.keys.first, hash.values.first)
end
def current_url
return page.current_url
end
end
end
While this works I don't want to have to include the Capybara::DSL inside RSpec or have to include the page object in the classes. These classes have had some things removed for the example but show the general structure. Ideally I would like to have the Browser module include the Capybara::DSL and be able to handle all of the interaction with Capybara.
Your update completely changes the question so I'm adding a second answer. There is no need to include the Capybara::DSL in your RSpec configure if you don't call any Capybara methods from outside your Browser class, just as there is no need to pass 'page' to all your Pages classes if you limit all Capybara interaction to your Browser class. One thing to note is that the page method provided by Capybara::DSL is just an alias for Capybara.current_session so technically you could just always call that.
You don't show in your code how you're handling any assertions/expectations on the page content - so depending on how you're doing that you may need to include Capybara::RSpecMatchers in your RSpec config and/or your WebPages::Pages class.
Your example code has a couple of issues that immediately pop out, firstly your Browser#find_elements (assuming I'm reading your intention for having find first correctly) should probably just be
def find_elements(hash)
page.all(hash.keys.first, hash.values.first, minimum: 1)
end
Secondly, your LoginPage#login method should have an assertion/expectation on a visual change that indicates login succeeded as its final line (verify some message is displayed/logged in menu exists/ etc), to ensure the browser has received the auth cookies, etc before the tests move on. What that line looks like depends on exactly how you're architecting your expectations.
If this doesn't answer your question, please provide a concrete example of what exactly isn't working for you since none of the code you're showing indicates any need for Capybara::DSL to be included in either of the places you say you don't want it.
Capybara doesn't depend on visit having completed, instead the finders and matchers will retry up to a specified period of time until they succeed. You can increase this amount of time by increasing the value of Capybara.default_max_wait_time. The only methods that don't wait by default are first and all, but can be made to wait/retry by specifying any of the count options
first('.some_class', minimum: 1) # will wait up to Capybara.default_max_wait_time seconds for the element to exist on the page.
although you should always prefer find over first/all whenever possible
If increasing the maximum wait time doesn't solve your issue, add an example of a test that fails to your question.

Ruby: 'require' returns false even though file not loaded

I have this code:
puts require './item'
puts $"
class Light < Item
#code
end
Item class in item.rb:
require './v3d'
require './ray'
class Item
attr_accessor :pos
def initialize(pos)
#pos = pos
end
def check(pos, dir)
return nil
end
def normal(ray)
return nil
end
end
that when I run my program prints this output:
false
enumerator.so
thread.rb
rational.so
complex.so
/usr/lib/x86_64-linux-gnu/ruby/2.3.0/enc/encdb.so
/usr/lib/x86_64-linux-gnu/ruby/2.3.0/enc/trans/transdb.so
/usr/lib/ruby/2.3.0/unicode_normalize.rb
/usr/lib/x86_64-linux-gnu/ruby/2.3.0/rbconfig.rb
/usr/lib/ruby/2.3.0/rubygems/compatibility.rb
/usr/lib/ruby/2.3.0/rubygems/defaults.rb
/usr/lib/ruby/2.3.0/rubygems/deprecate.rb
/usr/lib/ruby/2.3.0/rubygems/errors.rb
/usr/lib/ruby/2.3.0/rubygems/version.rb
/usr/lib/ruby/2.3.0/rubygems/requirement.rb
/usr/lib/ruby/2.3.0/rubygems/platform.rb
/usr/lib/ruby/2.3.0/rubygems/basic_specification.rb
/usr/lib/ruby/2.3.0/rubygems/stub_specification.rb
/usr/lib/ruby/2.3.0/rubygems/util/list.rb
/usr/lib/x86_64-linux-gnu/ruby/2.3.0/stringio.so
/usr/lib/ruby/2.3.0/rubygems/specification.rb
/usr/lib/ruby/2.3.0/rubygems/exceptions.rb
/usr/lib/ruby/vendor_ruby/rubygems/defaults/operating_system.rb
/usr/lib/ruby/2.3.0/rubygems/core_ext/kernel_gem.rb
/usr/lib/ruby/2.3.0/monitor.rb
/usr/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb
/usr/lib/ruby/2.3.0/rubygems.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/version.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/core_ext/name_error.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/levenshtein.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/jaro_winkler.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/spell_checkable.rb
/usr/lib/ruby/2.3.0/delegate.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/spell_checkers/name_error_checkers/class_name_checker.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/spell_checkers/name_error_checkers/variable_name_checker.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/spell_checkers/name_error_checkers.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/spell_checkers/method_name_checker.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/spell_checkers/null_checker.rb
/usr/lib/ruby/vendor_ruby/did_you_mean/formatter.rb
/usr/lib/ruby/vendor_ruby/did_you_mean.rb
/home/<user>/Documents/ruby/ray/write_ppm.rb
/home/<user>/Documents/ruby/ray/v3d.rb
/home/<user>/Documents/ruby/ray/pixel.rb
/home/<user>/Documents/ruby/ray/image.rb
/home/<user>/Documents/ruby/ray/material.rb
then throws:
/home/<user>/Documents/ruby/ray/light.rb:4:in `<top (required)>': uninitialized constant Item (NameError)
When require './item' is called, there is no error AND it returns false. From my understanding of how require works, it seems that the program incorrectly thinks it does not need to load item.rb. Why does this happen and how can I fix it?
Edit: expanded on some code
As a generic answer, not related to op, but because I had a similar issue and was pointed here by search engines.
Basically require 'http' returned false while gem was not loaded.
I figured out that there is a http.rb file inside load path and it is being loaded instead of the standard gem. So double check there are no conflicting file names of ruby files under library load path and the gem name.
I solved the problem by totally rewriting my require statements for every file. What I think the problem was, was this:
item.rb contained require './ray'
ray.rb contained require './light'
light.rb contained require './item' and class Light < Item
While loading item.rb, the interpreter saw it needed to also load ray.rb and therefore light.rb. When it reached the require './item' inside light.rb, it returned false because it was in the process of loading that file. However, since it was not yet finished loading, it did not show up in $". The interpreter then needed access to the definition of the Item class to finish loading light.rb, but because it needed to finish loading light.rb to load item.rb, the interpreter thew a NameError.
I think you may want require_relative instead.
http://ruby-doc.org/core-2.4.0/Kernel.html#method-i-require_relative
vs
http://ruby-doc.org/core-2.4.0/Kernel.html#method-i-require
You are correct in saying that if require 'my_lib' returns false, then 'my_lib' has already been loaded. However this is different from saying that a MyLib class is defined. Does your item.rb define an Item class?
Also, it may be possible that Item is defined somewhere else in the namespace hierarchy. e.g. if your item.rb is in some_gem/item.rb, and you're calling require from some_gem/, it will load successfully, but the name of the class might be SomeGem::Item. In this case you wouldn't be able to access it directly from the root namespace.
Last thing I can think of is that the item.rb file is changing under, or otherwise has some very dynamic pieces that are confusing the interpreter.
I would think that the issue is one of these before thinking that require is somehow messing up.

(Ruby) Cannot use selenium commands in cucumber tests?

I am trying to save object definitions in a "home page" file and simply call those methods whenever I need to use that button/link/image/etc. But for some reason the selenium commands are bringing up a NoMethodError. When I simply run the cucumber command while on the features folder in the terminal, I get these errors:
When I click on Site Management # features/step_definitions/steps.rb:17
undefined method `find_element' for nil:NilClass (NoMethodError)
./features/lib/pages/home.rb:3:in `siteMgmt'
./features/step_definitions/steps.rb:18:in `/^I click on Site Management$/'
features/test.feature:6:in `When I click on Site Management'
So in other words, it's trying to "click on site management," the code moves to the Home class, the SiteMgmt method (great!) and then fails when trying to run the selenium find_method method. I thought I might have to add a require selenium-webdriver at the top of home.rb, but a) that's NOT the case in steps.rb and, even if I add it, it doesn't work.
Here is the folder structure:
features/
--test.feature
lib/
pages/
--home.rb
step_definitions/
--steps.rb
support/
--env.rb
env.rb
require 'selenium-webdriver'
Dir[File.dirname(__FILE__) + "/../lib/pages/*.rb"].each {|file| require file }
Before do |scenario|
#driver = Selenium::WebDriver.for :chrome
#url = "URL goes here"
end
After do |scenario|
#driver.quit
end
test.feature
Feature: Proof of Concept
Stack overflow help!
Scenario:
Given I am logged into the site
When I click on Site Management
Then the Site Management page should load
steps.rb
Given(/^I am logged into AMP$/) do
#driver.get #amp_url
end
When(/^I click on Site Management$/) do
link = Home.new.siteMgmt
link.click
end
Then(/^the Site Management page should load$/) do
# assert here
end
home.rb
class Home
def siteMgmt
elem = #driver.find_element(:xpath, '//*[#id="body"]/section[2]/ul/li[1]/h3/a')
return elem
end
end
Thanks for all your help!
The #driver instance variable that's created in the Before block isn't available to an instantiated Home object. You could add a parameter to the site_mgmt method and pass the #driver instance variable in. Here's a contrived example:
class Home
def site_mgmt(driver)
elem = driver.find_element(:id, "logo")
end
end
require 'selenium-webdriver'
#driver = Selenium::WebDriver.for :chrome
#driver.navigate.to "http://www.iana.org/domains/reserved"
link = Home.new.site_mgmt(#driver)
link.click
A couple of notes: 1) variables in ruby are snake_case'd (i.e. site_mgmt instead of siteMgmt; and 2) return elem in site_mgmt isn't needed because ruby methods implicitly return.
Well, it turns out all I had to do is turn #driver to $driver. I'm still learning Ruby and didn't realize the difference.

How can I refactor my Sinatra app?

I've just started writing a reasonably straightforward site using sinatra. My problem is that I wanted to refactor the main app.rb file but am getting errors trying to access the url params.
In my get '/' action, Sinatra's looking at which params are set and then needs to do a few different things depending on what's in the url. Something like this.
class App < Sinatra::Application
...
get '/' do
if params['code1']
#network = 'code1'
mode code here
elsif params['called'] && params['mac']
#network = 'code2'
mode code here
elsif params['code3']
#network = 'code3'
mode code here
end
end
The problem is that I need to require a file that also uses the params.
I've put the following in the above code:
require File.dirname(__FILE__) + '/lib/networks/code1.rb'
Where code1.rb includes:
class App < Sinatra::Application
if params['login'] # == 'login'
pass = 'uampass'
elsif
...
But that gives me the following error:
undefined local variable or method `params' for main:Object
How can I refactor this without causing an error
As far as i know you can't use two (or more) Sinatra applications in, well one application. Since both files define a Sinatra::Application descendant this isn't possible.
Also if you want to use values from the params-hash you should define helper methods Helper Documentation, which you call when processing the route, or you just create Class which has class or instance methods which take params-values as parameters. Actually calling params from another file/class doesn't seem like good practice.
To put this in context: Sinatra applications are organised as handlers. The Sinatra::Application descendant is something like the main handler which uses support methods(helpers and instance methods of the Sinatra::Application descendant) or support Classes, which are usually defined in other files, but do not descend from Sinatra::Application.
To make this a little bit more clearly:
Your main Sinatra file:
require_relative 'another_file.rb'
class App < Sinatra::Application
# ...
#a_handler = MyHandler.new
get '/' do
if params['something'] == 'wanted_value'
#a_handler.handle_it(params)
end
end
Another file ('another_file.rb'):
class MyHandler
def initialize
#an_instance_variable = 'foobar'
end
def handle_it(params_hash)
if params_hash['login'] # == 'login'
pass = 'uampass'
elsif
# ...
end
# ...
# do some stuff
# ....
return pass
end
end
Actual code would of course depend on the real problem you're trying to solve, so if you would elaborate i could be more precise...
The error message contains everything you need to know, and it's nothing to do with Sinatra.
You are requiring code1.rb, which contains this (slightly edited so it will run):
require 'sinatra'
class App < Sinatra::Application
if params['login'] # == 'login'
pass = 'uampass'
end
end
Ruby evaluates code as it encounters it. In this case, you've required 'code1.rb', so it evaluates the code in that file. It encounters 'params' and asks "is there a local variable or method with that name?". There isn't, so it fails as you've seen. Open an irb session and check it out.
Class definitions in ruby are just an expression with a scope.
In relation to Sinatra: params is available in the block that a route declaration takes.
I'd recommend reading Sinatra: Up and Running, which explains some of the 'magic' that is going on (a good companion to the Sinatra Book).

How can I run Selenium (used through Capybara) at a lower speed?

By default Selenium runs as fast as possible through the scenarios I defined using Cucumber.
I would like to set it to run at a lower speed, so I am able to capture a video of the process.
I figured out that an instance of Selenium::Client::Driver has a set_speed method. Which corresponds with the Java API.
How can I obtain an instance of the Selenium::Client::Driver class? I can get as far as page.driver, but that returns an instance of Capybara::Driver::Selenium.
Thanks to http://groups.google.com/group/ruby-capybara/msg/6079b122979ffad2 for a hint.
Just a note that this uses Ruby's sleep, so it's somewhat imprecise - but should do the job for you. Also, execute is called for everything so that's why it's sub-second waiting. The intermediate steps - wait until ready, check field, focus, enter text - each pause.
Create a "throttle.rb" in your features/support directory (if using Cucumber) and fill it with:
require 'selenium-webdriver'
module ::Selenium::WebDriver::Firefox
class Bridge
attr_accessor :speed
def execute(*args)
result = raw_execute(*args)['value']
case speed
when :slow
sleep 0.3
when :medium
sleep 0.1
end
result
end
end
end
def set_speed(speed)
begin
page.driver.browser.send(:bridge).speed=speed
rescue
end
end
Then, in a step definition, call:
set_speed(:slow)
or:
set_speed(:medium)
To reset, call:
set_speed(:fast)
This will work, and is less brittle (for some small value of "less")
require 'selenium-webdriver'
module ::Selenium::WebDriver::Remote
class Bridge
alias_method :old_execute, :execute
def execute(*args)
sleep(0.1)
old_execute(*args)
end
end
end
As an update, the execute method in that class is no longer available. It is now here only:
module ::Selenium::WebDriver::Remote
I needed to throttle some tests in IE and this worked.
The methods mentioned in this thread no longer work with Selenium Webdriver v3.
You'll instead need to add a sleep to the execution command.
module Selenium::WebDriver::Remote
class Bridge
def execute(command, opts = {}, command_hash = nil)
verb, path = commands(command) || raise(ArgumentError, "unknown command: #{command.inspect}")
path = path.dup
path[':session_id'] = session_id if path.include?(':session_id')
begin
opts.each { |key, value| path[key.inspect] = escaper.escape(value.to_s) }
rescue IndexError
raise ArgumentError, "#{opts.inspect} invalid for #{command.inspect}"
end
Selenium::WebDriver.logger.info("-> #{verb.to_s.upcase} #{path}")
res = http.call(verb, path, command_hash)
sleep(0.1) # <--- Add your sleep here.
res
end
end
end
Note this is a very brittle way to slow down the tests since you're monkey patching a private API.
I wanted to slow down the page load speeds in my Capybara test suite to see if I could trigger some intermittently failing tests. I achieved this by creating an nginx reverse proxy container and sitting it between my test container and the phantomjs container I was using as a headless browser. The speed was limited by using the limit_rate directive. It didn't help me to achieve my goal in the end, but it did work and it may be a useful strategy for others to use!

Resources