# Page Object Model (POM) — Designing a Maintainable Selenium Framework

Source: https://skills101.in/learn/test-automation/page-object-model-selenium — Skills101 free tutorial (Software Testing & Automation). Updated 2026-11-09.

Imagine 200 tests that each contain `driver.findElement(By.id("login-btn"))`. The developer renames the button's id — and 200 tests break. The **Page Object Model** puts each page's locators and actions in **one class**, so a change is fixed in one place.

![LoginPageTest talks to LoginPage and DashboardPage, which talk to WebDriver; when the HTML changes only the page class needs fixing](https://skills101.in/learn-img/test-automation/pom.svg)

## 1. A page class

```java
// File: src/test/java/com/skills101/qa/pages/LoginPage.java
package com.skills101.qa.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

/** Page Object: everything a test may do on the login page — locators live here, not in tests. */
public class LoginPage {

    private final WebDriver driver;

    private final By email = By.id("email");
    private final By password = By.id("password");
    private final By loginButton = By.id("login-btn");
    private final By error = By.id("error");

    public LoginPage(WebDriver driver) {
        this.driver = driver;
    }

    public LoginPage open(String baseUrl) {
        driver.get(baseUrl + "/login.html");
        return this;
    }

    public DashboardPage loginAs(String userEmail, String userPassword) {
        typeCredentials(userEmail, userPassword);
        return new DashboardPage(driver);
    }

    public LoginPage loginExpectingError(String userEmail, String userPassword) {
        typeCredentials(userEmail, userPassword);
        return this;
    }

    public String errorMessage() {
        return driver.findElement(error).getText();
    }

    private void typeCredentials(String userEmail, String userPassword) {
        driver.findElement(email).clear();
        driver.findElement(email).sendKeys(userEmail);
        driver.findElement(password).sendKeys(userPassword);
        driver.findElement(loginButton).click();
    }
}
```

Rules that make page objects good:

- **Locators are private** `By` fields — tests never see them.
- **Methods are user actions** (`loginAs`), not element pokes (`clickButton3`).
- Methods **return the next page**: a successful login returns a `DashboardPage`; a failed one stays on `LoginPage`. Tests read like a story and can't call a method on the wrong page.
- **No assertions inside pages** — pages *do* things and *read* values; tests decide what's correct.
- Store `By`, not `WebElement`, so elements are found fresh (no stale elements).

```java
// File: src/test/java/com/skills101/qa/pages/DashboardPage.java
package com.skills101.qa.pages;

import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

public class DashboardPage {

    private final WebDriver driver;

    public DashboardPage(WebDriver driver) {
        this.driver = driver;
    }

    public String welcomeText() {
        return driver.findElement(By.id("welcome")).getText();
    }

    public List<String> courseNames() {
        return driver.findElements(By.cssSelector("#my-courses tbody td:first-child"))
                .stream().map(WebElement::getText).toList();
    }

    public LoginPage logout() {
        driver.findElement(By.id("logout")).click();
        return new LoginPage(driver);
    }
}
```

## 2. Tests become short and readable

```java
// File: src/test/java/com/skills101/qa/ui/LoginPageTest.java
package com.skills101.qa.ui;

import static org.testng.Assert.assertEquals;

import com.skills101.qa.base.BaseUiTest;
import com.skills101.qa.pages.DashboardPage;
import com.skills101.qa.pages.LoginPage;
import java.util.List;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

// Tests read like the user's story; if the page's HTML changes, only the page objects change
public class LoginPageTest extends BaseUiTest {

    @Test(groups = "smoke")
    public void validLoginShowsDashboard() {
        DashboardPage dashboard = new LoginPage(driver).open(baseUrl).loginAs("asha@skills101.in", "asha123");

        assertEquals(dashboard.welcomeText(), "Welcome, Asha!");
        assertEquals(dashboard.courseNames(), List.of("Java Full Stack", "DSA in Python"));
    }

    @DataProvider(name = "badLogins")
    public Object[][] badLogins() {
        return new Object[][] {
            {"asha@skills101.in", "wrong-password"},
            {"nobody@skills101.in", "asha123"},
            {"", ""},
        };
    }

    // runs once per row of the data provider
    @Test(dataProvider = "badLogins")
    public void invalidLoginShowsError(String email, String password) {
        LoginPage page = new LoginPage(driver).open(baseUrl).loginExpectingError(email, password);

        assertEquals(page.errorMessage(), "Wrong email or password");
    }
}
```

All four tests pass (one valid login plus three rows from the data provider). Compare with `FirstSeleniumTest` from lesson 2 — same behaviour, but no locators in the test.

(Selenium also has `PageFactory` with `@FindBy` fields; many teams now prefer plain `By` fields as shown, which are simpler and avoid stale elements.)

## 3. The whole framework

![Framework layout: pom.xml, testng.xml, base classes, page objects, UI tests, API tests, a Postman collection and CI files](https://skills101.in/learn-img/test-automation/framework.svg)

| Part | Our files |
|---|---|
| Driver management | `DriverFactory` (browser + headless from `-D` properties) |
| Test lifecycle | `BaseUiTest` (open, screenshot on failure, quit) |
| Page objects | `LoginPage`, `DashboardPage` |
| Tests | `ui/…`, `api/…` |
| Configuration | `-Dbrowser`, `-Dheadless`, `-DapiUrl` (no URLs hard-coded in tests) |
| Reporting | `TestLogger` listener + Surefire reports |
| CI | GitHub Actions workflow, Jenkinsfile (lesson 10) |

When an interviewer says *"explain your framework"*, describe these layers, why each exists, and one problem you solved (e.g. "flaky waits — replaced sleeps with explicit waits").

## Common mistakes

- Assertions inside page classes.
- One giant page class for the whole site — make one per page or component (header, cart).
- Tests that still use `driver.findElement` directly next to page objects.
- Hard-coded URLs, users and passwords inside tests.

## Quick recap

- POM: one class per page holds its locators and actions.
- Methods = user actions, returning the next page; no assertions in pages.
- Framework layers: driver factory, base test, pages, tests, config, reports, CI.


Practice quiz and free certificate: https://skills101.in/learn/test-automation/page-object-model-selenium