AMA: Page Object Best Practices

Anonymous asks..

What do you think is best practice for storing element locations? I’ve heard about having an “Element map”, a single place where all element IDs or methods for location are stored. Is that a good idea? Should each Page Object have it’s own element map?

My response…

First let me preface my answer with a quote:

“best practices are useful reference points, but they must come with a warning label: the more you rely on external intelligence, the less you will value an internal idea. And this is the age of the idea”

~  Gyan Nagpal, Talent Economics

I tend to store the element locators, or selectors, in the page objects themselves. Typically if I am going to use an element locator more than once within a page object I will store this as a property of the page object, otherwise I will just use it within a method.

So I would typically do this in ES6:

import { By } from 'selenium-webdriver';
import config from 'config';

import BaseContainer from '../base-container.js';
import * as driverHelper from '../driver-helper.js';

export default class LoginPage extends BaseContainer {
	constructor( driver, visit ) {
		const loginURL = `${config.get( 'authURL' )}?redirect_to=${config.get( 'baseURL' )}`;
		super( driver, By.css( '#loginform' ), visit, loginURL );
	}

	login( username, password ) {
		driverHelper.setWhenSettable( this.driver, By.css( '#user_login' ), username );
		driverHelper.setWhenSettable( this.driver, By.css( '#user_pass' ), password, { secureValue: true } );
		return driverHelper.clickWhenClickable( this.driver, By.css( '#wp-submit' ) );
	}
}

over

import { By } from 'selenium-webdriver';
import config from 'config';

import BaseContainer from '../base-container.js';
import * as driverHelper from '../driver-helper.js';

export default class LoginPage extends BaseContainer {
	constructor( driver, visit ) {
		const loginURL = `${config.get( 'authURL' )}?redirect_to=${config.get( 'baseURL' )}`;
		super( driver, By.css( '#loginform' ), visit, loginURL );
		this.userNameSelector = By.css( '#user_login' );
		this.passwordSelector = By.css( '#user_pass' );
		this.submitSelector = By.css( '#wp-submit' );
	}

	login( username, password ) {
		driverHelper.setWhenSettable( this.driver, this.userNameSelector, username );
		driverHelper.setWhenSettable( this.driver, this.passwordSelector, password, { secureValue: true } );
		return driverHelper.clickWhenClickable( this.driver, this.submitSelector );
	}
}

Although I would use the second style (properties) if I had a second method that used the same element locators.

Anonymous also asks..

When creating a new instance of a Page Object, should there be any validation that you’re a) on the correct page and/or b) the correct elements exist? From what I understand, best practice is to keep assertions in the actual tests. Should an error be thrown if I create a LoginPage object but the login page isn’t displayed?

I like to do b) as I find it keeps tests very deterministic meaning if you know at all times you’re on the correct page, then it’s easier to work out when things go wrong. This also works well for single page applications since the DOM ready state doesn’t necessarily mean the page is loaded, so passing in an element you expect you can poll for its existence.

Other ways to check you’re on the right page would be to check the browser title and/or URL but I have found these aren’t as reliable for example the title might be translated and change depending on your locale, and the URL might be dynamic or not change for each dynamic page update.

In the above examples, the base container that all pages/components extend requires an element locator which it uses to check that page exists within a given explicit wait time. It also optionally takes visit and url parameters which allows you to navigate to the page in the browser before checking that the element exists. This is only used for specific pages like login.

Using WebDriver to automatically check for JavaScript errors on every page

Update: please see my newer post on this that doesn’t require changes to your application.


One of the benefits of using a page-object model is that you can perform certain actions on every page in your application that you visit, such as checking for accessibility.

One such check is automatically checking for JavaScript errors on page load. There’s a couple of approaches out there, one involves copying and pasting a small snippet of JavaScript into each page. Since our application we are working on uses a standard template for every page, we simply add some JavaScript to the common page header that is the first thing to load on the page, and catches any JavaScript errors that occur:

define(["amdUtils/string/interpolate"], function(interpolate) {
    window.jsErrors = [];

    window.onerror = function (errorMessage, url, lineNumber) {
        var message = interpolate("Error: [{{0}}], url: [{{1}}], line: [{{2}}]", [errorMessage, url, lineNumber]);
        window.jsErrors.push(message);
        return false;
    };
});

It’s then a matter of checking the jsErrors each time we visit a page, this is example C# code we use in the base page class for every page.

var js = driver as IJavaScriptExecutor;
ICollection javascriptErrors = null;
for (var i = 0; i < 20; i++)
{
  javascriptErrors = js.ExecuteScript("return window.jsErrors") as ICollection;
  if (javascriptErrors != null) break;
  System.Threading.Thread.Sleep(1000);
}
Assert.IsNotNull(javascriptErrors, "Can't seem to load JavaScript on the page to find JavaScript errors. Check that JavaScript is enabled.");
var javaScriptErrorsAsString = javascriptErrors.Cast<string>().Aggregate("", (current, error) => current + (error + ", "));
Assert.AreEqual("", javaScriptErrorsAsString, "Found JavaScript errors on page load: " + javaScriptErrorsAsString);

This code waits until it can read the jsErrors on the page. If it can’t, it means that JavaScript didn’t load and this is an error. Once it gets the jsErrors, it checks that there are none.

This code has been very useful for us. It has caught a number of JavaScript errors, and is especially great for finding cross browser JavaScript issues, as we run our acceptance tests in 5 different browsers.