In Selenium's PageFactory, how do @FindBys and @FindAll differ when they annotate the same page-object field?
answer
- Two ways to combine nested locators
- One narrows, the other merges
- Think containment versus concatenation
- ByChained against ByAll
- Union can repeat an element
basics
~10 s@FindBys builds a ByChained: each locator is searched inside the elements the previous one matched. @FindAll builds a ByAll: every locator runs against the same context and the matches are merged into one union.
solid answer
~40 sBoth take a list of nested `@FindBy` entries, but they combine them differently. `@FindBys` builds a `ByChained`, which searches the second locator **inside** the elements matched by the first, so it narrows by containment — `@FindBys({@FindBy(id = "on-air-queue"), @FindBy(className = "track-row")})` binds only the rows under the on-air queue, and an empty stage empties the whole result. `@FindAll` builds a `ByAll`, which runs each locator against the same context and concatenates the results, so it is a union: duplicates appear when two locators match the same element, and the list is grouped per locator rather than in document order. On a single `WebElement` field, `ByAll` returns the first hit of the first locator that matched, which makes `@FindAll` a useful ordered fallback across two markup spellings.
code
java · 23 linesimport java.util.List;
import org.openqa.selenium.*;
import org.openqa.selenium.support.*;
public class PlaylistEditorPage {
@FindBys({@FindBy(id = "on-air-queue"), @FindBy(className = "track-row")})
private List<WebElement> onAirRows;
@FindAll({@FindBy(className = "jingle-cart"), @FindBy(className = "sponsor-slot")})
private List<WebElement> insertableItems;
@FindAll({@FindBy(id = "save-playlist"), @FindBy(css = "button[name='savePlaylist']")})
private WebElement saveButton;
public PlaylistEditorPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
public void save() {
saveButton.click();
}
}go deeper
Recall that both annotations wrap several @FindBy entries, that @FindBys narrows by searching inside the previous match, and that @FindAll gathers everything the locators match into one list.
Be ready to name the classes the annotations build, ByChained and ByAll, and to explain the consequences: an empty stage empties a chain, a union can repeat an element and is not in document order.
Show judgment about when each earns its place: containment when a class is only unique inside a container, a union when one control has two markup spellings you must tolerate across skins or releases.
Own the convention. Decide whether tolerating two spellings in a union is acceptable drift or a signal that the page layer is absorbing markup churn the product team should be asked to fix.
## What each annotation actually builds Selenium's `PageFactory` never stores your annotation. When `PageFactory.initElements` reaches a field, `Annotations.buildBy()` looks at which of the three locator annotations is present and turns it into a single `By` object, which the field's proxy then uses for every lookup. - `@FindBy` builds one plain `By`, such as `By.className("track-row")`. - `@FindBys({...})` builds a **`ByChained`** over the nested `@FindBy` entries, in the order written. - `@FindAll({...})` builds a **`ByAll`** over the nested entries, again in the order written. Both `ByChained` and `ByAll` live in `org.openqa.selenium.support.pagefactory` and are ordinary `By` subclasses, so nothing about them is specific to page objects — the annotations are just a declarative way to construct them. ## How ByChained walks the page `@FindBys` is a **descendant chain**, not a conjunction. On a radio-station playlist editor, `@FindBys({@FindBy(id = "on-air-queue"), @FindBy(className = "track-row")})` resolves like this: 1. Find every element matching the first locator — here the element whose id is `on-air-queue`. 2. For each element found, search **inside it** with the next locator, flattening all the results into one list. 3. Repeat for every remaining locator. If any stage returns nothing, the chain short-circuits and the whole result is empty. So the field ends up holding the `track-row` elements that sit **under** the on-air queue, and rows elsewhere on the page are excluded. `ByChained.findElement` returns the first survivor and throws `NoSuchElementException` when the list is empty. ## How ByAll collects `@FindAll` is a **union**. Every nested locator is run against the same search context and the result lists are concatenated in annotation order. - An element matched by two of the nested locators appears **twice** in the list. - The results are grouped by locator, so the list is **not in document order** — `ByAll` documents this explicitly. - On a `WebElement` field rather than a list, `ByAll.findElement` returns the first hit of the first nested locator that matched anything at all. That makes `@FindAll` a genuine ordered fallback: a save control spelled `#save-playlist` on the new station skin and `button[name='savePlaylist']` on the old one can be bound once. ## The two side by side | | `@FindBys` | `@FindAll` | |---|---|---| | Builds | `ByChained` | `ByAll` | | Meaning | each locator searched inside the previous match | every locator searched against the same context | | Result | narrowing by containment | union of all matches | | Ordering | document order within the final stage | grouped per locator, not document order | | Duplicates | not produced | produced when locators overlap | | One stage matches nothing | whole result is empty | the other locators still contribute | | Single-element use | first element under the chain | first match of the first locator that hit | ## Rules that bind both forms - Each nested `@FindBy` is validated on its own: **at most one location strategy per entry**, so `@FindBy(id = "on-air-queue", className = "track-row")` inside a `@FindBys` still throws `IllegalArgumentException`. - A field may carry only **one** of `@FindBy`, `@FindBys` and `@FindAll`. Combining them throws `IllegalArgumentException` with a message such as "If you use a '@FindBys' annotation, you must not also use a '@FindBy' annotation". - Both annotations work on a `WebElement` field and on a `List<WebElement>` field; the same `By` is resolved with `findElement` or `findElements` according to the field's type. - Validation is **eager**: the `By` is built while `PageFactory.initElements` decorates the field, so a malformed annotation fails immediately rather than at first use. ## Choosing one on a real page Reach for `@FindBys` when the second locator is only unique **inside** a container — the playlist editor's `.track-row` class appears in the on-air queue, in the archive drawer and in the sponsor scheduler, and only containment tells them apart. Reach for `@FindAll` when one logical control has two markup spellings you must tolerate at once, or when you deliberately want every jingle cart and every sponsor slot collected into a single list. Reaching for `@FindAll` because it sounds like "find all matching elements" is the usual mistake: a plain `@FindBy` on a `List<WebElement>` field already returns every match of one locator, and `@FindAll` only adds the union over several. Neither annotation changes when the lookup happens: like a plain `@FindBy`, the composed `By` is resolved against the live page each time the field is used, and the annotation itself only describes it.
- What happens if the first locator in a @FindBys chain matches nothing?The chain short-circuits: `ByChained` stops as soon as a stage returns no elements, so the field resolves to an empty list. On a `WebElement` field the lookup then throws `NoSuchElementException`, because `ByChained.findElement` returns the first element of that empty result or fails.
- Can a field carry @FindBy and @FindAll at the same time?No. `Annotations` rejects the combination and throws `IllegalArgumentException` while `PageFactory.initElements` decorates the field, with a message saying that if you use `@FindAll` you must not also use `@FindBy`. The same rule blocks `@FindBy` with `@FindBys`, and `@FindAll` with `@FindBys`.
- Why might @FindAll return the same element twice?`ByAll` runs each nested locator separately and concatenates the result lists without de-duplicating. An element that satisfies two of the locators — say a control carrying both the class you match on and the id you match on — is added once per locator, so the list can be longer than the number of distinct elements.
@FindBys is like giving directions room by room — go into the studio, then find the desk. @FindAll is like pooling two search results into one pile, duplicates and all.
saying these in an interview costs you the question
- Saying @FindBys matches elements satisfying every locator at once
- Calling @FindAll an AND of the nested locators
- Assuming @FindAll returns elements in document order
- Expecting @FindAll to remove duplicate matches
- Adding @FindBy alongside @FindBys on the same field