How to Use PageFactory in Selenium
Learn to initialize Selenium Java Page Objects with PageFactory, use @FindBy and lazy element proxies, and handle waits, stale elements, and alternatives.
PageFactory in Selenium Java initializes Page Object fields such as WebElement and List<WebElement>. Create the page object, annotate fields with @FindBy, then call PageFactory.initElements(driver, this) in its constructor. PageFactory decorates those fields with proxies; by default, the browser lookup happens when code uses a field, not necessarily when the page object is constructed.
This guide uses Selenium’s Java API. PageFactory is an optional convenience for Page Objects, not a requirement for Selenium or for the Page Object pattern. The official [Selenium Java PageFactory API](https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html) documents initialization and field behavior; Selenium’s [Page Object guidance](https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models/) describes the design principles.
1. Add Selenium and create a driver
Use the Selenium Java dependency already managed by your project, or add it through your build tool. This Maven example uses a version property so you can keep the dependency aligned with the version your project has selected:
<properties>
<maven.compiler.release>17</maven.compiler.release>
<selenium.version>YOUR_SELENIUM_VERSION</selenium.version>
</properties>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
</dependency>
</dependencies>
Replace YOUR_SELENIUM_VERSION with the version pinned by your project. The following minimal test setup creates the driver before constructing the page object; driver lifecycle and browser availability remain the test’s responsibility.
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class LoginSmokeTest {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
try {
driver.get("https://example.com/login");
LoginPage login = new LoginPage(driver);
login.signIn("reader", "secret");
} finally {
driver.quit();
}
}
}
The example assumes Chrome and its driver can be started in the environment. In a test framework, create and quit the driver in that framework’s setup and teardown hooks rather than in main.
2. Build a Page Object with @FindBy
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 final WebDriver driver;
@FindBy(id = "username")
private WebElement username;
@FindBy(id = "password")
private WebElement password;
@FindBy(css = "button[type='submit']")
private WebElement submit;
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void signIn(String user, String pass) {
username.sendKeys(user);
password.sendKeys(pass);
submit.click();
}
public String title() {
return driver.getTitle();
}
}
Pass an already-created WebDriver to the page. The essential initialization is PageFactory.initElements(driver, this), which decorates eligible fields on this instance. Keep page-specific locators and actions in the page object; let the test decide what to assert.
3. Understand initialization and lookup timing
The default PageFactory field decorator supports WebElement and List<WebElement> fields. It creates lazy proxies. For example, calling username.sendKeys(...) causes the proxy to locate the element when that method is invoked. Initialization therefore does not prove that the locator matches an element or that the page is ready.
For an eligible field without a locator annotation, the default lookup treats the Java field name as an HTML id or name candidate. That convention is useful only when the markup follows it. Prefer an explicit annotation when the relationship is not obvious:
@FindBy(name = "email")
private WebElement emailAddress;
@FindBy(css = "form button[type='submit']")
private WebElement submit;
When the class overload is useful, PageFactory can instantiate and return the page:
LoginPage login = PageFactory.initElements(driver, LoginPage.class);
The API prefers a constructor that takes WebDriver as its only argument and falls back to a no-argument constructor. If the class cannot be instantiated, initialization fails. The instance overload is often clearer when your page has required constructor arguments or explicit setup.
4. Choose and read locator annotations
@FindBy declares an explicit locator. Common forms include:
| Annotation | Example | Use |
|---|---|---|
id |
@FindBy(id = "email") |
An element with a stable HTML id. |
name |
@FindBy(name = "q") |
A form field or element with a name attribute. |
css |
@FindBy(css = "button.primary") |
A CSS selector for a class, attribute, or relationship. |
xpath |
@FindBy(xpath = "//button[@type='submit']") |
An XPath when the needed relationship is awkward in CSS. |
className |
@FindBy(className = "notice") |
A single class name, not a space-separated class list. |
tagName |
@FindBy(tagName = "button") |
Elements selected by tag; often pair with a narrower container. |
PageFactory’s annotation API also supports locator composition, including @FindBys and @FindAll. Use these only when their chained or alternative matching behavior is the intent, and keep selectors specific enough to identify the expected element. For complex or dynamically computed locators, using By directly in a method can be easier to follow.
5. Lists, dynamic pages, and caching
A list field can represent repeated matching elements:
@FindBy(css = "ul.results > li")
private List<WebElement> results;
public int resultCount() {
return results.size();
}
As with a single element, the list is proxied by default and lookup occurs when code uses it. If the list changes as the page updates, access it after the relevant state change and use an explicit wait where timing is uncertain.
@CacheLookup changes the default repeated lookup behavior by caching the located element. Use it only when the same DOM element remains valid for the object’s lifetime. Pages that replace nodes during navigation, rerendering, form updates, or component refresh can leave a cached reference stale. Do not add it as a blanket speed optimization.
6. Wait for the page state you need
Lazy lookup is not a wait strategy. If the page loads asynchronously, wait for a meaningful condition before using the proxy. One option is an explicit wait with Selenium’s expected conditions:
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("username")));
LoginPage login = new LoginPage(driver);
The PageFactory support package also provides AjaxElementLocatorFactory, which creates locators that wait up to a configured duration for an element to appear before lookup fails:
import java.time.Duration;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(
new AjaxElementLocatorFactory(driver, 10), this);
}
Choose one wait approach deliberately. Explicit waits make the condition visible at the point where the test needs it; the Ajax locator factory applies waiting behavior to decorated field lookup. Neither makes an incorrect selector valid, and neither guarantees that a located element is clickable or that the whole application is ready.
7. Keep PageFactory and Page Object responsibilities clear
PageFactory is an initialization helper; the Page Object is the design pattern. Selenium’s guidance recommends modeling page or component services behind methods, keeping page-specific details inside the object, and generally leaving assertions to the test. A page object can model a component such as a navigation bar as well as a full page.
For example, expose signIn(user, pass) rather than making every test manipulate the username and password fields. This keeps locators together and gives tests a stable interface when page markup changes.
8. PageFactory fields versus direct By locators
| Choice | Locator declaration | Lookup and refresh | Good fit |
|---|---|---|---|
| PageFactory | Annotated fields on the page object. | Default proxies locate on use; caching changes repeated lookup behavior. | Teams that prefer field-based page definitions and simple stable locators. |
Direct By |
Locators stored as By values and passed to driver methods. |
Each call to findElement performs a visible lookup; refresh behavior is explicit in code. |
Teams that want lookup timing and dynamic locator choices visible at each action. |
Selenium’s own Page Object example uses direct By locators; PageFactory is one available Java style, not a required layer. A compact direct-locator version looks like this:
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class LoginPageBy {
private final WebDriver driver;
private final By username = By.id("username");
private final By password = By.id("password");
private final By submit = By.cssSelector("button[type='submit']");
public LoginPageBy(WebDriver driver) {
this.driver = driver;
}
public void signIn(String user, String pass) {
driver.findElement(username).sendKeys(user);
driver.findElement(password).sendKeys(pass);
driver.findElement(submit).click();
}
}
Pick the convention that makes locator use and page behavior easiest for your team to review. Avoid mixing both styles on one page without a practical reason.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
NullPointerException when using a field |
The page’s fields were not initialized, or initialization happened on a different instance. | Call PageFactory.initElements(driver, this) in the constructor and use that same page object. |
NoSuchElementException on first field use |
The selector does not match, the page has not reached the needed state, or the element is in a frame or shadow root. | Inspect the live DOM and locator; wait for the condition; switch to the frame or use the appropriate shadow-root API. |
| Field finds the wrong element | A broad selector or implicit field-name lookup matches an unintended element. | Use an explicit, scoped @FindBy selector and check for duplicate matches. |
StaleElementReferenceException |
The page rerendered or replaced a node after a cached or previously resolved reference was obtained. | Do not cache changing elements; wait for the new state and resolve the element again. Consider a direct By lookup for frequently replaced nodes. |
| Timeout or slow field access | A wait condition never becomes true, or the configured timeout is too short or being applied broadly. | Confirm the expected state can occur, use a focused condition, and set a timeout appropriate to that operation. |
| Page class cannot be instantiated | The class overload cannot use an accessible supported constructor. | Provide a constructor taking only WebDriver, or construct the page yourself and use the instance overload. |
| Annotation is ignored or field stays unsupported | The field type is not among the default decorator’s eligible types, or imports are from the wrong package. | Use Selenium’s WebElement or List<WebElement> fields and imports under org.openqa.selenium. |
10. Performance, reliability, and cost
PageFactory affects how page fields are initialized and looked up; it does not make the browser, network, or application faster. Lazy proxies avoid requiring every field to be found during construction, but each actual interaction still depends on Selenium’s browser communication and page state. Avoid speculative caching: stale references can make tests less reliable and add retries or debugging time.
For reliability, use stable locators, wait for specific state transitions, keep driver setup and teardown deterministic, and avoid sharing one mutable driver across parallel tests unless your test design isolates sessions. Selenium itself has no per-screenshot charge in this example; browser execution, CI capacity, and any separately hosted grid are the operational costs to account for.
11. Capture a screenshot of the result
If you need a visual artifact after a Selenium flow, Selenium can save the current viewport as a PNG. This is separate from PageFactory and captures the browser state reached by the test:
import java.nio.file.Files;
import java.nio.file.Path;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
Path output = Path.of("artifacts", "login-result.png");
Files.createDirectories(output.getParent());
byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
Files.write(output, png);
Capture after waiting for the state you want to document. The screenshot reflects the current viewport and browser session; it is not automatically a full-page capture.
Or skip the browser setup
If your goal is to capture a website rather than test its interactions, [ScreenshotNeo](https://screenshotneo.com) provides a one-request screenshot API and MCP server. Its [API documentation](https://screenshotneo.com/docs/) covers request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for free and capture 1,000 screenshots a month with no card.
12. FAQ
Does PageFactory work with Selenium in languages other than Java?
This PageFactory API and its annotations are part of Selenium’s Java support package. The examples here apply to Java.
Does calling initElements verify that every locator exists?
No. Default fields are lazy proxies, so a lookup may not occur until code uses a field. Wait for and verify the page state your test requires.
Should every page object use PageFactory?
No. Selenium’s Page Object design can use direct By locators. Choose the style that keeps your team’s page behavior clear and maintainable.


