Why Create Page Objects?
Page objects provide a clean interface between your tests and the application UI. They:
- Centralize element selectors in one location
- Provide reusable methods for common actions
- Make tests more readable and maintainable
- Enable type safety with TypeScript
Page Object Creation Workflow
Analyze the Page
Identify the elements and actions users can perform on the page.
Create the Class File
Create a new TypeScript file in cypress/support/pages/.
Extend BasePage
Inherit common functionality from the BasePage class.
Define Selectors
Add private properties for all element selectors.
Implement Methods
Create public methods for actions and verifications.
Use in Tests
Import and instantiate the page object in your test files.
Step-by-Step: Creating a Page Object
Step 1: Create the File
Create a new file in cypress/support/pages/:
touch cypress/support/pages/LoginPage.ts
Step 2: Set Up the Class Structure
import BasePage from "./BasePage";
export default class LoginPage extends BasePage {
// Constructor will go here
// Methods will go here
}
Step 3: Define Selectors
Add private properties for all element selectors:
import BasePage from "./BasePage";
export default class LoginPage extends BasePage {
private usernameField: string
private passwordField: string
private loginBtnField: string
private errorMsgAlert: string
private closeErrorBtn: string
private loginMainPage: string
constructor() {
super() // Call BasePage constructor
this.usernameField = '#user-name'
this.passwordField = '#password'
this.loginBtnField = '#login-button'
this.errorMsgAlert = '[data-test="error"]'
this.closeErrorBtn = '[data-test="error-button"]'
this.loginMainPage = '.login_container'
}
}
Use private for selectors to prevent direct access from tests. This ensures that if selectors change, you only update them in one place.
Step 4: Add Action Methods
Create public methods for user actions:
export default class LoginPage extends BasePage {
// ... selectors from above ...
public typeUsername(username: string) {
cy.get(this.usernameField).type(username)
}
public typePassword(password: string) {
cy.get(this.passwordField).type(password)
}
public clickOnLoginButton() {
cy.get(this.loginBtnField).click({force: true})
}
public clearUsernameField() {
cy.get(this.usernameField).clear()
}
public clearPasswordField() {
cy.get(this.passwordField).clear()
}
public closeErrorMessage() {
cy.get(this.closeErrorBtn).click({force: true})
}
}
Step 5: Add Verification Methods
Create public methods for assertions:
export default class LoginPage extends BasePage {
// ... action methods from above ...
public verifyLoginMainPage() {
cy.get(this.loginMainPage).should('be.visible')
}
public verifyErrorMessageIsShown() {
cy.get(this.errorMsgAlert).should('be.visible')
}
public verifyErrorMessageIsHidden() {
cy.get(this.errorMsgAlert).should('not.exist')
}
public verifyErrorMessage(message: string) {
cy.get(this.errorMsgAlert).should('have.text', message)
}
public verifyUsernameFieldCleared() {
cy.get(this.usernameField).should('be.empty')
}
public verifyPasswordFieldCleared() {
cy.get(this.passwordField).should('be.empty')
}
}
Step 6: Add Composite Methods
Create methods that combine multiple actions for common workflows:
export default class LoginPage extends BasePage {
// ... other methods ...
/**
* Complete login workflow
* @param username - Username to login with
* @param password - Password to login with
*/
public userLogin(username: string, password: string) {
this.typeUsername(username)
this.typePassword(password)
this.clickOnLoginButton()
}
}
Composite methods like userLogin() are useful for beforeEach hooks where you need to set up a common state.
Complete Page Object Example
Here’s the complete LoginPage class:
import BasePage from "./BasePage";
export default class LoginPage extends BasePage {
private usernameField: string
private passwordField: string
private loginBtnField: string
private errorMsgAlert: string
private closeErrorBtn: string
private loginMainPage: string
constructor() {
super()
this.usernameField = '#user-name'
this.passwordField = '#password'
this.loginBtnField = '#login-button'
this.errorMsgAlert = '[data-test="error"]'
this.closeErrorBtn = '[data-test="error-button"]'
this.loginMainPage = '.login_container'
}
// Action methods
public typeUsername(username: string) {
cy.get(this.usernameField).type(username)
}
public typePassword(password: string) {
cy.get(this.passwordField).type(password)
}
public clickOnLoginButton() {
cy.get(this.loginBtnField).click({force: true})
}
public clearUsernameField() {
cy.get(this.usernameField).clear()
}
public clearPasswordField() {
cy.get(this.passwordField).clear()
}
public closeErrorMessage() {
cy.get(this.closeErrorBtn).click({force: true})
}
// Verification methods
public verifyLoginMainPage() {
cy.get(this.loginMainPage).should('be.visible')
}
public verifyErrorMessageIsShown() {
cy.get(this.errorMsgAlert).should('be.visible')
}
public verifyErrorMessageIsHidden() {
cy.get(this.errorMsgAlert).should('not.exist')
}
public verifyErrorMessage(message: string) {
cy.get(this.errorMsgAlert).should('have.text', message)
}
public verifyUsernameFieldCleared() {
cy.get(this.usernameField).should('be.empty')
}
public verifyPasswordFieldCleared() {
cy.get(this.passwordField).should('be.empty')
}
// Composite methods
public userLogin(username: string, password: string) {
this.typeUsername(username)
this.typePassword(password)
this.clickOnLoginButton()
}
}
Advanced Page Object Example
The HomePage class shows more advanced patterns:
import BasePage from "./BasePage";
export default class HomePage extends BasePage {
private inventoryContainer: string
private prodSortContainer: string
constructor() {
super()
this.inventoryContainer = '#inventory_container'
this.prodSortContainer = '[data-test="product-sort-container"]'
}
public verifyHomePageIsVisible() {
cy.get(this.inventoryContainer).should('be.visible')
}
// Dropdown selection
public selectFilterAtoZ() {
cy.get(this.prodSortContainer).select('Name (A to Z)')
}
public selectFilterZtoA() {
cy.get(this.prodSortContainer).select('Name (Z to A)')
}
// Complex verification with data manipulation
public verifyFilterAtoZ() {
cy.get(this.inventoryItem).then(($els) => {
const names = [...$els].map(el => el.innerText.trim())
const sorted = [...names].sort((a, b) => a.localeCompare(b))
expect(names).to.deep.equal(sorted)
})
}
// Parameterized method
public addProductToCart(productIndex: number) {
cy.get(this.inventoryItem).eq(productIndex)
.find('button').click({force: true})
}
// Iteration method
public addAllProductsToCart() {
cy.get(this.inventoryItem).each(($el) => {
cy.wrap($el).find('button').click({force: true})
})
}
}
Advanced Patterns Explained
Using Inherited Selectors
inventoryItem is defined in BasePage and used in HomePage:// In BasePage.ts
public inventoryItem: string = '[data-test="inventory-item"]'
// In HomePage.ts
public verifyFilterAtoZ() {
cy.get(this.inventoryItem).then(($els) => {
// Use inherited selector
})
}
Encapsulate complex verification logic in page object methods:public verifyFilterAtoZ() {
cy.get(this.inventoryItem).then(($els) => {
const names = [...$els].map(el => el.innerText.trim())
const sorted = [...names].sort((a, b) => a.localeCompare(b))
expect(names).to.deep.equal(sorted)
})
}
This keeps the test clean:// In test
homePage.selectFilterAtoZ()
homePage.verifyFilterAtoZ() // Complex logic hidden
Accept parameters for flexible interactions:public addProductToCart(productIndex: number) {
cy.get(this.inventoryItem).eq(productIndex)
.find('button').click({force: true})
}
// Usage
homePage.addProductToCart(0) // Add first product
homePage.addProductToCart(2) // Add third product
Creating Component Objects
For reusable UI components, create component objects:
export default class Navbar {
private menuButton: string
private logoutLink: string
constructor() {
this.menuButton = '#react-burger-menu-btn'
this.logoutLink = '#logout_sidebar_link'
}
public openMenu() {
cy.get(this.menuButton).click()
}
public userLogout() {
cy.get(this.menuButton).click()
cy.get(this.logoutLink).click()
}
}
Component objects don’t need to extend BasePage since they represent reusable components, not full pages.
Using Page Objects in Tests
import LoginPage from "../../support/pages/LoginPage";
import HomePage from "../../support/pages/HomePage";
let loginPage: LoginPage
let homePage: HomePage
describe('User Authentication', () => {
beforeEach(() => {
cy.fixture('users').as('userData')
loginPage = new LoginPage()
homePage = new HomePage()
cy.visit('/')
});
it('Should log in successfully', () => {
cy.get('@userData').then((user: any) => {
loginPage.verifyLoginMainPage()
loginPage.typeUsername(user.standard)
loginPage.typePassword(user.password)
loginPage.clickOnLoginButton()
homePage.verifyHomePageIsVisible()
})
});
});
Selector Best Practices
Use data-test Attributes
Prefer [data-test="..."] selectors for stability
Avoid CSS Classes
CSS classes change frequently with styling updates
Use IDs When Available
IDs are unique and reliable selectors
XPath as Last Resort
Only use XPath when no better selector exists
Selector Priority
// ✅ Best - data-test attribute
this.loginButton = '[data-test="login-button"]'
// ✅ Good - unique ID
this.loginButton = '#login-button'
// ⚠️ Okay - specific attribute
this.loginButton = 'button[type="submit"]'
// ❌ Avoid - CSS class (fragile)
this.loginButton = '.btn-primary'
// ❌ Last resort - XPath
this.loginButton = '//button[text()="Login"]'
Method Naming Conventions
Action Methods
Use verbs: click, type, select, checkpublic clickOnLoginButton()
public typeUsername(username: string)
public selectCountry(country: string)
Verification Methods
Start with verify: verifyVisible, verifyText, verifyEnabledpublic verifyLoginMainPage()
public verifyErrorMessage(message: string)
public verifyButtonEnabled()
Composite Methods
Describe the workflow: userLogin, completeCheckout, addToCartpublic userLogin(username: string, password: string)
public completeCheckout()
public searchProduct(productName: string)
TypeScript Features
Return Type Annotations
public typeUsername(username: string): void {
cy.get(this.usernameField).type(username)
}
public isLoggedIn(): Cypress.Chainable<boolean> {
return cy.get(this.logoutButton).should('be.visible')
}
Method Chaining
public typeUsername(username: string): this {
cy.get(this.usernameField).type(username)
return this
}
public typePassword(password: string): this {
cy.get(this.passwordField).type(password)
return this
}
// Usage
loginPage
.typeUsername('user')
.typePassword('pass')
.clickOnLoginButton()
Common Pitfalls
Don’t expose selectors publicly// ❌ Bad
public usernameField = '#user-name'
// ✅ Good
private usernameField = '#user-name'
Don’t add test logic to page objects// ❌ Bad
public loginAndVerifySuccess(username: string, password: string) {
this.typeUsername(username)
this.typePassword(password)
this.clickOnLoginButton()
// Don't verify different pages in one method
cy.get('#home-page').should('be.visible')
}
// ✅ Good
public userLogin(username: string, password: string) {
this.typeUsername(username)
this.typePassword(password)
this.clickOnLoginButton()
}
Next Steps
Writing UI Tests
Use your page objects in tests
Page Object Model
Deep dive into POM concepts
TypeScript Usage
Leverage TypeScript effectively
Maintainability
Keep your page objects maintainable