How to Use the Page Object Model in Selenium with Java
Build maintainable Selenium tests in Java with page objects, direct By locators, optional PageFactory, clear assertions, reusable components, and practical troubleshooting.
The Page Object Model (POM) puts the locators and user-facing operations for a page or UI component in a Java class. Tests use that class to perform actions and read values, then make behavioral assertions themselves. Pass each page object a WebDriver, keep its locators private, and expose methods such as loginAs or messageText. PageFactory is optional: Selenium’s own POM example uses direct By locators. Selenium’s Page Object Models guide describes the pattern and its design guidance.
1. What a page object should contain
A page object represents the services available on a screen, or on a discrete part of one. Its public methods describe user intent; its private implementation holds the page’s structural details, including selectors and the mechanics of interacting with them.
- Page object: owns page-specific locators, interactions, and observations such as visible text.
- Test: describes the scenario and asserts whether the observed behavior is correct.
- Component object: models a reusable section, such as a navigation bar or product card, and can be composed into a page object.
This centralizes knowledge of a page: when its markup changes, the corresponding page or component class is the natural place to update. Keep objects focused on the UI the tests actually use rather than turning one class into a model of the entire application. Selenium also advises page objects to seldom expose the underlying driver.
2. Build a basic Java Page Object Model
The following example uses private By fields, injects the existing driver, exposes a login operation, and returns the page reached after login. Replace the sample selectors and page-ready condition with those from your application. The Selenium pattern does not depend on a particular test framework or browser.
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class SignInPage {
private final WebDriver driver;
private final By username = By.name("user_name");
private final By password = By.name("password");
private final By signIn = By.name("sign_in");
public SignInPage(WebDriver driver) {
this.driver = driver;
if (!"Sign In Page".equals(driver.getTitle())) {
throw new IllegalStateException("Expected the sign-in page, got: "
+ driver.getCurrentUrl());
}
}
public HomePage loginAs(String userName, String passwordText) {
driver.findElement(username).sendKeys(userName);
driver.findElement(password).sendKeys(passwordText);
driver.findElement(signIn).click();
return new HomePage(driver);
}
}
public class HomePage {
private final WebDriver driver;
private final By message = By.tagName("h1");
public HomePage(WebDriver driver) {
this.driver = driver;
}
public String messageText() {
return driver.findElement(message).getText();
}
}
In Java, each public top-level class belongs in its own correspondingly named file (for example, SignInPage.java and HomePage.java). The snippet shows the two classes together for readability.
Use the page objects from a test
The test owns the behavioral assertion. For example, the core of a test using a Java test framework can be written as:
SignInPage signInPage = new SignInPage(driver);
HomePage homePage = signInPage.loginAs("userName", "password");
assertEquals("Hello userName", homePage.messageText());
Place this in a test method with your framework’s assertion import and a driver created for the test. The example intentionally does not prescribe a Selenium version, Java minimum, Maven coordinate, or test runner: those change over time, and should be selected from the current Selenium documentation and your project’s requirements.
3. Design the page API around the user journey
- Identify the screens and components used by a scenario. Model what the test interacts with, not every part of the application.
- Move selectors into the matching object. Keep locators private and define each page-specific locator in one place.
- Expose operations and observations. Prefer
loginAs,openProfile, ormessageTextover having tests locate and click raw elements. - Return the destination object for transitions. A successful login can return
HomePage; a different outcome can returnSignInPageor another state object. - Assert in the test. Return text, a list, or another useful observation; let the scenario decide whether it matches expectations.
- Extract components when reuse becomes real. A shared header or repeated card is a candidate when it makes page classes clearer or removes duplicated UI knowledge.
Return types make the journey visible in code. If an interaction’s destination changes, changing its method signature can help identify callers that also need updates.
Model different outcomes explicitly
A login attempt can succeed or be rejected. Avoid hiding that distinction in a generic method whose result is unclear. One simple API can make both paths apparent:
public HomePage loginAs(String userName, String passwordText) {
enterCredentials(userName, passwordText);
return new HomePage(driver);
}
public SignInPage loginExpectingError(String userName, String passwordText) {
enterCredentials(userName, passwordText);
return this;
}
public String errorMessage() {
return driver.findElement(error).getText();
}
private void enterCredentials(String userName, String passwordText) {
driver.findElement(username).sendKeys(userName);
driver.findElement(password).sendKeys(passwordText);
driver.findElement(signIn).click();
}
The test then checks the rejection message. Keep the page methods focused on performing the action and exposing what appeared, rather than embedding the scenario’s expected result.
4. Keep assertions in the test
Selenium’s guidance says page objects generally should not verify scenario outcomes or make assertions. There is a limited page-object check that is useful: verifying during construction that the expected page, or a critical ready element, is present. The title check in SignInPage is such a guard; it catches constructing the wrong page object early. It should not replace the test’s behavioral assertion.
For example, prefer a page method that returns an error message and a test assertion on that value. Avoid methods named assertLoginWorked that bury test expectations inside the page class. This separation keeps page services reusable across scenarios with different expected outcomes.
5. Compose reusable page components
A page object need not represent a whole page. If a site repeats a product card, menu, or other discrete section, model that section in a component class. A page can own a collection of components, and components can be nested when the UI structure warrants it.
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
public class ProductCard {
private final WebElement root;
private final By name = By.cssSelector(".inventory_item_name");
private final By price = By.cssSelector(".inventory_item_price");
public ProductCard(WebElement root) {
this.root = root;
}
public String productName() {
return root.findElement(name).getText();
}
public String priceText() {
return root.findElement(price).getText();
}
}
public class ProductsPage {
private final WebDriver driver;
private final By cards = By.cssSelector(".inventory_item");
public ProductsPage(WebDriver driver) {
this.driver = driver;
}
public java.util.List<ProductCard> products() {
return driver.findElements(cards).stream()
.map(ProductCard::new)
.toList();
}
}
As above, put public top-level classes in separate files. This example uses Java’s stream toList(); if your selected Java level does not support it, collect with a compatible collection operation. Component methods expose product information while the page owns the repeated-card locator.
6. Direct By locators or PageFactory?
The Page Object Model is a design pattern; PageFactory is a Selenium Java helper for initializing fields on a page object. You can use the pattern without PageFactory. Selenium’s official model example uses direct By locators. The Java API documents PageFactory as creating lazy proxies for declared WebElement and List<WebElement> fields; by default, field names are assumed to match an element’s HTML id or name, and @FindBy can specify a lookup. See the PageFactory API reference.
| Choice | What it means | Useful when |
|---|---|---|
Direct By |
A page method calls driver.findElement(locator) when it performs an operation. |
You want locator strategy and lookup point to be explicit. This is the style in Selenium’s POM example. |
| PageFactory | Annotated fields are decorated as lazy proxies; proxy behavior determines when lookup occurs. | Your team prefers the field-decorator style and understands its lookup behavior. |
Do not treat either style as a universal performance or reliability winner; the documented mechanics do not establish a benchmark comparison. PageFactory’s documented default looks up proxied elements when a field method is called. @CacheLookup changes that behavior, so avoid caching dynamic elements without a specific reason and a stable page lifecycle.
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class SignInPageWithFactory {
private final WebDriver driver;
@FindBy(name = "user_name")
private WebElement username;
@FindBy(name = "password")
private WebElement password;
@FindBy(name = "sign_in")
private WebElement signIn;
public SignInPageWithFactory(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void enterCredentials(String userName, String passwordText) {
username.sendKeys(userName);
password.sendKeys(passwordText);
signIn.click();
}
}
This alternative also keeps the fields private. Keep the same responsibility boundary: page objects perform page operations and tests assert outcomes.
7. Reliability, speed, and cost considerations
- Reliability: Centralized selectors make UI changes easier to locate and repair. Page readiness checks can catch use of an object on the wrong screen. They do not by themselves make a locator stable or guarantee that an asynchronous page has finished loading.
- Dynamic content: Prefer locating an element when an operation needs it, especially if the DOM may replace it. A previously located
WebElementcan become stale after a rerender or navigation; reacquire it through its page or component operation. - Waits: POM is compatible with explicit synchronization, but this design guide does not prescribe a timing recipe. Follow Selenium’s current wait guidance for the condition your application requires; avoid papering over uncertain readiness with arbitrary sleeps.
- Performance: The pattern adds Java objects and organization, but no cited Selenium source establishes a speed benefit or penalty for either locator style. Keep methods focused and avoid unnecessary repeated browser interactions.
- Cost: The pattern itself has no service fee. Browser execution cost depends on your local or hosted test infrastructure and is not determined by POM.
- Driver lifecycle: Create and quit the WebDriver according to your test framework’s lifecycle. Pass the test’s driver into page objects instead of silently starting additional browsers in each constructor.
8. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
NoSuchElementException |
The selector is wrong, the page is not ready, or the object represents the wrong page. | Check the selector against the current application markup, verify the page transition, and ensure the required condition is ready before interacting. |
StaleElementReferenceException |
The DOM changed after an element was found, such as after rerender or navigation. | Reacquire the element from its locator at the time of the operation; avoid holding dynamic elements across page changes. |
| Wrong page object constructed | A transition did not produce the page assumed by the test. | Add a focused page-ready guard in the constructor or expose an outcome that represents the actual transition; keep the scenario assertion in the test. |
| Assertions duplicated across tests and page methods | Scenario expectations have leaked into the page API. | Have the page return a value or destination object and make the test assert the expected behavior. |
| Many tests break after a selector change | Selectors or UI mechanics are duplicated in tests or unrelated classes. | Move that page knowledge into its page or component object and use its service methods. |
| PageFactory field resolves the wrong element or none | The default field-name-to-id/name assumption does not match the markup. | Use an explicit @FindBy locator and verify it against the current DOM. |
| PageFactory element remains outdated | A cached element was retained even though the UI replaced it. | Do not use @CacheLookup for dynamic elements; choose a lookup lifecycle appropriate to the page. |
| Java compile error from sample layout | Multiple public top-level classes were copied into one file. | Move each public class to its own file named after the class. |
9. Or skip the browser setup
If the task is to capture a page image for documentation, review, or an agent workflow, you can use ScreenshotNeo’s screenshot API instead of maintaining a browser capture script. It is a website screenshot API and MCP server from ScreenshotNeo. One GET request takes a URL; see the API documentation for its options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
ScreenshotNeo accepts cookie or consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
10. Frequently asked questions
Can I use the Page Object Model without PageFactory?
Yes. PageFactory is optional. Private By locators and page methods are enough to implement the pattern.
Should every URL have its own page class?
No. Model the screens and components the tests interact with. A component can be shared across multiple page objects.
Should a page object expose WebDriver?
Usually not. Expose page services and observations so tests do not depend on the driver or raw page structure.
Can a page object contain a readiness check?
Yes. A narrow check that the expected page or a critical element is ready during construction is consistent with Selenium’s guidance. Scenario assertions still belong in tests.
Sources
- Selenium: Page object models — page responsibilities, assertions, transitions, and component objects.
- Selenium Java API: PageFactory — initialization and proxy behavior.
- Selenium documentation — current setup and version-specific guidance.


