← Blog

"Selenium Series #8: Page Object Model — Scalable, Maintainable Test Architecture"

The Page Object Model (POM) is the most important design pattern in Selenium. Learn how to structure page classes, use PageFactory, and build a framework that scales to hundreds of tests.

reading now
views
comments

Series Navigation

Part 7: TestNG Integration

Part 9: Handling Windows, Frames, Alerts and Pop-ups


The Problem Without POM

Consider 50 tests that all start with a login. Each has:

driver.findElement(By.id("username")).sendKeys("user@example.com");
driver.findElement(By.id("password")).sendKeys("password");
driver.findElement(By.id("loginBtn")).click();

The login button's ID changes from loginBtn to login-submit. You update 50 tests.

The Page Object Model fixes this: one change in one place.


What Is the Page Object Model?

Each page of your application gets a Java class. The class:

  • Declares locators as fields
  • Exposes methods representing user actions
  • Never contains assertions (keep tests and pages separate)
Test Classes          Page Object Classes
─────────────         ───────────────────
LoginTest         →   LoginPage
                      (username, password, loginBtn locators)
                      (login(), clickForgotPassword() methods)

DashboardTest     →   DashboardPage
                      (welcomeMsg, logoutLink locators)
                      (getWelcomeMessage(), logout() methods)

Project Structure with POM

src/test/java/com/yourname/selenium/
├── base/
│   └── BaseTest.java          ← browser setup/teardown
├── pages/
│   ├── BasePage.java          ← shared page utilities
│   ├── LoginPage.java
│   ├── DashboardPage.java
│   ├── RegisterPage.java
│   └── components/
│       ├── Header.java        ← reusable nav bar component
│       └── Footer.java
├── tests/
│   ├── LoginTest.java
│   ├── DashboardTest.java
│   └── RegisterTest.java
├── utils/
│   ├── WaitUtils.java
│   └── ScreenshotUtils.java
└── listeners/
    └── TestListener.java

BasePage — Shared Utilities

// src/test/java/com/yourname/selenium/pages/BasePage.java
package com.yourname.selenium.pages;

import org.openqa.selenium.*;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public abstract class BasePage {

    protected WebDriver driver;
    protected WebDriverWait wait;

    public BasePage(WebDriver driver) {
        this.driver = driver;
        this.wait   = new WebDriverWait(driver, 15);
        // Initialize @FindBy annotations
        PageFactory.initElements(driver, this);
    }

    // Protected helpers available to all page objects

    protected void click(WebElement element) {
        wait.until(ExpectedConditions.elementToBeClickable(element));
        element.click();
    }

    protected void type(WebElement element, String text) {
        wait.until(ExpectedConditions.visibilityOf(element));
        element.clear();
        element.sendKeys(text);
    }

    protected String getText(WebElement element) {
        wait.until(ExpectedConditions.visibilityOf(element));
        return element.getText();
    }

    protected boolean isDisplayed(WebElement element) {
        try {
            return element.isDisplayed();
        } catch (NoSuchElementException | StaleElementReferenceException e) {
            return false;
        }
    }

    protected void waitForUrl(String urlFragment) {
        wait.until(ExpectedConditions.urlContains(urlFragment));
    }

    protected void scrollToElement(WebElement element) {
        ((JavascriptExecutor) driver)
            .executeScript("arguments[0].scrollIntoView(true);", element);
    }

    public String getPageTitle() {
        return driver.getTitle();
    }

    public String getCurrentUrl() {
        return driver.getCurrentUrl();
    }
}

LoginPage with PageFactory

// src/test/java/com/yourname/selenium/pages/LoginPage.java
package com.yourname.selenium.pages;

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

public class LoginPage extends BasePage {

    // ── Locators declared as fields with @FindBy ───────────────────────────
    @FindBy(id = "username")
    private WebElement usernameInput;

    @FindBy(id = "password")
    private WebElement passwordInput;

    @FindBy(css = "button[type='submit']")
    private WebElement loginButton;

    @FindBy(id = "flash")
    private WebElement flashMessage;

    @FindBy(css = "a[href='/forgot-password']")
    private WebElement forgotPasswordLink;

    @FindBy(css = ".error-container")
    private WebElement errorContainer;

    // ── Constructor ────────────────────────────────────────────────────────
    public LoginPage(WebDriver driver) {
        super(driver); // calls PageFactory.initElements
    }

    // ── Navigation ─────────────────────────────────────────────────────────
    public LoginPage open() {
        driver.get("https://the-internet.herokuapp.com/login");
        return this;
    }

    // ── Actions ────────────────────────────────────────────────────────────
    public LoginPage enterUsername(String username) {
        type(usernameInput, username);
        return this; // enables method chaining
    }

    public LoginPage enterPassword(String password) {
        type(passwordInput, password);
        return this;
    }

    public DashboardPage clickLogin() {
        click(loginButton);
        return new DashboardPage(driver); // return next page
    }

    public LoginPage clickLoginExpectingError() {
        click(loginButton);
        return this; // stay on login page on failure
    }

