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

Table of contents
- Why Use the Page Object Model?
- Step 1: Identifying Pages in Our Test
- Step 2: Creating the Landing Page Class
- Step 3: Creating the Product Catalogue Class
- Step 4: Adding the Cart Icon to AbstractComponent
- Step 5: Creating the Cart Page Class
- Step 6: Creating the Checkout Page Class
- Step 7: Creating the Confirmation Page Class
- Final Test Code
- What’s Next?

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 adriver
variable to interact with the browser.@FindBy
: Specifies the locator (e.g.,id="userEmail"
) and assigns it to aWebElement
variable (e.g.,userEmail
).Constructor: Takes a
WebDriver
object as an argument and assigns it to the localdriver
. 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 likedriver.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:
Entering an email.
Entering a password.
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
andpassword
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
: Usescss=".mb-3"
to store all product cards. TheList
ensuresfindElements
is used instead offindElement
.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
Create a new package:
abstractcomponents
.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 aBy
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 aBoolean
.goToCheckout
: Clicks the checkout button and returns aCheckoutPage
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 theActions
class to enter a country name, waits for suggestions, and selects the second option (e.g., India).submitOrder
: Clicks the submit button and returns aConfirmationPage
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! 🚀
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!