A Page Object Model DSL for Capybara
SitePrism gives you a simple, clean and semantic DSL for describing your site using the Page Object Model pattern, for use with Capybara in automated acceptance testing.
Find the pretty documentation here: https://rdoc.info/gems/site_prism/frames
Make sure to add your project/company to https://github.com/site-prism/site_prism/wiki/Who-is-using-SitePrism
We love it when people want to get involved with our Open Source Project.
We have a brief set of setup docs HERE
SitePrism is built and tested to work on Ruby 2.4 - 2.6. Ruby 2.3 (Now EOL), is supported but not tested against. If you are using SitePrism with Ruby 2.3 it is highly advisable to upgrade to a more modern Ruby such as 2.6 or 2.7, if for any other reason, to get a noticeable speed boost!
SitePrism should run on all major browsers. The gem's integration tests are run on Chrome and Firefox.
If you find you cannot integrate nicely with SitePrism, please open an issue request
Here's an overview of how SitePrism is designed to be used:
# define our site's pagesclass Home < SitePrism::Page set_url '/index.htm' set_url_matcher(/google.com/?/)
element :search_field, 'input[name="q"]' element :search_button, 'button[name="btnK"]' elements :footer_links, '#footer a' section :menu, Menu, '#gbx3' end
class SearchResults < SitePrism::Page set_url_matcher(/google.com/results?.*/)
section :menu, Menu, '#gbx3' sections :search_results, SearchResults, '#results li'
def search_result_links search_results.map { |result| result.title['href'] } end end
define sections used on multiple pages or multiple times on one page
class Menu < SitePrism::Section element :search, 'a.search' element :images, 'a.image-search' element :maps, 'a.map-search' end
class SearchResults < SitePrism::Section element :title, 'a.title' element :blurb, 'span.result-description' end
now for some tests
When(/^I navigate to the google home page$/) do @home = Home.new @home.load end
Then(/^the home page should contain the menu and the search form$/) do @home.wait_until_menu_visible # menu loads after a second or 2, give it time to arrive expect(@home).to have_menu expect(@home).to have_search_field expect(@home).to have_search_button end
When(/^I search for Sausages$/) do @home.search_field.set 'Sausages' @home.search_button.click end
Then(/^the search results page is displayed$/) do @results_page = SearchResults.new expect(@results_page).to be_displayed end
Then(/^the search results page contains 10 individual search results$/) do @results_page.wait_until_search_results_visible expect(@results_page).to have_search_results(count: 10) end
Then(/^the search results contain a link to the wikipedia sausages page$/) do expect(@results_page.search_result_links).to include('http://en.wikipedia.org/wiki/Sausage') end
Now for the details...
To install SitePrism:
gem install site_prism
If you are using cucumber, here's what needs requiring:
require 'capybara' require 'capybara/cucumber' require 'selenium-webdriver' require 'site_prism' require 'site_prism/all_there' # Optional but needed to perform more complex matching
The driver creation is identical to how you would normally create a Capybara driver, a sample Selenium one could look something like this...
Capybara.register_driver :site_prism do |app| browser = ENV.fetch('browser', 'firefox').to_sym Capybara::Selenium::Driver.new(app, browser: browser, desired_capabilities: capabilities) endThen tell Capybara to use the Driver you've just defined as its default driver
Capybara.configure do |config| config.default_driver = :site_prism end
If you're using rspec instead, here's what needs requiring:
require 'capybara' require 'capybara/rspec' require 'selenium-webdriver' require 'site_prism' require 'site_prism/all_there' # Optional but needed to perform more complex matching
And again, as above, a sample driver is no different to a normal driver instantiation in Capybara.
The Page Object Model is a test automation pattern that aims to create an abstraction of your site's user interface that can be used in tests. The most common way to do this is to model each page as a class, and to then use instances of those classes in your tests.
If a class represents a page then each element of the page is represented by a method that, when called, returns a reference to that element that can then be acted upon (clicked, set text value), or queried (is it enabled? / visible?).
SitePrism is based around this concept, but goes further as you'll see below by also allowing modelling of repeated sections that appear on multiple pages, or many times on a page using the concept of sections.
As you might be able to guess from the name, pages are fairly central to the Page Object Model. Here's how SitePrism models them:
The simplest page is one that has nothing defined in it. Here's an example of how to begin modelling a home page:
class Home < SitePrism::Page end
The above has nothing useful defined, so to start with lets give it some properties.
A page usually has a URL. If you want to be able to navigate to a page, you'll need to set its URL. Here's how:
class Home < SitePrism::Page set_url 'http://www.mysite.com/home.htm' end
If you've set Capybara's
app_hostthen you can set the URL as follows:
class Home < SitePrism::Page set_url '/home.htm' end
Note that setting a URL is optional - you only need to set a url if you want to be able to navigate directly to that page. It makes sense to set the URL for a page model of a home page or a login page, but probably not a search results page.
SitePrism uses the
addressablegem and therefore allows for parameterization of URLs. Here is a simple example:
class UserProfile < SitePrism::Page set_url '/users{/username}' end
...and a more complex example:
class Search < SitePrism::Page set_url '/search{?query*}' end
See https://github.com/sporkmonger/addressable for more details on parameterized URLs.
Once the URL has been set (using
set_url), you can navigate directly to the page using
#load:
@home_page = Home.new @home_page.load
The
#loadmethod takes parameters and will apply them to the URL. Using the examples above:
class UserProfile < SitePrism::Page set_url '/users{/username}' end@user_profile = UserProfile.new @user_profile.load #=> /users @user_profile.load(username: 'bob') #=> loads /users/bob
...and...
class Search < SitePrism::Page set_url '/search{?query*}' end@search = Search.new @search.load(query: 'simple') #=> loads /search?query=simple @search.load(query: {'color'=> 'red', 'text'=> 'blue'}) #=> loads /search?color=red&text=blue
This will tell whichever capybara driver you have configured to navigate to the URL set against that page's class.
See https://github.com/sporkmonger/addressable for more details on parameterized URLs.
Automated tests often need to verify that a particular page is displayed. SitePrism can automatically parse your URL template and verify that whatever components your template specifies match the currently viewed page. For example, with the following URL template:
class Account < SitePrism::Page set_url '/accounts/{id}{?query*}' end
The following test code would pass:
@account_page = Account.new @account_page.load(id: 22, query: { token: 'ca2786616a4285bc' })expect(@account_page.current_url).to end_with('/accounts/22?token=ca2786616a4285bc') expect(@account_page).to be_displayed
Calling
#displayed?will return true if the browser's current URL matches the page's template and false if it doesn't. It will wait for
Capybara.default_max_wait_timeseconds or you can pass an explicit wait time in seconds as the first argument like this:
@account_page.displayed?(10) # wait up to 10 seconds for display
Sometimes you want to verify not just that the current URL matches the template, but that you're looking at a specific page matching that template.
Given the previous example, if you wanted to ensure that the browser had loaded account number 22, you could assert the following:
expect(@account_page).to be_displayed(id: 22)
You can even use regular expressions. If for example, you wanted to ensure that the browser was displaying an account with an id ending with 2, you could do:
expect(@account_page).to be_displayed(id: /2\z/)
If passing options to
displayed?isn't powerful enough to meet your needs, you can directly access and assert on the
url_matchesfound when comparing your page's URL template to the current_url:
@account_page = Account.new @account_page.load(id: 22, query: { token: 'ca2786616a4285bc', color: 'irrelevant' })expect(@account_page).to be_displayed(id: 22) expect(@account_page.url_matches['query']['token']).to eq('ca2786616a4285bc')
If SitePrism's built-in URL matching is not sufficient for your needs you can override and use SitePrism's previous support for regular expression-based URL matchers by it by calling
set_url_matcher:
class Account < SitePrism::Page set_url_matcher(/accounts/\d+/) end
SitePrism's
#displayed?predicate method allows for semantic code in your tests:
Then(/^the account page is displayed$/) do expect(@account_page).to be_displayed expect(@some_other_page).not_to be_displayed end
SitePrism allows you to get the current page's URL. Here's how it's done:
class Account < SitePrism::Page end@account = Account.new @account.current_url #=> "http://www.example.com/account/123" expect(@account.current_url).to include('example.com/account/')
Getting a page's title isn't hard:
class Account < SitePrism::Page end@account = Account.new @account.title #=> "Welcome to Your Account"
You can easily tell if the page is secure or not by checking to see if the current URL begins with 'https' or not. SitePrism provides the
secure?method that will return true if the current url begins with 'https' and false if it doesn't. For example:
class Account < SitePrism::Page end@account = Account.new @account.secure? #=> true/false expect(@account).to be_secure
Pages are made up of elements (text fields, buttons, combo boxes, etc), either individual elements or groups of them. Examples of individual elements would be a search field or a company logo image; examples of element collections would be items in any sort of list, eg: menu items, images in a carousel, etc.
To interact with individual elements, they need to be defined as part of the relevant page. SitePrism makes this easy:
class Home < SitePrism::Page element :search_field, 'input[name="q"]' end
Here we're adding a search field to the Home page. The
elementmethod takes 2 arguments: the name of the element as a symbol, and a css selector as a string.
The
elementmethod will add a number of methods to instances of the particular Page class. The first method added is the name of the element. It finds the element using Capybara::Node::Finders#find returning a Capybara::Node::Element or raising Capybara::ElementNotFound if the element can not be found.
class Home < SitePrism::Page set_url 'http://www.google.com'element :search_field, 'input[name="q"]' end
... the following shows how to get hold of the search field:
@home = Home.new @home.load@home.search_field #=> will return the capybara element found using the selector @home.search_field.set 'the search string' #=>
search_field
returns a capybara element, so use the capybara API to deal with it @home.search_field.text #=> standard method on a capybara element; returns a string
Another method added to the Page class by the
elementmethod is the
has_?method. This method delegates to Capybara::Node::Matchers#has_selector?. Using the same example as above:
class Home < SitePrism::Page set_url 'http://www.google.com'element :search_field, 'input[name="q"]' end
... you can test for the existence of the element on the page like this:
@home = Home.new @home.load @home.has_search_field? #=> returns true if it exists, false if it doesn't
...which makes for nice test code:
Then(/^the search field exists$/) do expect(@home).to have_search_field end
To test that an element does not exist on the page, it is not possible to just call
#not_to have_search_field. SitePrism supplies the
#has_no_?method that should be used to test for non-existence. This method delegates to Capybara::Node::Matchers#hasnoselector? Using the above example:
@home = Home.new @home.load @home.has_no_search_field? #=> returns true if it doesn't exist, false if it does
...which makes for nice test code:
Then(/^the search field exists$/)do expect(@home).to have_no_search_field #NB: NOT => expect(@home).not_to have_search_field end
A method that gets added by calling
elementis the
wait_until__visiblemethod. This method delegates to Capybara::Node::Matchers#has_selector?. Calling this method will cause the test to wait for Capybara's default wait time for the element to become visible. You can customise the wait time by supplying a number of seconds to wait in-line or configuring the default wait time.
@home.wait_until_search_field_visible # or... @home.wait_until_search_field_visible(wait: 10)
Another method added by calling
elementis the
wait_until__invisiblemethod. This method delegates to Capybara::Node::Matchers#hasnoselector?. Calling this method will cause the test to wait for Capybara's default wait time for the element to become invisible. You can as with the visibility waiter, customise the wait time in the same way.
@home.wait_until_search_field_invisible # or... @home.wait_until_search_field_invisible(wait: 10)
While the above examples all use CSS selectors to find elements, it is possible to use XPath expressions too. In SitePrism, everywhere that you can use a CSS selector, you can use an XPath expression.
An example:
class Home < SitePrism::Page # CSS Selector element :first_name, 'div#signup input[name="first-name"]'Identical selector as an XPath expression
element :first_name, :xpath, '//div[@id="signup"]//input[@name="first-name"]' end
Given:
class Home < SitePrism::Page element :search_field, 'input[name="q"]' end
Then the following methods are available:
@home.search_field @home.has_search_field? @home.has_no_search_field? @home.wait_until_search_field_visible @home.wait_until_search_field_invisible
Sometimes you don't want to deal with an individual element but rather with a collection of similar elements, for example, a list of names. To enable this, SitePrism provides the
elementsmethod on the Page class. Here's how it works:
class Friends < SitePrism::Page elements :names, 'ul#names li a' end
Just like the
elementmethod, the
elementsmethod takes 2 arguments: the first being the name of the elements as a symbol, the second is the css selector (Or locator strategy), that would return capybara elements.
Just like the
elementmethod, the
elementsmethod adds a few methods to the Page class. The first one is of the name of the element collection which returns an array of capybara elements that match the css selector. Using the example above:
class Friends < SitePrism::Page elements :names, 'ul#names li a' end
You can access the element collection like this:
@friends_page = Friends.new # ... @friends_page.names #=> [<:element>, <:element>, <:element>]
With that you can do all the normal things that are possible with arrays:
@friends_page.names.each { |name| puts name.text } @friends_page.names.map { |name| name.text }
Or even run some tests ...
expect(@friends_page.names.map { |name| name.text }).to eq(['Alice', 'Bob', 'Fred']) expect(@friends_page.names.size).to eq(3)
Just like the
elementmethod, the
elementsmethod adds a method to the page that will allow you to check for the existence of the collection, called
has_?. As long as there is at least 1 element in the array, the method will return
true, otherwise it wil return
false. For example, with the following page:
class Friends < SitePrism::Page elements :names, 'ul#names li a' end
Then the following method is available:
@friends_page.has_names? #=> returns true if at least one `name` element is found
This in turn allows the following nice test code
Then(/^there should be some names listed on the page$/) do expect(@friends_page).to have_names #=> This only passes if there is at least one `name` end
Like an individual element, calling the
elementsmethod will create two methods:
wait_until__visibleand
wait_until__invisible. Calling these methods will cause your test to wait for the elements to become visible or invisible. Using the above example:
@friends_page.wait_until_names_visible # and... @friends_page.wait_until_names_invisible
It is possible to wait for a specific amount of time instead of using the default Capybara wait time:
@friends_page.wait_until_names_visible(wait: 5) # and... @friends_page.wait_until_names_invisible(wait: 7)
Throughout my time in test automation I keep getting asked to provide the ability to check that all elements that should be on the page are on the page. Why people would want to test this, I don't know. But if that's what you want to do, SitePrism provides the
#all_there?method that will return
trueif all mapped items are present in the browser and
falseif they're not all there.
@friends_page.all_there? #=> true/falseand...
Then(/^the friends page contains all the expected elements$/) do expect(@friends_page).to be_all_there end
You may wish to have elements declared in a page object class that are not always guaranteed to be present (success or error messages, etc.). If you'd still like to test such a page with
all_there?you can declare
expected_elementson your page object class that narrows the elements included in
all_there?check to those that definitely should be present.
class TestPage < SitePrism::Page element :name_field, '#name' element :address_field, '#address' element :success_message, 'span.alert-success'expected_elements :name_field, :address_field end
And if you aren't sure which elements will be present, Then ask SitePrism to tell you!
class TestPage < SitePrism::Page element :name_field, '#name' element :address_field, '#address' element :success_message, 'span.alert-success' endand... Only
address_field
is on the page@test_page.elements_present #=> [:address_field]
If you are specifying a highly nested set of sections inside a Page and need to recurse through them to find out if all of your items are present then you can also do this.
Simply pass a recursion parameter to the
#all_there?check. Note that the only valid values for this at the moment are
:noneand
:one
Passing
:none(default), will not change the functionality. However passing in
:onewill cause
site_prismto recurse through all
section/
sectionsitems defined in your present scope.
Work alongside developing this functionality further is being continued in the siteprism-allthere repo. So head on over there if you're interested in how this feature will work going forwards
NB: At the moment a "primitive" but working copy of this is hosted inside this gem. But if you wish to use the bleeding edge version of the logic. Then simply set the following configuration parameter
`require 'site_prism/all_there'`SitePrism.use_all_there_gem = true
If
#all_there?returns false and you wish to get the list of missing elements for debugging purposes you may want to use
#elements_missingmethod. It will return all missing elements from the expected_elements list
If you do not provide a list of
expected_elementsthis method will return all elements that are missing on the page; from those which are defined.
class Home < SitePrism::Page element :name, '#name' element :address, '#address' element :success_message, 'span.alert-success'expected_elements :name, :address end
and... Only
address
is on the page@test_page.elements_missing #=> [:name]
SitePrism allows you to model sections of a page that appear on multiple pages or that appear a number of times on a page separately from Pages. SitePrism provides the Section class for this task.
In the same way that SitePrism provides
elementand
elements, it provides
sectionand
sections. The first returns an instance of a page section, the second returns an array of section instances, one for each capybara element found by the supplied css selector. What follows is an explanation of
section.
A section is similar to a page in that it inherits from a SitePrism class:
class Menu < SitePrism::Section end
At the moment, this section does nothing.
Pages include sections that's how SitePrism works. Here's a page that includes the above
Menusection:
class Home < SitePrism::Page section :menu, Menu, '#gbx3' end
The way to add a section to a page (or another section - which is possible) is to call the
sectionmethod. It takes 3 arguments: the first is the name of the section as referred to on the page (sections that appear on multiple pages can be named differently). The second argument is the class of which an instance will be created to represent the page section, and the following arguments are Capybara::Node::Finders. These identify the root node of the section on this page (note that the css selector can be different for different pages as the whole point of sections is that they can appear in different places / ways on different pages).
You can define a section as a class and/or an Anonymous section. This will then allow you to have some handy constructs like the one below
class People < SitePrism::Section element :footer, 'h4' endclass Home < SitePrism::Page
section people_with_block will have
headline
and
footer
elements in itsection :people_with_block, People do element :headline, 'h2' end end
The 3rd argument (Locators), can be omitted if you are re-using the same locator for all references to the section Class. In order to do this, simply tell SitePrism that you want to use default search arguments.
class People < SitePrism::Section set_default_search_arguments '.people' endclass Home < SitePrism::Page section :people, People end
The
sectionmethod (like the
elementmethod) adds a few methods to the page or section class it was called against. The first method that is added is one that returns an instance of the section, the method name being the first argument to the
sectionmethod. Here's an example:
# the sectionclass Menu < SitePrism::Section end
the page that includes the section
class Home < SitePrism::Page section :menu, Menu, '#gbx3' end
the page and section in action
@home = Home.new @home.menu #=> <menusection...> </menusection...>
When the
menumethod is called against
@home, an instance of
Menu(the second argument to the
sectionmethod) is returned. The third argument that is passed to the
sectionmethod is the locator that will be used to find the root element of the section; this root node becomes the 'scope' of the section.
The following shows that though the same section can appear on multiple pages, it can take a different root node:
# define the section that appears on both pagesclass Menu < SitePrism::Section end
define 2 pages, each containing the same section
class Home < SitePrism::Page section :menu, Menu, '#gbx3' end
class SearchResults < SitePrism::Page section :menu, Menu, '#gbx48' end
You can see that the
Menuis used in both the
Homeand
SearchResultspages, but each has slightly different root node. The capybara element that is found by the css selector becomes the root node for the relevant page's instance of the
Menusection.
This works just the same as adding elements to a page:
class Menu < SitePrism::Section element :search, 'a.search' element :images, 'a.image-search' element :maps, 'a.map-search' end
Note that the locators used to find elements are searched for within the scope of the root element of that section. The search for the element won't be page-wide but it will only look in the section.
When the section is added to a page ...
class Home < SitePrism::Page section :menu, Menu, '#gbx3' end
...then the section's elements can be accessed like this:
@home = Home.new @home.load@home.menu.search #=> returns a capybara element representing the link to the search page @home.menu.search.click #=> clicks the search link in the home page menu @home.menu.search['href'] #=> returns the value for the href attribute of the capybara element representing the search link @home.menu.has_images? #=> returns true or false based on whether the link is present in the section on the page @home.menu.wait_until_images_visible #=> waits for capybara's default wait time until the element is visible in the page section
This then leads to some pretty test code ...
Then(/^the home page menu contains a link to the various search functions$/) do expect(@home.menu).to have_search expect(@home.menu.search['href']).to include('google.com') expect(@home.menu).to have_images expect(@home.menu).to have_maps end
You can execute a block within the context of a Section. This is similar to Capybara's
withinmethod and allows for shorter test code particularly with nested sections. Test code that might have to repeat the block name can be shortened up this way.
Then(/^the home page menu contains a link to the various search functions$/) do @home.menu.within do |menu| expect(menu).to have_search expect(menu.search['href']).to include('google.com') expect(menu).to have_images expect(menu).to have_maps end end
Note that on an individual section it's possible to pass a block directly to the section without using
within. Because the block is executed only during
Sectioninitialization this won't work when accessing a single Section from an array of Sections. For that reason we recommend using
withinwhich works in either case.
Then(/^the home page menu contains a link to the various search functions$/) do @home.menu do |menu| # possible, but prefer: `@home.menu.within` expect(menu).to have_search end end
It is possible to ask a section for its parent (page, or section if this section is a subsection). For example, given the following setup:
class DestinationFilters < SitePrism::Section element :morocco, 'abc' endclass FilterPanel < SitePrism::Section section :destination_filters, DestinationFilters, 'def' end
class Home < SitePrism::Page section :filter_panel, FilterPanel, 'ghi' end
Then calling
#parentwill return the following:
@home = Home.new @home.load@home.filter_panel.parent #=> returns @home @home.filter_panel.destination_filters.parent #=> returns @home.filter_panel
It is possible to ask a section for the page that it belongs to. For example, given the following setup:
class Menu < SitePrism::Section element :search, 'a.search' element :images, 'a.image-search' element :maps, 'a.map-search' endclass Home < SitePrism::Page section :menu, Menu, '#gbx3' end
...you can get the section's parent page:
@home = Home.new @home.load @home.menu.parent_page #=> returns @home
Just like elements, it is possible to test for the existence of a section. The
sectionmethod adds a method called
has_to the page or section it's been added to - same idea as what the?
has_?method. Given the following setup:
class Menu < SitePrism::Section element :search, 'a.search' element :images, 'a.image-search' element :maps, 'a.map-search' endclass Home < SitePrism::Page section :menu, Menu, '#gbx3' end
You can check whether the section is present on the page or not:
@home = Home.new #... @home.has_menu? #=> returns true or false
Again, this allows pretty test code:
expect(@home).to have_menu expect(@home).not_to have_menu
Like an element, it is possible to wait for a section to become visible or invisible. Calling the
sectionmethod creates two methods on the relevant page or section:
wait_until__visibleand
wait_until__invisible. Using the above example, here's how they're used:
@home = Home.new @home.wait_until_menu_visible # and... @home.wait_until_menu_invisible
Again, as for an element, it is possible to give a specific amount of time to wait for visibility/invisibility of a section. Here's how:
@home = Home.new @home.wait_until_menu_visible(wait: 5) # and... @home.wait_until_menu_invisible(wait: 3)
You are not limited to adding sections only to pages; you can nest sections within sections within sections within sections!
# define a page that contains an area that contains a section for both # logging in and registration. Modelling each of the sub-sections separatelyclass Login < SitePrism::Section element :username, '#username' element :password, '#password' element :sign_in, 'button' end
class Registration < SitePrism::Section element :first_name, '#first_name' element :last_name, '#last_name' element :next_step, 'button.next-reg-step' end
class LoginRegistrationForm < SitePrism::Section section :login, Login, 'div.login-area' section :registration, Registration, 'div.reg-area' end
class Home < SitePrism::Page section :login_and_registration, LoginRegistrationForm, 'div.login-registration' end
how to login (fatuous, but demonstrates the point):
Then(/^I sign in$/) do @home = Home.new @home.load expect(@home).to have_login_and_registration expect(@home.login_and_registration).to have_username @home.login_and_registration.login.username.set 'bob' @home.login_and_registration.login.password.set 'p4ssw0rd' @home.login_and_registration.login.sign_in.click end
how to sign up:
When(/^I enter my name into the home page's registration form$/) do @home = Home.new @home.load expect(@home.login_and_registration).to have_first_name expect(@home.login_and_registration).to have_last_name @home.login_and_registration.first_name.set 'Bob'
...
end
If you want to use a section more as a namespace for elements and are not planning on re-using it, you may find it more convenient to define an anonymous section using a block:
class Home < SitePrism::Page section :menu, '.menu' do element :title, '.title' elements :items, 'a' end end
This code will create an anonymous section that you can use in the same way as an ordinary section:
@home = Home.new expect(@home.menu).to have_title
An individual section represents a discrete section of a page, but often sections are repeated on a page, an example is a search result listing - each listing contains a title, a url and a description of the content. It makes sense to model this only once and then to be able to access each instance of a search result on a page as an array of SitePrism sections. To achieve this, SitePrism provides the
sectionsmethod that can be called in a page or a section.
The only difference between
sectionand
sectionsis that whereas the first returns an instance of the supplied section class, the second returns an array containing as many instances of the section class as there are capybara elements found by the supplied css selector. This is better explained in the following example ...
Given the following setup:
class SearchResults < SitePrism::Section element :title, 'a.title' element :blurb, 'span.result-description' endclass Home < SitePrism::Page sections :search_results, SearchResults, '#results li' end
It is possible to access each of the search results:
@home = Home.new # ... @home.search_results.each do |result| puts result.title.text end
This allows for pretty tests ...
Then(/^there are lots of search_results$/) do expect(@results_page.search_results.size).to eq(10)@home.search_results.each do |result| expect(result).to have_title expect(result.blurb.text).not_to be_empty end end
The css selector that is passed as the 3rd argument to the
sectionsmethod (
#results li) is used to find a number of capybara elements. Each capybara element found using the css selector is used to create a new instance of
SearchResultsand becomes its root element. So if the css selector finds 3
lielements, calling
search_resultswill return an array containing 3 instances of
SearchResults, each with one of the
lielements as it's root element.
When using an iterator such as
eachto pass a block through to a collection of sections it is possible to skip using
within. However some caution is warranted when accessing the Sections directly from an array, as the block can only be executed when the section is being initialized. The following does not work:
@home.search_results.first do |result| # This block is silently ignored. expect(result).to have_title end
Instead use
withinto access the inner-context of the Section.
@home.search_results.first.within do |result| # This block is run within the context of the Section. expect(result).to have_title end
You can define collections of anonymous sections the same way you would define a single anonymous section:
class Home < SitePrism::Page sections :search_results, '#results li' do element :title, 'a.title' element :blurb, 'span.result-description' end end
Using the example above, it is possible to test for the existence of the sections. As long as there is at least one section in the array, the sections item is said to exist. The
sectionsmethod adds a
has_?method to the page/section that our section has been added to.
So given the example below, we can do the following ...
class SearchResults < SitePrism::Section element :title, 'a.title' element :blurb, 'span.result-description' endclass Home < SitePrism::Page sections :search_results, SearchResults, '#results li' end
Here's how to test for the existence of the section:
@home = Home.new # ... @home.has_search_results? #=> Only returns `true` if there is 1 or more results
This allows for some pretty tests ...
Then(/^there are search results on the page$/) do expect(@home).to have_search_results end
The last methods added by
sectionsto the page/section we're adding our sections to are
wait_until__visibleand
wait_until__invisible. They will wait for capybara's default wait time for there to be at least one of the section items in the array of sections to be visible or for one of the section items to be invisible respectively.
For example:
class SearchResults < SitePrism::Section element :title, 'a.title' element :blurb, 'span.result-decription' endclass Home < SitePrism::Page sections :search_results, SearchResults, '#results li' end
... here's how to wait for one the section items to become visible:
@home = Home.new # ... @home.wait_until_search_results_visible @home.wait_until_search_results_visible(wait: 3) #=> waits for 3 seconds instead of the default capybara timeout
... and how to wait for the sections to disappear
@home = Home.new # ... @home.wait_until_search_results_invisible @home.wait_until_search_results_invisible(wait: 6) #=> waits for 6 seconds instead of the default capybara timeout
Load validations enable common validations to be abstracted and performed on a Page or Section to determine when it has finished loading and is ready for interaction in your tests.
For example, suppose you have a page which displays a 'Loading...' message while the body of the page is loaded in the background. Load validations can be used to ensure tests wait for the correct url to be displayed and the loading message is no longer present before trying to interact with the page.
Other use cases include Sections which are displayed conditionally and may take time to become ready to interact with, such as animated lightboxes.
Load validations can be used in three constructs:
Page#load
Loadable#when_loaded
Loadable#loaded?
When a block is passed to the
Page#loadmethod, the url will be loaded normally and then the block will be executed within the context of
when_loaded. See
when_loadeddocumentation below for further details.
Example:
# Load the page and then execute a block after all load validations pass: my_page_instance.load do |page| page.do_something end
The
Loadable#when_loadedmethod on a Loadable class instance will yield the instance of the class into a block after all load validations have passed.
If any load validation fails, an error will be raised with the reason, if given, for the failure.
Example:
# Execute a block after all load validations pass: a_loadable_page_or_section.when_loaded do |loadable| loadable.do_something end
You can explicitly run load validations on a Loadable via the
loaded?method. This method will execute all load validations on the object and return a boolean value. In the event of a validation failure, a validation error can be accessed via the
load_errormethod on the object, if any error message was emitted by the failing validation.
Example:
it 'loads the page' do some_page.load some_page.loaded? #=> true if/when all load validations pass another_page.loaded? #=> false if any load validations fail another_page.load_error #=> A string error message if one was supplied by the failing load validation, or nil end
A load validation is a block which returns a boolean value when evaluated against an instance of the Page or Section where defined.
class SomePage < SitePrism::Page element :foo_element, '.foo' load_validation { has_foo_element? } end
The block may be defined as a two-element array which includes the boolean check as the first element and an error message as the second element. It is highly recommended to supply an error message, as they are extremely useful in debugging validation errors.
The error message is ignored unless the boolean value is evaluated as falsey.
class SomePage < SitePrism::Page element :foo_element, '.foo' load_validation { [has_foo_element?, 'did not have foo element!'] } end
Load validations may be defined on
SitePrism::Pageand
SitePrism::Sectionclasses (herein referred to as
Loadables) and are evaluated against an instance of the class when created.
Defined load validations can be skipped for one
loadcall by passing in
with_validations: false.
it 'loads the page without validations' do some_page.load(with_validations: false) some_page.loaded? #=> true unless something has gone wrong end
Any number of load validations may be defined on a Loadable and they are inherited by its subclasses (if any exist).
Load validations are executed in the order that they are defined. Inherited load validations are executed from the top of the inheritance chain (e.g.
SitePrism::Pageor
SitePrism::Section) to the bottom.
For example:
class BasePage < SitePrism::Page element :loading_message, '.loader'load_validation do [has_no_loading_message?(wait: 10), 'loading message was still displayed'] end end
class FooPage < BasePage set_url '/foo'
section :form, '#form' element :some_other_element, '.myelement'
load_validation { [has_form?, 'form did not appear'] } load_validation { [has_some_other_element?, 'some other element did not appear'] } end
In the above example, when
loaded?is called on an instance of
FooPage, the validations will be performed in the following order:
BasePagevalidation will wait for the loading message to disappear.
FooPagevalidation will wait for the
formelement to be present.
FooPagevalidation will wait for the
some_other_elementelement to be present.
NB:
SitePrism::Pageused to include a default load validation on
page.displayed?however for v3 this has been removed. It is therefore necessary to re-define this if you want to retain the behaviour from site_prism v2. See UPGRADING.md for more info on this.
When querying an element, section or a collection of elements or sections, you may supply Capybara query options as arguments to the element and section methods in order to refine the results of the query and enable Capybara to wait for all of the conditions necessary to properly fulfill your request.
Given the following sample page and elements:
class SearchResults < SitePrism::Section element :title, 'a.title' element :blurb, 'span.result-decription' endclass Home < SitePrism::Page element :footer, '.footer' sections :search_results, SearchResults, '#results li' end
Asserting the attributes of an element or section returned by any method may fail if the page has not finished loading the section(s):
@home = Home.new # ... expect(@home.search_results.size).to == 25 # This may fail!
The above query can be rewritten to utilize the Capybara
:countoption when querying for the sections to be present and countable, which in turn causes Capybara to expect some number of results to be returned. The method calls below will succeed, provided the elements appear on the page within the timeout:
@home = Home.new @home.has_search_results?(count: 25) # OR @home.search_results(count: 25)
Now we can write pretty, non-failing tests without hard coding these options into our page and section classes:
Then(/^there are search results on the page$/) do expect(@results_page).to have_search_results(count: 25) end
This is supported for all of the Capybara options including, but not limited to
:count,
:textetc. This can also be used when defining page objects.
class Home < SitePrism::Page element :footer, '.footer' element :view_more, 'li', text: 'View More' sections :search_results, SearchResults, '#results li', count: 5 end
The following element methods allow Capybara options to be passed as arguments to the method:
@results_page.(text: 'Welcome!') @results_page.has_?(count: 25) @results_page.has_no_?(text: 'Logout') @results_page.wait_until__visible(text: 'Some ajaxy text appears!') @results_page.wait_until__invisible(text: 'Some ajaxy text disappears!')
It's possible to use the same page objects of integration tests for view tests, too, just pass the rendered HTML to the
loadmethod:
require 'spec_helper'describe 'admin/things/index' do let(:list_page) { AdminThingsListPage.new } let(:thing) { build(:thing, some_attribute: 'some attribute') }
it 'contains the things we expect' do assign(:things, [thing])
render template: 'admin/things/index' list_page.load(rendered) expect(list_page.rows.first.some_attribute).to have_text('some attribute')
end end
SitePrism allows you to interact with iframes. An iframe is declared as a
SitePrism::Pageclass, and then referenced by the page or section it is embedded into. Like a section, it is possible to test for the existence of the iframe, wait for it to exist as well as interact with the page it contains.
An iframe is declared in the same way as a Page:
class MyIframe < SitePrism::Page element :some_text_field, 'input.username' end
To expose the iframe, reference it from another page or class using the
iframemethod. The
iframemethod takes 3 arguments; the name by which you would like to reference the iframe, the page class that represents the iframe, and the CSS selector by which you can locate the iframe. For example:
class PageContainingIframe < SitePrism::Page iframe :my_iframe, MyIframe, '#my_iframe_id' end
While the above example uses a CSS selector to find the iframe, it is also possible to use an XPath expression or the index of the iframe in its parent (a shortcut for an
nth-of-typeCSS selector). For example:
class PageContainingIframe < SitePrism::Page # XPath Expression: iframe :my_iframe, MyIframe, :xpath, '//iframe[@id="my_iframe_id"]'Index (nth-of-type) Selector:
iframe :my_iframe, MyIframe, 0 end
Like an element or section, it is possible to test for an iframe's existence using the auto-generated
has_?method. Using the above example, here's how it's done:
@page = PageContainingIframe.new # ... @page.has_my_iframe? #=> true expect(@page).to have_my_iframe
Since an iframe contains a fully fledged
SitePrism::Page, you are able to interact with the elements and sections defined within it. Due to capybara internals it is necessary to pass a block to the iframe instead of simply calling methods on it; the block argument is the
SitePrism::Pagethat represents the iframe's contents. For example:
# SitePrism::Page representing the iframe class LoginFrame < SitePrism::Page element :username, 'input.username' element :password, 'input.password' endSitePrism::Page representing the page that contains the iframe
class Home < SitePrism::Page set_url 'http://www.example.com'
iframe :login_frame, LoginFrame, '#login_and_registration' end
cucumber step that performs login
When(/^I log in$/) do @home = Home.new @home.load
@home.login_frame do |frame| #
frame
is an instance of theLoginFrame
class frame.username.set 'admin' frame.password.set 'p4ssword' end end
SitePrism can be configured to change its behaviour.
For each of the following configuration options, either add it in the
spec_helper.rbfile if you are running SitePrism as a Unit Test framework, or in your
env.rbif you are running a Cucumber based framework.
By default, SitePrism element and section methods utilize Capybara's implicit wait methodology and will return only once the shorter of the Capybara timeout limit has been reached or the required query has passed.
If you want to tweak the waiting time or disable it completely, configure it as per the code below
Capybara.configure do |config| config.default_max_wait_time = 11 #=> Wait up to 11 seconds for all queries to fail # or if you don't want to ever wait config.default_max_wait_time = 0 #=> Don't ever wait! end
Note that even with implicit waits on you can dynamically modify the wait times in any SitePrism method to help work-around special circumstances.
# Option 1: using wait key assignment @home.search_results(wait: 20) # will wait up to 20 secondsOption 2: using Capybara directly, this will wait up to 20 seconds
Capybara.using_wait_time(20) do @home.search_results end
There's a SitePrism plugin called
site_prism.vcrthat lets you use SitePrism with the VCR gem. Check it out HERE
Note that as of 2016 this plugin doesn't appear to have been under active development. Also it is still pinned to the
2.xseries of site_prism so use it of your own accord.
So, we've seen how to use SitePrism to put together page objects made up of pages, elements, sections and iframes. But how to organise this stuff? There are a few ways of saving yourself having to create instances of pages all over the place. Here's an example of this common problem:
@home = Home.new #The annoyance (and, later, maintenance nightmare) is having to create
@homeand@results_page. It would be better to not have to create instances of pages all over your tests.The way I've dealt with this problem is to create a class containing methods that return instances of the pages. Eg:
# our pagesclass Home < SitePrism::Page #... end
class SearchResults < SitePrism::Page #... end
class Maps < 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
def maps Maps.new end end
and here's how to use it
#first line of the test... Given(/^I start on the home page$/) do @app = App.new @app.home.load end
When(/^I search for Sausages$/) do @app.home.search_field.set 'Sausages' @app.home.search_button.click end
Then(/^I am on the results page$/) do expect(@app.results_page).to be_displayed end
etc...
The only thing that needs instantiating is the
Appclass - from then on pages don't need to be initialized, they are now returned by methods on@app.It is possible to further optimise this, by using Cucumber/RSpec hooks, amongst other things. However the investigation and optimisation of this (and other aspects of SitePrism), is left as an exercise to the Reader.
Happy testing from all of the SitePrism team!