skip to content

How does Selenium's SlowLoadableComponent change the get() contract for an asynchronous screen?

level: middleimportance: should knowfreq 32%

answer

  1. For screens still settling after navigation
  2. A clock, a deadline and a sleep
  3. The loop re-checks but never re-navigates
  4. Two hundred milliseconds between polls
  5. A hook to abandon the wait early

basics

~20 s

It overrides get() so that after load() returns it polls isLoaded() against a deadline built from an injected Clock and Duration, sleeping 200 ms between checks and consulting an isError() hook, before one final unguarded check.

solid answer

~40 s

`SlowLoadableComponent<T>` extends `LoadableComponent<T>` for screens that are not finished when `load()` returns. Its constructor takes a `java.time.Clock` and a `java.time.Duration`, and its overridden `get()` keeps the same opening move - check, and `load()` only if the check throws - then computes `Instant end = clock.instant().plus(timeOut)` and loops while the clock is before that instant: try `isLoaded()` and return on success, swallow the `Error` on failure, call the overridable `isError()` hook, then sleep `sleepFor()` milliseconds, which defaults to `200`. After the deadline it runs `isLoaded()` once more, unguarded, so a timeout surfaces as **your** `Error`, not as a dedicated timeout type. Injecting the `Clock` is what makes the timeout testable without real waiting.

code

java · 39 lines
java
import java.time.Clock;
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.SlowLoadableComponent;

public class HiveLogPage extends SlowLoadableComponent<HiveLogPage> {

  private final WebDriver driver;

  public HiveLogPage(WebDriver driver) {
    super(Clock.systemDefaultZone(), Duration.ofSeconds(10));
    this.driver = driver;
  }

  @Override
  protected void load() {
    driver.get("https://apiary.example/hives/17/log");
  }

  @Override
  protected void isLoaded() throws Error {
    if (driver.findElements(By.cssSelector("#hive-log-table tbody tr")).isEmpty()) {
      throw new AssertionError("No inspection rows in the hive log yet");
    }
  }

  @Override
  protected void isError() throws Error {
    if (!driver.findElements(By.cssSelector("[data-error='hive-log']")).isEmpty()) {
      throw new AssertionError("Hive log reported an error instead of loading");
    }
  }

  @Override
  protected long sleepFor() {
    return 500;
  }
}

go deeper

for a junior

Know that this subclass exists for screens that finish rendering after navigation, and that it takes a clock and a timeout. Recognising the class name and its purpose is enough at this level.

for a middle

Walk the loop out loud: load once, compute the deadline, poll and sleep, then one final unguarded check. Be able to say where the poll interval and the early-abort hook live.

for a senior

Demonstrate the operational consequences: a timeout reports your own assertion message, so weak messages cost debugging time, and the isError() hook is what stops a broken screen burning the whole budget.

for a principal

Take a view on readiness budgets living inside screen objects at all - who owns the numbers, how they are kept consistent across a suite, and whether a hand-rolled guard would serve the codebase better.

