Table of content
You can import the PageObject object using the import
construct as follows:
import PO from '../page-object';
The previous example assumes that your test file is one level deep under
tests/
folder. i.e. tests/unit/my-unit-test.js
.
In order to create a new PageObject definition use the .build
method.
var page = PO.build({
// page attributes
});
You can define attributes using any JavaScript construct
var page = PO.build({
title: function() {
return $('.title').text();
},
text: 'A text'
});
assert.equal(page.title(), 'My title');
assert.equal(page.text, 'A text');
There are many special attributes you can use defined under the PO namespace that simplify common patterns, i.e.
var page = PO.build({
title: PO.text('.title')
});
The following is a comprehensive documentation of the available PO
attribute
helpers.
Test conditions on elements
Returns true
if the element has the css class.
Attribute signature
PO.hasClass(cssClass, selector [, scope: ''])
Examples
<img class="img is-active" src="...">
var page = PO.build({
isImageActive: PO.hasClass('is-active', '.img')
});
assert.ok(page.isImageActive(), 'Image is active');
Returns true
if the element doesn't have the css class.
Attribute signature
PO.notHasClass(cssClass, selector [, scope: ''])
Examples
<img class="img is-active" src="...">
var page = PO.build({
isImageDeactivated: PO.notHasClass('is-active', '.img')
});
assert.ok(page.isImageDeactivated(), 'Image is not active');
Returns true
if the element exists and is visible.
Attribute signature
PO.isVisible(selector [, scope: ''])
Examples
<img class="img" src="...">
var page = PO.build({
isImageVisible: PO.isVisible('.img')
});
assert.ok(page.isImageVisible(), 'Image is visible');
.isHidden
Returns true
if the element doesn't exist or it exists and is hidden.
Attribute signature
PO.isHidden(selector [, scope: ''])
Examples
<img class="img" style="display:none" src="...">
var page = PO.build({
isImageHidden: PO.isVisible('.img')
});
assert.ok(page.isImageHidden(), 'Image is hidden');
Retrieve values from elements
Returns the element's attribute value.
Attribute signature
PO.attribute(attributeName, selector [, scope: ''])
Examples
<img class="img" alt="Logo" src="...">
var page = PO.build({
imageAlternateText: PO.attribute('alt', '.img')
});
assert.equal(page.imageAlternateText(), 'Logo');
Returns the count of elements that match the css selector.
Attribute signature
PO.count(selector [, scope: ''])
Examples
<img class="img" src="...">
<img class="img" src="...">
var page = PO.build({
imageCount: PO.count('.img')
});
assert.equal(page.imageCount(), 2);
Returns the inner text of the element. Note that whitespace from the beginning and end of the string is removed for convenience.
Attribute signature
PO.text(selector [, scope: ''])
Examples
<h1>Page title</h1>
var page = PO.build({
title: PO.text('h1')
});
assert.equal(page.title(), 'Page title');
Returns the value of an input.
Attribute signature
PO.value(selector [, scope: ''])
Examples
<input id="name" value="John Doe" />
var page = PO.build({
name: PO.value('#name')
});
assert.equal(page.name(), 'John Doe');
Encapsulates and extend ember-testing
async helpers, supporting chaining and
other features.
Creates an action to click an element.
Attribute signature
PO.clickable(selector [, scope: ''])
Examples
<button id="submit">Send</button>
var page = PO.build({
submitForm: PO.clickable('#submit')
});
page.submitForm();
andThen(function() {
// form was submitted
});
Creates an action to click on an element by text. The text is case sensitive.
Attribute signature
PO.clickOnText(selector, [, scope: ''])
Examples
<button class="btn">Create</button>
<button class="btn">Cancel</button>
var page = PO.build({
click: clickOnText('.btn')
});
page.click("Create");
andThen(function() {
// ...
});
page.click("Cancel");
andThen(function() {
// ...
});
A string of text to look for. It's case sensitive. The text must have matching case to be selected. gwill match elements with the desired text block:
Fills an input.
Attribute signature
PO.fillable(selector [, scope: ''])
Examples
<input id="name" />
var page = PO.build({
name: PO.fillable('#name')
});
page.name('John Doe');
andThen(function() {
// the input value is set
});
Selects an option.
Attribute signature
PO.selectable(selector [, scope: ''])
Examples
<select id="gender">
<option>Male</options>
<option>Female</options>
</select>
var page = PO.build({
selectGender: PO.selectable('#gender')
});
page.selectGender('Female');
andThen(function() {
// the option is selected
});
Visits a page.
Attribute signature
PO.visitable(routePath)
Examples
var page = PO.build({
visit: PO.visitable('/users')
});
page.visit();
andThen(function() {
// the page is loaded
});
You can define dynamic segments in the path as follows
var page = PO.build({
visit: PO.visitable('/users/:user_id/comments/:comment_id')
});
page.visit({ user_id: 5, comment_id: 1 });
andThen(function() {
assert.equal(currentURL(), '/users/5/comments/1');
});
Actions can be chained.
Example
<input id="name" />
<button id="submit">Send</button>
var page = PO.build({
visit: PO.visitable('/user/new'),
submitForm: PO.clickable('#submit'),
name: PO.fillable('#name')
});
page
.visit()
.name('John Doe')
.submitForm();
andThen(function() {
// form was submitted
});
Allows to easily model a table or list of items.
Attribute signature
PO.collection(definition)
The collection definition has the following structure
{
itemScope: '', // css selector
item: {
// item attributes
},
// collection attributes
}
The attributes defined in the item
object are scoped using the itemScope
selector. The attributes defined outside the item
object are available at
collection scope.
Examples
<table id="users">
<caption>The list of users</caption>
<tr>
<td>Jane</td>
<td>Doe</td>
</tr>
<tr>
<td>John</td>
<td>Doe</td>
</tr>
</table>
var page = PO.build({
visit: PO.visitable('/users'),
users: PO.collection({
itemScope: '#users tr',
item: {
firstName: PO.text('td:nth-of-type(1)'),
lastName: PO.text('td:nth-of-type(2)')
},
caption: PO.text('#users caption')
})
});
test('show all users', function(assert) {
page.visit();
andThen(function() {
assert.equal(login.users().caption(), 'The list of users');
assert.equal(login.users().count(), 2); // count attribute is added for free
assert.equal(login.users(1).firstName(), 'Jane');
assert.equal(login.users(1).lastName(), 'Doe');
assert.equal(login.users(2).firstName(), 'John');
assert.equal(login.users(2).lastName(), 'Doe');
});
});
Allows to group attributes together.
Attribute signature
PO.component(definition)
Examples
<h1>New user</h1>
<form>
<input id="firstName" placeholder="First name">
<input id="lastName" placeholder="Last name">
<button>Create</button>
</form>
var page = PO.build({
visit: PO.visitable('/user/create'),
title: PO.text('h1'),
form: PO.component({
firstName: PO.fillable('#firstName'),
lastName: PO.fillable('#lastName'),
submit: PO.clickable('button')
})
});
page.visit();
andThen(function() {
assert.equal(page.title(), 'New user');
});
page
.form()
.firstName('John')
.lastName('Doe')
.submit();
andThen(function() {
// the form was submitted
});
You can define components implicity by creating a plain object with attributes on it
var page = PO.build({
visit: PO.visitable('/user/create'),
title: PO.text('h1'),
form: {
firstName: PO.fillable('#firstName'),
lastName: PO.fillable('#lastName'),
submit: PO.clickable('button')
}
});
Note that if the plain object doesn't have attributes defined, the object is returned as is.
Allows to define reusable helpers using information of the surrounding context.
PO.customHelper(function(selector, options) {
// user magic goes here
return value;
});
There are three different types of custom helpers and are differentiated by the return value. You can define custom helpers that return:
- A basic type value
- A plain object value
- A function value
Given this HTML snippet, the following is an example of each type of custom helpers
<form>
<label class="has-error">
User name
<input id="userName" />
</label>
</form>
This type of custom helper is useful to return the result of a calculation, for example the result of a jQuery expression.
var disabled = customHelper(function(selector, options) {
return $(selector).prop('disabled');
});
var page = PageObject.build({
userName: {
disabled: disabled('#userName')
}
});
assert.ok(!page.userName().disabled(), 'user name input is not disabled');
As you can see the jQuery expression is returned returned.
This is very similar to a component
. The difference with components is that we
can do calculations or use custom options before returning the component.
var input = customHelper(function(selector, options) {
return {
value: value(selector),
hasError: function() {
return $(selector).parent().hasClass('has-error');
}
};
});
var page = PageObject.build({
scope: 'form',
userName: input('#userName')
});
assert.ok(page.userName().hasError(), 'user name has errors');
As you can see the returned plain object is converted to a component.
The main difference with the previous custom helpers is that the returned
functions receive invocation parameters. This is most useful when creating
custom actions that receives options when invoked (like fillIn
helper).
/* global click */
var clickManyTimes = customHelper(function(selector, options) {
return function(numberOfTimes) {
click(selector);
for(let i = 0; i < numberOfTimes - 1; i++) {
click(selector);
}
};
});
var page = PageObject.build({
clickAgeSelector: clickManyTimes('#ageSelector .spinner-button'),
ageValue: value('#ageSelector input')
});
page.visit().clickOnAgeSelector(18 /* times*/);
andThen(function() {
assert.equal(page.ageValue(), 18, 'User is 18 years old');
});
We can see that our clickOnAgeSelector
takes one parameter that's used by the
returned function.
Custom helpers can receive custom options, here's an example of this:
var prop = customHelper(function(selector, options) {
return $(selector).prop(options.name);
});
var page = PageObject.build({
userName: {
disabled: prop('#userName', { name: 'disabled' })
}
});
assert.ok(!page.userName().disabled(), 'user name input is not disabled');
A set of options can be passed as parameters when defining attributes.
The scope
option can be used to override the page's scope
configuration.
Given the following HTML
<div class="article">
<p>Lorem ipsum dolor</p>
</div>
<div class="footer">
<p>Copyright 2015 - Acme Inc.</p>
</p>
the following configuration will match the footer element
var page = PO.build({
scope: '.article',
textBody: PO.text('p'),
copyrightNotice: PO.text('p', { scope: '.footer' })
});
andThen(function() {
assert.equal(page.copyrightNotice(), 'Copyright 2015 - Acme Inc.');
});
The index
option can be used to reduce the set of matched elements to the one
at the specified index.
Given the following HTML
<span>Lorem</span>
<span>ipsum</span>
<span>dolor</span>
the following configuration will match the second span
element
var page = PO.build({
word: PO.text('span', { index: 2 })
});
andThen(function() {
assert.equal(page.word(), 'ipsum'); // => ok
});
The scope
attribute can be used to reduce the set of matched elements to the
ones enclosed by the given selector.
Given the following HTML
<div class="article">
<p>Lorem ipsum dolor</p>
</div>
<div class="footer">
<p>Copyright 2015 - Acme Inc.</p>
</div>
the following configuration will match the article paragraph element
var page = PO.build({
scope: '.article',
textBody: PO.text('p'),
});
andThen(function() {
assert.equal(page.textBody(), 'Lorem ipsum dolor.');
});
The attribute's selector can be omited when the scope matches the element we want to use.
Given the following HTML
<form>
<input id="userName" value="a value" />
<button>Submit</button>
</form>
We can define several attributes on the same input
element as follows
var page = PO.build({
input: {
scope: '#userName',
hasError: hasClass('has-error'),
value: value(),
fillIn: fillable()
},
submit: clickable('button')
});
page
.input()
.fillIn('an invalid value');
page.submit();
andThen(function() {
assert.ok(page.input().hasError(), 'Input has an error');
});
<div class="todo">
<input type="text" value="invalid value" class="error" placeholder="To do..." />
<input type="text" placeholder="To do..." />
<input type="text" placeholder="To do..." />
<input type="text" placeholder="To do..." />
<button>Create</button>
</div>
var page = PageObject.build({
scope: '.todo',
todos: collection({
itemScope: 'input',
item: {
value: value(),
hasError: hasClass('error')
},
create: clickable('button')
});
});
translates to | |
---|---|
page.todos().create() |
click('.todo button') |
page.todos(1).value() |
find('.todo input:nth-of-type(1)').val() |
You can reset parent scope by setting the scope
attribute on the collection declaration.
var page = PageObject.build({
scope: '.todo',
todos: collection({
scope: '',
itemScope: 'input',
item: {
value: value(),
hasError: hasClass('error')
},
create: clickable('button')
});
});
translates to | |
---|---|
page.todos().create() |
click('button') |
page.todos(1).value() |
find('input:nth-of-type(1)').val() |
itemScope
is inherited as default scope on components defined inside the item object.
<ul class="todos">
<li>
<span>To do</span>
<input value="" />
</li>
...
</ul>
var page = PageObject.build({
scope: '.todos',
todos: collection({
itemScope: 'li',
item: {
label: text('span'),
input: {
value: value('input')
}
}
});
});
translates to | |
---|---|
page.todos(1).input().value() |
find('.todos li:nth-of-child(1) input).val() |
<div class="search">
<input placeholder="Search..." />
<button>Search</button>
</div>
var page = PageObject.build({
search: {
scope: '.search',
input: {
fillIn: fillable('input'),
value: value('input')
}
}
});
translates | |
---|---|
page.search().input().value() |
find('.search input').val() |
You can reset parent scope by setting the scope
attribute on the component declaration.
var page = PageObject.build({
search: {
scope: '.search',
input: {
scope: 'input',
fillIn: fillable(),
value: value()
}
}
});
translates | |
---|---|
page.search().input().value() |
find('input').val() |