top of page

Refactoring Playwright Tests Using Page Object Model (POM)

In the previous parts of this series, we gradually built a realistic Playwright automation workflow for our demo e-commerce store.

import { test, expect, BrowserContext } from "@playwright/test";

const dataSet = JSON.parse(
  JSON.stringify(
    require("../data/demostore_purchase_data.json")
  )
);

// =====================================================
// REGRESSION SUITE (Independent checks)
// Runs safely in parallel
// =====================================================
test.describe("Regression (Independent checks)", () => {
  test.describe.configure({ mode: "parallel" });

  test("@regression Shop: DemoShop opens and search returns results", async ({ page }) => {
     await page.goto("/");
    await page.getByRole("link", { name: "DemoShop" }).click();
    await expect(page.getByRole("heading", { name: "DemoShop" })).toBeVisible();
    await page.getByRole("searchbox", { name: /search/i }).fill("organic");
    await page.getByRole("button", { name: /search/i }).click();
    await expect(page.getByRole("heading", { name: /search results.*organic/i })).toBeVisible();
  });

  test(`@regression Filter: max price (${maxPrice}) limits product prices`, async ({ page }) => {
    await page.goto("/demoshop/");

    await page.getByRole("searchbox", { name: /search/i }).fill("organic");
    await page.getByRole("button", { name: /search/i }).click();
    await expect(page.getByRole("heading", { name: /search results.*organic/i })).toBeVisible();

    const maxPriceInput = page.getByRole("textbox", { name: /maximum price/i });
    await maxPriceInput.clear();
    await maxPriceInput.fill(String(maxPrice));
    await maxPriceInput.press("Enter");

    await expect(page.getByRole("button", { name: /remove price up to/i })).toBeVisible();
    await expect(page.getByRole("alert")).toContainText(/\d+ results/i);

    const productGrid = page.locator("ul.products");
    await expect(productGrid).toBeVisible();

    const products = productGrid.locator(":scope > li.product");
    const count = await products.count();
    expect(count).toBeGreaterThan(0);

    for (let i = 0; i < count; i++) {
      const priceText = await products.nth(i).locator("span.price bdi").first().innerText();
      const price = parseFloat(priceText.replace("$", "").trim());
      expect(price).toBeLessThanOrEqual(maxPrice);
    }
  });

  test("@regression Cart: add first product and verify it appears in cart", async ({ page }) => {

    await page.goto("/demoshop/");

    const productGrid = page.locator("ul.products");
    await expect(productGrid).toBeVisible();

    const firstProduct = productGrid.locator(":scope > li.product").first();
    const productName = (await firstProduct.locator(".woocommerce-loop-product__title").innerText()).trim();
    expect(productName).toBeTruthy();

    const addBtn = firstProduct.locator("a.add_to_cart_button");
    await addBtn.click();

    await expect(addBtn).not.toHaveClass(/loading/, { timeout: 15000 });
    await expect(addBtn).toHaveClass(/added/, { timeout: 15000 });

    await page.goto("/mycart/");
    await expect(page).toHaveURL(/mycart/);

    const cartRows = page.locator("table.cart tr.cart_item");
    await expect(cartRows.first()).toBeVisible();

    const countRows = await cartRows.count();
    expect(countRows).toBeGreaterThan(0);

    let found = false;
    for (let i = 0; i < countRows; i++) {
      const cartProductName = (await cartRows.nth(i).locator("td.product-name").innerText()).trim();
      if (cartProductName.includes(productName)) {
        found = true;
        break;
      }
    }
    expect(found).toBeTruthy();
  });
});

// =====================================================
// SMOKE SUITE (Complete workflow)
// Runs serially to keep business journey stable
// =====================================================

