Page Objects in Cypress

import { login } from 
  '../../../../support/pageObjects/login.page'; 

describe('Logging In - HTML Web Form - The Internet', () => {
  context('Unauthorized', () => {
    it('User denied access with invalid details', () => {
      login.visit()
      login.username.type('invalidUser')
      login.password.type('invalidPassword{enter}')
      login.errorMsg.contains('Your username is invalid!')
      login.logOutButton.should('not.exist')
    })
  })
})

Cypress test using Page Object model

import { loginIds } from 
  '../dataTestIds/login.ids';
class LoginPage {
  visit() {
    cy.visit('/login');
    cy.url().should('include', 'login')
  }
  get username() {
    return cy.get(`${loginIds.username}`);
  }
  get password() {
    return cy.get(`${loginIds.password}`);
  }
  get successMsg() {
    return cy.get(`${loginIds.successMsg}`);
  }
  ...
}
export const login = new LoginPage();

Page Object

import { loginIds } from 
  '../dataTestIds/login.ids';
class LoginPage {
  visit() {
    cy.visit('/login');
    cy.url().should('include', 'login')
  }
  get username() {
    return cy.get(`${loginIds.username}`);
  }
  get password() {
    return cy.get(`${loginIds.password}`);
  }
  get successMsg() {
    return cy.get(`${loginIds.successMsg}`);
  }
  ...
}
export const login = new LoginPage();
import { login } from 
  '../../../../support/pageObjects/login.page'; 

describe('Logging In - HTML Web Form - The Internet', () => {
  context('Unauthorized', () => {
    it('User denied access with invalid details', () => {
      login.visit()
      login.username.type('invalidUser')
      login.password.type('invalidPassword{enter}')
      login.errorMsg.contains('Your username is invalid!')
      login.logOutButton.should('not.exist')
    })
  })
})

If you like Page Object approach - go ahead

import { loginIds } from 
  '../dataTestIds/login.ids';

export const visit = () => {
  cy.visit('/login');
  cy.url().should('include', 'login')
}
export const username = () =>
  cy.get(`${loginIds.username}`);
export const password = () =>
  cy.get(`${loginIds.password}`);
export const successMsg = () =>
  cy.get(`${loginIds.successMsg}`);
...
import * as login from 
  '../../../../support/pageObjects/login.page'; 

describe('Logging In - HTML Web Form - The Internet', () => {
  context('Unauthorized', () => {
    it('User denied access with invalid details', () => {
      login.visit()
      login.username().type('invalidUser')
      login.password().type('invalidPassword{enter}')
      login.errorMsg().contains('Your username is invalid!')
      login.logOutButton().should('not.exist')
    })
  })
})

You might as well just use small functions

import { loginIds } from 
  '../dataTestIds/login.ids';

let loggedIn = false

export const visit = () => {
  cy.visit('/login');
  cy.url().should('include', 'login')
}

export const login = (username, password) => {
  // do login via UI
  // assert logged in
  loggedIn = true
}
...
import * as login from 
  '../../../../support/pageObjects/login.page'; 

describe('Logging In - HTML Web Form - The Internet', () => {
  context('Unauthorized', () => {
    it('User denied access with invalid details', () => {
      login.visit()
      login.login('username', 'password')
    })
  })
})

But once you start storing state in PageObjects ...

PageObjects with state = another abstraction layer above your web application

  • No documentation

  • Only used for testing

  • Harder to maintain

✅ stateless Page Object methods

✅ pure utility functions

Cypress includes bundler!

✅ drive app through actions

✅ custom commands

✅ pure utility functions

export const resetDatabase = () => {
  console.log('resetDatabase')
  cy.request({
    method: 'POST',
    url: '/reset',
    body: {
      todos: []
    }
  })
}
export const getTodoApp = () => cy.get('.todoapp')
export const getTodoItems = () =>
  getTodoApp().find('.todo-list').find('li')

✅ pure utility functions

import { 
  getTodoApp,
  resetDatabase,
} from '../../support/utils'

beforeEach(resetDatabase)
beforeEach(() => cy.visit('/'))

it('loads application', () => {
  getTodoApp().should('be.visible')
})

✅ drive app through actions

const store = createStore(reducer)

// expose store when run in Cypress
if (window.Cypress) {
  window.store = store
}

app code

✅ drive app through actions

it('can drive app by dispatching actions', () => {
  cy.visit('/')
  // dispatch Redux action
  cy.window().its('store')
    .invoke('dispatch', { type: 'LOGIN', username, password })
  // check if the user is logged in
})

test code

✅ drive app through actions

// share code with the application
import { login } from '../../src/actions'
const dispatch = action => 
  cy.window().its('store').invoke('dispatch', action)
it('can drive app by dispatching actions', () => {
  cy.visit('/')
  dispatch(login(username, password))
  // check if the user is logged in
})

test code

✅ custom commands

Cypress.Commands.add('login', (username, password) => {
  // response cookies are automatically set
  // https://on.cypress.io/request#Cookies
  cy.request({
    url: '/login',
    method: 'POST',
    body: {
      username, 
      password
    })
})

cypress/support/commands.js

✅ custom commands

it('can login using custom command', () => {
  cy.visit('/')
  cy.login(username, password)
  // check if the user is logged in
})

cypress/support/commands.js

✅ pure utility functions

⚠️ custom commands

⚠️ page objects

✅ app actions

Recommended

Deep diving PageObject pattern and using it with Cypress

WIP: Page Objects in Cypress

By Gleb Bahmutov

WIP: Page Objects in Cypress

Page Objects vs functions vs custom commands in writing Cypress end-to-end tests. Work in progress. Read full blog post at https://www.cypress.io/blog/2019/01/03/stop-using-page-objects-and-start-using-app-actions/

  • 1,530
Loading comments...

More from Gleb Bahmutov