Building a Selenium Automation Framework: Using Page Object Model (Part 2)

Samiksha KuteSamiksha Kute
10 min read

Welcome back to the second part of our Selenium Framework Design! In Part 1, we set up a Maven project, added dependencies, and created a standalone Selenium test to automate an end-to-end e-commerce flow. Now, in Part 2, we’ll take that test and transform it into a professional-grade automation framework using the Page Object Model (POM) design pattern. Let’s get started!

Why Use the Page Object Model?

In our standalone test from Part 1, we wrote all the locators (like IDs and CSS selectors) directly in the test script. This works fine for a single test, but what happens when you have 50 or 100 test cases? If a locator changes (e.g., the login ID changes), you’d need to update it in every test script. That’s a lot of work and prone to errors!

The Page Object Model solves this problem by:

  • Centralizing locators: Instead of scattering locators across tests, we store them in one place for each page of the application.

  • Reducing maintenance: If a locator changes, you update it once, and all tests using it are automatically updated.

  • Organizing code: Each page of the application (e.g., login page, product catalog) gets its own Java class, making the code easier to manage.

However, dumping all locators into a single file isn’t ideal either. If you have 200 locators, finding and updating them becomes tedious, and a mistake could break all tests. POM addresses this by creating a separate class for each page, grouping only the locators and actions relevant to that page. For example:

  • LoginPage class: Contains locators and actions for the login page (e.g., email field, password field, login button).

  • ProductCatalogue class: Handles locators and actions for the product catalog page.

This way, if the login ID changes, you update only the LoginPage class, and all tests using it are fixed. POM makes your framework maintainable, readable, and scalable!

Step 1: Identifying Pages in Our Test

Our e-commerce application (from Part 1) has several pages:

  • Landing Page: The login page where users enter their email and password.

  • Product Catalogue: The dashboard displaying products after login.

  • Cart Page: The page showing items added to the cart.

  • Checkout Page: Where users select a country and place the order.

  • Confirmation Page: The final page displaying the “Thank you for the order” message.

We’ll create a separate Java class for each page and move the relevant locators and actions into those classes.

Step 2: Creating the Landing Page Class

Let’s start by organizing the locators for the landing page (login page). We’ll create a new package for our page objects to keep things tidy.

Setting Up the Package

In your project, create a new package under src/main/java named pageobjects. This package will hold all page object classes.

Inside this package, create a new Java class called LandingPage.

Defining Locators

The landing page has three elements:

  • Email field (ID: userEmail)

  • Password field (ID: userPassword)

  • Login button (ID: login)

Instead of writing locators directly in the test, we’ll define them in the LandingPage class using Page Factory, a Selenium feature that simplifies locator management.

Using Page Factory

Page Factory uses the @FindBy annotation to declare locators. Here’s how we set it up:

package pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

public class LandingPage {
    WebDriver driver;

    @FindBy(id = "userEmail")
    WebElement userEmail;

    @FindBy(id = "userPassword")
    WebElement passwordElement;

    @FindBy(id = "login")
    WebElement submitButton;

    public LandingPage(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }
}

Explanation:

  • WebDriver driver: We declare a driver variable to interact with the browser.

  • @FindBy: Specifies the locator (e.g., id="userEmail") and assigns it to a WebElement variable (e.g., userEmail).

  • Constructor: Takes a WebDriver object as an argument and assigns it to the local driver. This ensures the page object uses the same driver as the test.

  • PageFactory.initElements(driver, this): Initializes all @FindBy elements using the provided driver. This constructs locators like driver.findElement(By.id("userEmail")) at runtime.

If you need to use other locators (e.g., XPath or CSS), you can specify them like this:

  • XPath: @FindBy(xpath = "//input[@id='userEmail']")

  • CSS: @FindBy(css = "#userEmail")

To check valid locator types, inspect the @FindBy annotation in Selenium’s source code, it lists options like id, xpath, cssSelector, etc.

Writing Action Methods

Locators alone don’t perform actions. We need methods to interact with the elements (e.g., entering text, clicking buttons). Let’s create action methods for the landing page.

In our standalone test, the login process involves:

  1. Entering an email.

  2. Entering a password.

  3. Clicking the login button.

We’ll combine these into a single action method called loginApplication:

public void loginApplication(String email, String password) {
    userEmail.sendKeys(email);
    passwordElement.sendKeys(password);
    submitButton.click();
}

Explanation:

  • Parameters: email and password are passed from the test, keeping the page object free of hard coded data.

  • Actions: Sends the email and password to their respective fields and clicks the login button.

We’ll also add a method to navigate to the landing page:

public void goTo() {
    driver.get("https://rahulshettyacademy.com/client");
}

This method replaces the driver.get() call in the test, making the URL configurable for different environments (e.g., QA, production).

Updating the Test

Create a new test class called SubmitOrderTest to use the page objects (keep the StandAloneTest as a reference). Here’s how we update the test:

