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();
    }
}
Compare: every method body above reads like plain field access — no By, no driver lookups. The test class calling new LoginPage(driver).login("tester@example.com", "Test@1234") looks identical to the plain-POM version from the previous lesson; only the class internals changed.

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.).

StepWhat 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(...) againThe 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
BoilerplateA driver.findElement(By...) line in every methodOne annotation per field, methods stay short
BehaviorExplicit, easy to reason about — you can see exactly when a lookup happensProxy-based, "magic" — the lookup timing is implicit
Stale elementsRare, since you re-find fresh each call anywayCan still hit StaleElementReferenceException in tricky SPA re-render cases, and it's harder to reason about why
DebuggabilityStepping through in a debugger shows a normal method callStepping through shows proxy/reflection internals, which confuses newcomers
Industry usageVery common, especially in newer frameworksStill common, especially in older/larger legacy suites
The honest answer: plenty of real, well-maintained automation frameworks use Page Factory, and plenty deliberately avoid it in favor of plain findElement() calls inside Page Objects, precisely because the lazy-proxy behavior can be confusing to debug when something goes wrong. Both are legitimate. Pick whichever your team finds easier to read and debug, and stay consistent across the codebase — mixing the two styles in the same project is the actual mistake.

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);
    }
}
Next: real automation suites don't hard-code test data in Java files — they read it from a spreadsheet. Head to Reading Test Data from Excel/CSV to load 20 RecipeBox test titles from an actual .xlsx file.