PageMagic
PageMagic is an API for testing web applications.
It has a simple but powerful DSL which makes modelling and interacting with your pages easy.
Wouldn't it be great if there was a framework that could:
- Model your pages
- Fluently define event hooks / waiters on page elements
- Map paths to pages so that the correct page object is loaded as you navigate
- Be super dynamic
Well PageMagic might just be the answer!
Give it a try and let us know what you think! There will undoubtedly be things that can be improved and issues that we are not aware of so your feedback/pull requests are greatly appreciated!
Contents
- Installation
- Quick Start
- Defining Pages
- Starting a session
- Page mapping
- Watchers
- Waiting
- Drivers
- Cucumber Quick Start
Installation
gem install page_magic
Quick Start
Getting started with PageMagic is as easy, try running this:
require 'page_magic'
class Google
include PageMagic
url 'https://www.google.com'
text_field :search_field, name: 'q'
:search_button, name: 'btnG'
def search term
search_field.set term
.click
end
end
google = Google.visit(browser: :chrome)
google.search('page_magic')
This example defines a simple page to represent Google's search page, visits it and performs a search.
This code models a single page and will let you interact with the elements defined on it as well as use the helper method we defined.
You can do lots with PageMagic including mapping pages to a session so that they are fluidly switched in for you. You can even define hooks to run when ever a element is interacted with. So what are you wating for? there' no place better to start than the beginning. Have fun! :)
Defining Pages
To define something that PageMagic can work with, simply include PageMagic in to a class.
class LoginPage
include PageMagic
end
Elements
Defining elements is easy see the example below.
class LoginPage
include PageMagic
text_field(:username, label: 'username')
text_field(:password, label: 'password')
(:login_button, text: 'login')
end
Interacting with elements
Elements are defined with an id which is the name of the method you will use to reference it. In the above example, the textfields and button were defined with the id's, :username
, :password
, and :login_button
After visiting a page with a PageMagic session, you can access all of the elements of that page through the session itself.
session.username.set '[email protected]'
session.password.set 'passw0rd'
session..click
Multple Results
Where an element has been scoped to return multple results, these will be returned in an array. These elements can be defined using all of the same features as described in this readme and behave in exactly the same way.
class LoginPage
element :links, css: 'a'
end
session.link => Array<Element>
Sub Elements
If your pages are complex you can use PageMagic to compose pages, their elements and subelements to as many levels as you need to.
class MailBox
include PageMagic
element :message, id: 'message_id' do
link(:read, text: 'read')
end
end
Sub elements can be accessed through their parent elements e.g:
session..read.click
Custom elements
PageMagic allows you to define your own custom elements.
class Nav < PageMagic::Element
selector css: '.nav'
element :options, css: '.options' do
link(:link1, id: 'link1')
link(:link2, id: 'link2')
link(:link3, id: 'link3')
end
end
class MyPage
include PageMagic
element Nav
end
If an id is not specified then the name of the element class will be used. The selector for the element can be specified on the class itself or overiden when defining the element on the page. The custom element can also be extended as with other elements.
class MyPage
include PageMagic
element Nav, :navigation, selector: '.custom' do
link(:extr_link, id: 'extra-link')
do
end
Hooks
PageMagic provides hooks to allow you to interact at the right moments with your pages.
Note:
- with hooks you may well find PageMagic's watchers useful.
- The following examples wait for actions to happen. You can of course write you own wait code or try out our wait_until helper:)
On load hook
PageMagic lets you define an on_load hook for your pages. This lets you write any custom wait logic you might need before letting execution continue.
class LoginPage
# ... code defining elements as shown above
on_load do
wait_until{login_fields_have_appeared?}
end
end
Element event hooks
Frequently, you are going to have to work with pages that make heavy use of ajax. This means that just because you've clicked something, it doesn't mean that the action is finished. For these occasions PageMagic provides before_events
and after_events
hooks that you use to perform custom actions and wait for things to happen.
class EmailMessagePage
include PageMagic
## code defining other elements, such as subject and body
link(:delete id: 'delete-message') do
after_events do
wait_until{fancy_animation_has_disappeared?}
end
end
end
Helper methods
Using elements that are defined on a page is great, but if you are enacting a procedure through interacting with a few of them then your code could end up with some pretty repetitive code. In this case you can define helper methods instead.
class LoginPage
# ... code defining elements as shown above
def login(user, pass)
username.set user
password.set pass
.click
end
end
We can interact with helper in the same way as we did page elements.
session.login('joe', 'blogs')
Dynamic Selectors
In some cases you will able to specify the selector for an element until runtime. PageMagic allows you to handle such situations with support for dynamic selectors.
class MailBox
include PageMagic
element :message do |subject:|
selector xpath: '//tr[text()="#{subject}"]'
link(:read, text: 'read')
end
end
Here we have defined the 'message' element using a block that takes subject argument. This is passed in at run time and given to the xpath selector.
session.(subject: 'test message')
Starting a session
To start a PageMagic session simply decide what browser you want to use and pass it to PageMagic's .session
method
session = PageMagic.session(browser: :chrome, url: 'https://21st-century-mail.com')
Your session won't you do much besides navigating to the given url until you have mapped pages to it, so take a look at this next!
Note PageMagic supports having multiple sessions pointed at different urls using different browsers at the same time :)
Rack applications and Rack::Test
To run a session against a rack application instead of a live site, simply supply the rack application when creating the session
session = PageMagic.session(application: YourRackApp, url: '/path_to_start_at')
By default PageMagic uses the Rack::Test driver for capybara however you are free to use any browser you like as long as the driver is registered for it.
session = PageMagic.session(application: YourRackApp, browser: :your_chosen_browser, url: '/path_to_start_at')
Out of the box, PageMagic supports the following as parameters to browser:
- :chrome
- :firefox
- :poltergeist
- :rack_test
Under the hood, PageMagic is using Capybara so you can register any Capybara specific driver you want. See below for how to do this.
Note: We don't want to impose particular driver versions so PageMagic does not list any as dependencies. Therefore you will need add the requiste gem to your Gemfile.
Page mapping
With PageMagic you can map which pages should be used to handle which URL paths. This is a pretty killer feature that will remove a lot of the juggling and bring back fluency to your code!
# define what pages map to what
browser.define_page_mappings %r{/messages/\d+} => MessagePage,
'/login' => LoginPage,
'/' => MailBox
You can use even use regular expressions to map multiple paths to the same page. In the above example we are mapping paths that that starts with '/messages/' and are followed by one ore more digits to the MessagePage
class.
Watchers
PageMagic lets you set a watcher on any of the elements that you have defined on your pages. Use watchers to decide when
things have changed. The watch
method can be called from anywhere within an element definition. For PageObjects it can
only be called from within hooks and helper methods.
Note: Watchers are not inherited
Method watchers
Method watchers watch the output of the given method name.
:javascript_button, css: '.fancy_button' do
watch(:url)
after_events do
wait_until{changed?(:url)}
end
end
Simple watchers
Simple watchers use the watch
method passing two parameters, the first is the name of the element you want to keep an
eye and the second is the method that needs to be called to get the value that should be observed.
element :product_row, css '.cta' do
watch(:total, :text)
after_events do
wait_until{changed?(:total)}
end
end
Custom watchers
Custom watchers are defined by passing a name and block parameter to the watch
method. The block returns the value
that needs to be observed. Use watch in this way if you need to do something non standard to obtain a value or to
access an element not located within the current element but elsewhere within the page.
element :product_row, css '.cta' do
watch(:total) do
session.nav.total.text
end
after_events do
wait_until{changed?(:total)}
end
end
Waiting
It's inevitable that if there is JavaScript on the page that you are going to have to wait for things to happen before you can move on. PageMagic supplies the wait_until
method that can be used anywhere you might need it. The wait_until method takes a block that it will execute until either that block returns true or the timeout occurs. See the method docs for details on configuring timeouts and retry intervals.
Drivers
Registering a custom driver
You can register any Capybara compliant driver as follows
#1. Define driver, constructor to PageMagic::Driver takes a list of browser aliases.
# Selenium Webdriver for example supports driving more than one.
Webkit = PageMagic::Driver.new(:webkit) do |app, , browser_alias_chosen|
# Write the code necessary to initialise the driver you have chosen
require 'capybara/webkit'
Capybara::Webkit::Driver.new(app, )
end
#2. Register driver
PageMagic.drivers.register Webkit
#3. Use registered driver
session = PageMagic.session(browser: webkit, url: 'https://21st-century-mail.com')
Cucumber quick start
You can obviously use PageMagic anywhere you fancy but one of the places you might decide to use it is within a Cucumber test suite. If that's the case something like the following could prove useful.
Helper methods
Put the following in to features/support/page_magic.rb
to make these helpers available to all of your steps.
require 'page_magic'
require 'active_support/inflector'
require 'your_pages'
World(Module.new do
def page_class(string)
"#{string}Page".delete(' ').constantize
end
def snake_case(string)
string.delete(' ').underscore
end
def session
$session ||= begin
PageMagic.session(browser: :chrome, url: the_base_url).tap do |session|
session.define_page_mappings '/login' => LoginPage,
'/' => HomePage
end
end
end
end)
Example steps
Use the above helpers to navigate to pages with steps like the following.
Given /^I am on the '(.*)' page$/ do |page_name|
session.visit(page_class(page_name))
end
And /^I set '(.*)' to be '(.*)'$/ do |field, value|
session.send(snake_case(field)).set value
end
When /^I click '(.*)'$/ do |element|
session.send(snake_case(element)).click
end
Then /^I should be on the '(.*)' page$/ do |page_name|
current_page = session.current_page.class
expected_page = page_class(page_name)
fail "On #{current_page}, expected #{expected_page}" unless current_page == expected_page
end