Home Manual Reference Source
Manual » Tutorial

Creating Page Objects

Creating page objects is the one of the core fundamentals of this framework. The page objects paired with controllers create the engine that drives the applications.

In it's simplest form, a page object is a class that has 2 things:

  • the elements we want to interact with
  • methods that use these elements to take actions

Basic Shell

So let's start and stub out our new page object, then we'll talk about what we are looking at:

import BasePage from '../../base.page.js'

/** @class MyNewPage */
class MyNewPage extends BasePage {
  constructor(browser) {
    super(browser)
  }

  // Elements

  // Actions

}

export default MyNewPage

With that we have the shell of a new page object. On the first line we are importing our BasePage (which itself inherits from the main PageObjectCore). It effectively gives us methods that all pages/elements need.

The one other thing I want to call out is /** @class MyNewPage */. This is our inline documentation. Any class you create you should immediately start documenting it. When we get ot methods soon we will cover documenting those.

Integrating sub-pages

Let's say your new page actually has a generic sidebar menu on it that you may want to access at times. Simply import it, then assign it in the constructor like this:

import BasePage from '../../base.page.js'
import MySideBar from '~/src/page_objects/mySideBar.page.js' // <-- Our import

/** @class MyNewPage */
class MyNewPage extends BasePage {
  constructor(browser) {
    super(browser)
    this.mySideBar = new MySideBar(browser) // <-- Our assignment
  }

  // Elements

  // Actions

}

export default MyNewPage

Elements

Our page needs elements we want to interact with, this can be done by creating a function for each element we need, like so:

elementName() {
    return this.element(ELEMENT_SELECTOR);
  }

So, say we want to add a button to our page, we could declare our element like:

myButton() {
  return this.element("button.awesomeClass")
}

One of the benefits of using elements as functions is that we can give them arguments. For example, say we want a div of that's text is the name of a contact we dynamically generated.

contactNamed(name) {
  return this.element(`div=${name}`)
}

When declaring elements like this, we can call actions on the elements themselves like

  • this.myButton().click()
  • this.myField().setValue('hey there!')
  • this.myDiv().getAttribute('class')
  • this.myComplexThing().waitForVisible()

Let's go and add some more elements to our page below.

import BasePage from '../../base.page.js'
import MySideBar from '~/src/page_objects/mySideBar.page.js'

/** @class MyNewPage */
class MyNewPage extends BasePage {
  constructor(browser) {
    super(browser)
    this.mySideBar = new MySideBar(browser)
  }

  // Elements
  mySearchField() {
    return this.element("input#myElementId")
  }

  myButton() {
    return this.element("button.sweetClassName")
  }

  theResults() {
    return this.element("div[class^=awkwardClass__Name-that-starts_WithThis]")
  }

  // Actions

}

export default MyNewPage

Actions

Now that we have our elements, we want to actually use these in some capacity. To do so we are simply creating functions on our class. These functions could be simple wrapper just to cause less typing in a tests, or it could a longer complex chain of commands where we parse multiple elements divs, or map through an array of list items.

Generally speaking though, a basic function will look like this, and note the two words async and await.

async clickMyButton() {
  await this.myButton().click()
}

Async/await is critical. All of our commands are effectively asynchronous when it make the request to the browser. As a result, we need to ensure that our funcitons are async, and that we await for the inner function calls to be completed before continuing.

Let's add a few more methods to our page...

import BasePage from '../../base.page.js'
import MySideBar from '~/src/page_objects/mySideBar.page.js'

/** @class MyNewPage */
class MyNewPage extends BasePage {
  constructor(browser) {
    super(browser)
    this.mySideBar = new MySideBar(browser)
  }

  // Elements
  mySearchField() {
    return this.element("input#myElementId")
  }

  myButton() {
    return this.element("button.sweetClassName")
  }

  theResults() {
    return this.element("div[class^=awkwardClass__Name-that-starts_WithThis]")
  }

  // Actions
  async searchForSomething(value) { // <-- new action!
    await this.mySearchField().setValue(value)
    await this.myButton().click()
  }

  async getResultsText() { // <-- new action!
    return await this.theResults().getText()
  }

  async openSearchtab() { // <-- action using a sub-component
    await this.mySideBar.searchTab().click()
  }

}

export default MyNewPage

Adding to WebPageObjects Module

When you are ready to user you page object, don't forget to add it to our WebPageObjects module by adding lines like the following

 import MyNewPage from './path/to/MyNewPage.page.js'

 // ... more imports and pages...

 /**
  * MyNewPage
  * @see MyNewPage
  */
  MyNewPage

We can then get access to that page from WebPageObjects like

myNewPage = new WebPageObjects.MyNewPage(browser)

Next steps

With that our new page object has enough stuff for our controllers. Lets continue this in the next section about Controllers.