Page Model
Page Model is a test automation pattern that allows you to create an abstraction of the tested page and use it in test code to refer to page elements.
- Why Use Page Model
- Create a Page Model
- Step 1 - Declare a Page Model Class
- Step 2 - Add a Page Element to the Page Model
- Step 3 - Write a Test That Uses the Page Model
- Step 4 - Add a New Class for Check Boxes
- Step 5 - Add a List of Check Boxes to the Page Model
- Step 6 - Write a Test That Iterates Through Check Boxes
- Step 7 - Add Actions to the Page Model
- Step 8 - Write a Test That Calls Actions From the Page Model
- Page Model Example
Why Use Page Model #
Consider the following fixture with two tests: one that types and edits the developer name on the example webpage and the other that checks check boxes in the Features section.
import { Selector } from 'testcafe';
fixture `My fixture`
.page `https://devexpress.github.io/testcafe/example/`;
test('Text typing basics', async t => {
await t
.typeText('#developer-name', 'Peter')
.typeText('#developer-name', 'Paker', { replace: true })
.typeText('#developer-name', 'r', { caretPos: 2 })
.expect(Selector('#developer-name').value).eql('Parker');
});
test('Click check boxes and then verify their state', async t => {
await t
.click('input[id=remote-testing]')
.expect(Selector('input[id=remote-testing]').checked).ok()
.click('input[id=reusing-js-code]')
.expect(Selector('input[id=reusing-js-code]').checked).ok()
.click('input[id=continuous-integration-embedding]')
.expect(Selector('input[id=continuous-integration-embedding]').checked).ok();
});
Note that both tests contain excessive code.
In the first test, the #developer-name
CSS selector is duplicated in code each time the test refers to the input element.
In the second test, test logic is duplicated for each check box.
In a rapidly developing web application, page markup and design may change often. When this happens, you need to modify selectors in all your tests. The Page Model allows you to keep all selectors in one place, so the next time the webpage changes, you will only need to modify the page model.
Generally speaking, the Page Model pattern allows you to follow the separation of concerns principle - you keep page representation in the Page Model, while tests remain focused on the behavior.
Create a Page Model #
Step 1 - Declare a Page Model Class #
Begin with a new .js
file, declare the Page
class there, and export its instance.
class Page {
constructor () {
}
}
export default new Page();
This class will contain the Page Model, so name the file page-model.js
.
Step 2 - Add a Page Element to the Page Model #
Add the Developer Name
input element to the model. To do this,
introduce the nameInput
property and assign a selector to it.
import { Selector } from 'testcafe';
class Page {
constructor () {
this.nameInput = Selector('#developer-name');
}
}
export default new Page();
Step 3 - Write a Test That Uses the Page Model #
In the test file, import the page model instance from page-model.js
.
After that, you can use the page.nameInput
property to identify the Developer Name
input element.
import page from './page-model';
fixture `My fixture`
.page `https://devexpress.github.io/testcafe/example/`;
test('Text typing basics', async t => {
await t
.typeText(page.nameInput, 'Peter')
.typeText(page.nameInput, 'Paker', { replace: true })
.typeText(page.nameInput, 'r', { caretPos: 2 })
.expect(page.nameInput.value).eql('Parker');
});
Step 4 - Add a New Class for Check Boxes #
Add check boxes from the Features section to the Page Model.
As long as each item in the Features section contains a check box and a label,
introduce a new class Feature
with two properties: label
and checkbox
.
import { Selector } from 'testcafe';
const label = Selector('label');
class Feature {
constructor (text) {
this.label = label.withText(text);
this.checkbox = this.label.find('input[type=checkbox]');
}
}
class Page {
constructor () {
this.nameInput = Selector('#developer-name');
}
}
export default new Page();
Step 5 - Add a List of Check Boxes to the Page Model #
In the Page
class, add the featureList
property with an array of Feature
objects.
import { Selector } from 'testcafe';
const label = Selector('label');
class Feature {
constructor (text) {
this.label = label.withText(text);
this.checkbox = this.label.find('input[type=checkbox]');
}
}
class Page {
constructor () {
this.nameInput = Selector('#developer-name');
this.featureList = [
new Feature('Support for testing on remote devices'),
new Feature('Re-using existing JavaScript code for testing'),
new Feature('Easy embedding into a Continuous integration system')
];
}
}
export default new Page();
Organizing check boxes in an array makes the page model semantically correct and simplifies iterating through the check boxes.
Step 6 - Write a Test That Iterates Through Check Boxes #
The second test now boils down to a single loop.
import page from './page-model';
fixture `My fixture`
.page `https://devexpress.github.io/testcafe/example/`;
test('Text typing basics', async t => {
await t
.typeText(page.nameInput, 'Peter')
.typeText(page.nameInput, 'Paker', { replace: true })
.typeText(page.nameInput, 'r', { caretPos: 2 })
.expect(page.nameInput.value).eql('Parker');
});
test('Click check boxes and then verify their state', async t => {
for (const feature of page.featureList) {
await t
.click(feature.label)
.expect(feature.checkbox.checked).ok();
}
});
Step 7 - Add Actions to the Page Model #
Add an action that enters the developer name and clicks the Submit button.
Import
t
, a test controller, from thetestcafe
module.import { Selector, t } from 'testcafe';
Add a Submit button to the page model.
this.submitButton = Selector('#submit-button');
Declare an asynchronous function in the
Page
class. This function uses the test controller to perform several actions on the tested page: enter the developer name and click the Submit button.async submitName (name) { await t .typeText(this.nameInput, name) .click(this.submitButton); }
Here is how the page model looks now.
import { Selector, t } from 'testcafe';
const label = Selector('label');
class Feature {
constructor (text) {
this.label = label.withText(text);
this.checkbox = this.label.find('input[type=checkbox]');
}
}
class Page {
constructor () {
this.nameInput = Selector('#developer-name');
this.featureList = [
new Feature('Support for testing on remote devices'),
new Feature('Re-using existing JavaScript code for testing'),
new Feature('Easy embedding into a Continuous integration system')
];
this.submitButton = Selector('#submit-button');
}
async submitName (name) {
await t
.typeText(this.nameInput, name)
.click(this.submitButton);
}
}
export default new Page();
Step 8 - Write a Test That Calls Actions From the Page Model #
Now write a test that calls page.submitName
and checks the message on the Thank You page.
test('Submit a developer name and check the header', async t => {
const header = Selector('#article-header');
await page.submitName('Peter');
await t.expect(header.innerText).eql('Thank you, Peter!');
});
This test works with a different page for which there is no page model. That is why it uses a selector. Don't forget to import it to the test file.
import { Selector } from 'testcafe';
Page Model Example #
This sample shows a page model for the example page at https://devexpress.github.io/testcafe/example/.
import { Selector, t } from 'testcafe';
const label = Selector('label');
class Feature {
constructor (text) {
this.label = label.withText(text);
this.checkbox = this.label.find('input[type=checkbox]');
}
}
class OperatingSystem {
constructor (text) {
this.label = label.withText(text);
this.radioButton = this.label.find('input[type=radio]');
}
}
class Page {
constructor () {
this.nameInput = Selector('#developer-name');
this.triedTestCafeCheckbox = Selector('#tried-test-cafe');
this.populateButton = Selector('#populate');
this.submitButton = Selector('#submit-button');
this.results = Selector('.result-content');
this.commentsTextArea = Selector('#comments');
this.featureList = [
new Feature('Support for testing on remote devices'),
new Feature('Re-using existing JavaScript code for testing'),
new Feature('Running tests in background and/or in parallel in multiple browsers'),
new Feature('Easy embedding into a Continuous integration system'),
new Feature('Advanced traffic and markup analysis')
];
this.osList = [
new OperatingSystem('Windows'),
new OperatingSystem('MacOS'),
new OperatingSystem('Linux')
];
this.slider = {
handle: Selector('.ui-slider-handle'),
tick: Selector('.slider-value')
};
this.interfaceSelect = Selector('#preferred-interface');
this.interfaceSelectOption = this.interfaceSelect.find('option');
this.submitButton = Selector('#submit-button');
}
async submitName (name) {
await t
.typeText(this.nameInput, name)
.click(this.submitButton);
}
}
export default new Page();