Skip to content
Ryan Johnson edited this page Jun 13, 2016 · 6 revisions

Writing Page Objects

Page objects aid in performing actions in the UI in the same manner as a user would perform them.

0. Simple Input/Output

Page objects should accept and return simple values ([arrays of] strings, numbers, booleans), because a user isn't going to construct a Date or moment object to set a date picker, and they probably aren't going to be looking at the value of a dropdown through an object. By writing your page object functions/properties in a manner in which they accept and return these simple values, the page object API becomes much simpler to understand.

  • Use standard formats, when applicable (date, time, currency, telephone, etc.)

NOTE: The examples below show how to avoid problems using date/time values, but the concept can be applied to any type of value.

DON'T

  • The properties and params below all deal with date/time values in one form or another.
  • They accept/return three different forms of date/time values (Date, moment, and 'String').
  • The returned String value is not in a typical format for a date and time.

This API is confusing and unintuitive for the consumer of the page objects.

/**
 * @property {Date} startDate - beginning of date range
 * @property {Date} endDate - end of date range
 */

/**
 * @param {moment} date - selected date
 * @param {moment} time - selected time
 */

/**
 * @returns {String} - full date/time value in format of DD-MM-YY-HH:mm
 */

DO

  • Standardize on Strings as the least common denominator.
  • Use accepted standards when formatting the values (ISO8601 in the case of date/time strings)

Making this simple change, we've simplified the interface, and standardized the output. A benefit of doing this is that a date string return value can be used directly as an input value for a date property/param without any unnecessary conversion.

/**
 * @property {String} startDate - beginning of date range (ISO8601 Format: YYYY-MM-DD)
 * @property {String} endDate - end of date range (ISO8601 Format: YYYY-MM-DD)
 */

/**
 * @param {String} date - selected date (ISO8601 Format: YYYY-MM-DD)
 * @param {String} time - selected time (ISO8601 Format: HH:mmZ)
 */

/**
 * @returns {String} - full date/time value (ISO8601 Format: YYYY-MM-DDTHH:mmZ)
 */

1. Promise-based Return Values

Why is console.log outputting an object?

Protractor is based on promises, so all page object functions and properties that return a then-able promise should be documented as such.

DON'T

/**
 * @returns {Boolean} Can do stuff?
 * @property {Number} x - Horizontal, X value
 */

DO

/**
 * @returns {Promise<Boolean>} Can do stuff?
 * @property {Promise<Number>} x - Horizontal, X value
 */

2. Dates and Times

... because time zones.

Accept and Return Strings

All dates and times are to be presented to page objects as string literals. A property that accepts a date/time string when setting, should return a date/time string when getting.

DON'T
var fooDate = new Date('2016-12-09');
encore.rxComponent.date = fooDate;

encore.rxComponent.date; // Date object
DO
var fooDate = '2016-12-09';
encore.rxComponent.date = fooDate;

encore.rxComponent.date; // '2016-12-09'

3. Predicate Functions

Positive-Only Predicates API

Protractor doesn't do it, why should we?

Only provide the positive predicate function for a value. Consumers can determine the correct boolean to expect when used.

DON'T
isEnabled: function () { /* ... */ },
isDisabled: function () {
  return this.isEnabled().then(encore.rxMisc.negate);
},

isOpen: function () { /* ... */ },
isClosed: function () {
  return this.isOpen().then(encore.rxMisc.negate);
},

isValid: function () { /* ... */ },
isInvalid: function () {
  return this.isValid().then(encore.rxMisc.negate);
},

isDisplayed: function () { /* ... */ },
isInvisible: function () { 
  return this.isVisible().then(encore.rxMisc.negate);
}
DO
isEnabled: function () { /* ... */ },
isOpen: function () { /* ... */ },
isValid: function () { /* ... */ },
isDisplayed: function () { /* ... */ },

Mimic Protractor When Possible

Don't rename the wheel.

When defining a predicate function in your page objects, try to use the following names to match what protractor provides.

  • isPresent()
  • isEnabled()
  • isSelected()
  • isDisplayed()

This reduces confusion when testers consume your page object.

4. Intermittent States

Y U NO pending?

Avoid testing intermittent element states (between true/false). Protractor does not have a straightforward way to test state as an element is transitioning, so just save yourself the trouble.

5. ES6 Class Syntax

ES6 is the future... and page objects run on Node.

To be continued...

6. Getters/Setters (under review)

Use native JS get and set syntax.

Object.defineProperty(myObject, 'myProperty', {
  get: function () { return this.myProperty; },
  set: function (val) { this.myProperty = val; }
});

7. Function Naming

Action Functions

Action functions should begin with a verb to denote that they perform some sort of functionality.

// BAD
obj.previousMonth();
obj.nextMonth();

// GOOD
obj.goToPreviousMonth();
obj.goToNextMonth();

Predicate Functions (under review)

Predicate functions are those that return boolean values. These functions should begin with "is", "can", or "has".

  • "is"
    • used to query state of an object (e.g. isOpen())
  • "can"
    • used to query capability of an object (e.g. canOpen())
  • "has"
    • used to query possession (e.g. hasItem(foo), hasAction(bar))
// BAD
obj.foo();

// GOOD
obj.isBar();
obj.canFoo();
obj.hasThing(someThing);

7. ???