Migration Guide
In the update to ember-cli-page-object v1.x, we’ve defined more intuitive behavior and moved to a more polished and mature API.
This sounds great, but it also comes with a cost: you need to migrate your test suite. This page includes a list of breaking changes and API enhancements to help you upgrade as quickly and painlessly as possible.
- Change
build()
calls tocreate()
calls - Components are now just plain objects
.customHelper
is deprecated- Collections are now 0-based
index
option renamed toat
and is 0-based- Remove parentheses when getting a value for a query or predicate
- Scope and
resetScope
- The
multiple
option .visitable()
.clickOnText()
Change build()
calls to create()
calls
This is very simple:
const page = PageObject.build({
// ...
});
Should be changed to:
const page = PageObject.create({
// ...
});
Components are now just plain objects
In v0.x
we deprecated the component
function. In v1.0
we
removed it completely in favor of using plain JS objects.
const page = PageObject.create({
// ...
modal: component({
// modal component definition
}),
// ...
});
Should be changed to:
const page = PageObject.create({
// ...
modal: {
// modal component definition
},
// ...
});
.customHelper
.customHelper
is now deprecated. Use Ceibo
descriptors instead. (Ceibo
is a small library for parsing trees. You can check it out here.)
With the old v0.x
syntax, you would define a custom helper like:
var disabled = customHelper(function(selector, options) {
return $(selector).prop('disabled');
});
On version 1.x
this can be represented as:
import { findElement } from 'ember-cli-page-object/extend';
export default function disabled(selector, options = {}) {
return {
isDescriptor: true,
get() {
return findElement(this, selector, options).is(':disabled');
}
}
}
Example usage:
let page = PageObject.create({
scope: '.page',
isAdmin: disabled('#override-name')
});
page.isAdmin
will look for elements in the DOM that match “.page #override-name” and check if they are disabled.
Collections are now 0-based
When we first implemented the collection
function, we were using the
nth-of-type
CSS pseudo-class which is 1-based, so we though it would
be clearer to also make collections 1-based. Later we decided to
change to an implementation to use :eq
, which is 0-based
. We decided v1.0
was the moment to break compatibility and switch to 0-based collections.
<table>
<tbody>
<tr>
<td>Jane</td>
</tr>
<tr>
<td>John</td>
</tr>
</tbody>
</table>
Example from the old v0.x
syntax:
const page = create({
users: collection({
itemScope: 'table tr',
item: {
name: text('td')
}
})
});
page.users(1).name(); // returns 'Jane'
page.users(2).name(); // returns 'John'
Example in v1.x
syntax:
const page = create({
users: collection({
itemScope: 'table tr',
item: {
name: text('td')
}
})
});
page.users(0).name; // returns 'Jane'
page.users(1).name; // returns 'John'
index
option renamed to at
and is 0-based
In v0.x
, the index
option was used to reduce the set of matched elements to the
one at the specified index which was 1-based. A small example from v0.x
:
const page = create({
secondTitle: text('h1', { index: 2 })
});
page.secondTitle(); // translates into $('h1:eq(1)').text()
In v1.x
this should be changed to:
const page = create({
secondTitle: text('h1', { at: 1 })
});
page.secondTitle; // translates into $('h1:eq(1)').text()
Remove parentheses when getting a value for a query or predicate
In v1
we decided to go a step further on improving the code and
polished the tree structure we already used when defining page objects.
The Ceibo
project was born (you can see it over
here) which defines a simple way to
create complex properties within an object. So for most cases
properties used only to get a value will no longer need parentheses
when accessed.
const page = create({
scope: '#my-page',
title: text('h1'),
fillInName: fillable('#name')
});
In v0.x
the following code was used within tests:
assert.equal(page.title(), 'My page title');
page.fillInName('Juan'); // fill #name with 'Juan'
In v1.x
this should be changed to:
assert.equal(page.title, 'My page title');
page.fillInName('Juan'); // Doesn't change
Scope and resetScope
In v0.x
defining the scope
attribute on a page object used to override how the element was looked up in the DOM. Example:
const page = create({
scope: '#my-page',
title: text('h1'),
fillInName: fillable('#name')
modal: {
scope: '#my-modal',
title: PageObject.text('h3')
}
});
When running tests in v0.x
:
page.title(); // translates to `find('#my-page h1').text()`
page.modal().title() // transaltes to `find('#my-modal h3').text()`
In v1.0
we decided to implement scope inheritance, this means that if a
component defines a scope and has a child component, the latter will
inherit its parent scope.
page.title; // translates into find('#my-page h1').text()
page.modal.title // transaltes into find('#my-page #my-modalh3').text()
In some scenarios, this change of behavior will not affect test
assertions but in some cases, it will. If you want to make sure lookups
work as in v0.x
you can use the resetScope
option (you can see more
options on the documentation site).
Changed definition to keep lookups the same:
const page = create({
scope: '#my-page',
title: text('h1'),
fillInName: fillable('#name')
modal: {
scope: '#my-modal',
resetScope: true,
title: text('h3')
}
});
The multiple
option
Another cause of failure when upgrading to v1.x
is that, by default, an error will be thrown if multiple elements match a query or predicate.
For example, if the previous page object definition is used with the following template:
<div class="#my-page">
<h1>My title</h1>
<h1>My other title</h1>
</div>
This call will throw an error:
page.title; // Kaboom!
This behavior applies to every DOM lookup except count
.
If you need to match multiple elements you can use the multiple
option on
your properties. The resulting behavior will vary depending on the
property. As an example, you can check how the multiple
option behaves on the text
property here.
.visitable()
The signature for .visitable()
has changed. Instead of receiving two distinct object parameters (dynamic segments and query params) now it receives only one.
The idea is to fill the dynamic segments first, using the values from the param object and then use the rest of the keys and values as query params.
var page = create({
visit: visitable('/users/:user_id')
});
page.visit({ user_id: 1, expanded: true });
// is equivalent to
visit("/users/1?expanded=true");
.clickOnText()
The behaviour of .clickOnText()
has improved. When looking for elements to click (based on text), the property now considers the parent element as a valid element to click. This allows to do things like
<div class="modal">
...
<button>Save</button><button>Cancel</button>
var page = PageObject.create({
clickButton: clickOnText('button'),
clickOn: clickOnText('.modal')
});
// ...
page.clickButton('Save');
page.clickOn('Save');
Before, the first action (clickButton
) would not have worked, only the second action would have found the element. Now, both actions work and both actions do click the same button.