skip to content

In Selenium, how does making each LoadableComponent's load() call its parent's get() change failure diagnosis?

level: seniorimportance: should knowfreq 27%

answer

  1. One line at the top of load()
  2. The parent is a field, not a superclass
  3. One call resolves the whole chain
  4. Each level checks before it acts
  5. Failure names the level that broke

basics

~20 s

Each screen holds its parent as a field and starts load() with parent.get(), so one call loads the whole chain top down. Each level checks its own condition, and a failure reports at the level that actually broke.

solid answer

~40 s

Nesting is composition, not inheritance: a hive-log screen object holds a `LoadableComponent<?> parent` field and begins its `load()` with `if (parent != null) parent.get();` before navigating. Calling `get()` on the leaf therefore drives the whole chain - apiary home, signed-in apiary, hive log - each level running its own `isLoaded()` first, so an ancestor that is already satisfied costs one cheap check and no navigation. The diagnostic payoff is attribution: when a session has quietly expired, the failure is the signed-in level's `AssertionError` naming a missing beekeeper menu, thrown from inside the hive log's `load()`, rather than a bare complaint that the hive-log table is missing. The stack of nested `get()` calls shows exactly how far the chain progressed before it stopped.

code

java · 31 lines
java
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.LoadableComponent;

public class SignedInApiary extends LoadableComponent<SignedInApiary> {

  private final WebDriver driver;
  private final LoadableComponent<?> parent;

  public SignedInApiary(WebDriver driver, LoadableComponent<?> parent) {
    this.driver = driver;
    this.parent = parent;
  }

  @Override
  protected void load() {
    if (parent != null) {
      parent.get();
    }
    driver.findElement(By.id("beekeeper-id")).sendKeys("hive-keeper");
    driver.findElement(By.id("passphrase")).sendKeys("smoker-and-veil");
    driver.findElement(By.cssSelector("button[type='submit']")).click();
  }

  @Override
  protected void isLoaded() throws Error {
    if (driver.findElements(By.id("beekeeper-menu")).isEmpty()) {
      throw new AssertionError("Not signed in: no beekeeper menu at " + driver.getCurrentUrl());
    }
  }
}

go deeper

for a junior

Know that a nested screen object calls parent.get() as the first statement of its own load(), so opening a deep screen from a test is a single call rather than a sequence of manual steps.

for a middle

Trace the call order out loud for a three-level chain, and explain why an ancestor that is already loaded costs one check and no navigation because get() always checks before it acts.

for a senior

Argue the diagnostic payoff: a failure names the level that could not be satisfied, so an expired session reports as a missing menu rather than as a missing table, and the get() frames show how far the chain got.

for a principal

Own the tradeoff at suite scale: how deep the tree should go, which conditions earn a level of their own, and how to keep per-call verification cost bounded as more tests hang off the same chain.

