skip to content

In Selenium's PageFactory, how do you bind a List<WebElement> field, and what happens to a field left unannotated?

level: juniorimportance: should knowfreq 47%

answer

  1. One annotation, two field shapes
  2. The list case has a requirement
  3. A bare element field still binds
  4. Field name becomes the locator
  5. Unannotated lists stay null

basics

~20 s

A list field takes the same @FindBy and is resolved with findElements, but the annotation is required or the field stays null. An unannotated WebElement field is bound using its own name as an id, then a name.

solid answer

~40 s

`@FindBy(className = "track-row") List<WebElement> trackRows;` uses exactly the same annotation a single element field would; the field's type decides that the locator is resolved with `findElements`, so a locator matching nothing gives an empty list. The generic type must be `List<WebElement>` precisely, and a list field **must** carry `@FindBy`, `@FindBys` or `@FindAll` — without one it is never bound and stays `null`, so the first call on it throws `NullPointerException`. A `WebElement` field is the opposite: with no annotation it is still bound, using the field's own name as `ByIdOrName`, which tries `By.id` and then `By.name`. Field types that are neither `WebElement` nor `List<WebElement>` — a `Select`, a `String` — are skipped in silence.

code

java · 20 lines
java
import java.util.List;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.CacheLookup;
import org.openqa.selenium.support.FindBy;

public class PlaylistEditorPage {

  @FindBy(className = "track-row")
  private List<WebElement> trackRows;

  @FindBy(id = "now-playing")
  @CacheLookup
  private WebElement nowPlaying;

  // no annotation: located by id, then name, "playlistTitle"
  private WebElement playlistTitle;

  // no annotation on a list: never bound, stays null
  private List<WebElement> jingleCarts;
}

go deeper

for a junior

Recall that a list field takes the same @FindBy as a single element and must have one, and that an unannotated WebElement field falls back to its own field name used as an id and then a name.

for a middle

Explain the asymmetry and its consequences: the null list surfaces as a NullPointerException far from the declaration, and unsupported field types are skipped with no error at all.

for a senior

Diagnose the failures these rules produce in a real suite, and argue for annotating even where it is optional so that renaming a field cannot silently retarget a lookup.

for a principal

Decide whether the name-based fallback is allowed at all in your page layer, since it couples Java identifiers to markup ids and makes a routine rename a functional change.

## What PageFactory will and will not bind When `PageFactory.initElements` walks a page object it inspects each declared field and decides whether to assign it a lazy proxy. Only two shapes qualify: - a field whose type is `WebElement` (or a subtype of it), and - a field declared exactly as `List<WebElement>` **that carries** `@FindBy`, `@FindBys` or `@FindAll`. Everything else is skipped in silence. No exception is thrown, no warning is logged, and the field keeps whatever value it already had — which for an uninitialised field is `null`. ## The asymmetry that catches people The annotation is **optional on a `WebElement` field and mandatory on a list field**. - An unannotated `WebElement` field is still bound. Its locator is built from the **field's own name**, as `ByIdOrName("playlistTitle")`, which tries `By.id` first and falls back to `By.name`. That is why a page object whose fields are named after the markup's ids can work with no annotations at all — and why renaming such a field quietly changes the locator. - An unannotated `List<WebElement>` field is **not** bound. It stays `null`, and the first call on it — `trackRows.size()` — throws `NullPointerException`, far from the declaration that caused it. ## Declaring a list field `@FindBy(className = "track-row") List<WebElement> trackRows;` uses exactly the same `By` a single element field would use; the field's type decides how it is resolved. Points that follow from that: 1. The generic type must be `WebElement` itself. A raw `List`, a `List<String>` or a `List<TrackRowComponent>` is not bound even with an annotation. 2. Because the lookup resolves through `findElements`, a locator matching nothing yields an **empty list** rather than an error, so `trackRows.isEmpty()` is a legitimate assertion about an empty playlist. 3. `@FindBys` and `@FindAll` bind list fields too, so a list can be the rows inside one container or the union of several locators. ## A quick reference | Declaration | Bound? | Locator used | |---|---|---| | `@FindBy(id = "track-search") WebElement trackSearch;` | yes | `By.id("track-search")` | | `WebElement playlistTitle;` | yes | `ByIdOrName("playlistTitle")` | | `@FindBy(className = "track-row") List<WebElement> trackRows;` | yes | `By.className("track-row")` | | `List<WebElement> jingleCarts;` | no | none — field stays `null` | | `@FindBy(id = "genre") Select genreSelect;` | no | none — `Select` is not a `WebElement` | | `@FindBy(css = ".track-row") List<String> titles;` | no | none — wrong element type | The `Select` row is worth dwelling on, because wrapping a dropdown is a natural thing to want on a playlist editor's genre filter. `Select` is a helper class that wraps a `WebElement`; it is not one, so the field is skipped and stays `null`. The working shape is an annotated `WebElement` field that the page object wraps where it is used. ## Other rules the declaration must respect - The fields are assigned **reflectively**, so they may be `private` — that is the normal style — but they must not be `final`, because a final field cannot be assigned this way. - `@CacheLookup` is a **marker**: it takes no members, targets fields only, and is read alongside the locator annotation on the same field. It cannot be put on a class. - `@FindBy`, `@FindBys` and `@FindAll` may be written on a type as well as a field, but the Java client documents that a type-level annotation **is not processed by default** — putting one on the page class compiles and does nothing. - Fields declared on superclasses are bound too, because the factory walks the class hierarchy, so an annotated field on a shared base page is decorated on every subclass. ## Why the defaults are shaped this way The `ByIdOrName` fallback exists so a trivial page object needs no annotations at all, and the list rule exists because the factory cannot guess a sensible default for a collection: a field name is a plausible id for one element and a poor locator for many. The practical consequence for a page layer is that annotations are worth writing even where they are optional — they let the field be named for the domain (`playlistTitle`) while the locator keeps whatever the markup actually ships (`id="playlist_title_input"`), and they stop a rename from silently retargeting a lookup. The rule of thumb that follows: annotate every bound field, and treat an unannotated one as an accident rather than as a deliberate use of the fallback.

  • Why is the annotation optional on an element field but required on a list?
    A field name is a plausible id for one element, so Selenium falls back to `ByIdOrName(fieldName)`. For a collection that guess makes no sense — a name matching one id is a poor locator for many elements — so a list is bound only when an annotation states the locator explicitly.
  • What happens to an annotated field whose type is Select?
    Nothing. `Select` wraps a `WebElement` but is not one, so the factory does not decorate the field and it stays `null`, with no error at initialisation. The working shape is an annotated `WebElement` field that the page object wraps at the point of use.
  • Can these fields be private or final?
    Private is fine and is the usual style, because the factory assigns them reflectively after making them accessible. Final is not: a final field cannot be assigned that way, so an annotated field must be declared non-final for the binding to succeed.

saying these in an interview costs you the question

  • Expecting an unannotated list field to be populated
  • Thinking a missing annotation causes an error at initElements
  • Annotating a Select field and expecting it to be created
  • Believing List<WebElement> needs a different annotation
  • Assuming an empty match throws instead of giving an empty list