## What problem it solves The base `LoadableComponent` assumes `load()` finishes the job: it re-checks readiness exactly once, immediately. A hive-log table that is fetched after navigation - rows arriving from the server while the shell is already painted - fails that single re-check and throws. `SlowLoadableComponent<T extends LoadableComponent<T>>` extends `LoadableComponent<T>` for exactly that case. Its own javadoc describes it as a component "which might not have finished loading when `load()` returns", where `isLoaded()` should **keep failing** until the screen is genuinely ready. ## The constructor and the two knobs ```java public SlowLoadableComponent(Clock clock, Duration timeOut) ``` - **`clock`** is a `java.time.Clock`. Every time reading in the loop goes through it, so a unit test can drive a fake ticking clock and exercise the timeout without sleeping in real time. Production code passes `Clock.systemDefaultZone()`. - **`timeOut`** is a `java.time.Duration` - how long the component may keep polling after `load()` has returned. - **`sleepFor()`** is a `protected long` method returning `200`, the pause in milliseconds between polls. It is not a constructor argument; you change it by overriding the method. - **`isError()`** is a `protected void` hook that does nothing by default. Override it to throw an `Error` when a **known failure state** appears, and the wait is abandoned at once. ## What the overridden get() executes 1. Call `isLoaded()`; if it returns, the screen is already there, so return immediately - `load()` never runs. 2. If it throws an `Error`, call `load()` **once**. Only once, for the whole call. 3. Compute the deadline: `Instant end = clock.instant().plus(timeOut)`. 4. While `clock.instant().isBefore(end)`: try `isLoaded()` and return on success; on `Error`, discard it, call `isError()`, then sleep `sleepFor()` milliseconds. 5. Once the deadline has passed, call `isLoaded()` one last time, **unguarded**, and return. Three consequences follow from the ordering, and each is a common interview probe: - **The deadline starts after `load()` returns.** Navigation time is not inside the budget; the `Duration` measures only the settling period afterwards. - **`load()` is never repeated.** The loop re-checks; it does not re-navigate. A hive log that needs a second navigation is a different problem. - **A timeout is not a distinct type.** Step 5 rethrows whatever your `isLoaded()` throws, so the failure a test sees at the deadline is the same `AssertionError` message it would have seen without any waiting - which is precisely why that message should name the screen and the missing condition. ## Base versus slow, side by side | Aspect | `LoadableComponent` | `SlowLoadableComponent` | |---|---|---| | Construction | no arguments | `(Clock clock, Duration timeOut)` | | Checks after `load()` | exactly one | polls until the deadline, then one final check | | Pause between checks | none | `sleepFor()`, `200` ms by default, overridable | | Early abort hook | none | `isError()`, a no-op unless overridden | | Failure at the end | the `Error` from `isLoaded()` | the `Error` from `isLoaded()`, or from `isError()` | | Time source | none | the injected `Clock` | ## The isError() hook in practice The hook exists because "not ready yet" and "definitely broken" look identical to a check that only knows how to throw. On a hive-log screen the polling condition might be "the table has at least one inspection row", and the known failure might be an error banner saying the hive record could not be read. Without the hook the component would sit there sleeping for the whole `Duration` before failing. With it: ```java @Override protected void isError() throws Error { if (!driver.findElements(By.cssSelector("[data-error='hive-log']")).isEmpty()) { throw new AssertionError("Hive log reported an error instead of loading"); } } ``` `isError()` is called **inside** the loop, right after a failed readiness check and before the sleep, and the `Error` it throws is **not** caught - so it terminates `get()` immediately with a message that names the real problem instead of a generic timeout. ## Practical notes - The sleep is a plain `Thread.sleep`. If the thread is interrupted, the component restores the interrupt flag and throws an `AssertionError`, so an interrupted wait fails loudly rather than silently continuing. - Because the poll interval is a method rather than a field, a component that needs a gentler cadence overrides `sleepFor()` to return a larger number of milliseconds. - Keep `isLoaded()` cheap and side-effect free: on a slow screen it runs many times per `get()` call. - This class is part of the Java support library (`org.openqa.selenium.support.ui`). The .NET port has a `SlowLoadableComponent<T>` too, with an `IClock`, a `TimeSpan` timeout, a `SleepInterval` property defaulting to 200 ms and a `HandleErrors()` hook; the other bindings ship no equivalent.

  • Why does the constructor take a Clock instead of reading the system time directly?
    So the timeout is testable. Every reading in the loop goes through `clock.instant()`, which lets a unit test advance a fake clock and prove that the component gives up at the deadline without waiting in real time. Selenium's own tests for the class do exactly that; production code passes `Clock.systemDefaultZone()`.
  • What does a test see when the Duration expires and the screen never loaded?
    The `Error` thrown by its own `isLoaded()`. After the loop, `get()` calls the check one final time outside any try block, so the failure is the same `AssertionError` and message the check always throws. There is no dedicated timeout exception type, which is why that message must be specific.
  • How would you change the poll interval from its default?
    Override `sleepFor()`, which is a `protected long` returning `200` milliseconds. It is a method rather than a constructor argument or a field, so a subclass returning `500` polls every half second. Nothing in the constructor exposes the interval.

saying these in an interview costs you the question

  • Thinks SlowLoadableComponent calls load() again on every poll
  • Expects a dedicated timeout exception when the Duration expires
  • Believes the timeout window also covers the load() navigation
  • Says the poll interval is a constructor argument
  • Treats isError() as something called only at the deadline