LandingPage landingPage = new LandingPage(driver);
landingPage.goTo();
ProductCatalogue productCatalogue = landingPage.loginApplication("your_email@example.com", "your_password");

This replaces the original login code, making it more readable and maintainable.

Step 3: Creating the Product Catalogue Class

Next, let’s create a page object for the product catalog page, where users select products and add them to the cart.

Copying the Landing Page

To save time, copy the LandingPage class, rename it to ProductCatalogue, and modify it:

package pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import java.util.List;

public class ProductCatalogue extends AbstractComponent {
    WebDriver driver;

    @FindBy(css = ".mb-3")
    List<WebElement> products;

    @FindBy(css = ".ng-animating")
    WebElement spinner;

    public ProductCatalogue(WebDriver driver) {
        super(driver);
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }
}

Changes:

  • Extends AbstractComponent: We’ll create this parent class later to store reusable code.

  • List<WebElement> products: Uses css=".mb-3" to store all product cards. The List ensures findElements is used instead of findElement.

  • Spinner: The loading icon (css=".ng-animating") for synchronization.

Handling Reusable Code with AbstractComponent

In our test, we use synchronization code like wait.until(ExpectedConditions.visibilityOfElementLocated()) multiple times. Repeating this in every page object is inefficient. Instead, we’ll create a parent class called AbstractComponent to store reusable methods.

Creating AbstractComponent

  1. Create a new package: abstractcomponents.

  2. Add a new class: AbstractComponent.

package abstractcomponents;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;

public class AbstractComponent {
    WebDriver driver;

    public AbstractComponent(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public void waitForElementToAppear(By findBy) {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
        wait.until(ExpectedConditions.visibilityOfElementLocated(findBy));
    }

    public void waitForElementToDisappear(WebElement ele) {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
        wait.until(ExpectedConditions.invisibilityOf(ele));
    }
}

Explanation:

  • Constructor: Accepts a WebDriver and initializes Page Factory.

  • waitForElementToAppear: Waits for an element to be visible using a By locator.

  • waitForElementToDisappear: Waits for an element (e.g., spinner) to disappear.

Passing the Driver

The AbstractComponent needs the driver from the test. We pass it through the page object classes using the super keyword:

public ProductCatalogue(WebDriver driver) {
    super(driver); // Sends driver to AbstractComponent
    this.driver = driver;
    PageFactory.initElements(driver, this);
}

Every page object class must extend AbstractComponent and call super(driver) to ensure the parent class receives the driver.

Action Methods for ProductCatalogue

Let’s add methods to:

  • Get the product list.

  • Find a product by name.

  • Add a product to the cart.

public List<WebElement> getProductList() {
    waitForElementToAppear(By.cssSelector(".mb-3"));
    return products;
}

public WebElement getProductByName(String productName) {
    return getProductList().stream()
        .filter(product -> product.findElement(By.cssSelector("b")).getText().equals(productName))
        .findFirst()
        .orElse(null);
}

public void addProductToCart(String productName) {
    WebElement prod = getProductByName(productName);
    prod.findElement(By.cssSelector(".card-body button:last-of-type")).click();
    waitForElementToAppear(By.cssSelector("#toast-container"));
    waitForElementToDisappear(spinner);
}

Explanation:

  • getProductList: Waits for products to load and returns the product list.

  • getProductByName: Uses Java Streams to find a product by name (e.g., “ZARA COAT 3”).

  • addProductToCart: Finds the product, clicks “Add to Cart,” waits for the toast message, and ensures the spinner disappears.

Note: The “Add to Cart” button locator (By.cssSelector(".card-body button:last-of-type")) can’t use Page Factory because it’s scoped to a specific product (prod.findElement), not the driver.

Updating the Test

In SubmitOrderTest, replace the product selection code:

productCatalogue.addProductToCart("ZARA COAT 3");

This single line replaces the original stream and click code, making the test cleaner.

Step 4: Adding the Cart Icon to AbstractComponent

The cart icon in the header is visible on all pages (home, cart, checkout). Instead of adding it to every page object, we’ll put it in AbstractComponent:

@FindBy(css = "[routerlink*='cart']")
WebElement cartHeader;

public CartPage goToCartPage() {
    cartHeader.click();
    return new CartPage(driver);
}

This method clicks the cart icon and returns a CartPage object, reducing object creation in the test.

Step 5: Creating the Cart Page Class

The cart page verifies if the selected product (e.g., “ZARA COAT 3”) is in the cart and clicks the checkout button.

package pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import abstractcomponents.AbstractComponent;
import java.util.List;

public class CartPage extends AbstractComponent {
    WebDriver driver;

    @FindBy(css = ".cartSection h3")
    List<WebElement> cartProducts;

    @FindBy(css = ".totalRow button")
    WebElement checkoutEle;

    public CartPage(WebDriver driver) {
        super(driver);
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public Boolean verifyProductDisplay(String productName) {
        return cartProducts.stream()
            .anyMatch(product -> product.getText().equalsIgnoreCase(productName));
    }

    public CheckoutPage goToCheckout() {
        checkoutEle.click();
        return new CheckoutPage(driver);
    }
}

Explanation:

