Support for the Page Object pattern in Ruby - ruby

In Ruby-land we have Capybara and Webrat to drive our web browsers during functional testing with Cucumber.
What I can't find is something like Geb in Groovy/Java-land which seems like it works on one level of abstraction higher than Capybara. This is the description of Geb from the Geb website.
Geb is a browser automation solution.
It brings together the power of WebDriver, the elegance of jQuery
content selection, the robustness of Page Object modelling and the
expressiveness of the Groovy language.
Capybara already brings together WebDriver (usually Selenium) and jQuery-style content selection. But it doesn't have any support for the Page Object idea. (You create classes to represent the pages under test, so the steps carry out actions upon them rather than look at the DOM directly all the time. Like a mini-API for your page.)
To give an example of the kind of useful feature I'm looking for, I understand from a colleague that Geb can automatically assert that the page under test matches the attributes in the virtual page object which represents the page to your Cucumber tests.

I've made use of Site Prism for page-objects in a fairly large application. Cheezy's page-object gem was the other gem that I considered at the time but it didn't make use of Capybara (which when used correctly can aid with timing issues). The page-object gem has it's own "wait" mechanism.
There's also another gem but I suspect it's abandoned.
The page-object gem will give you test code along these lines:
class LoginPage
include PageObject
page_url "http://example.com/login"
text_field(:username, :id => 'username')
text_field(:password, :id => 'password')
button(:login, :id => 'login')
def login_with(username, password)
self.username = username
self.password = password
login
end
end
# in your tests
visit_page LoginPage do |page|
page.login_with('testuser1#example.com', 'incorrect')
page.wait_until do # using default of 30s for this asynch call
page.text.include? 'invalid user or password'
end
expect(page).to have_content 'invalid user or password'
More examples can be seen in this project: https://github.com/JonKernPA/pageobject and on the wiki https://github.com/cheezy/page-object/wiki/Elements
Site Prism looks like this:
class LoginPage < SitePrism::Page
set_url '/login'
element :username_field, '#username'
element :password_field, '#password'
element :login_button, '#login'
def login_with(username, password)
username_field.set username
password_field.set password
login_button.click # this uses capybara to find('#login').click
end
end
# in your tests
#page = LoginPage.new
#page.load
#page.login_with('testuser1#example.com', 'incorrect')
# capybara automatically waits for us
expect(#page).to have_content 'invalid user or password'
The Site Prism README has a lot of good examples. Everything else you need to know is in Capybara's excellent README and documentation.
There are of course far more differences than these small example shows.
I would advise you to take a look at both and decide what your requirements are.

Related

Including shared example from rspec inside of cucumber

The end goal is to include the shared_examples.rb which is included in rails_helper.rb. shared_examples.rb is a copy of this file
https://github.com/tinfoil/devise-two-factor/blob/master/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb
I want to reference the shared_examples.rb in my cucumber test so I can use the method it_behaves_like 'two_factor_authenticatable'
I have the following folder structure:
Updated to include #morissetcl suggested structure
features
step_definitions
sample_step.rb
support
env.rb
sample.feature
spec
models
user_spec.rb
support
shared_examples
shared_example.rb
rails_helper.rb
spec_helper.rb
Both the features and spec folders are at the root of my rails project.
I am trying to include inside the sample_step.rb file the rails_helper.rb which is located in the spec folder.
I tried using different types of require as shown below inside the sample_step.rb file.
require 'spec/spec_helpers/shared_examples'
require '../../spec/spec_helpers/shared_examples'
require_relative '../../spec/spec_helpers/shared_examples'
I keep getting the following error
undefined method `it_behaves_like' for main:Object (NoMethodError)
it_behaves_like is rspec specific. What it does is allow one spec to run another spec for a particular object, so that is not going to work in Cucumber.
What you need to do is have a test suite that has some cukes and some specs. You cover the two factor authentication in detail in rspec, and if you have to always use two factor authentication to login in your cukes you need to write a helper method so you can do that.
To do this I would do the following
Write some step definitions to support login, that delegate the work to a helper method.
Write the helper method
Have the helper method call additional methods to support two factor auth
Add the helper methods to cucumber's world object so they can be called in a step def
So
# features/step_defintions/login
module LoginStepHelper
def login(user: )
login_fill_form_and_submit(user: user)
login_two_factor(user: user)
submit_form
end
def login_fill_form_and_submit(user: )
fill_in :email, user.email
fill_in :password, user.password
end
def login_two_factor(user: )
code = retrieve_2factor_code(user: user)
fill_in :2factor, code
end
...
end
World(LoginStepHeler)
So now you have to work out how the test can get the 2factor code.
Once you have this done you have a tool your step definitions can use to login, so you can write things like
Given 'I am logged in' do
login user: #i
end
Given 'I login as Fred' do
login user: #fred
end
...
note: how many step defs can use your helper method.
You can find more detail about this approach here https://github.com/diabolo/cuke_up which includes details about how to create the test-users that we are passing into the login function in the above code
The solution I came up with was using some of the suggestions that #diabolist made. I looked into the shared examples file: https://github.com/tinfoil/devise-two-factor/blob/master/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb
I then used the #validate_and_consume_otp! scenario and setup the data using the info in the before :each block.
The result was as so in my cucumber spec:
When('I fill in the login form with two factor code') do
Timecop.freeze(Time.current)
otp_secret = '2z6hxkdwi3uvrnpn'
#user.otp_secret = otp_secret
otp = ROTP::TOTP.new(otp_secret).at(Time.now)
#user.save # <- This is important to save it so the user has the otp you will pass
Timecop.return
fill_in 'user_email', with: 'user#example.com'
fill_in 'user_password', with: 'password'
fill_in 'user_otp_attempt', with: otp
click_button 'Login'
end
The setup can be cleaned up and put into a method just as #diabolist described so it can be reused by other tests.

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.

Using SitePrism with Rspec and Capybara feature specs

I recently discovered SitePrism via the rubyweekly email.
It looks amazing. I can see its going to be the future.
The examples I have seen are mostly for cucumber steps.
I am trying to figure out how one would go about using SitePrism with rspec.
Assuming #home_page for the home page, and #login_page for the login_page
I can understand that
#home_page.load # => visit #home.expanded_url
however, the part I am not sure about, is if I think click on for example the "login" link, and the browser in Capybara goes to the login page - how I can then access an instance of the login page, without loading it.
#home_page = HomePage.new
#home_page.load
#home.login_link.click
# Here I know the login page should be loaded, so I can perhaps do
#login_page = LoginPage.new
#login_page.should be_displayed
#login_page.email_field.set("some#email.com")
#login_page.password_field.set("password")
#login_page.submit_button.click
etc...
That seems like it might work. So, when you know you are supposed to be on a specific page, you create an instance of that page, and somehow the capybara "page" context, as in page.find("a[href='/sessions/new']") is transferred to the last SitePrism object?
I just feel like I am missing something here.
I'll play around and see what I can figure out - just figured I might be missing something.
I am looking through the source, but if anyone has figured this out... feel free to share :)
What you've assumed turns out to be exactly how SitePrism works :) Though you may want to check the epilogue of the readme that explains how to save yourself from having to instantiate page objects all over your test code. Here's an example:
# our pages
class Home < SitePrism::Page
#...
end
class SearchResults < SitePrism::Page
#...
end
# here's the app class that represents our entire site:
class App
def home
Home.new
end
def results_page
SearchResults.new
end
end
# and here's how to use it:
#first line of the test...
#app = App.new
#app.home.load
#app.home.search_field.set "sausages"
#app.home.search_button.click
#app.results_page.should be_displayed

Why the object references are not correctly passed in this RSpec script?

I have to say I am new both to Ruby and to RSpec. Anyway I completed one RSpec script but after refactoring it failed. Here is the original working version:
describe Site do
browser = Watir::Browser.new :ie
site = Site.new(browser, "http://localhost:8080/site")
it "can navigate to any page at the site" do
site.pages_names.each do |page_name|
site.goto(page_name)
site.actual_page.name.should eq page_name
end
end
browser.close
end
and here is the modified version - I wanted to have reported all the pages which were visited during the test:
describe Site do
browser = Watir::Browser.new :ie
site = Site.new(browser, "http://localhost:8080/site")
site.pages_names.each do |page_name|
it "can navigate to #{page_name}" do
site.goto(page_name)
site.actual_page.name.should eq page_name
end
end
browser.close
end
The problem in the latter case is that site gets evaluated to nil within the code block associated with 'it' method.
But when I did this:
...
s = site
it "can navigate to #{page_name}" do
s.goto(page_name)
s.actual_page.name.should eq page_name
end
...
the nil problem was gone but tests failed with the reason "browser was closed"
Apparently I am missing something very basic Ruby knowledge - because the browser reference is not working correctly in modified script. Where did I go wrong? What refactoring shall be applied to make this work?
Thanks for your help!
It's important to understand that RSpec, like many ruby programs, has two runtime stages:
During the first stage, RSpec loads each of your spec files, and executes each of the describe and context blocks. During this stage, the execution of your code defines your examples, the hooks, etc. But your examples and hooks are NOT executed during this stage.
Once RSpec has finished loading the spec files (and all examples have been defined), it executes them.
So...trimming down your example to a simpler form, here's what you've got:
describe Site do
browser = Watir::Browser.new :ie
it 'does something with the browser' do
# do something with the browser
end
browser.close
end
While visually it looks like the browser instance is instantiated, then used in the example, then closed, here's what's really happening:
The browser instance is instantiated
The example is defined (but not run)
The browser is closed
(Later, after all examples have been defined...) The example is run
As O.Powell's answer shows, you can close the browser in an after(:all) hook to delay the closing until after all examples in this example group have run. That said, I'd question if you really need the browser instance at example definition time. Generally you're best off lazily creating resources (such as the browser instance) when examples need them as they are running, rather than during the example definition phase.
I replicated your code above using fake classes for Site and Watir. It worked perfectly. My only conclusion then is that the issue must lie with either one of the above classes. I noticed the Site instance only had to visit one page in your first working version, but has to visit multiple pages in the non working version. There may be an issue there involving the mutation happening inside the instance.
See if this makes a difference:
describe Site do
uri = "http://localhost:8080/site"
browser = Watir::Browser.new :ie
page_names = Site.new(browser, uri).page_names
before(:each) { #site = Site.new(browser, uri) }
after(:all) { browser.close }
pages_names.each do |page_name|
it "can navigate to #{page_name}" do
#site.goto(page_name)
#site.actual_page.name.should eq page_name
end
end
end

Page objects in watir splash -- how to ensure that the proper page object is instantiated

I am using the Watir Splash framework to test a web application, and I have setup two page classes. The first is the "Login" page which is detailed here:
module App
module Page
class Login < WatirSplash::Page::Base
url "http://[removed].com"
def login_btn
modify button(:id => 'btnLogin'), :click => lambda {redirect_to VehicleSelection}
end
The other page class is the "Vehicle Selection" page. I have used the modify method as shown in the documentation here to ensure that the vehicle selection page object is available for RSpec after a successful login.
But what happens if the login failed? I have some test cases that deliberately feed incorrect information into the login form to ensure that the authentication is working properly. RSpec would need the methods defined in the "Login" class to access the correct elements to complete the test case. In this case, the way that I have specified the method a "VehicleSeleciton" object will be returned regardless. (or so it appears)
Any help is appreciated. Also, I'm open to other suggestions for testing frameworks, especially if there is more example code for me to reference.
Below are a couple of approaches I have tried. I was not using the WatirSplash framework, but the same concepts applied (though the attempted WatirSplash example code might not be 100% accurate).
Solution 1: Do return page objects
My personal preference is to not have page objects returning page objects. Instead, I find it easier to read/work with explicit initializations of each page object within the test. Alister Scott discussed this in his blog.
Your tests would then look like:
#For login successful tests
page = App::Page::Login.new
page.login_btn.click
page = App::Page::VehicleSelection.new #The VehicleSelection page is explicitly initialized
page.validate_page #or whatever you want to do with the page
#For login failed tests
page = App::Page::Login.new
page.login_btn.click
page.validate_page #or whatever you want to do with the page
Solution 2: Create multiple methods for login
Another solution, would be to create two login methods - one for successful login and one for unsuccessful login.
The page object could be:
module App
module Page
class Login < WatirSplash::Page::Base
url "http://[removed].com"
def login(user, password)
#Do whatever code to input name and password and then click the button
#Then redirect to the VehicleSelection page since that is where you will want to go most often
redirect_to VehicleSelection
end
def login_failed(user, password)
login(user, password)
#Return the Login page (instead of the VehicleSelection page).
redirect_to Login
end
end
end
end
With the tests being:
#For login successful tests
login_page = App::Page::Login.new
vehicle_page = login_page.login(user, password)
vehicle_page.validate_page #or whatever you want to do with the Vehicle Selection page
#For login failed tests
login_page = App::Page::Login.new
login_page.login_failed(user, password)
login_page.validate_page #or whatever you want to do with the Login page
Solution 3: Make the button know where it is going
Another solution, would be to have the login button know which page to redirect to.
The page object could be:
module App
module Page
class Login < WatirSplash::Page::Base
url "http://[removed].com"
def login_btn(login_successful=true)
if login_successful
modify button(:id => 'btnLogin'), :click => lambda {redirect_to VehicleSelection}
else
modify button(:id => 'btnLogin'), :click => lambda {redirect_to Login}
end
end
end
end
end
With the tests being:
#For login successful tests
login_page= App::Page::Login.new
vehicle_page = login_page.login_btn.click
vehicle_page.validate_page #or whatever you want to do with the Vehicle Selection page
#For login failed tests
login_page= App::Page::Login.new
login_page.login_btn(false).click
login_page.validate_page #or whatever you want to do with the Login page
Thanks for trying out my gem WatirSplash. I would have written something in the lines of solution #2 - e.g. create two separate methods for successful login and failed login. Using #modify is not needed in either method, like Justin did.
Also, i'd suggest you to use my other gem test-page instead, which is more or less the same, as Page Objects in WatirSplash, but it is extracted into separate gem - WatirSplash will be deprecated in the long term due to all of its parts being exctracted into separate gems giving better control of which functionality is needed in each project.

Resources