16/02/2022

The Page Object pattern aims to introduce modularity in tests by separating the test logic into one place (the test file) and the actual page interaction logic into another place (the page object).
The benefits of using the Page Object pattern include:
👉 Reducing the usage of selectors in tests, which improves code readability. 🤌 Making small changes to the tested application only affect the Page Object, avoiding changes in the test code. 🤜 Simplifying test maintenance.
In summary, the Page Object pattern:
Page objects are typically stored in a dedicated folder (e.g., pageObjects) located outside the integrations folder, alongside other test code files.
It is common to create Page Objects based on classes. However, in Cypress, it is not necessary to use classes for Page Objects (as mentioned below).
export class CreditsPgObj {
getCreditsCheckbox() {
cy.get('[data-cy="creditBox"]')
}
static getCreditsInfo() {
cy.get('[data-cy="creditsInfo"]')
}
[...]
}
export class ThanksCreditsPg {
getProviderName() {
return cy.get('[data-cy="providerName"]')
}
static getTransactionId() {
return cy.get('[data-cy="transactionId"]')
}
}Usage
/// <reference types="cypress" />
import { viewports } from '../../support/main'
// Import the page object class
import { CreditsPage } from '../../support/credits'
viewports.forEach(viewport => {
describe(`Bonus credits management - (${viewport})`, () => {
// Create a new instance of the page object class
const creditsPage = new CreditsPgObj()
beforeEach(() => {
cy.viewport(viewport)
cy.visit('').wait('@xyz').wait('@yz').wait('@zx')
})
it('Bonus credits offer should be displayed', () => {
CreditsPage.getLimitedTimeOffer().should('be.visible')
CreditsPage.getCreditsProductBonus().should('be.visible')
CreditsPage.getCreditsProductBonus().first().should('include.text', '100 Credits')
})
})
})Another example of using Page Objects can be found in Gleb Bahmutov’s article titled Stop using Page Objects and Start using App Actions.
Page Object -> HomePage
import Header from './Headers';
import SignInPage from './SignIn';
class HomePage {
constructor() {
this.header = new Header();
}
visit() {
cy.visit('/');
}
getUserAvatar() {
return cy.get(`[data-testid=UserAvatar]`);
}
goToSignIn() {
const link = this.header.getSignInLink();
link.click();
const signIn = new SignInPage();
return signIn;
}
}
export default HomePage;Usage in a test (notice where the assertions are located)
import HomePage from '../elements/pages/HomePage';
describe('Sign In', () => {
it('should show an error message on empty input', () => {
const home = new HomePage();
home.visit();
const signIn = home.goToSignIn();
signIn.submit();
signIn.getEmailError()
.should('exist')
.contains('Email is required');
signIn.getPasswordError()
.should('exist')
.contains('Password is required');
});
});In Cypress, there is no need to create Page Objects as classes or instantiate them since they don’t require prototypes of other classes and don’t have to be classes themselves. Instead, Page Objects can be composed of individual functions or collected within an object for organization purposes.
export const menuPage = {
menuOpen: () => {
cy.get(menuSelectors.openMenuBtn).click();
},
menuClose: () => {
cy.get(menuSelectors.closMenuBtn).click().should('not.be.visible');
},
logOut: () => {
cy.get(menuSelectors.menuItems.logout).click();
cy.url().should('include', '/login');
},
};In the past, I wrote about good testing practices in Good Testing Principles, where I mentioned the 3A principle (Arrange, Act, Assert) and how it relates to the POM (Page Object Model). Here’s how I understand it:
The Page Object is responsible for interacting with the page and stores reusable actions. However, the assertion (verification of action execution) should be placed inside the test itself. Test preparation can be done in different places and in different ways (excluding environment setup, such as cy.visit or cy.intercept, or localStorage, which can be set up in beforeEach). The focus should be on collecting selectors (in a separate class or object) that will be used both within the test and in the Page Object.
A test scenario for testing a blog application may look like this: login, navigate to the section with new articles, create a new article content, submit it, and then verify if the article was added/published.
A commonly used approach (linear approach) includes:
cy.get() and other functionalities.const SELECTORS = {
ACCEPT_BUTTON: "#accept-cookies",
REJECT_BUTTON: "#reject-cookies",
LOCALSTORAGE_DISABLED_WARNING: "#localstorage-disabled-warning",
};Translate given text: Alternative Approach: Splitting Logic into 3 Classes/Objects/Components (Functional Approach) => 1. Store getters = cy.get() + selectors. 2. Actions/Functionality (utilizes logic from 1.). 3. Test (uses logic from 1. and 2.).
const getSubmitSearchButton = () => cy.get('[cypress-id]=submit-search');Above is an example of a single function, but they can also be collected in objects/components (as mentioned earlier).
Using cypress-selectors, the code can look like this:
@ByType('input') static searchInput: Selector;Sample Usage
it('change language between different languages', () => {
Object.keys(i18nextLngs).forEach((language) => {
cy.wrap(footerPage.changeLanguage(language)).then(() =>
expect(localStorage.getItem('i18nextLng')).to.eq(i18nextLngs[language])
);
});
});where footerPage.js (in this case, selectors are stored in a separate file and object ‘footerSelectors’ as selectors, not getters, so it follows the linear approach):
import { footerSelectors } from '../support/selectors/footerSelectors';
const i18nextLngs = {
en: 'en',
pl: 'pl',
};
export const footerPage = {
changeLanguage: (language) => {
cy.get(footerSelectors.languageBtn).click({ waitForAnimations: false });
cy.get(footerSelectors.languageOption)
.contains(language[0].toUpperCase() + language.substring(1))
.click({ waitForAnimations: false });
},
};The article by Gleb Bahmutov titled Stop using Page Objects and Start using App Actions has reached a significant number of people, but the actual adoption doesn’t seem to be widespread… - 🤔 In which cases do Page Objects not work or it would be considered best practice not to use them? 👉 They are not suitable when used for triggering application state changes through the UI / preparing the state for testing - why? - because it essentially means retesting the same thing. In such cases, App Actions can be used, which means using functions within the application/component directly in the test and then checking its state.
Sources:
Using PageObject pattern with Cypress => anton-kravchenko/cypress-page-object-example => cypress-selectors