Skip to main content

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

1

Analyze the Page

Identify the elements and actions users can perform on the page.
2

Create the Class File

Create a new TypeScript file in cypress/support/pages/.
3

Extend BasePage

Inherit common functionality from the BasePage class.
4

Define Selectors

Add private properties for all element selectors.
5

Implement Methods

Create public methods for actions and verifications.
6

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

LoginPage.ts
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:
LoginPage.ts
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:
LoginPage.ts
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:
LoginPage.ts
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:
LoginPage.ts
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:
LoginPage.ts
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:
HomePage.ts
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

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:
Navbar.ts
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

LoginPageSpec.cy.ts
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

1

Action Methods

Use verbs: click, type, select, check
public clickOnLoginButton()
public typeUsername(username: string)
public selectCountry(country: string)
2

Verification Methods

Start with verify: verifyVisible, verifyText, verifyEnabled
public verifyLoginMainPage()
public verifyErrorMessage(message: string)
public verifyButtonEnabled()
3

Composite Methods

Describe the workflow: userLogin, completeCheckout, addToCart
public 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

Build docs developers (and LLMs) love