Understanding Page Object Model (POM)
Page Object Model is a design pattern that creates an object repository for web UI elements, making test code more maintainable and reusable.
Why Page Object Model?
Without POM:
// ❌ Hard to maintain - locators scattered everywhere
@Test
public void testLogin() {
driver.findElement(By.id("username")).sendKeys("user");
driver.findElement(By.id("password")).sendKeys("pass");
driver.findElement(By.id("loginBtn")).click();
}
@Test
public void testInvalidLogin() {
driver.findElement(By.id("username")).sendKeys("wrong");
driver.findElement(By.id("password")).sendKeys("wrong");
driver.findElement(By.id("loginBtn")).click();
}
With POM:
// ✅ Clean, maintainable, reusable
@Test
public void testLogin() {
loginPage.login("user", "pass");
}
@Test
public void testInvalidLogin() {
loginPage.login("wrong", "wrong");
}
Core POM Principles
- Encapsulation - Keep locators and actions within page classes
- Abstraction - Hide implementation details from tests
- Reusability - Share page objects across multiple tests
- Maintainability - Change locators in one place
Basic Page Object Structure
package com.automation.pages;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
public class LoginPage {
private WebDriver driver;
// Locators
private By usernameField = By.id("username");
private By passwordField = By.id("password");
private By loginButton = By.id("loginBtn");
private By errorMessage = By.className("error-message");
// Constructor
public LoginPage(WebDriver driver) {
this.driver = driver;
}
// Actions
public void enterUsername(String username) {
driver.findElement(usernameField).sendKeys(username);
}
public void enterPassword(String password) {
driver.findElement(passwordField).sendKeys(password);
}
public void clickLogin() {
driver.findElement(loginButton).click();
}
// Combined action
public void login(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLogin();
}
// Verification
public String getErrorMessage() {
return driver.findElement(errorMessage).getText();
}
public boolean isErrorDisplayed() {
try {
return driver.findElement(errorMessage).isDisplayed();
} catch (Exception e) {
return false;
}
}
}
Using PageFactory
Selenium provides PageFactory for cleaner initialization:
package com.automation.pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class LoginPage {
private WebDriver driver;
// Page elements using @FindBy
@FindBy(id = "username")
private WebElement usernameField;
@FindBy(id = "password")
private WebElement passwordField;
@FindBy(id = "loginBtn")
private WebElement loginButton;
@FindBy(className = "error-message")
private WebElement errorMessage;
// Constructor with PageFactory initialization
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
// Actions - cleaner code
public void enterUsername(String username) {
usernameField.sendKeys(username);
}
public void enterPassword(String password) {
passwordField.sendKeys(password);
}
public void clickLogin() {
loginButton.click();
}
public HomePage login(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLogin();
return new HomePage(driver); // Page navigation
}
public String getErrorMessage() {
return errorMessage.getText();
}
}
Advanced FindBy Strategies
// By ID
@FindBy(id = "username")
private WebElement usernameField;
// By Name
@FindBy(name = "email")
private WebElement emailField;
// By ClassName
@FindBy(className = "btn-primary")
private WebElement submitButton;
// By CSS Selector
@FindBy(css = "input[type='email']")
private WebElement emailInput;
// By XPath
@FindBy(xpath = "//button[text()='Submit']")
private WebElement submitBtn;
// By LinkText
@FindBy(linkText = "Sign Up")
private WebElement signUpLink;
// By PartialLinkText
@FindBy(partialLinkText = "Sign")
private WebElement signLink;
// By TagName
@FindBy(tagName = "h1")
private WebElement pageHeading;
// Multiple locators (fallback)
@FindBy(how = How.ID, using = "username")
private WebElement username;
// List of elements
@FindBy(className = "product-item")
private List<WebElement> products;
Base Page Class
Create a base class for common functionality:
package com.automation.pages;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
public class BasePage {
protected WebDriver driver;
protected WebDriverWait wait;
public BasePage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
// Wait methods
protected void waitForElementToBeClickable(WebElement element) {
wait.until(ExpectedConditions.elementToBeClickable(element));
}
protected void waitForElementToBeVisible(WebElement element) {
wait.until(ExpectedConditions.visibilityOf(element));
}
// Click methods
protected void click(WebElement element) {
waitForElementToBeClickable(element);
element.click();
}
protected void jsClick(WebElement element) {
JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("arguments[0].click();", element);
}
// Input methods
protected void type(WebElement element, String text) {
waitForElementToBeVisible(element);
element.clear();
element.sendKeys(text);
}
// Get methods
protected String getText(WebElement element) {
waitForElementToBeVisible(element);
return element.getText();
}
protected String getTitle() {
return driver.getTitle();
}
// Scroll methods
protected void scrollToElement(WebElement element) {
JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("arguments[0].scrollIntoView(true);", element);
}
// Verification methods
protected boolean isDisplayed(WebElement element) {
try {
return element.isDisplayed();
} catch (Exception e) {
return false;
}
}
}
Complete Page Object Example
package com.automation.pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class ProductPage extends BasePage {
@FindBy(id = "product-name")
private WebElement productName;
@FindBy(id = "product-price")
private WebElement productPrice;
@FindBy(id = "add-to-cart")
private WebElement addToCartButton;
@FindBy(className = "success-message")
private WebElement successMessage;
@FindBy(css = ".quantity-input")
private WebElement quantityInput;
public ProductPage(WebDriver driver) {
super(driver);
PageFactory.initElements(driver, this);
}
public String getProductName() {
return getText(productName);
}
public String getProductPrice() {
return getText(productPrice);
}
public void setQuantity(int quantity) {
type(quantityInput, String.valueOf(quantity));
}
public void addToCart() {
click(addToCartButton);
}
public boolean isSuccessMessageDisplayed() {
return isDisplayed(successMessage);
}
public String getSuccessMessage() {
return getText(successMessage);
}
// Fluent interface
public ProductPage selectQuantity(int quantity) {
setQuantity(quantity);
return this;
}
public CartPage addToCartAndGoToCart() {
addToCart();
// Navigate to cart
return new CartPage(driver);
}
}
Test Class Using Page Objects
package com.automation.tests;
import com.automation.pages.LoginPage;
import com.automation.pages.HomePage;
import com.automation.pages.ProductPage;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.*;
public class ProductTest {
private WebDriver driver;
private LoginPage loginPage;
private HomePage homePage;
private ProductPage productPage;
@BeforeMethod
public void setUp() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.get("https://example.com");
// Initialize page objects
loginPage = new LoginPage(driver);
homePage = new HomePage(driver);
productPage = new ProductPage(driver);
}
@Test
public void testAddProductToCart() {
// Login
homePage = loginPage.login("testuser", "password123");
// Navigate to product
productPage = homePage.selectProduct("Laptop");
// Verify product details
Assert.assertEquals(productPage.getProductName(), "Laptop");
// Add to cart
productPage.selectQuantity(2).addToCart();
// Verify success
Assert.assertTrue(productPage.isSuccessMessageDisplayed());
Assert.assertEquals(productPage.getSuccessMessage(),
"Product added to cart");
}
@AfterMethod
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
}
Page Object Best Practices
- One page class per web page or logical component
- Keep tests in separate packages from page objects
- Use meaningful method names that describe actions
- Return page objects for navigation (fluent interface)
- Avoid assertions in page objects - only in tests
- Use BasePage for common functionality
- Keep page objects simple - one method per action
Fluent Interface Pattern
// Chain methods for better readability
loginPage
.enterUsername("testuser")
.enterPassword("password")
.clickLogin()
.verifyLoginSuccess();
// With return types
HomePage homePage = loginPage
.login("user", "pass")
.verifyWelcomeMessage();
Handling Dynamic Elements
@FindBy(css = ".product-item")
private List<WebElement> productItems;
public ProductPage selectProductByName(String productName) {
for (WebElement product : productItems) {
if (product.getText().contains(productName)) {
click(product);
break;
}
}
return new ProductPage(driver);
}
public int getProductCount() {
return productItems.size();
}
Next Steps
In the next lesson, we’ll integrate TestNG framework for better test organization and execution.
Key Takeaways
✅ POM separates page structure from test logic
✅ PageFactory simplifies element initialization
✅ BasePage reduces code duplication
✅ Fluent interfaces improve test readability