Page Object Model is a design pattern that focuses on making a Test Automation Framework extensible, maintainable, and easy to understand. In this article, we will see how we can create a simple Page Object structure for cypress.
To implement POM for Cypress we would be doing two things:
1. A place for managing locators – ‘pages’ folder
2. A place to write reusable functions – ‘utils’ folder
Let’s further deep dive by automating a scenario:
1. Successful Login to https://opensource-demo.orangehrmlive.com/
2. Successful Logout
Step 1: Create two folders pages and utils inside integration folder. So, for each page of the application, there will be two files; one containing all the locators of that page and the other containing all the reusable codes/functions. For our test case, we would be working with two pages – the login page and dashboard page.
So, the pages folder will have two files ‘loginPage.js’ and ‘dashboardPage.js’ which contain all the locators of the respective pages. Similarly, the utils folder will also have two files ‘login.js’ and ‘dashboard.js’ containing all the reusable codes/functions for the respective pages.
Step 2: Since we will be using custom commands in a file other than commands.js, we have to import files where we will be writing our custom commands in support/index.js
Step 3: Write the page-specific locators in the respective files under ‘pages’ folder. For our test script, we will write the locators under loginPage.js and dashboardPage.js.
loginPage.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class loginPage { usernameInput() { return cy.get('#txtUsername') } passwordInput() { return cy.get('#txtPassword') } loginBtn() { return cy.get('#btnLogin') } } export default loginPage |
dashboardPage.js
1 2 3 4 5 6 7 8 9 10 11 12 | class dashboardPage { welcomeTxt() { return cy.get('#welcome') } logoutTxt() { return cy.contains('Logout') } } export default dashboardPage |
Step 4: Next step is to write our reusable functions in one place. This will again be page-specific. So Under ‘utils’ folder we will create two files login.js and dashboard.js.
login.js:
1 2 3 4 5 6 7 8 9 10 11 12 | import loginPage from '../pages/loginPage.js' import dashboardPage from '../pages/dashboardPage.js' const login = new loginPage(); const dashboard = new dashboardPage(); Cypress.Commands.add('login', (data) => { cy.visit('/') login.usernameInput().type(data.username) login.passwordInput().type(data.password) login.loginBtn().click() dashboard.welcomeTxt().contains(data.welcomeText) }) |
1 2 | import loginPage from '../pages/loginPage.js' const login = new loginPage(); |
This will enable us to access all the locators from loginPage.js in the login.js file.
1 2 | import dashboardPage from '../pages/dashboardPage.js' const dashboard = new dashboardPage(); |
This will enable us to access all the locators from dashboardPage.js in the login.js file.
1 2 3 4 5 6 7 | Cypress.Commands.add('login', (data) => { cy.visit('/') login.usernameInput().type(data.username) login.passwordInput().type(data.password) login.loginBtn().click() dashboard.welcomeTxt().contains(data.welcomeText) }) |
We are creating a custom command login which takes the parameter ‘data’ which we will be passing from our test.spec file. ‘data’ is a reference of our fixture file ‘testdata.json’ which contains our test data. This is how our fixture file looks like:
Next, we are visiting the website https://opensource-demo.orangehrmlive.com/ using the cy.visit(‘/’) command. If you have mentioned baseUrl in your cypress.json file then you can use the command like this.
Now login.usernameInput().type(data.username) will input username into the username input field and login.passwordInput().type(data.password) will input password into the password input field.
login.loginBtn().click() will click on the login button.
dashboard.welcomeTxt().contains(data.welcomeText) confirms successful login by checking the Welcome text on dashboard page.
dashboard.js:
1 2 3 4 5 6 7 8 9 10 | import loginPage from '../pages/loginPage.js' import dashboardPage from '../pages/dashboardPage.js' const login = new loginPage(); const dashboard = new dashboardPage(); Cypress.Commands.add('logout', () => { dashboard.welcomeTxt().click() dashboard.logoutTxt().click() login.usernameInput().should('be.visible') }) |
1 2 | import loginPage from '../pages/loginPage.js' const login = new loginPage(); |
This will enable us to access all the locators from loginPage.js in the dashboard.js file.
1 2 | import dashboardPage from '../pages/dashboardPage.js' const dashboard = new dashboardPage(); |
This will enable us to access all the locators from dashboardPage.js in the dashboard.js file.
1 2 3 4 5 | Cypress.Commands.add('logout', () => { dashboard.welcomeTxt().click() dashboard.logoutTxt().click() login.usernameInput().should('be.visible') }) |
We are creating a custom command logout which has all the required steps to perform the logout function. Unlike before this function is not taking any parameters.
dashboard.welcomeTxt().click() will click on the Welcome Text on the page to open the drop down which has the Logout button.
dashboard.logoutTxt().click() will click on the Logout text.
login.usernameInput().should(‘be.visible’) will make sure that after logout, user is redirected to login screen and to validate that we are making sure that the username field is visible.
Step 5: Now, we will write our test script. Since all of the stuff has been written already in pages and utils, our spec file will look very minimalistic (easy to read).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | describe('Validate Login and Logout on OrangeHRM website', function () { beforeEach(function () { cy.fixture('testdata').then(function (testdata) { this.testdata = testdata }) }) it('Validate successful Login', function () { cy.login(this.testdata) }) it('Validate successful Logout', function () { cy.logout() }) }) |
Here in the beforeEach, we are referencing our fixture file testdata.json from fixtures folder as ‘testdata’, then passing it as a parameter in cy.login(this.testdata).
cy.login(this.testdata) executes the login custom command written under integration/utils/login.js
cy.logout() executes the logout custom command written under integration/utils/dashboard.js
Step 6: After Execution:
Do check out 🙂
Github:Â https://github.com/alapanme/Cypress-Automation
All Cypress Articles: https://testersdock.com/cypress-tutorial/
Hello Alapan,
Thank you. This is really useful.
However I have a question. Is it possible to call the below import and object initialization statements just once instead within each spec file.
In our project we have 100’s of pages and instead of deciding which one to be called in which spec I was trying to understand if this can be just once so that any spec file is able to use and refer the object
import loginPage from ‘../pages/loginPage.js’
import dashboardPage from ‘../pages/dashboardPage.js’
const login = new loginPage();
const dashboard = new dashboardPage();
Unfortunately with this method, you have to write it manually for each spec file.
Ok thank you taking time and responding Alapan
I saw that people use classes and methods inside utils as reusable functions instead of Custom Cypress commands, which one is better?
Please share the example that you’re mentioning.
I am getting TypeError
Cannot read properties of undefined (reading ‘testdata’)
cypress/integration/test_suites/loginTests.js:15:23
13 |
14 | it(‘validate login’, ()=> {
> 15 | cy.login(this.testdata);
| ^
16 | });
17 |
18 | });
dont know whats i am missing
testdata is a fixture file. For this, you have to create a file ‘testdata.json’ under the cypress/fixtures folder. Please refer to Step 4.
Hi! it’s very clear, and the examples are very good too, but just one thing to note. The way you are approaching your page objects building is breaking a principle that states that no assertions should be made in the page objects. There is a general guideline for automation framework that states that all assertions should be in the test, and not buried somewhere else… Your logout test only shows it’s calling a logout function from somewhere, and if someone wants to know exactly what are you asserting, they need to dive in to the code, find the logic wherever it is, and try to find out…
PageObjects should only retrieve data from the UI and act on UI elements (type on textboxes, click on buttons, etc). NEVER should make assertions.
This is a common mistake I am seeing more and more often, and very common for people who want to take advantage of Cypress’ flexibility… but we need to fight against the temptation of oversimplifying code… Our tests need to be clear and transparent to whoever comes to look at them…