skip to content

A Selenium relative locator chaining below() and toRightOf() returns nothing after a layout change — what do you check?

level: seniorimportance: should knowfreq 41%

answer

  1. Add the filters back one at a time
  2. Every filter subtracts, none restores
  3. Read both rectangles before anything else
  4. A locator anchor keeps only its first match
  5. Re-flow turns a row into a column

basics

~10 s

Bisect the chain: run the root locator alone, then with each filter added, to find which filter empties the set. Then compare the anchor and candidate rectangles, since the directional filters reject any overlap.

solid answer

~50 s

Chained filters are ANDed, so every link can only subtract and adding another can never rescue an empty result. Run the root `By` alone, then the root plus one filter, then the whole chain, and see where the count reaches zero. Then read the geometry: `getRect()` on the anchor and on the element you expected, remembering that `toRightOf` needs the candidate's `left` to be at or beyond the anchor's `left + width`, so a single pixel of overlap disqualifies it. Check what the anchor resolved to as well — a `By` anchor is resolved to its **first** match only, so on a panel with one card per lot you may be measuring against a different lot. Finally, remember the filters test rectangles only: no visibility check exists, and a re-flow that stacks the controls kills a horizontal filter outright.

code

java · 24 lines
java
import static org.openqa.selenium.support.locators.RelativeLocator.with;

import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.Rectangle;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

public class BidChainDiagnosis {

  public static void bisect(WebDriver driver) {
    By button = By.cssSelector(".lot-card button");
    By bidFigure = By.cssSelector(".lot-card .current-bid");
    By retract = By.id("retract-bid");

    List<WebElement> roots = driver.findElements(button);
    List<WebElement> afterFirst = driver.findElements(with(button).below(bidFigure));
    List<WebElement> afterBoth =
        driver.findElements(with(button).below(bidFigure).toRightOf(retract));

    Rectangle anchor = driver.findElement(bidFigure).getRect();
    System.out.println(roots.size() + " " + afterFirst.size() + " " + afterBoth.size() + " " + anchor);
  }
}

go deeper

for a junior

Recall that filters can be chained on a relative locator and that each one further restricts the result, so a chain that matches nothing has one link making a claim the page no longer satisfies.

for a middle

Be ready to explain that filters are ANDed, that a By anchor resolves to its first match only, and that the whole decision comes from rectangles read in a single script call.

for a senior

An interviewer expects a method: bisect the chain, print both rectangles, confirm which element the anchor resolved to, and tie the failure to the specific layout change rather than guessing.

for a principal

Own the diagnosis pattern rather than the individual fix. Be able to say what evidence a failure of this shape must produce so the next person does not have to rediscover the geometry by hand.

