skip to content

In Selenium's PageFactory, what does initElements put into a WebElement field, and when is the browser actually queried?

level: middleimportance: should knowfreq 64%

answer

  1. Look closely at when the lookup happens
  2. The constructor sends no browser command
  3. The field holds a stand-in object
  4. A dynamic proxy wrapping an ElementLocator
  5. LocatingElementHandler re-locates on every invocation

basics

~20 s

It assigns a dynamic proxy object, not a found element. Selenium sends no find command while initElements runs; the proxy asks its ElementLocator for a fresh lookup each time a method is called on the field.

solid answer

~40 s

`PageFactory.initElements(searchContext, page)` walks every declared field, including inherited ones, and hands each to a `FieldDecorator`. The stock `DefaultFieldDecorator` replaces `WebElement` fields with a `java.lang.reflect.Proxy` implementing `WebElement`, `WrapsElement` and `Locatable`, backed by a `LocatingElementHandler` holding one `ElementLocator`. Nothing is looked up at that moment, so a taxi-dispatch board page object is constructed with every field non-null and nothing resolved. The first round trip happens when you call a method on the field: the handler calls `locator.findElement()`, which runs `searchContext.findElement(by)`, then reflectively invokes the requested method on the real element. Because the handler re-locates on every invocation, `assignRideButton.isDisplayed()` followed by `assignRideButton.click()` issues two separate find commands unless the field is cached.

code

java · 27 lines
java
import java.util.List;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;

public class DispatchBoardPage {

  @FindBy(css = "[data-role='assign-ride']")
  private WebElement assignRideButton;

  @FindBy(css = "tr.waiting-ride")
  private List<WebElement> waitingRideRows;

  public DispatchBoardPage(WebDriver driver) {
    // No find command is sent here; both fields become proxies.
    PageFactory.initElements(driver, this);
  }

  public int waitingRideCount() {
    return waitingRideRows.size(); // first lookup happens here
  }

  public void assignFirstRide() {
    assignRideButton.click(); // a second, independent lookup
  }
}

go deeper

for a junior

Be ready to say that a PageFactory field is filled in by initElements and that the element itself is not fetched from the browser until you call a method on that field.

for a middle

Explain the machinery: DefaultFieldDecorator builds a java.lang.reflect.Proxy over an ElementLocator, and LocatingElementHandler calls findElement on every single method invocation.

for a senior

Show that you count round trips. An interviewer expects you to notice that a chain of calls on one proxied field costs a find command each, and to say when that traffic starts to matter.

for a principal

Own the trade: lazy re-location buys freshness against a re-rendering page and pays in traffic and in fields that are opaque under a debugger. Be able to say when a team should keep the default.