  • cartProducts: Stores the list of products in the cart.

  • verifyProductDisplay: Checks if the specified product is in the cart and returns a Boolean.

  • goToCheckout: Clicks the checkout button and returns a CheckoutPage object.

No Assertions: Validations (e.g., assertions) stay in the test, not the page object.

Updating the Test

Add to SubmitOrderTest:

CartPage cartPage = productCatalogue.goToCartPage();
Boolean match = cartPage.verifyProductDisplay("ZARA COAT 3");
Assert.assertTrue(match);
CheckoutPage checkoutPage = cartPage.goToCheckout();

Step 6: Creating the Checkout Page Class

The checkout page handles the country dropdown and order submission.

package pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import abstractcomponents.AbstractComponent;
import org.openqa.selenium.interactions.Actions;

public class CheckoutPage extends AbstractComponent {
    WebDriver driver;

    @FindBy(css = "[placeholder='Select Country']")
    WebElement country;

    @FindBy(xpath = "(//button[contains(@class,'ta-item')])[2]")
    WebElement selectCountry;

    @FindBy(css = ".action__submit")
    WebElement submit;

    public CheckoutPage(WebDriver driver) {
        super(driver);
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public void selectCountry(String countryName) {
        Actions a = new Actions(driver);
        a.sendKeys(country, countryName).build().perform();
        waitForElementToAppear(By.cssSelector(".ta-results"));
        selectCountry.click();
    }

    public ConfirmationPage submitOrder() {
        submit.click();
        return new ConfirmationPage(driver);
    }
}

Explanation:

  • selectCountry: Uses the Actions class to enter a country name, waits for suggestions, and selects the second option (e.g., India).

  • submitOrder: Clicks the submit button and returns a ConfirmationPage object.

Step 7: Creating the Confirmation Page Class

The confirmation page verifies the success message.

package pageobjects;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import abstractcomponents.AbstractComponent;

public class ConfirmationPage extends AbstractComponent {
    WebDriver driver;

    @FindBy(css = ".hero-primary")
    WebElement confirmationMessage;

    public ConfirmationPage(WebDriver driver) {
        super(driver);
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    public String getConfirmationMessage() {
        return confirmationMessage.getText();
    }
}

Explanation:

  • getConfirmationMessage: Returns the confirmation message text for validation in the test.

Final Test Code

Here’s the updated SubmitOrderTest:

import java.time.Duration;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import org.testng.Assert;

import pageobjects.CartPage;
import pageobjects.CheckoutPage;
import pageobjects.ConfirmationPage;
import pageobjects.LandingPage;
import pageobjects.ProductCatalogue;

public class SubmitOrderTest {
    public static void main(String[] args) {
        String productName = "ZARA COAT 3";
        WebDriver driver = new ChromeDriver();
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

        LandingPage landingPage = new LandingPage(driver);
        landingPage.goTo();
        ProductCatalogue productCatalogue = landingPage.loginApplication("your_email@example.com", "your_password");

        productCatalogue.getProducList();
        productCatalogue.addProductToCart(productName);
        CartPage cartPage = productCatalogue.goToCartPage();

        boolean match = cartPage.verifyProductIsDisplayed(productName);
        Assert.assertTrue(match);
        CheckoutPage checkoutPage = cartPage.goToCheckOutPage();
        checkoutPage.selectCountry("india");

        ConfirmationPage confirmationPage = checkoutPage.submitOrder();
        String confirmMessage = confirmationPage.verifyConfirmationMessage();
        Assert.assertTrue(confirmMessage.equalsIgnoreCase("THANKYOU FOR THE ORDER."));
        driver.close();
    }
}

Why It’s Better:

  • Readable: The test reads like a manual test case, with clear actions (e.g., loginApplication, addProductToCart).

  • Maintainable: Locators and actions are encapsulated in page objects.

  • Reusable: The AbstractComponent class handles synchronization.

  • Efficient: Object creation is minimized by returning page objects from action methods.

Running the Test

Run SubmitOrderTest to ensure it passes. It should:

  • Log in.

  • Add “ZARA COAT 3” to the cart.

  • Verify the cart contents.

  • Complete the checkout.

  • Validate the confirmation message.

The test should pass without errors, confirming our POM implementation works!

Check out the complete code repository below:

What’s Next?

Our test is now a well-structured framework, but there’s room for optimization. In Part 3, we’ll explore:

  • Further refactoring to remove remaining “junk” code.

  • Adding TestNG for better test management.

  • Implementing data-driven testing, parallel execution, and reporting.

Stay tuned for Part 3, where we’ll continue building and refining our framework. If you found this guide helpful, share it with your fellow testers and leave a comment below!

Happy testing! 🚀

0
Subscribe to my newsletter

Read articles from Samiksha Kute directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Samiksha Kute
Samiksha Kute

Passionate Learner!