test("@smoke @regression E2E: Shop → Cart → Checkout → Verify Order", async ({ page }) => {

  let productName = "";
  let orderId = "";

  await test.step("Open shop and add first product to cart", async () => {
    await page.goto("/demoshop/");


    const productGrid = page.locator("ul.products");
    await expect(productGrid).toBeVisible();

    const firstProduct = productGrid.locator(":scope > li.product").first();
    productName = (await firstProduct.locator(".woocommerce-loop-product__title").innerText()).trim();
    expect(productName).toBeTruthy();

    const addBtn = firstProduct.locator("a.add_to_cart_button");
    await addBtn.click();

    await expect(addBtn).not.toHaveClass(/loading/, { timeout: 15000 });
    await expect(addBtn).toHaveClass(/added/, { timeout: 15000 });
  });

  await test.step("Validate product exists in cart", async () => {
    await page.goto("/mycart/");
    await expect(page).toHaveURL(/mycart/);

    const cartRows = page.locator("table.cart tr.cart_item");
    await expect(cartRows.first()).toBeVisible();

    const countRows = await cartRows.count();
    expect(countRows).toBeGreaterThan(0);

    let productFound = false;
    for (let i = 0; i < countRows; i++) {
      const cartProductName = (await cartRows.nth(i).locator("td.product-name").innerText()).trim();
      if (cartProductName.includes(productName)) {
        productFound = true;
        break;
      }
    }
    expect(productFound).toBeTruthy();
  });

  await test.step("Checkout and place the order", async () => {
    const checkoutLink = page.getByRole("link", { name: /proceed to checkout/i });
    await checkoutLink.scrollIntoViewIfNeeded();
    await checkoutLink.click();

    await expect(page).toHaveURL(/checkout/i, { timeout: 15000 });

    const placeOrder = page.locator("#place_order");
    await expect(placeOrder).toBeVisible({ timeout: 15000 });
    await placeOrder.scrollIntoViewIfNeeded();
    await placeOrder.click();

    await expect(page.getByText(/your order has been received/i)).toBeVisible({ timeout: 15000 });

    orderId = (await page.locator("ul.order_details>li.order strong").innerText()).trim();
    expect(orderId).toBeTruthy();
  });

  await test.step("Verify order appears in My Account → Orders", async () => {
    await page.goto("/");
    await expect(page.getByRole("heading", { name: /my account/i })).toBeVisible();

    await page.getByRole("link", { name: /^orders$/i }).first().click();
    await expect(page).toHaveURL(/orders/i);

    const ordersTable = page.locator("table.woocommerce-orders-table");
    await expect(ordersTable).toBeVisible();

    const orderLink = ordersTable.getByRole("link", { name: `View order number ${orderId}` });
    await expect(orderLink).toBeVisible();
    await orderLink.click();

    await expect(page).toHaveURL(new RegExp(`view-order.*${orderId}`), { timeout: 15000 });
    await expect(page.getByRole("heading", { name: new RegExp(`Order\\s*#${orderId}`, "i") })).toBeVisible();
  });

  // await page.close();
  // await context.close();
});

We started with the fundamentals—understanding the DOM, learning CSS selectors, and then upgrading to Playwright’s built-in locators such as getByRole() and getByText() to create more reliable and user-centric tests.


From there, we expanded our automation framework step by step:


  • implementing login workflows

  • organising tests using hooks

  • reusing authentication with storageState

  • separating regression and smoke tests

  • enabling parallel execution

  • Adding console and HTML reporting

  • capturing debug artifacts such as screenshots, videos, and traces

  • and finally integrating Allure reporting for richer test dashboards


At this point, our framework already looks quite powerful from an execution and reporting perspective.

However, if we step back and look at the test code itself, another challenge begins to appear.


The Growing Test File Problem


Let’s look at the purchase workflow we implemented earlier.


Our end-to-end test performs a complete user journey:


  1. Open the DemoShop

  2. Add a product to the cart

  3. Validate the product in the cart

  4. Proceed to checkout

  5. Place the order

  6. Verify the order in the My Account → Orders page


This workflow gives us excellent coverage and represents a realistic user journey.

But as we added validations, locators, and loops for verifying product details, the test file began to grow significantly.


Inside the same test file, we now have:


  • navigation logic

  • UI interaction code

  • locator definitions

  • loops validating table rows

  • business validation logic


All mixed in one place.


Although the test works correctly, a few common automation problems start to emerge:


  • Locators are repeated across tests

  • UI interaction logic is mixed with test validation

  • Test files become long and harder to read

  • A small UI change could require updates in many places


These problems are very common as automation projects scale.


In small examples, they may not seem serious, but in real projects with hundreds or thousands of tests, this structure quickly becomes difficult to maintain.


Introducing the Page Object Model


To solve this problem, automation frameworks use a design pattern called the Page Object Model (POM).


The idea behind POM is simple:


Instead of writing UI interaction logic directly inside test files, we create separate classes that represent application pages.


Each page class contains:

  • locators for that page

  • Reusable methods for interacting with the page


The test files then focus only on business logic and assertions, while the page objects handle the UI interaction details.


For example:


Instead of writing inside multiple tests, we can move these interactions into a ShopPage class and call them as reusable methods.:

