Capybara instance of "browser" for Page Object Model? - ruby

Im writing a framework using Capybara and the Page Object Model for a web application. It's my first time writing my own framework and using PoM for automation.
My base "Page Object" essentially initializes the driver and is used in every other page object child class (for the individual pages)
class PageObject
include Capybara::DSL
BASE_URL = 'https://www.atesturl.com'
Capybara.default_max_wait_time = 5
def initialize
Capybara.register_driver :selenium_chrome do |app|
Capybara::Selenium::Driver.load_selenium
browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
# Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
opts.args << '--disable-site-isolation-trials'
end
Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options)
end
Capybara.register_driver :selenium_chrome_headless do |app|
Capybara::Selenium::Driver.load_selenium
browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
opts.args << '--headless'
opts.args << 'window-size=2880,1800'
opts.args << '--disable-gpu' if Gem.win_platform?
#opts.args << '--remote-debugging-port=9222'
# Workaround https://bugs.chromium.org/p/chromedriver/issues/detail?id=2650&q=load&sort=-id&colspec=ID%20Status%20Pri%20Owner%20Summary
opts.args << '--disable-site-isolation-trials'
end
Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options)
end
Capybara.current_driver = :selenium_chrome
end
def visit_url
visit BASE_URL
end
end
In most examples of PoM I see the methods returning an instance of that page object, but generally they use some #browser instance variable that is passed around. Within my test scripts I simple call an instance of the Base page object class via let(:p) {PageObject.new} and then p.visit_url then make new instances of the other page objects via new....but this seems like the wrong way to do it.
How exactly do I return a instance of the #browser or driver that I can pass around? And how should I be calling it in my spec?

You don't want to be registering your drivers in the initialize for the base PageObject since that means every object created will be registering new driver configs - which isn't desirable.
When you include Capybara::DSL into your class you're including methods that end up calling methods on Capybara.current_session. ie visit => Capybara.current_session.visit. The result of Capybara.current_session is the "#browser" instance you're asking about since it encapsulates the driver/browser instance. The issue with the way you've currently implemented that is that if any code changes the current session then all your objects will suddenly refer to the new session. If instead you store a reference to the session you want the object to use in each object and call the Capybara methods on that session rather than using Capybara::DSL (#session.visit ...) then you can be sure the session an object is using doesn't change unexpectedly.
Also note that things like Capybara.default_max_wait_time, `Capybara.current_driver', etc are global settings so setting them inside your PageObject class isn't a great idea.

Related

Capybara.page not in scope after extending capybara-screenshot's after_failed_example method

I'm trying to override the after_failed_example method so I can inflict some custom file naming on our screenshots. I'm loading the module as an initializer.
So far, so good, but the Capybara.page.current_url is blank, making me think I need to require something additional?
require "capybara-screenshot/rspec"
module Capybara
module Screenshot
module RSpec
class << self
attr_accessor :use_description_as_filename
attr_accessor :save_html_file
end
self.use_description_as_filename = true
self.save_html_file = true
def self.after_failed_example(example)
if example.example_group.include?(Capybara::DSL) # Capybara DSL method has been included for a feature we can snapshot
Capybara.using_session(Capybara::Screenshot.final_session_name) do
puts ">>>> Capybara.page.current_url: " + Capybara.page.current_url.to_s
if Capybara::Screenshot.autosave_on_failure && failed?(example) && Capybara.page.current_url != ''
saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, Capybara::Screenshot.save_html_file?, set_saver_filename_prefix(example))
saver.save
example.metadata[:screenshot] = {}
example.metadata[:screenshot][:html] = saver.html_path if saver.html_saved?
example.metadata[:screenshot][:image] = saver.screenshot_path if saver.screenshot_saved?
end
end
end
private
def self.set_saver_filename_prefix(example)
return example.description.to_s.gsub(" ", "-") if Capybara::Screenshot.use_description_as_filename?
return Capybara::Screenshot.filename_prefix_for(:rspec, example)
end
end
end
end
end
This is successfully overriding the capybara-screenshot/rspec method, and any of the Capybara::Screenshot static information is accessible, but not Capybara session related information (afa I can tell).
For example, Capybara.page.current_url.to_s is null when overridden, but present when not.
I was missing a require (kind of silly mistake):
require 'capybara/rspec'

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...
...

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.

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,

Ruby Threads with Watir

I have several classes written that govern how I want to handle several websites, with similar methods in both (ie. login, refresh). Each class opens up its own WATIR browser instance.
class Site1
def initialize
#ie = Watir::Browser.new
end
def login
#ie.goto "www.blah.com"
end
end
a sample of code in the main with no threads is as follows
require 'watir'
require_relative 'site1'
agents = []
agents << Site1.new
agents.each{ |agent|
agent.login
}
This works fine, but doesnt move onto the next agent until the current one has finished logging in. I would like to incorporate multithreading to handle this, but cant seem to get it to work.
require 'watir'
require_relative 'site1'
agents = []; threads = []
agents << Site1.new
agents.each{ |agent|
threads << Thread.new(agent){ agent.login }
}
threads.each { |t| t.join }
this gives me the error: unknown property or method: navigate. HRESULT error code:0x8001010e. The application called an interface that was marshalled for a different thread.
does anyone know how to fix this, or how to implement a similar functionality?
Not really sure on this, but here is a swing using threads.
require 'thread'
threads = [] # Setting an array to store threaded commands
c_thread = Thread.new do # Start a new thread
login # Call our command in the thread
end
threads << c_thread

Resources