Transcripted Summary

Page objects! They are a staple of designing a test suite.

They are the standard way of coping with large test suites, because, first of all, they enable us to write the tests using objects that are relevant to the application; and limit the use of selectors and other page-specific code, which makes the test code incomprehensible.

So instead of writing this:



I can just write:



Page objects also enable some level of flexibility when the structure of a page changes and we need to change our code to accommodate that.

If the change is small, all we need to do is change the code in the page object class, and all the tests will now work.

So, if the new todo has a different class, or maybe instead of pressing Enter, one needs to click on the “Add” button, then we don’t need to change any test — we just need to change the implementation of the addTodo function.

But let’s not deal with theory. Let’s jump into the practice.


Let’s change the code in this beforeEach to use a page object.



First let’s write the page object itself, which will have 2 methods: navigate and addTodo.

We don’t want the TodoPage class to live in the integrations folder because it is not a test, so we create a new folder (called “page-objects”) for all our page object classes. In this case, we will have only one such class, but in a large project you will definitely have more than one.

So, let’s create a file for that classes code and we’ll call it “todo-page.js”.

JavaScript Modules

In JavaScript, a file such as this is called a “module”. In JavaScript a file and a module are one and the same.

This module will export one thing: the page object class. Let’s write it.


First, we write an empty class:


export class TodoPage {

}

As you can see, the syntax is similar to Java and C#.

The export is there to say that this class can be imported and used from outside this module. If we hadn’t added the export, no other module would have been able to import the class.


Let’s populate it with the first method: navigate.

navigate is the function, it doesn't accept parameters. Feel free to copy the cy.visit line from todo-actions.spec.js


navigate() {
    cy.visit('http://todomvc-app-for-testing.surge.sh/')
}

There, we have our first method inside the class.

Again, very similar to Java, except for the fact that we don’t need to declare the type of the parameters or of the return value!


Let’s write the other method to complete the class: addTodo

Let's copy the code from todo-actions.spec.js and tweak it a bit.

We get the new-todo; we can remove the timeout. And it's not “Clean room”; we need to add the “todoText”.


addTodo(todoText) {
    cy.get('.new-todo').type(todoText + '{enter}')
}

Perfect!

We already have a usable class that we can use to make some code in the class be better. Let’s do that.

Let’s open todo-actions.js.


Let’s change the beforeEach code to use the pageObject.

First, let’s create the page object itself so we can use it.


/// <reference types="cypress" />

import {TodoPage} from '../page-objects/todo-page'

describe('todo actions', () => {
  const todoPage = new TodoPage()
})

We create JavaScript variables using const or let.

Note about JavaScript Variables

let is for when you want to change the value of the variable afterwards, but in this case, we don't want to change the value, so we enforce this using const. This is usually what you will use.

The rest is similar to Java: we create an object from the class using new.

Notice that Visual Studio Code added the import statement for our class! If your IDE doesn’t support that, you can add it manually.

Contrary to Java and C#, we reference modules using their path on disk the. This import references the todo-page.js module by its path relative to the current file.

Now let’s use this object that we created.


Let's replace cy.visit with todoPage.navigate:



There, we just replaced our complicated and incomprehensible line with a nice and very clear line.


Let’s do that to the next line.

Notice how helpful Visual Studio Code is in completion even with classes that we've created.


beforeEach(() => {
    todoPage.navigate()
    todoPage.addTodo('Clean room')
})

Voila!

Another complicated line replaced with a nice and easy to understand line. Hooray for page objects!

We’re not going to replace the whole thing, but let’s add one more method.


Let’s replace the validation in the first it[cy.get('label')], to use the page Object.



We'll create a new method called validateTodoTxt in our page object file, “todo-page.js”.


validateTodoTxt(todoIndex, expectedText) {
    cy.get(`.todo-list li:nth-child(${todoIndex + 1}) label`).should('have.text', expectedText)
}

Let’s look at this method.

It accepts a todoIndex, which is zero-based, meaning index 0 is the first todo, as is customary in most programming languages, and also accepts the expectedText which it will use to validate the text of the todo.

The cy.get is a bit complicated. The selector finds the todo list using .todo-list then goes to the n-th li using nth-child.

Unfortunately, CSS is 1-based, so we need to add 1 to todoIndex.


But what’s that “dollar thingy [ ${...}]?

Template Strings

In JavaScript, if the String uses backticks(``), then you can add `${..}` and anything between the curly braces will be evaluated and inserted into the String. This feature is called Template Strings, but a lot of people call it string interpolation.

The rest of the code now makes sense: we check that it should have the text.


Remember that Cypress makes things easy for us, as it retries the whole thing, if needed, to make our tests less flaky!

There, we made the method. Let’s use it.

We’ll go to todo-actions.spec.js and replace the first line in the first it with: todoPage.validateTodoText(0, 'Clean room')