await page.getByRole("link", { name: "DemoShop" }).click();
await page.getByRole("searchbox", { name: /search/i }).fill("organic");

This brings several advantages:


  • Cleaner test files

  • Reusable UI actions

  • Centralized locator management

  • Better maintainability when the UI changes


Our Goal in This Tutorial


In this tutorial, we will refactor our existing purchase workflow step by step using the Page Object Model.


Instead of rewriting everything from scratch, we will gradually transform the current workflow by:


  1. Introducing basic TypeScript class concepts

  2. Creating our first page object

  3. Moving locators and UI interactions into reusable methods

  4. Updating the test file to use these page classes

  5. Keeping assertions inside the test while page objects handle UI behaviour


By the end of this refactoring, our automation framework will become:


  • easier to read

  • easier to maintain

  • easier to extend with new tests


Let’s begin by first understanding how TypeScript classes help us model application pages.


Understanding Classes with a Simple Example


Before we refactor our Playwright tests into a Page Object Model (POM) structure, we need to understand one important concept from TypeScript and object-oriented programming:

Classes

If you are new to programming, the word class may sound complex, but the idea is actually very simple.

A class is a blueprint for creating objects.

Think about it like an architectural plan.

An architectural blueprint describes:

  • What the building contains

  • What features it has?

  • how different parts of the building are organized


But the blueprint itself is not the building. The actual building is created from that blueprint.


Let’s imagine we want to represent a Car in our program.


A car has characteristics (properties) such as:

  • brand

  • color

  • speed


A car can also perform actions (methods) like:

  • start

  • accelerate

  • stop


In TypeScript, we can represent this using a class.


Understanding the Parts of the Class


1. Class Declaration

class Car

This defines a blueprint called Car.


2. Properties

brand: string;
color: string;
speed: number;

These are properties of the class.


They describe the data associated with a car.


Understanding the Constructor


Now let’s focus on the constructor, which is a very important concept.



A constructor is a special method that runs automatically when a new object is created from a class.


It is typically used to initialize properties of the object.


Think of it as the initial setup when an object is created.


When we create a car, we want to specify:


  • its brand

  • its color

  • its initial speed


The constructor allows us to pass these values during object creation.


The keyword this refers to the current instance of the class.


So when we write:

this.brand = brand

we are assigning the value passed into the constructor to the object's property.


Creating an Object from the Class


Once the class is defined, we can create objects from the blueprint.


Here:


  • new Car() creates a new object

  • The constructor automatically runs

  • The properties get initialized


Now we can use the object's methods.

myCar.start();
myCar.accelerate();
myCar.stop();

Output:



So the flow becomes:

Class → Object → Method Execution

Connecting Classes to Page Object Model


In automation testing, every web page contains:


Elements

  • username field

  • password field

  • login button


Actions

  • enter username

  • enter password

  • click login


Instead of writing these steps inside every test script, we organize them into a class representing that page.


For example:



In this example:


  • The class represents a web page

  • The constructor receives the Playwright page object

  • Methods represent actions a user can perform on the page


Now that we understand how classes and constructors work in TypeScript, we can apply the same concept to build Page Objects in Playwright.




Preparing the Framework for Page Objects


In the previous post of this series, we introduced an important framework improvement before moving to the Page Object Model.

Instead of performing login directly inside test files, we configured Playwright project dependencies so that a setup project runs before the main test suite.

This setup project performs two tasks:


• Cleaning old test artifacts

• Creating an authenticated session that tests can reuse


This allows our regression and smoke tests to start from an already authenticated state.

Initial Global Setup Implementation

In the earlier implementation, the setup file handled both responsibilities directly.

Refer posts:


import { test, expect } from "@playwright/test";
import fs from "fs";
import path from "path";

test("global setup: clean reports and login", async ({ page }) => {

  const foldersToClean = [
    "playwright-report",
    "test-results",
    "allure-results",
    "allure-report"
  ];

  for (const folder of foldersToClean) {
    const folderPath = path.join(process.cwd(), folder);
    if (fs.existsSync(folderPath)) {
      fs.rmSync(folderPath, { recursive: true, force: true });
      console.log(`Cleaned folder: ${folder}`);
    }
  }
})

  // Continue with login setup

