Your Selenium page object reads the wrong lot's dose count on a ledger listing many lot cards. How do you restructure it?
answer
- The object cannot say which card
- Repeated markup, one silent winner
- Find the roots, not the fields
- One instance per rendered card
- Identity moves from selector to instance
basics
~20 sStop scoping the object to the page. Find the card roots, build one component object per root, and let each search only through its own root, so a lookup cannot reach a node in a neighbouring card.
solid answer
~40 sThe page object searches the document, so `.doses-on-hand` resolves to the first matching node anywhere on the ledger — a real element with a plausible number, and no exception. The fix is structural: have the page object find the card **roots** (`driver.findElements(By.cssSelector("div.lot-card"))`), map each root to a `LotCard` component, and move every field lookup inside that component so it searches from its own root. The page object then exposes selection — `lot("B-4417")` or `cards()` — and returns components rather than elements. Which lot you are asserting on is carried by which instance you hold, not by a cleverer selector. Note that `PageFactory` will not populate a `List<LotCard>` field; you map the roots yourself.
code
java · 22 linespublic class LedgerPage {
private final WebDriver driver;
public LedgerPage(WebDriver driver) {
this.driver = driver;
}
public List<LotCard> cards() {
return driver.findElements(By.cssSelector("div.lot-card")).stream()
.map(LotCard::new)
.collect(Collectors.toList());
}
public LotCard lot(String lotId) {
return cards().stream()
.filter(card -> lotId.equals(card.lotId()))
.findFirst()
.orElseThrow(
() -> new NoSuchElementException("no lot card for " + lotId));
}
}go deeper
Recognise that a document-wide lookup on a page with repeated widgets returns the first match, and that the fix involves searching from a card's own element rather than from the driver.
Explain the mechanics of the restructure: find roots with findElements, construct one component per root, and move each field lookup inside it so it runs against that root.
Show the diagnosis as well as the fix, including the fixture change that makes the bug reproducible, and account for re-render: components must come from a call that re-finds roots rather than a cached list.
Own the convention that stops this class of bug recurring: which repeated structures get component classes, where the single document-wide search is allowed to live, and how reviews catch card-internal selectors on a page object.
## Why a page-scoped object reads a neighbour's data A ledger that renders one card per vaccine lot repeats the same internal markup many times over: every card has a `.lot-id`, a `.doses-on-hand`, an `.expiry`. A page object scoped to the driver asks the document for `.doses-on-hand` and gets back the **first** node that matches — a real element, on the right page, with a plausible number in it. Nothing throws. The assertion runs against whichever lot happens to render first, and the test is green or red for reasons unrelated to the lot under test. The tell is that the bug is invisible when one card is on screen and appears the moment the fixture seeds a second lot. Adding an index to the selector or a `nth-of-type` suppresses the symptom for one test and moves the problem, because the object still has no notion of *which* card it is talking about. ## The restructure: one instance per root 1. Give the page object a method that finds the **card roots**, not the fields: `driver.findElements(By.cssSelector("div.lot-card"))`. 2. Map each returned root to a component instance — `new LotCard(root)` — so there is one object per rendered card. 3. Move every field lookup into that component, searched from its own root (`root.findElement(...)`, or `@FindBy` fields decorated by `PageFactory.initElements(root, this)`). 4. Have the page object expose a way to pick the card you mean — by lot id, by position, or by a predicate over the components — and return the component, not an element. 5. Let the test talk only to the component: `ledger.lot("B-4417").dosesOnHand()`. After that, "wrong lot" is not a bug the object model can express. The only nodes a `LotCard` can reach are descendants of the root it was constructed with. ## Before and after | | one page-scoped object | page object plus components | |---|---|---| | what a field lookup searches | the whole document | one card's subtree | | identifying a specific lot | encoded in the selector | encoded in which instance you hold | | a second lot appearing | silently changes the result | adds one more instance | | a card-level method | needs a lot argument every call | is just a method on the card | | where the ambiguity lives | in every accessor | in one call that finds the roots | ## `PageFactory` will not fill a `List<LotCard>` for you This is the mechanical detail that catches people mid-restructure. `DefaultFieldDecorator` decorates a `WebElement` field, and it decorates a list field only when both conditions hold: the list's generic argument is exactly `WebElement`, and the field carries `@FindBy`, `@FindBys` or `@FindAll`. Its `isDecoratableList` check compares the type argument with `WebElement.class` directly, so: - `@FindBy(css = "div.lot-card") private List<WebElement> cardRoots;` **is** decorated and works. - `@FindBy(css = "div.lot-card") private List<LotCard> cards;` is **not** decorated. The decorator returns `null`, `PageFactory` leaves the field untouched, and it stays `null` — no exception at `initElements`, just a `NullPointerException` later. So the mapping from roots to components is code you write. A `List<WebElement>` field of roots plus a small mapping method is the usual shape, and it keeps the re-query semantics of the proxied list: each access to the list field runs `findElements` again, which is what you want on a ledger that re-renders after an adjustment. ## What stays on the page object - **Finding the roots.** The one document-wide search is legitimate and belongs here, because it is the only place that should know how the ledger lays out its cards. - **Selection.** `lot(String lotId)` filtering the components, or `cards()` returning them all. - **Page-level actions** that are not part of any card, such as opening the add-stock dialog. - **Nothing about a card's internals** — the moment a `.expiry` selector appears on the page object, the boundary has leaked back. ## Checking the restructure actually holds - Seed **two** lots in the fixture, not one. A component model passes; a page-scoped model fails or reads the wrong values, and that is the regression test for the restructure itself. - Grep the page object for selectors that describe card internals; every one of them is a piece of the component that has not moved yet. - Assert on a card you deliberately place second, so a first-match bug cannot pass by luck. - Re-fetch the components after an action that re-renders the ledger, rather than holding instances across the change — each new instance gets a freshly located root. The last point is the one production cost of the pattern. A component instance is only as good as the root it holds, so a model that re-renders its list wants the components produced by a method call rather than cached in a field, so that every use starts from a current root.
- Why not just make the selector more specific instead of restructuring?It works for one assertion and pushes the problem into every other one. A card-specific selector has to encode the lot in the selector string at every call site, so each new field repeats the encoding. Holding a component instance carries that identity once, for every field the card has.
- Can a @FindBy-annotated List<LotCard> field be filled by PageFactory?No. `DefaultFieldDecorator` only decorates list fields whose generic argument is exactly `WebElement`, so a list of component types is skipped and left null — no error at initElements, just a failure at first use. Declare a `List<WebElement>` of roots and map it to components yourself.
- What breaks if the test holds component instances across a re-render of the ledger?Each component holds a root located before the re-render, so its lookups run against a node the document no longer uses. Produce components from a method that re-finds the roots rather than caching a list in a field, so every interaction starts from a current root.
saying these in an interview costs you the question
- Adds an index to the selector so it points at the second card instead.
- Keeps one page object and adds a method per lot id.
- Declares a List<LotCard> field with @FindBy and expects PageFactory to fill it.
- Searches the document, then filters the results inside the test.
- Assumes the ledger renders one card, so the page-wide lookup is safe.