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!
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
Pages have
Pages are presented as
Here’s an example GitHub repo with tests for Swag Labs example web app.
pages
and place it in your cypress directory, like this:
page.js
inside of pages
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.
Page
a class inside of page.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
auth.page.js
right near page.js
and fill it with an AuthPage
class by extending existing Page
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()
spec
or cy
) 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.
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()
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).