Hello everyone!
I was inspired to write this article by the fact that thereās no official documentation on Page Object Model (POM) implementation in Cypress. You might find some articles online, but I find them imperfect - Iāve got a thing or two to add.
I also suggest that youāve already searched online with keywords cypress page object model and read the article that says not to use this design pattern. Donāt fool yourself that this is the Cypress canon usage just because the article is posted in Cypress blog (at least scroll down to the comments section and youāll see what I mean). POM is still the most popular test automation design pattern and it works pretty well with Cypress!
WHATāS POM?
Page Object Model (POM) is a test automation programming (or design) pattern. Itās built on top of the Inheritance paradigm of OOP (object-oriented programming).
As itās a
Key concepts of POM are:
-
Some pages have
mutual logic - This logic may be shared in a single place to avoid code repetition (
parent class tochild classes )
- This logic may be shared in a single place to avoid code repetition (
-
Pages have
elements andmethods - Element = DOM tree element
- Method = sequence of actions on the page
-
Pages are presented as
classes , elements asclass attributes , and methods asclass methods
IMPLEMENTATION
Hereās an example GitHub repo with tests for Swag Labs example web app.
- Create a folder for your Page Objects, preferably name it
pages
and place it in your cypress directory, like this:
- Create
page.js
inside ofpages
folder - thatāll be a parent page (class) for all other pages. Itāll contain selectors/methods that can be used on EVERY page of the app.
- Create
Page
a class inside ofpage.js
and fill it with some basic stuff. No worries if thereās not much content in the beginning - youāll fill it up later!
class Page {
open(path) {
return cy.visit(path)
}
}
export default Page
- Create
auth.page.js
right nearpage.js
and fill it with anAuthPage
class by extending existingPage
class:
import Page from './page'
class AuthPage extends Page {
get inputUsername() {return cy.get('[data-test="username"]')}
get inputPassword() {return cy.get('[data-test="password"]')}
get buttonLogIn() {return cy.get('[data-test="login-button"]')}
get containerError() {return cy.get('[data-test="error"]')}
open() {
return super.open('/')
}
logIn(username, password) {
this.inputUsername.type(username)
this.inputPassword.type(password)
this.buttonLogIn.click()
}
}
export default new AuthPage()
- Now you simply import the page that you need in your test (
spec
orcy
) files and call its selectors/methods:
import AuthPage from '../pages/auth.page'
import user from '../fixtures/user.json'
import error from '../fixtures/error.json'
describe('Authentication', () => {
beforeEach(() => {
AuthPage.open()
})
it('With existing credentials', () => {
AuthPage.logIn(user.username, user.password)
cy.location('pathname')
.should('include', 'inventory')
})
it('With non-existing credentials', () => {
AuthPage.logIn('foo', 'bar')
AuthPage.containerError
.should('have.text', error.credentials)
})
})
And thatās pretty much it āļø
Now compare it with the raw approach, when you donāt use POM:
import user from '../fixtures/user.json'
import error from '../fixtures/error.json'
describe('Authentication', () => {
beforeEach(() => {
cy.visit('/')
})
it('With existing credentials', () => {
cy.get('[data-test="username"]')
.type(user.username)
cy.get('[data-test="password"]')
.type(user.password)
cy.get('[data-test="login-button"]')
.click()
cy.location('pathname')
.should('include', 'inventory')
})
it('With non-existing credentials', () => {
cy.get('[data-test="username"]')
.type('foo')
cy.get('[data-test="password"]')
.type('bar')
cy.get('[data-test="login-button"]')
.click()
cy.get('[data-test="error"]')
.should('have.text', error.credentials)
})
})
I think itās straightforward that POM lets you get rid of repetitive code blocks and make tests much more readable.
BEST PRACTICES
Method creation for every action
It doesnāt make any sense if you create methods for simple actions - youāre just writing more code and making Page Objects larger, which canāt be good.
ā DO NOT create methods for simple actions (i.e. click, type text, etc):
LoginPage.clickLogInButton()
ā USE THIS instead:
LoginPage.buttonLogIn.click()
Huge base page class
You should only add those selectors & methods to the parent page, which are related to ALL child pages.
If thereās a logic thatās not related to any page, consider using Cypress Custom Commands.
If you have multiple pages that share the same elements/methods but you canāt group them under a single parent class - you may create a Page Element for such logic and include it in pages where itās required (but itās a topic for another article).