## The shape: a parent field and one line in load() Selenium's own design-strategies guidance models a site as a tree of components, each in its own class, where "the `load` method in each of them would `get` the parent". For an apiary application the tree is: ``` + ApiaryHome +---+ SignedInApiary +---+ HiveLogPage ``` Each class extends `LoadableComponent<T>` and holds its parent as a **field**, typically typed `LoadableComponent<?>` and injected through the constructor: ```java class HiveLogPage extends LoadableComponent<HiveLogPage> { private final WebDriver driver; private final LoadableComponent<?> parent; HiveLogPage(WebDriver driver, LoadableComponent<?> parent) { this.driver = driver; this.parent = parent; } @Override protected void load() { if (parent != null) { parent.get(); } driver.get("https://apiary.example/hives/17/log"); } @Override protected void isLoaded() throws Error { if (driver.findElements(By.id("hive-log-table")).isEmpty()) { throw new AssertionError("Hive log table not rendered at " + driver.getCurrentUrl()); } } } ``` The relationship is **a field, not a superclass**. Nothing is inherited from `SignedInApiary`; the hive log merely knows something has to be true before it can navigate. ## What one call to the leaf actually runs 1. `new HiveLogPage(driver, new SignedInApiary(driver, new ApiaryHome(driver))).get()` calls the hive log's `isLoaded()`. 2. The hive-log table is absent, so it throws, and `get()` calls the hive log's `load()`. 3. That `load()` first calls `parent.get()` on `SignedInApiary`, which runs **its** `isLoaded()` - "is the beekeeper menu present?" 4. If that fails, `SignedInApiary.load()` runs, which itself calls `ApiaryHome.get()` before signing in. 5. Control unwinds back down the stack: home is ready, sign-in completes, and only then does the hive log's own `driver.get(...)` fire. 6. Back in the hive log's `get()`, the second `isLoaded()` confirms the table. The test wrote one line. The chain resolved itself top down, and each level was **checked before it was acted on**. ## Where the failure surfaces This is the property worth defending in an interview. Because every level owns a check with its own message, a broken run names the level that broke: | What is really wrong | Level whose `isLoaded()` fails last | What the test reports | |---|---|---| | The app is down | `ApiaryHome` | the home screen's own message - no navigation is attempted below it | | Credentials rejected, session never established | `SignedInApiary` | a missing beekeeper menu, thrown from inside the hive log's `load()` | | Hive 17 returns no rows | `HiveLogPage` | the hive-log table message, after sign-in demonstrably succeeded | Without nesting, all three failures look the same from a test that only opens the hive log: the table is missing. The engineer then goes hunting on the wrong screen. With nesting, the exception is thrown at the level that could not be satisfied, and the surrounding `get()` frames on the stack record how far the chain progressed. ## What it costs, and how to keep the cost small - **Every level's `isLoaded()` runs on every `get()` of the leaf.** That is the price of the guarantee. Keep each check to a cheap DOM query - one presence check, not a page-wide scan. - **Checks must be side-effect free.** A readiness check that clicks something will fire repeatedly as the chain is re-entered from other tests. - **An already-satisfied ancestor is close to free.** Its `get()` runs one check and returns; `load()` is skipped, so nothing is re-navigated and no work in progress is destroyed. - **The chain is only as good as its weakest condition.** A check that passes on a page it should not - matching an element the whole site shares - lets the chain proceed and pushes the failure down to the next level, undoing the attribution. ## Traps to name before the interviewer does - **Cycles recurse forever.** If two components each name the other as parent, the mutual `get()` calls end in a `StackOverflowError`. The tree must be a tree. - **A shared instance is not a shared state.** Two different `HiveLogPage` objects constructed with two different parent chains will each re-verify from the top; the components hold no cross-instance memory of what is loaded. - **Do not call ancestors by hand.** `home.get(); signedIn.get(); log.get();` in the test body recreates in the test the exact ordering the chain already encodes, and it drifts the moment the tree changes. - **`load()` on a child is not the place for assertions about the parent.** The parent's `isLoaded()` owns that condition; duplicating it in the child produces two messages for one failure. - **Deep chains cost per call.** A five-level tree means five checks every time any test touches the leaf, so trees are worth keeping shallow and each level's condition worth keeping narrow.

  • What is the cost of re-verifying every ancestor on each call to the leaf's get()?
    One `isLoaded()` per level per call. Each is meant to be a single cheap DOM query, so a three-level chain adds three lookups - negligible next to a navigation. The cost only becomes real if a check does heavy work, which is why readiness conditions should stay narrow and side-effect free.
  • Why hold the parent as a field rather than making the child extend the parent class?
    Because the relationship is ordering, not identity: the hive log is not a kind of signed-in apiary, it merely requires one. A field also lets the same screen be reached from more than one chain, and it keeps the generic self-type of `LoadableComponent<T>` intact rather than tangling two subclass hierarchies.
  • What happens if two components name each other as parent?
    Their `load()` methods call each other's `get()` without a base case, so the recursion runs until the stack is exhausted and a `StackOverflowError` is thrown. The structure has to be an acyclic tree; if two screens genuinely require each other, one of the conditions belongs in a shared ancestor instead.

saying these in an interview costs you the question

  • Calls every ancestor's get() by hand in the test body
  • Thinks the parent must be a superclass of the child
  • Assumes an already-loaded ancestor is navigated to again
  • Duplicates the parent's readiness condition in the child
  • Lets two components name each other as parent