I recently shared how to get started with WebDriverJS and Mocha, and how to use hooks.
This post continues on from there: I will share how to set up our own page objects to model our application for maintainability.
We finished last time with two tests, both duplicating information about our app like the URLs and page elements:
test.it('shows a quote container', function() { driver.get('http://ralphsays.github.io'); driver.isElementPresent(webdriver.By.id('quote')).then(function(present) { assert.equal(present, true, "Quote container not displayed"); }); }); test.it('shows a non-empty quote', function() { driver.get('http://ralphsays.github.io'); driver.findElement(webdriver.By.id('quote')).getText().then(function(text) { assert.notEqual(text, '', 'Quote is empty'); }); });
We can start by creating a simple page object which is just a basic JavaScript class with two properties: a driver and a URL.
var webdriver = require('selenium-webdriver'); RalphSaysPage = function RalphSaysPage(driver) { this.driver = driver; this.url = 'http://ralphsays.github.io'; }; RalphSaysPage.prototype.visit = function() { this.driver.get(this.url); return webdriver.promise.fulfilled(true); }; module.exports = RalphSaysPage;
We can require this class in our Mocha spec like so:
var RalphSaysPage = require('../lib/ralph-says-page.js');
…and we create an instance of if, but it won’t do us much good at the moment as it doesn’t have any functions.
test.it('shows a quote container', function() { var ralphSaysPage = new RalphSaysPage(driver); driver.get('http://ralphsays.github.io'); driver.isElementPresent(webdriver.By.id('quote')).then(function(present) { assert.equal(present, true, "Quote container not displayed"); }); }); test.it('shows a non-empty quote', function() { var ralphSaysPage = new RalphSaysPage(driver); driver.get('http://ralphsays.github.io'); driver.findElement(webdriver.By.id('quote')).getText().then(function(text) { assert.notEqual(text, '', 'Quote is empty'); }); });
What we need to be able to do is visit our page! So we create a visit function.
RalphSaysPage.prototype.visit = function() { this.driver.get(this.url); return webdriver.promise.fulfilled(true); };
Note: we need to return a promise, which is what WebDriver and Mocha uses to make sure our test steps run in correct order.
At that point our tests get slightly better as we can call visit and get rid of the URLs.
test.it('shows a quote container', function() { var ralphSaysPage = new RalphSaysPage(driver); ralphSaysPage.visit(); driver.isElementPresent(webdriver.By.id('quote')).then(function(present) { assert.equal(present, true, "Quote container not displayed"); }); }); test.it('shows a non-empty quote', function() { var ralphSaysPage = new RalphSaysPage(driver); ralphSaysPage.visit(); driver.findElement(webdriver.By.id('quote')).getText().then(function(text) { assert.notEqual(text, '', 'Quote is empty'); }); });
We do similar for checking the existence and the text of our quote container. These are slightly different in that they need to still return a promise, but they are only fulfilled themselves within the promise of the functions of they calling. This means our page model looks like this now:
var webdriver = require('selenium-webdriver'); RalphSaysPage = function RalphSaysPage(driver) { this.driver = driver; this.url = 'http://ralphsays.github.io'; }; RalphSaysPage.prototype.visit = function() { this.driver.get(this.url); return webdriver.promise.fulfilled(true); }; RalphSaysPage.prototype.quoteContainerPresent = function() { var d = webdriver.promise.defer(); this.driver.isElementPresent(webdriver.By.id('quote')).then(function(present) { d.fulfill(present); }); return d.promise; }; RalphSaysPage.prototype.quoteTextDisplayed = function() { var d = webdriver.promise.defer(); this.driver.findElement(webdriver.By.id('quote')).getText().then(function(text) { d.fulfill(text); }); return d.promise; }; module.exports = RalphSaysPage;
The only issue I see is we’re still duplicating the selector for the quote container. We can store this as a property of the page as well.
var webdriver = require('selenium-webdriver'); RalphSaysPage = function RalphSaysPage(driver) { this.driver = driver; this.url = 'http://ralphsays.github.io'; this.quoteSelector = webdriver.By.id('quote'); }; RalphSaysPage.prototype.visit = function() { this.driver.get(this.url); return webdriver.promise.fulfilled(true); }; RalphSaysPage.prototype.quoteContainerPresent = function() { var d = webdriver.promise.defer(); this.driver.isElementPresent(this.quoteSelector).then(function(present) { d.fulfill(present); }); return d.promise; }; RalphSaysPage.prototype.quoteTextDisplayed = function() { var d = webdriver.promise.defer(); this.driver.findElement(this.quoteSelector).getText().then(function(text) { d.fulfill(text); }); return d.promise; }; module.exports = RalphSaysPage;
This means our tests now look a bit nicer as they have no selectors in them:
test.it('shows a quote container', function() { var ralphSaysPage = new RalphSaysPage(driver); ralphSaysPage.visit(); ralphSaysPage.quoteContainerPresent().then(function(present) { assert.equal(present, true, "Quote container not displayed"); }); }); test.it('shows a non-empty quote', function() { var ralphSaysPage = new RalphSaysPage(driver); ralphSaysPage.visit(); ralphSaysPage.quoteTextDisplayed().then(function(text) { assert.notEqual(text, '', 'Quote is empty'); }); });
I make them both fail, and pass again, so I am confident they are testing the correct thing.
Ralph Says ✓ shows a quote container (1392ms) ✓ shows a non-empty quote (104ms) 2 passing (2s)
So we’ve managed to roll our own WebDriverJS page objects in JavaScript!
You can see the full source code in this repository, and see it running on CircleCI here.