test("global setup: login and save storageState", async ({ page }) => {
  // const baseURL = "https://qa-cart.com";
  const storageStatePath = "state.json";

const username = process.env.DEMO_USER;
const password = process.env.DEMO_PASS;

if (!username || !password) {
  throw new Error("Missing DEMO_USER or DEMO_PASS in .env file");
}

  await page.goto("/");

  await expect(page.getByRole("heading", { name: /login/i })).toBeVisible();

  await page.locator("input[name='username']").fill(username);
  await page.locator("input[name='password']").fill(password);

  await page.getByRole("button", { name: /log in/i }).click();

  // Verify login on a stable page
  await page.goto('/');
  await expect(page.getByRole("link", { name: /log out|logout/i }).first()).toBeVisible();

  // Save login session
  await page.context().storageState({ path: storageStatePath });
});

This worked correctly and ensured that our main test suite could reuse the authenticated session.


Refactoring the Setup Using Utilities and Page Objects

To keep the framework modular and consistent with our Page Object Model design, we performed a small refactor.


Two improvements were introduced:


1. Moving cleanup logic into a utility


Instead of keeping filesystem cleanup logic inside the setup file, we created a small helper utility:

Example:

import fs from "fs";
import path from "path";

export function cleanArtifacts(): void {
  const foldersToClean = [
    "playwright-report",
    "test-results",
    "allure-results",
    "allure-report",
  ];

  for (const folder of foldersToClean) {
    const folderPath = path.join(process.cwd(), folder);
    if (fs.existsSync(folderPath)) {
      fs.rmSync(folderPath, { recursive: true, force: true });
      console.log(`Cleaned folder: ${folder}`);
    }
  }
}

This keeps the setup file cleaner and allows the same cleanup function to be reused if needed.


2. Using a Login Page Object


Since we are adopting the Page Object Model, it makes sense for authentication logic to also follow the same design.


So we created a LoginPage class inside the pages folder.

Example:

import { Page, Locator, expect } from "@playwright/test";

export class LoginPage {
  page: Page;
  loginHeading: Locator;
  usernameInput: Locator;
  passwordInput: Locator;
  loginButton: Locator;
  logoutLink: Locator;

  constructor(page: Page) {
    this.page = page;
    this.loginHeading = page.getByRole("heading", { name: /login/i });
    this.usernameInput = page.locator("input[name='username']");
    this.passwordInput = page.locator("input[name='password']");
    this.loginButton = page.getByRole("button", { name: /log in/i });
    this.logoutLink = page.getByRole("link", { name: /log out|logout/i }).first();
  }

  async goto() {
    await this.page.goto("/");
  }

  async verifyPageLoaded() {
    await expect(this.loginHeading).toBeVisible();
  }

  async login(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async verifyLoginSuccessful() {
    await this.page.goto("/");
    await expect(this.logoutLink).toBeVisible();
  }
}

Now the setup file becomes much cleaner.

Final Global Setup Implementation

After refactoring, the setup file looks like this:

import { test } from "@playwright/test";
import { cleanArtifacts } from "../helpers/cleanup";
import { LoginPage } from "../pages/LoginPage";

test("global setup: clean reports and create authenticated session", async ({ page }) => {
  cleanArtifacts();

  const storageStatePath = "state.json";
  const username = process.env.DEMO_USER;
  const password = process.env.DEMO_PASS;

  if (!username || !password) {
    throw new Error("Missing DEMO_USER or DEMO_PASS in .env file");
  }

  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.verifyPageLoaded();
  await loginPage.login(username, password);
  await loginPage.verifyLoginSuccessful();

  await page.context().storageState({ path: storageStatePath });
});

This version follows better design principles:


• The setup file orchestrates the process

• Cleanup logic lives in a utility

• Authentication logic lives in a page object

This structure keeps the framework consistent, modular, and easier to maintain.



With authentication and environment preparation handled by the setup project, our test files no longer need to worry about login steps.


Every test now begins with an already authenticated session.

This allows us to focus purely on business workflows, which makes the upcoming Page Object Model refactoring much cleaner.


Now we can start transforming our purchase workflow into reusable page classes.

Now that our framework preparation is complete, let's start refactoring the purchase workflow using the Page Object Model.


POM Model for Purchase Workflow


Now that we understand how TypeScript classes work, we can start applying the same concept to our Playwright automation framework.


Instead of writing all UI interactions directly inside the test files, we will gradually move them into page classes.


Each page class will represent a specific part of our application.


For our demo store purchase workflow, the main pages involved are:

  • Home Page

  • DemoShop Page

  • Cart Page

  • Checkout Page