We’re done.

I’ve already prepared the whole page object, and the whole tests to use this, so let’s check out the branch with the existing solution.

As you can see the page object module has a lot of methods now.


# todo-page.js

/// <reference types="cypress" />

export class TodoPage {
  navigate() {
    cy.visit('http://todomvc-app-for-testing.surge.sh/')
}

  addTodo(todoText) {
    cy.get('.new-todo').type(todoText + '{enter}')
  }

  toggleTodo(todoIndex) {
    cy.get(`.todo-list li:nth-child(${todoIndex + 1}) .toggle`).click()
  }

  showOnlyCompletedTodos() {
    cy.contains('Completed').click()
  }

  showOnlyActiveTodos() {
    cy.contains('Active').click()
  }

  showAllTodos() {
    cy.contains('All').click()
  }

  clearCompleted() {
    cy.contains('Clear completed').click()
  }

  validateNumberOfTodosShown(expectedNumberOfTodos) {
    cy.get('.todo-list li').should('have.length', expectedNumberOfTodos)
  }

  validateTodoCompletedState(todoIndex, shouldBeCompleted) {
    const l = cy.get(`.todo-list li:nth-child(${todoIndex + 1}) label`)
    l.should(`${shouldBeCompleted ? '' : 'not.'}have.css`, 'text-decoration-line', 'line-through')
  }

  validateTodoText(todoIndex, expectedText) {
    cy.get(`.todo-list li:nth-child(${todoIndex + 1}) label`).should('have.text', expectedText)
  }

  validateToggleState(todoIndex, shouldBeToggled) {
    const label = cy.get(`.todo-list li:nth-child(${todoIndex + 1}) label`)
    label.should(`${shouldBeToggled ? '' : 'not.'}be.checked`)
  }
}

See how much more understandable everything is!

The code reads more like a specification of a test, and less like a lot of selector and Cypress gobbledygook.

Here’s the code for one of our individual tests using page objects:


/// <reference types="cypress" />

import {TodoPage} from '../page-objects/todo-page'

describe('todo actions', () => {
  const todoPage = new TodoPage()

  beforeEach(() => {
    todoPage.navigate()
    todoPage.addTodo('Clean room')
  })

  it('should add a new todo to the list', () => {
    todoPage.validateTodoText(0, 'Clean room')
    todoPage.validateToggleState(0, false)
  })

You can find the link to the git repository for this page object code and the complete test using the page objects in the Reference section.

Let’s look at our TodoPage class — something here bothers me.



Why are we using a class? Is there a good reason to do so?

If we look at each method here, every method could be static, as it’s not referencing anything in the class itself.

Classes are Unnecessary

If you’re used to Selenium Page Objects, then those page objects usually store the driver in the object, so an object is needed, but in Cypress, we don’t need that as Cypress takes care of all the browser initialization. So why do we need the class at all? In Java and C#, everything's an object, so we must have a class, but idiomatic JavaScript doesn’t use classes when they’re not needed.


Can we get rid of the class and just export the functions? Yes!

Let’s do that.

  • First, we get rid of the class — we’ll delete the export class… line and its balanced curly braces at the end

  • Then we take each function and turn it into a function — add a function prefix to each method

  • Each of these functions need to be exported, so we export them.


export function navigate() {
  cy.visit('http://todomvc-app-for-testing.surge.sh/')
}

And we’re done!



The link to the git repository with the full code for this is in the resources.

  • Instead of exporting one class, we’re exporting a set of functions

  • Instead of a “page object”, we’ve created a “page module”

Now let’s use them.


Let’s import the functions we need, instead of the TodoPage class, and use them in our test.

  • Since we don’t need the object anymore, we can remove the object creation

  • Now, instead of importing todoPage, we'll import the functions like navigate, addTodo and validateTodoText, etc

I already wrote all the code so that it all works with page modules, so there we go.


# todo-actions.spec.js

/// <reference types="cypress" />

import {
  navigate,
  addTodo,
  validateTodoText,
  toggleTodo,
  clearCompleted,
  validateTodoCompletedState,
  validateToggleState,
  validateNumberOfTodosShown,
} from '../page-objects/todo-page'

describe('todo actions', () => {
  beforeEach(() => {
    navigate()
    addTodo('Clean room')
  })

  it('should add a new todo to the list', () => {
    validateTodoText(0, 'Clean room')
    validateToggleState(0, false)
  })

  describe('toggling todos', () => {
    it('should toggle test correctly', () => {
      toggleTodo(0)
      validateTodoCompletedState(0, true)
    })

    it('should clear completed', () => {
      toggleTodo(0)
      clearCompleted()
      validateNumberOfTodosShown(0)
    })
  })
})

And we’re done!

We’ve now seen how we can modularize our tests using modules, and also make them nicer to write using page objects and modules.



Resources