QA-Tools Documentation¶
...
Getting Started¶
...
Installation¶
The library can be installed easily via Composer.
- Define the dependencies in your
composer.json
file:
{
"require": {
"qa-tools/qa-tools": "~1.0"
}
}
- Install/update your vendors:
$ curl http://getcomposer.org/installer | php
$ php composer.phar install
To verify successful installation look for the qa-tools/qa-tools
folder within /vendor/
folder of your project.
Configuration¶
The library can be optionally configured using following approach:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php
use Behat\Mink\Driver\Selenium2Driver;
use Behat\Mink\Session;
use QATools\QATools\PageObject\Config\Config;
use QATools\QATools\PageObject\Container;
use QATools\QATools\PageObject\PageFactory;
// 1. Obtain/create Mink's session object:
$session = new Session(new Selenium2Driver());
// 2a. Either configure page factory via Config class:
$config = new Config(array(
'base_url' => 'http://www.example.com',
));
$page_factory = new PageFactory($session, $config);
// 2b. Or configure page factory via dependency injection container:
$container = new Container();
$container['config_options'] = array(
'base_url' => 'http://www.example.com',
);
$page_factory = new PageFactory($session, $container);
|
Then created PageFactory
class instance can be used to spawn Page
class instances at will.
Note
If several Mink’s sessions are used (e.g. for different browsers), then separate PageFactory
class
instance needs to be created for each of them. Configuration setting can be shared across different
PageFactory
class instances, when same container is used to create them.
Configuration Options¶
The following configuration options are available:
base_url
- allows to specify Base URL to be used to transform all relative urls from @page-url annotations into absolute urls.page_namespace_prefix
- array of namespaces in which the DefaultPageLocator will search for page classes. Defaults to:array('\\')
page_url_matchers
- array of classes, that are used to detect if givenPage
is currently opened. Defaults to:array( '\\QATools\\QATools\\PageObject\\PageUrlMatcher\\ExactPageUrlMatcher', '\\QATools\\QATools\\PageObject\\PageUrlMatcher\\RegexpPageUrlMatcher', '\\QATools\\QATools\\PageObject\\PageUrlMatcher\\ComponentPageUrlMatcher', )
If port is specified as part of base_url then it will be used in every built url unless specified explicitly in the @page-url annotation.
Connecting to Behat¶
Note
Throughout this tutorial it’s assumed that working Behat with MinkExtension is configured and connected to a project that needs to be tested.
To use library with Behat you are required to also install https://github.com/qa-tools/behat-extension.
Connecting to PHPUnit¶
Note
Throughout this tutorial it’s assumed that working PHPUnit is configured and connected to a project that needs to be tested.
To use library with PHPUnit you are required to also install https://github.com/qa-tools/phpunit-extension.
Important
TODO: Write about the obtaining Mink session need for PageFactory and that it can be easily done using PHPUnit-Mink.
Connecting PHPUnit-Mink¶
...
Creating Test Case File¶
...
Glossary¶
Important
TODO: Explain each term and how they are used to organize overall experience within a library.
Page Element¶
...
Page Object¶
...
Element Proxy¶
...
Annotation¶
...
Page Factory¶
...
Page Locator¶
The page locator, which is used by the page factory, transforms a page name into a fully qualified class name. For usage see How-To section.
Annotations¶
Annotations are widely used to provide access to the library’s functionality without a need to write any code.
@find-by¶
This annotation is used to specify means, by which elements can be found on a page. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <?php
use QATools\QATools\PageObject\Element\WebElement;
use QATools\QATools\PageObject\Page;
/**
* @find-by('id' => 'breadcrumbs')
* @timeout(2)
* @element-name('Breadcrumbs Control')
*/
class Breadcrumbs extends WebElement
{
}
class HomePage extends Page
{
/**
* @var Breadcrumbs
*/
protected $breadcrumbsDefault;
}
class AboutPage extends Page
{
/**
* @var Breadcrumbs
* @find-by('css' => 'nav.breadcrumbs')
* @timeout(3)
* @element-name('Small Breadcrumbs Control')
*/
protected $breadcrumbsOverride;
}
|
When defining an element (line 10) it’s possible to specify default @find-by
annotation (line 6), that will be used when
none was specified on a page property (line 17-19). If @find-by
annotation was specified on a page property (line 27),
then it will override any default annotation, that might have been specified on the element’s class.
Note
Apart then in page classes it’s also possible to have properties with @find-by
annotations on element
sub-classes, that allow presence of sub-elements (e.g. AbstractElementContainer
).
Locating elements by Class Name¶
@find-by('className' => 'example-class')
@find-by('how' => 'className', 'using' => 'example-class')
The above annotations would find any elements, which contains class example-class
among classes assigned to them. For example:
<div class="example-class"></div>
<div class="class-a example-class"></div>
<div class="example-class class-b"></div>
<div class="class-a example-class class-b"></div>
Locating elements by CSS Selector¶
@find-by('css' => '#test > input.example[type="hidden"]')
@find-by('how' => 'css', 'using' => '#test > input.example[type="hidden"]')
The above annotations would find elements, which matches #test > input.example[type="hidden"]
CSS selector. For example:
<input type="hidden" class="example"/>
Locating elements by ID Attribute¶
@find-by('id' => 'test')
@find-by('how' => 'id', 'using' => 'test')
The above annotations would find any elements which id
attribute matches test
. For example:
<div id="test"></div>
<input id="test"/>
<label id="test"></label>
Locating elements by NAME Attribute¶
@find-by('name' => 'test')
@find-by('how' => 'name', 'using' => 'test')
The above annotations would find any elements which name
attribute matches test
. For example:
<input name="test"/>
<select name="test"></select>
Locating elements by Tag Name¶
@find-by('tagName' => 'section')
@find-by('how' => 'tagName', 'using' => 'section')
The above annotations would find any elements which tag name matches section
. For example:
<section name="first">test</section>
<section name="second">test</section>
<section></section>
Locating links by Full Text¶
@find-by('linkText' => 'the link')
@find-by('how' => 'linkText', 'using' => 'the link')
The above annotations would find any links (the A
tag with href
attribute) which visible text (not including
HTML markup) is the link
. For example:
<a href="#">the link</a>
<a href="http://www.google.com">the link</a>
<a href="http://www.google.com">the <strong>link</strong></a>
Locating links by Partial Text¶
@find-by('partialLinkText' => 'the link')
@find-by('how' => 'partialLinkText', 'using' => 'the link')
The above annotations would find any links (the A
tag with href
attribute) which visible text (not including
HTML markup) contains the link
. For example:
<a href="#">the link</a>
<a href="http://www.google.com">this is the link</a>
<a href="http://www.google.com">the <strong>link</strong> is here</a>
Locating elements by XPATH¶
@find-by('xpath' => 'a[@href = "#"]')
@find-by('how' => 'xpath', 'using' => 'a[@href = "#"]')
The above annotations would find A
tags which href
attribute value matches #
. For example:
<a href="#">the link</a>
<a href="#">the link is here</a>
@page-url¶
This annotation is used to specify URL associated with each page class. For example:
1 2 3 4 5 6 7 8 9 10 11 | <?php
use QATools\QATools\PageObject\Page;
/**
* @page-url('http://www.example.com/products/shoes.html?color=red')
* @match-url-exact('http://www.example.com/products/shoes.html?color=red')
*/
class NewProductsPage extends Page
{
}
|
There are several parameters to configure the URL:
url
- specifies absolute or relative URL to the page (mandatory)params
- specifies additional URL parameters in associative array format (defaults toarray()
)secure
- specifies if secure connection (thehttps://
protocol) needs to be used (defaults tonull
)
Warning
If some/all parameter names are omitted (as seen in above example), then parameter order must be preserved.
Warning
This annotation should only be used on Page
classes and its subclasses.
Below is an example of annotation, where url
parameter name is omitted, but secure
parameter name is not:
@page-url('http://www.example.com/products/shoes.html', 'secure' => false)
Parameters in the URL¶
Parameters in the url can be specified using 3 ways:
- after question mark (
?
) inurl
annotation parameter (first) - in
params
annotation parameter via associative array (second) - as a combination of both methods from above
All of the following annotations will produce exactly same URL:
@page-url('http://www.example.com/products/shoes.html?color=red&size=42')
@page-url('http://www.example.com/products/shoes.html', array('color' => 'red', 'size' => 42))
@page-url('http://www.example.com/products/shoes.html?color=red', array('size' => 42))
Same annotations can also be written in long form:
@page-url('url' => 'http://www.example.com/products/shoes.html?color=red&size=42')
@page-url('url' => 'http://www.example.com/products/shoes.html', 'params' => array('color' => 'red', 'size' => 42))
@page-url('url' => 'http://www.example.com/products/shoes.html?color=red', 'params' => array('size' => 42))
Note
If same URL parameter is specified in both url
and params
annotation parameters, then it’s value from params
takes precedence.
Relative URLs¶
Specifying absolute URLs in each of created Page classes introduces fair amount of code duplication. This becomes even larger problem, when need arises to use Page classes in parallel on 2+ different domains (e.g. local development environment and Continuous Integration server).
After setting base_url
configuration option it will be possible to use relative URLs
along side (can use both at same time for different pages) with absolute ones:
@page-url('products/shoes.html?color=red&size=42')
@page-url('products/shoes.html', array('color' => 'red', 'size' => 42))
@page-url('products/shoes.html?color=red', array('size' => 42))
@page-url('url' => 'products/shoes.html?color=red&size=42')
@page-url('url' => 'products/shoes.html', 'params' => array('color' => 'red', 'size' => 42))
@page-url('url' => 'products/shoes.html?color=red', 'params' => array('size' => 42))
Secure/unsecure URLs¶
If secure
annotation parameter not specified, then protocol from following sources is used:
- for absolute urls: the
url
parameter of@page-url
annotation - for relative urls:
base_url
configuration option
By specifying secure
annotation parameter (3rd) value it’s possible to force secure/unsecure connection. For example:
// force secure:
@page-url('products/shoes.html', true)
@page-url('url' => 'products/shoes.html', 'secure' => true)
@page-url('url' => 'http://www.example.com/products/shoes.html', 'secure' => true)
// force non-secure:
@page-url('products/shoes.html', false)
@page-url('url' => 'products/shoes.html', 'secure' => false)
@page-url('url' => 'https://www.example.com/products/shoes.html', 'secure' => false)
Custom ports¶
Like for base_url it is possible to include a port in an absolute URL:
@page-url('http://www.example.com:8080/products/shoes.html')
URL parameter unmasking¶
It is possible to make url
parameter of @page-url
annotation more dynamic (currently it’s pretty static)
though usage of url masks. The url mask
is a query string parameter name wrapped within {
and }
like so:
{parameter_name}
. When a query string parameter is encountered in the url in such a form, then instead of being
added to the query string of built url it would be unmasked (substituted) in the main url part itself.
@page-url('products/{product-name}.html', 'params' => array('product-name' => 'shoes'))
It doesn’t look too powerful right now, but considering that params would be supplied later in Page::open
method call
it would be a major time saver for SEO url building.
Note
Every part of url, except anchor and query string itself can be unmasked in such a way.
@match-url-exact¶
This annotation allows to check if a specific page is open by comparing the specified full URL against the currently opened URL.
1 2 3 4 5 6 7 8 9 10 11 | <?php
use QATools\QATools\PageObject\Page;
/**
* @page-url('http://www.example.com/products/shoes.html?color=red')
* @match-url-exact('http://www.example.com/products/shoes.html?color=red')
*/
class NewProductsPage extends Page
{
}
|
Warning
This annotation should only be used on Page
classes and its subclasses.
@match-url-regexp¶
This annotation allows to check if a specific page is opened using a regular expression against the currently opened URL.
@match-url-regexp('/shoes\.html\?color=.+?$/')
@match-url-regexp('regexp' => '/shoes\.html\?color=.+?$/')
Warning
This annotation should only be used on Page
classes and its subclasses.
@match-url-component¶
This annotation allows to check if a specific page is opened by comparing different components of the URL against the currently opened URL.
There are several parameters to configure the matching, similar to @page-url
:
path
- specifies the path of an URLparams
- specifies parameters in associative array formatsecure
- specifies if secure connection is usedanchor
- specifies the fragment/anchor of an URLhost
- specifies the host of an URLport
- specifies the port of an URLuser
- specifies the user of an URLpass
- specifies the password of an URL
// matches one of the url components
@match-url-component('path' => '/products/shoes.html')
@match-url-component('secure' => false)
@match-url-component('params' => array('color' => 'red'))
@match-url-component('anchor' => 'fragment')
@match-url-component('host' => 'domain.tld')
@match-url-component('port' => 80)
@match-url-component('user' => 'username')
@match-url-component('pass' => 'password')
// matches several of the url components
@match-url-component('path' => '/products/shoes.html', 'params' => array('color' => 'red'), 'secure' => false, 'host' => 'domain.tld', 'port' => 80, 'user' => 'username', 'pass' => 'password')
Warning
This annotation should only be used on Page
classes and its subclasses.
Note
When several @match-url-...
annotations are present, then they’re checked until first match.
@timeout¶
This annotation is used to specify maximum waiting time (in seconds), after which search attempt for an element, that is absent on a page, will be considered as failure and exception will the thrown.
Note
When @timeout
annotation is not specified, then search attempt will be considered as failure immediately
after element won’t be found on a page.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <?php
use QATools\QATools\PageObject\Element\WebElement;
use QATools\QATools\PageObject\Page;
/**
* @find-by('id' => 'breadcrumbs')
* @timeout(2)
* @element-name('Breadcrumbs Control')
*/
class Breadcrumbs extends WebElement
{
}
class HomePage extends Page
{
/**
* @var Breadcrumbs
*/
protected $breadcrumbsDefault;
}
class AboutPage extends Page
{
/**
* @var Breadcrumbs
* @find-by('css' => 'nav.breadcrumbs')
* @timeout(3)
* @element-name('Small Breadcrumbs Control')
*/
protected $breadcrumbsOverride;
}
|
When defining an element (line 10) it’s possible to specify default @timeout
annotation (line 7), that will be used when
none was specified on a page property (line 17-19). If @timeout
annotation was specified on a page property (line 28),
then it will override any default annotation, that might have been specified on the element’s class.
Note
This annotation can be particularly useful, when dealing with AJAX requests in which element in question would be only present on a page after AJAX request is over.
@element-name¶
This annotation is used to specify human readable name (or semantic meaning) for a particular usage of the element. Later name, which is set with the help of this annotation will be used in all error messages related to particular element usage on that page.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <?php
use QATools\QATools\PageObject\Element\WebElement;
use QATools\QATools\PageObject\Page;
/**
* @find-by('id' => 'breadcrumbs')
* @timeout(2)
* @element-name('Breadcrumbs Control')
*/
class Breadcrumbs extends WebElement
{
}
class HomePage extends Page
{
/**
* @var Breadcrumbs
*/
protected $breadcrumbsDefault;
}
class AboutPage extends Page
{
/**
* @var Breadcrumbs
* @find-by('css' => 'nav.breadcrumbs')
* @timeout(3)
* @element-name('Small Breadcrumbs Control')
*/
protected $breadcrumbsOverride;
}
|
When defining an element (line 10) it’s possible to specify default @element-name
annotation (line 8), that will be used when
none was specified on a page property (line 17-19). If @element-name
annotation was specified on a page property (line 29),
then it will override any default annotation, that might have been specified on the element’s class.
Note
If element name is not specified, then page property name, e.g. HomePage::$breadcrumbsDefault
, would be
used instead.
@bem¶
...
Best Practices¶
Important
TODO: Write about how to use the each element to get most effect from it. Describe various usages (AJAX, non-ajax, combined element usage to get desired effect).
How-To¶
Using DefaultPageLocator¶
The default page locator uses the following strategy to locate pages:
- uppercases the first letter of each word and joining them to a CamelCase like class name
- prepending configured namespaces defined via page_namespace_prefix
- returning first found class
<?php
...
$page_locator = new DefaultPageLocator(array('\\shop\\pages', '\\shop\\account\\pages'));
$page_class = $page_locator->resolvePage('Login Page');
$page = new $page_class($page_factory);
...
?>
Depending on existence either \shop\pages\LoginPage
or \shop\account\pages\LoginPage
will be returned.
Defining a custom locator¶
In some cases it might be necessary to build a custom page locator. For example to map page names to specific classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <?php
use QATools\QATools\PageObject\Exception\PageFactoryException;
use QATools\QATools\PageObject\PageLocator\IPageLocator;
class MappingPageLocator implements IPageLocator
{
protected $pages = array(
'Login Page' => '\\shop\\pages\\LoginPage',
'Registration Page' => '\\shop\\pages\\LoginPage',
'Landing Page' => '\\shop\\pages\\LoginPage',
'Account Overview Page' => '\\shop\\account\\pages\\AccountPage',
);
public function resolvePage($name)
{
if ( !isset($this->pages[$name]) ) {
throw new PageFactoryException(
'Couldn\'t locate ' . $name,
PageFactoryException::TYPE_PAGE_CLASS_NOT_FOUND
);
}
return $this->pages[$name];
}
}
|
Now it is possible to either locate the page manually by its name:
<?php
...
$page_locator = new MappingPageLocator();
$registration_page_class = $page_locator->resolvePage('Registration Page');
$registration_page = new $registration_page_class($page_factory);
...
?>
or replace default locator with new one during PageFactory
construction time.
<?php
...
$container = new Container();
$container['page_locator'] = function () {
return new MappingPageLocator();
};
$page_factory = new PageFactory($session, $container);
$registration_page = $page_factory->getPage('Registration Page');
...
?>