## What "returns nothing" actually means here A relative locator has no partial-credit mode. `driver.findElements` with a `RelativeBy` returns an empty list, and `driver.findElement` raises `NoSuchElementException` with a message starting "Cannot locate an element using". There is no warning that a filter was over-strict and no indication of which link in the chain removed the last candidate, so diagnosis is a bisection exercise on geometry rather than a reading of an error message. ## Chained filters are ANDed, so every link can only subtract `RelativeLocator.with(By).below(a).toRightOf(b)` builds one `RelativeBy` carrying a root locator and an ordered list of filters. The injected `findElements.js` atom resolves the root, then keeps a candidate only if **every** filter in the list accepts it. Two consequences follow immediately: - Adding a filter can never rescue an empty result. If `with(button).below(bidFigure)` is already empty, no further call will produce a match. - The chain's order does not change *which* elements match, but it does change *which one is first*: the returned list is sorted by proximity to the **last** filter's anchor, so reversing two filters can change what `findElement` gives you while the matched set is identical. ## The four things to check, in order 1. **Bisect the chain.** Run the root `By` on its own, then the root plus the first filter, then the whole chain, and compare the sizes. The step where the count falls to zero is the filter that is wrong about the layout. 2. **Compare the rectangles by hand.** Call `getRect()` on the anchor and on the candidate you expected. The directional filters are strict, non-overlapping edge comparisons — `toRightOf` needs the candidate's `left` to be at or beyond the anchor's `left + width` — so a single pixel of overlap disqualifies a candidate that plainly looks correct on screen. 3. **Check what the anchor resolved to.** When the anchor is a `By` rather than a `WebElement`, the atom resolves it and takes the **first match only**; the rest are ignored. On a bidding panel with one `.lot-card` per lot, `toRightOf(By.cssSelector(".lot-card .retract"))` silently anchors on lot one's retract button, so the geometry you are reasoning about belongs to a different lot. If the anchor locator matches nothing at all, the atom raises a `no such element` error from inside the script rather than returning an empty list. 4. **Check the layout, not the code.** The filters read `getBoundingClientRect()`, so anything that changes rendering changes the answer: a responsive breakpoint that stacks the bid controls under the figure kills `toRightOf` outright, a wrapped line makes an element's box taller than the visible text, and a collapsed element reports a zero-size rectangle at the origin. ## Geometry is a snapshot, and the filter is not a visibility filter Two properties of the mechanism regularly get mistaken for bugs. | Assumption | What actually happens | |---|---| | The lookup settles once the layout is stable | The whole match is decided inside one `executeScript` call, from the rectangles as they are at that instant | | Hidden elements are skipped | No visibility or interactability test exists in the atom; a candidate is judged purely on its rectangle | The second one bites hardest on an auction panel that keeps a hidden "Retract bid" button in the markup for every lot. It has a rectangle, so it is a candidate like any other — and if it is collapsed to zero size at the document origin, it will sit *above* and *to the left of* almost everything on the page. ## A worked failure on the bidding panel The suite addresses the **Place bid** button as `with(By.cssSelector(".lot-card button")).below(By.cssSelector(".current-bid")).toRightOf(By.id("retract-bid"))`. It matched while the panel rendered as a wide row and returns nothing after the card was re-laid out into two columns. Bisecting shows the root matching four buttons, `below(...)` leaving two, and `toRightOf(...)` leaving none. Printing the rectangles shows why: **Place bid** now renders directly under **Retract**, so its `left` is no longer beyond `retract-bid`'s right edge. The filter is not broken and the button is not missing; the spatial claim the locator makes is simply no longer true of the rendered page. ## What the evidence should tell you - A zero at the root means the root locator is wrong, and no proximity reasoning is involved yet. - A zero at the first filter means the axis or the anchor is wrong. - A zero only at the last filter means the chain over-constrains — often two filters that cannot both hold once the layout re-flows. - A non-empty but wrong result means the ordering surprised you: check which anchor is last in the chain, since that is the one the sort is measured from.

  • The matched set is right but findElement returns the wrong element. What changed?
    The ordering, not the filtering. Results are sorted by proximity to the anchor of the **last** filter in the chain, so swapping two filters leaves the same matches but reorders them and changes what element zero is. Put the filter whose anchor you want to measure distance from at the end of the chain.
  • Why can a hidden element still affect a relative lookup?
    Because the filters read rectangles and apply no visibility or interactability test. A collapsed element reports a zero-size rectangle at the document origin, which places it above and to the left of nearly everything, so it can be returned as the closest match or can pass a filter you expected it to fail.
  • What happens when the anchor locator itself matches no element?
    The injected script raises a `no such element` error from inside the page rather than returning an empty list, so the failure surfaces as a script error rather than as an ordinary empty result. That difference is a useful signal: it tells you the anchor is wrong rather than the geometry.

saying these in an interview costs you the question

  • Assumes chained relative filters are ORed together
  • Thinks a By anchor considers all of its matches
  • Expects the filters to skip hidden or collapsed elements
  • Believes filter order cannot change which element is returned
  • Blames the browser instead of comparing the rendered rectangles