Page Factory
In the last lesson, our LoginPage called driver.findElement(...) inside every single method — fine for one page, tedious once a framework has dozens of them. Page Factory is a small utility built into Selenium that lets you declare locators as annotated fields instead, and have Selenium wire them up for you. It's not a replacement for the Page Object Model — it's an optional way to write a Page Object.
The @FindBy Annotation
@FindBy goes on a WebElement (or List<WebElement>) field and tells Selenium how to locate it — the same strategies you already know from By: id, name, className, css, xpath, and so on.
public class LoginPage {
@FindBy(id = "email")
private WebElement emailField;
@FindBy(id = "password")
private WebElement passwordField;
@FindBy(id = "loginBtn")
private WebElement loginButton;
@FindBy(css = ".error-message")
private WebElement errorMessage;
}
Notice there is no driver.findElement() anywhere in that snippet — the field declaration is the locator. Selenium fills in the actual WebElement behind the scenes, but only once you tell it to, via PageFactory.initElements().
PageFactory.initElements()
This static method scans a class for @FindBy-annotated fields and generates a proxy for each one. Call it once, typically from the Page Object's constructor:
public class LoginPage {
private WebDriver driver;
@FindBy(id = "email")
private WebElement emailField;
@FindBy(id = "password")
private WebElement passwordField;
@FindBy(id = "loginBtn")
private WebElement loginButton;
@FindBy(css = ".error-message")
private WebElement errorMessage;
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void enterEmail(String email) {
emailField.clear();
emailField.sendKeys(email);
}
public void enterPassword(String password) {
passwordField.clear();
passwordField.sendKeys(password);
}
public void clickLogin() {
loginButton.click();
}
public String getErrorMessage() {
return errorMessage.getText();
}
// Same combined action as before, now reading much cleaner
public void login(String email, String password) {
enterEmail(email);
enterPassword(password);
clickLogin();
}
}
Lazy Initialization: What's Actually Happening
A common misconception is that initElements() immediately calls findElement() for every annotated field. It doesn't. It hands each field a proxy object — a stand-in that only performs the real findElement() lookup the first time that field is actually used (a click, a sendKeys, reading text, etc.).
| Step | What happens |
|---|---|
| new LoginPage(driver) | Constructor runs, initElements() assigns a proxy to each field. No DOM lookup yet. |
| loginPage.enterEmail(...) | The proxy for emailField intercepts the call, runs driver.findElement(By.id("email")) right then, and forwards sendKeys() to the real element. |
| loginPage.enterEmail(...) again | The proxy re-locates the element every call — useful if the page re-renders the field between calls (e.g. after an Ajax refresh). |
This is why it's called lazy: elements that a particular test never touches (say, errorMessage on a test where login succeeds) are never located at all, which avoids a pointless NoSuchElementException for elements that legitimately aren't on the page in that scenario.
Honest Take: Page Factory vs Plain POM
| Plain POM (previous lesson) | Page Factory | |
|---|---|---|
| Boilerplate | A driver.findElement(By...) line in every method | One annotation per field, methods stay short |
| Behavior | Explicit, easy to reason about — you can see exactly when a lookup happens | Proxy-based, "magic" — the lookup timing is implicit |
| Stale elements | Rare, since you re-find fresh each call anyway | Can still hit StaleElementReferenceException in tricky SPA re-render cases, and it's harder to reason about why |
| Debuggability | Stepping through in a debugger shows a normal method call | Stepping through shows proxy/reflection internals, which confuses newcomers |
| Industry usage | Very common, especially in newer frameworks | Still common, especially in older/larger legacy suites |
Locator Lists with @FindBy
@FindBy also works on List<WebElement>, handy for RecipeBox's recipe list page where every card shares the same class:
public class RecipeListPage {
private WebDriver driver;
@FindBy(className = "recipe-card")
private List<WebElement> recipeCards;
@FindBy(id = "searchBox")
private WebElement searchBox;
public RecipeListPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public int getRecipeCount() {
return recipeCards.size();
}
public void searchFor(String term) {
searchBox.clear();
searchBox.sendKeys(term);
searchBox.sendKeys(Keys.ENTER);
}
}