skip to content

In Selenium's PageFactory, what changes when you pass a WebElement to initElements instead of the driver?

level: middleimportance: must knowfreq 58%

answer

  1. The context argument is not decorative
  2. One overload, one wrapped object
  3. A locator factory is built from it
  4. DefaultElementLocator calls context.findElement
  5. Find Element From Element, not Find Element

basics

~20 s

Every proxied field is then located through that element instead of the whole document. PageFactory wraps whatever context you pass in a DefaultElementLocatorFactory, so each lookup starts at that element rather than at the page root.

solid answer

~40 s

`PageFactory.initElements(SearchContext, Object)` is one line: it builds a `DefaultElementLocatorFactory` over the context you pass and decorates the object's fields with it. That factory hands every field a `DefaultElementLocator` holding the same context, and the lazy proxy calls `searchContext.findElement(by)` on first use. Because `WebElement` extends `SearchContext`, passing a lot card's root element makes each `@FindBy` field resolve inside that card — on the wire, Find Element From Element with the root as the start node, instead of a document-wide Find Element. The selector text is unchanged; only the node the search starts from moves. That is the whole mechanism behind a component-scoped object in Selenium 4.

code

java · 23 lines
java
public class LotCard {

  private final WebElement root;

  @FindBy(css = ".doses-on-hand")
  private WebElement dosesOnHand;

  @FindBy(css = ".expiry")
  private WebElement expiry;

  public LotCard(WebElement root) {
    this.root = root;
    PageFactory.initElements(root, this);
  }

  public int dosesOnHand() {
    return Integer.parseInt(dosesOnHand.getText().trim());
  }

  public String lotId() {
    return root.findElement(By.cssSelector(".lot-id")).getText();
  }
}

go deeper

for a junior

Be ready to say that the first argument is the thing the fields are searched from, and that an element can be passed there just as a driver can.

for a middle

You should be able to trace the call chain out loud: initElements builds a DefaultElementLocatorFactory, that creates a DefaultElementLocator per field, and the locator calls findElement on the stored context.

for a senior

An interviewer expects you to connect the mechanism to the bug it prevents: on a page with repeated widgets, a document-wide lookup returns a plausible but wrong node and the test passes on the wrong data.

for a principal

Own the consequence for a harness: element scoping is what lets one widget class be reused wherever the widget appears, so decide early whether your object model is keyed on URLs or on rendered components.

## The one line that does all the scoping In **Selenium 4** the Java bindings ship `PageFactory` (`org.openqa.selenium.support.PageFactory`), a helper that replaces an object's declared `WebElement` and `List<WebElement>` fields with lazy proxies. The overload a component object calls is a single line: ```java public static void initElements(SearchContext searchContext, Object page) { initElements(new DefaultElementLocatorFactory(searchContext), page); } ``` Everything the phrase **component-scoped** means in Selenium lives in that one argument. `DefaultElementLocatorFactory` keeps the `SearchContext` it was constructed with and, for each field it is asked about, returns a `DefaultElementLocator` built over that same context. When the proxy for a field is first touched, `DefaultElementLocator.findElement()` executes `searchContext.findElement(by)`. Hand it the driver and that is a document-wide search; hand it the root `WebElement` of one vaccine lot card on a stock ledger and the search cannot leave that card. ## Why a `WebElement` is a legal argument at all `WebElement` is declared `public interface WebElement extends SearchContext, TakesScreenshot`, so an element already carries `findElement(By)` and `findElements(By)`. `PageFactory` never asks whether the context is a driver — it only needs something able to find. That is why the parameter is typed `SearchContext` rather than `WebDriver`, and it is the whole reason a component object needs no extra API. ## What a field access does once the context is an element - The fields stay **lazy**: `initElements` locates nothing at all. Each proxy resolves the moment a method is invoked on it. - A driver-scoped proxy issues the W3C **Find Element** command against the document. - An element-scoped proxy issues **Find Element From Element** — `POST /session/{session id}/element/{element id}/element` — with the root's element id as the **start node**. - The locator itself is untouched. Scoping changes *where the search begins*, never the selector text. - `List<WebElement>` fields go through `findElements` on the same context, but `DefaultFieldDecorator` only decorates a list when the field carries `@FindBy`, `@FindBys` or `@FindAll`. - Two components built over two different lot-card roots share no state; the root is the only thing that distinguishes them. ## Driver context versus element context | | driver-scoped page object | element-scoped component object | |---|---|---| | argument to `initElements` | the `WebDriver` | the widget's root `WebElement` | | `@FindBy(css = ".doses-on-hand")` matches | the first such node in the document | the first such node inside the root | | command per field access | Find Element | Find Element From Element | | one instance models | a URL or screen | one rendered widget | | a second identical widget | is invisible to the object | gets its own instance | ## Wiring it on a vaccine-stock ledger 1. Locate the widget roots once from the driver, for example `driver.findElements(By.cssSelector("div.lot-card"))`. 2. Construct one component instance per root, passing that root into the constructor. 3. Inside the constructor call `PageFactory.initElements(root, this)`, so the object's own `@FindBy` fields are decorated against its root. 4. Keep the root in a field as well, so ad-hoc lookups such as `root.findElement(By.cssSelector(".expiry"))` stay inside the same subtree. Step 4 matters more than it looks. A component that only owns proxied fields has no handle on its own boundary; keeping the root means the class can also read the widget's own attributes and can search inside it for anything the annotations did not anticipate. ## What passing an element does not change - It does not make lookups eager, and it does not add retrying — `DefaultElementLocator` performs exactly one find per access unless the field is annotated to cache. - It does not validate that the root is still the node you meant; the context is whatever element reference you handed over. - It does not change how `By` strategies are evaluated, only the node the evaluation starts from. - It does not stop you nesting further: a component can locate a sub-widget's root through its own root and construct a nested component over that, giving a scope chain rather than a flat page. The practical payoff is the failure mode it removes. A ledger that renders one card per vaccine lot contains many nodes matching `.doses-on-hand`; a driver-scoped object silently resolves the first of them and reads a number that belongs to the wrong lot, with no exception and a green-looking selector. An element-scoped object cannot make that mistake, because the only nodes it can reach are the descendants of the root it was given.

  • If the selector text is identical, what actually differs between the driver-scoped and element-scoped lookup?
    Only the start node. Both send the same `using` and `value` pair, but the driver-scoped call goes to the session's Find Element endpoint and searches the document, while the element-scoped call goes to Find Element From Element and searches the root's subtree. Same strategy, different starting point, so a matching node outside the widget is unreachable.
  • Does passing a root element make PageFactory locate the fields any earlier?
    No. `initElements` only installs proxies; it performs no lookup. `DefaultElementLocator.findElement()` runs on the first method call against a field, so a field that never gets used is never searched for. Element scoping changes the search context, not the laziness.
  • Can a component object hold another component object as a field?
    Yes, but not as a proxied one. `DefaultFieldDecorator` only decorates `WebElement` and `List<WebElement>` fields, so a nested component is something you construct yourself: find the sub-widget's root through the parent's root, then build the child over it. That gives a chain of nested search contexts.

saying these in an interview costs you the question

  • Says the context argument only matters for constructing the object, not for field lookups.
  • Believes @FindBy always searches the whole document whatever context was passed.
  • Passes the driver to initElements and still expects the root element to scope lookups.
  • Claims a WebElement cannot be given to initElements because it is not a driver.
  • Assumes element scoping rewrites the locator string rather than moving the start node.