## The three overloads behind one call **`PageFactory`** is a static helper in `org.openqa.selenium.support`. The call a taxi-dispatch board page object usually makes is `PageFactory.initElements(driver, this)`, which binds to `initElements(SearchContext, Object)` because `WebDriver` extends `SearchContext`. That overload does almost nothing itself — it delegates down a three-step pipeline: 1. `initElements(SearchContext, Object)` builds a `DefaultElementLocatorFactory` over the context and calls the next overload. 2. `initElements(ElementLocatorFactory, Object)` wraps that factory in a `DefaultFieldDecorator` and calls the last one. 3. `initElements(FieldDecorator, Object)` does the real work. The last step walks the page object's own class, then its superclass, and so on until it reaches `Object`, calling `getDeclaredFields()` at each level. Every field is handed to `decorator.decorate(loader, field)`; when that returns a non-null value, `PageFactory` calls `setAccessible(true)` and assigns it. **No browser command is issued anywhere in that loop** — the whole method is reflection over class metadata. ## What the decorator installs in the field `DefaultFieldDecorator.decorate` returns `null` — meaning *leave this field alone* — unless the field's type is assignable to `WebElement`, or it is a `List<WebElement>` carrying `@FindBy`, `@FindBys` or `@FindAll`. For a field it accepts, it asks the `ElementLocatorFactory` for an `ElementLocator` and then builds a **dynamic proxy**: - A `WebElement` field receives `Proxy.newProxyInstance` with the interfaces `WebElement`, `WrapsElement` and `Locatable`, driven by a `LocatingElementHandler`. - A `List<WebElement>` field receives a proxy implementing only `List`, driven by a `LocatingElementListHandler`. - Each handler holds exactly one collaborator: the `ElementLocator` built for that one field. - A field with no `@FindBy` at all still gets decorated; `Annotations.buildByFromDefault()` falls back to `new ByIdOrName(field.getName())`. After `initElements` returns, a dispatch board's `assignRideButton` field is **non-null and tells you nothing** — it is a runtime-generated class that implements `WebElement`, and the browser has never been asked whether such a button exists. ## Where the first round trip really happens `LocatingElementHandler.invoke` runs for *every* method invoked through the field, and its order of business is fixed: 1. Call `locator.findElement()`. For the stock `DefaultElementLocator` that runs `searchContext.findElement(by)` — one real WebDriver find command. 2. If the lookup threw `NoSuchElementException` and the method being called is `toString`, return the string `"Proxy element for: "` plus the locator; otherwise rethrow. 3. If the method is `getWrappedElement`, return the located element directly — that is how you unwrap a proxy into a plain `WebElement`. 4. Otherwise call `method.invoke(element, objects)` and unwrap any `InvocationTargetException` so the caller sees the browser's own exception. Because step 1 has no memory, `assignRideButton.isDisplayed()` followed by `assignRideButton.click()` sends **two independent find commands**, and the click may act on a different DOM node than the one that was checked. That is the design's deliberate trade: freshness against a page that re-renders, paid for in traffic. ## Single field versus list field | aspect | `WebElement` field | `List<WebElement>` field | |---|---|---| | decorated when | the type is assignable to `WebElement` | the type is `List<WebElement>` **and** it is annotated | | proxy interfaces | `WebElement`, `WrapsElement`, `Locatable` | `List` only | | invocation handler | `LocatingElementHandler` | `LocatingElementListHandler` | | locator method used | `findElement()` | `findElements()` | | nothing matches | the underlying find throws | an empty list comes back | The asymmetry in the last row is inherited straight from the search context; the proxy adds nothing of its own. ## The proxy construction, in one snippet ```java WebElement proxy = (WebElement) Proxy.newProxyInstance( loader, new Class[] {WebElement.class, WrapsElement.class, Locatable.class}, new LocatingElementHandler(locator)); ``` That is the entire mechanism: a JDK proxy, three interfaces, one handler, one locator. ## Why laziness is the right default here - A page object is frequently constructed **before** the page it describes has been navigated to, and eager lookups would make that impossible. - Dispatch-board rows are replaced as rides are assigned, so a reference captured once would be pointing at a detached node within seconds. - The cost is visibility: a field that reads as a `WebElement` in your IDE is a stand-in, and a debugger inspecting it triggers the special-cased `toString` rather than showing an element. - Opting out is per field, with `@CacheLookup`, which flips the locator into remembering its first result. In interviews the whole question usually reduces to one sentence: `initElements` assigns proxies, not elements, and the browser is first contacted by the first method call on a field.

  • Does initElements decorate fields declared on a superclass of the page object?
    Yes. `initElements(FieldDecorator, Object)` loops from the page object's own class up the hierarchy, calling `getDeclaredFields()` on each class until it reaches `Object`, so a shared base page's fields are proxied too. Private fields are no obstacle: `PageFactory` calls `setAccessible(true)` before assigning. Only fields the decorator returns a non-null value for are replaced.
  • What happens if you call toString() on a proxy whose element is not on the page?
    `LocatingElementHandler` catches the `NoSuchElementException` from the locator and, only for `toString`, returns the string `"Proxy element for: "` followed by the locator, which prints its class name and its `By`. Every other method rethrows. That is why a debugger or a log line can happily display a proxy that no live element backs.
  • Which interfaces does the generated WebElement proxy implement?
    `DefaultFieldDecorator.proxyForLocator` calls `Proxy.newProxyInstance` with `WebElement`, `WrapsElement` and `Locatable`. `getWrappedElement` is special-cased in the handler to return the freshly located element itself, which is how you unwrap a proxy into a plain `WebElement`. A `List<WebElement>` field gets a separate proxy implementing only `List`.

The field behaves like a dispatcher who never memorises where a cab is parked: each time you ask, they walk back to the board and read the row again.

saying these in an interview costs you the question

  • Says initElements finds every annotated element immediately and stores it
  • Thinks the field holds a real WebElement returned by findElement
  • Assumes two calls on the same field reuse one lookup by default
  • Claims the page object constructor fails when an element is missing
  • Thinks PageFactory only decorates fields declared on the page class itself