    // Convenience method for successful login
    public DashboardPage loginAs(String username, String password) {
        return enterUsername(username)
               .enterPassword(password)
               .clickLogin();
    }

    public LoginPage clickForgotPassword() {
        click(forgotPasswordLink);
        return this;
    }

    // ── Getters for assertions ─────────────────────────────────────────────
    public String getFlashMessage() {
        return getText(flashMessage);
    }

    public boolean isErrorDisplayed() {
        return isDisplayed(errorContainer);
    }

    public String getErrorMessage() {
        return getText(errorContainer);
    }

    public boolean isLoginPageDisplayed() {
        return isDisplayed(usernameInput) && isDisplayed(passwordInput);
    }
}

DashboardPage

// src/test/java/com/yourname/selenium/pages/DashboardPage.java
package com.yourname.selenium.pages;

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

public class DashboardPage extends BasePage {

    @FindBy(css = "h2.subheader")
    private WebElement secureAreaHeading;

    @FindBy(css = "a.button.secondary")
    private WebElement logoutButton;

    @FindBy(id = "flash")
    private WebElement successMessage;

    public DashboardPage(WebDriver driver) {
        super(driver);
        // Verify we're on the right page
        waitForUrl("/secure");
    }

    public boolean isDashboardDisplayed() {
        return isDisplayed(secureAreaHeading);
    }

    public String getHeadingText() {
        return getText(secureAreaHeading);
    }

    public String getSuccessMessage() {
        return getText(successMessage);
    }

    public LoginPage logout() {
        click(logoutButton);
        return new LoginPage(driver);
    }
}

Clean Test Classes with POM

// src/test/java/com/yourname/selenium/tests/LoginTest.java
package com.yourname.selenium.tests;

import com.yourname.selenium.base.BaseTest;
import com.yourname.selenium.pages.DashboardPage;
import com.yourname.selenium.pages.LoginPage;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class LoginTest extends BaseTest {

    @Test(description = "Valid login redirects to dashboard")
    public void testSuccessfulLogin() {
        DashboardPage dashboard = new LoginPage(getDriver())
            .open()
            .loginAs("tomsmith", "SuperSecretPassword!");

        Assert.assertTrue(dashboard.isDashboardDisplayed(),
            "Dashboard should be visible after login");
        Assert.assertTrue(dashboard.getSuccessMessage().contains("logged into a secure area"),
            "Success message should appear");
    }

    @Test(description = "Invalid password shows error")
    public void testInvalidPassword() {
        LoginPage loginPage = new LoginPage(getDriver())
            .open()
            .enterUsername("tomsmith")
            .enterPassword("wrongpassword")
            .clickLoginExpectingError();

        Assert.assertTrue(loginPage.isLoginPageDisplayed(),
            "Should stay on login page");
        Assert.assertTrue(loginPage.getFlashMessage().contains("invalid"),
            "Error message should appear");
    }

    @Test(description = "Logout returns to login page")
    public void testLogout() {
        LoginPage loginPage = new LoginPage(getDriver())
            .open()
            .loginAs("tomsmith", "SuperSecretPassword!")
            .logout();

        Assert.assertTrue(loginPage.isLoginPageDisplayed(),
            "Should return to login page after logout");
        Assert.assertTrue(loginPage.getFlashMessage().contains("logged out"),
            "Logout confirmation message should appear");
    }

    @DataProvider(name = "invalidCredentials")
    public Object[][] invalidCredentials() {
        return new Object[][] {
            { "wronguser", "SuperSecretPassword!", "username is invalid" },
            { "tomsmith",  "wrongpassword",        "password is invalid" },
            { "",          "",                      "username is invalid" },
        };
    }

    @Test(dataProvider = "invalidCredentials")
    public void testInvalidCredentials(String user, String pass, String expectedMsg) {
        String actualMsg = new LoginPage(getDriver())
            .open()
            .enterUsername(user)
            .enterPassword(pass)
            .clickLoginExpectingError()
            .getFlashMessage();

        Assert.assertTrue(actualMsg.contains(expectedMsg),
            "Expected: '" + expectedMsg + "' in '" + actualMsg + "'");
    }
}

@FindBy Locator Strategies

// All supported @FindBy strategies
@FindBy(id = "username")
@FindBy(name = "email")
@FindBy(className = "btn-primary")
@FindBy(tagName = "h1")
@FindBy(linkText = "Sign in")
@FindBy(partialLinkText = "Sign")
@FindBy(xpath = "//input[@type='email']")
@FindBy(css = "input.form-control[type='email']")

// Multiple locators — tries each until one works
@FindBys({
    @FindBy(id = "submit"),
    @FindBy(css = "button[type='submit']")
})

// All elements matching ALL conditions (AND)
@FindAll({
    @FindBy(css = ".btn"),
    @FindBy(css = ".primary")
})

// List of elements
@FindBy(css = "table tbody tr")
private List<WebElement> tableRows;

What's Next

In Part 9 we handle browser complexity — switching between multiple windows and tabs, dealing with iframes that contain your content, and handling JavaScript alerts and confirmation dialogs.

Discussion

Loading...

Leave a Comment

All comments are reviewed before appearing. No links please.

0 / 1000