  • Orders Page


Each of these pages will become a Page Object in our framework.


Our test files will then interact with these pages using methods, rather than raw locators.


Step 1 – Creating the First Page Object (Home Page)


We begin with the Home Page, because this is where the user first lands when opening the application.


Previously, our test contained navigation logic like this:

await page.goto("/");
await page.getByRole("link", { name: "DemoShop" }).click();

This code works perfectly fine, but if multiple tests need to navigate to the DemoShop, the locator will be repeated in many places.


Instead, we move this logic into a HomePage class.


Example:

import { Page, Locator, expect } from "@playwright/test";

export class HomePage {
  page: Page;
  demoShopLink: Locator;
  myAccountHeading: Locator;
  ordersLink: Locator;

  constructor(page: Page) {
    this.page = page;
    this.demoShopLink = page.getByRole("link", { name: "DemoShop" });
    this.myAccountHeading = page.getByRole("heading", { name: /my account/i });
    this.ordersLink = page.getByRole("link", { name: /^orders$/i }).first();
  }

  async goto() {
    await this.page.goto("/");
  }

  async openDemoShop() {
    await this.demoShopLink.click();
  }

  async verifyMyAccountPageLoaded() {
    await expect(this.myAccountHeading).toBeVisible();
  }

  async openOrders() {
    await this.ordersLink.click();
  }
}

Here we can observe several important ideas:

  • The class represents a page

  • The constructor receives the Playwright page object

  • Locators are defined once

  • Methods represent user actions


Now, instead of repeating locators inside the test, we can write:

await homePage.goto();
await homePage.openDemoShop();

This makes the test cleaner and easier to read.


Step 2 – Creating the DemoShop Page Object


Next, we move to the DemoShop page, which contains the product listing and search functionality.


Earlier, our test contained repeated logic such as:

await page.getByRole("searchbox", { name: /search/i }).fill("organic");
await page.getByRole("button", { name: /search/i }).click();

Instead of keeping this in the test file, we move it to the DemoShopPage class.


Example:

export class DemoShopPage {  
page: Page;  
searchBox: Locator;  
searchButton: Locator;  
constructor(page: Page) {    
this.page = page;    
this.searchBox = page.getByRole("searchbox", { name: /search/i });    
this.searchButton = page.getByRole("button", { name: /search/i });  
}  
async searchForProduct(keyword: string) {    
await this.searchBox.fill(keyword);    
await this.searchButton.click();  }
}

Now our test becomes simpler:

await demoShopPage.searchForProduct(searchKeyword);

Notice that the test now focuses on business intent rather than UI mechanics.


Step 3 – Moving Validation Logic into Page Methods


As our workflow grew, we also added validations such as checking search results, verifying product grids, and validating price filters.


For example, earlier we had logic like:

const products = productGrid.locator(":scope > li.product");
const count = await products.count();
for (let i = 0; i < count; i++) {  
const priceText = await products.nth(i).locator("span.price bdi").innerText();  
const price = parseFloat(priceText.replace("$", "").trim());  
expect(price).toBeLessThanOrEqual(maxPrice);
}

This kind of logic can make tests long and difficult to read.


Instead, we move the validation into a method inside DemoShopPage.


Example:

async verifyAllDisplayedPricesAreAtMost(maxPrice: number) {  
const prices = await this.getAllDisplayedPrices();  
for (const price of prices) {    
expect(price).toBeLessThanOrEqual(maxPrice);  
}}

Now the test simply becomes:

await demoShopPage.verifyAllDisplayedPricesAreAtMost(maxPrice);

The test now reads like a clear business rule.


Step 4 – Introducing the Cart Page

As the workflow expanded, we also needed to verify that the product added in DemoShop appears inside the Cart page.

Originally, the test contained loops to scan the cart table rows.

Instead of keeping this inside the test file, we created a CartPage class.

Example method:

async verifyProductPresent(productName: string) {  
const rows = await this.cartRows.count();  
for (let i = 0; i < rows; i++) {    
const cartProductName = await 
this.cartRows.nth(i).locator("td.product-name").innerText();
    if (cartProductName.includes(productName)) {      
return;
    }
  }
  throw new Error("Product not found in cart");}

Now our test simply says:

await cartPage.verifyProductPresent(productName);

This hides the table scanning logic inside the page class.


Step 5 – Handling Checkout and Order Verification


Finally, we introduce two more page objects:

  • CheckoutPage

  • OrdersPage


These handle the final steps of the workflow:


  • placing the order

  • capturing the order ID

  • verifying the order appears in the My Account → Orders page


Example:

orderId = await checkoutPage.placeOrder();
await ordersPage.openOrderById(orderId);
await ordersPage.verifyOrderDetailsPage(orderId);

The test now clearly reflects the business journey:

  • shop

  • cart

  • checkout

  • order verification


Final Result – A Clean Test Workflow


After applying the Page Object Model, our tests become much easier to read.

Instead of dozens of lines of locators and loops, the workflow becomes:

import { test } from "@playwright/test";
import { HomePage } from "../pages/HomePage";
import { DemoShopPage } from "../pages/DemoShopPage";
import { CartPage } from "../pages/CartPage";
import { CheckoutPage } from "../pages/CheckoutPage";
import { OrdersPage } from "../pages/OrdersPage";

const dataSet = JSON.parse(
  JSON.stringify(
    require("../data/demostore_purchase_data.json")
  )
);

const maxPrice = dataSet[0].maxPrice;
const searchKeyword = dataSet[0].searchKeyword;

test("@regression Shop: DemoShop opens and search returns results", async ({ page }) => {
  const homePage = new HomePage(page);
  const demoShopPage = new DemoShopPage(page);

  await homePage.goto();
  await homePage.openDemoShop();
  await demoShopPage.verifyPageLoaded();
  await demoShopPage.searchForProduct("organic");
  await demoShopPage.verifySearchResultsFor("organic");
});


test(`@regression Filter: max price (${maxPrice}) limits product prices`, async ({ page }) => {
  const demoShopPage = new DemoShopPage(page);

  await demoShopPage.goto();
  await demoShopPage.searchForProduct("organic");
  await demoShopPage.verifySearchResultsFor("organic");
  await demoShopPage.applyMaxPriceFilter(maxPrice);
  await demoShopPage.verifyPriceFilterApplied();
  await demoShopPage.verifyResultsCountVisible();
  await demoShopPage.verifyProductGridVisible();
  await demoShopPage.verifyProductsDisplayed();
  await demoShopPage.verifyAllDisplayedPricesAreAtMost(maxPrice);
});



test("@regression Cart: add first product and verify it appears in cart", async ({ page }) => {
  const demoShopPage = new DemoShopPage(page);
  const cartPage = new CartPage(page);

  await demoShopPage.goto();
  await demoShopPage.verifyProductGridVisible();

  const productName = await demoShopPage.addFirstProductToCart();

  await cartPage.goto();
  await cartPage.verifyPageLoaded();
  await cartPage.verifyCartHasItems();
  await cartPage.verifyProductPresent(productName);
});




test("@smoke @regression E2E: Shop → Cart → Checkout → Verify Order", async ({ page }) => {
  const demoShopPage = new DemoShopPage(page);
  const cartPage = new CartPage(page);
  const checkoutPage = new CheckoutPage(page);
  const homePage = new HomePage(page);
  const ordersPage = new OrdersPage(page);

  let productName = "";
  let orderId = "";

  await test.step("Open shop and add first product to cart", async () => {
    await demoShopPage.goto();
    await demoShopPage.verifyProductGridVisible();
    productName = await demoShopPage.addFirstProductToCart();
  });

  await test.step("Validate product exists in cart", async () => {
    await cartPage.goto();
    await cartPage.verifyPageLoaded();
    await cartPage.verifyCartHasItems();
    await cartPage.verifyProductPresent(productName);
  });

  await test.step("Checkout and place the order", async () => {
    await cartPage.proceedToCheckout();
    await checkoutPage.verifyPageLoaded();
    orderId = await checkoutPage.placeOrder();
  });

  await test.step("Verify order appears in My Account → Orders", async () => {
    await homePage.goto();
    await homePage.verifyMyAccountPageLoaded();
    await homePage.openOrders();

    await ordersPage.verifyPageLoaded();
    await ordersPage.openOrderById(orderId);
    await ordersPage.verifyOrderDetailsPage(orderId);
  });
});

This structure makes the test read almost like a manual test case.


Key Benefits of This Refactoring


By introducing the Page Object Model, we achieved several improvements:


Cleaner Test Files

Tests now focus on business workflows rather than UI implementation details.


Reusable Page Logic

Page interactions can be reused across multiple tests.


Centralized Locators

If the UI changes, we only update the locator in one place.


Better Maintainability

Large automation projects become easier to manage.


Clear Separation of Responsibilities

Tests define what we are validating, while page objects define how the UI is interacted with

Comments


Never Miss a Post. Subscribe Now!

Thanks for submitting!

©anuradha agarwal knowledge hub

    bottom of page