Why must a custom condition passed to Selenium's WebDriverWait.until be free of side effects?
answer
- The body is a loop, not one call
- Roughly twenty runs in a ten-second wait
- Selenium documents conditions as idempotent
- Observation versus mutation, not cheap versus slow
- A click inside fires on every poll
basics
~20 sThe condition is re-evaluated on every poll until it succeeds or times out, so any action inside it runs many times over. Selenium documents conditions as idempotent: keep the body to reads and act after until returns.
solid answer
~40 sIn Selenium 4, `WebDriverWait` applies your `Function<WebDriver, T>` once immediately and then again after each polling interval — 500 ms by default — until it returns something that is neither `null` nor `false`, or the timeout expires. A ten-second wait therefore runs the body about twenty times. Selenium's own `ExpectedCondition` source says conditions are expected to be **idempotent**, and that modifying the application's state inside one has unexpected side effects. A condition that clicks a button, types into a field or increments a counter performs that action once per poll: on a plant-nursery order form, waiting for the confirmation banner while clicking 'Place order' inside the condition places several orders and still reports green. Keep the body to reads such as `findElements`, `getText` and `isDisplayed`, and perform actions before or after `until`.
go deeper
Remember the shape: do the action first, then wait, and let the condition only look at the page. If you find yourself clicking inside the lambda, move that click above the wait.
Be ready to explain the loop: the condition is applied immediately, then after every polling interval, until it returns a non-null non-false value or the deadline passes. Work out aloud how many times that is.
Show that you can find this in a real suite. Name the symptom, duplicate submissions from a test that still reports green, and explain why no assertion catches it and why the report looks clean.
Own it as a review rule rather than a war story. Decide how the team keeps readiness predicates read-only, where deliberate retries are allowed to live instead, and how that boundary is enforced consistently.
## What `until` actually does with your function `WebDriverWait` is a subclass of `FluentWait<WebDriver>`, and `FluentWait.until` is a **loop**, not a single call. You hand it a function; the wait owns how often that function runs. One call to `until` does this: 1. **Apply the condition to the driver immediately.** The deadline is only checked *after* an evaluation, so the body always runs at least once — even with a zero timeout. 2. **Inspect the result.** Anything that is neither `null` nor `Boolean.FALSE` is returned from `until` and the loop ends. 3. **Check the clock.** If the deadline has passed, throw `TimeoutException`. 4. **Sleep the polling interval and go back to step 1.** In Selenium 4 you build the wait as `new WebDriverWait(driver, Duration.ofSeconds(10))`, and the default gap between evaluations is 500 ms. So the body of your lambda is not "the check" — it is "the check, repeated roughly twenty times" for that ten-second wait. ## Why repetition turns a side effect into a defect Selenium states the rule in the `ExpectedCondition` source itself: conditions **are expected to be idempotent**, they are called in a loop by `WebDriverWait`, and any modification of the state of the application under test may have unexpected side effects. **Idempotent** here means running the body once and running it twenty times leave the page in the same state. On a **plant-nursery order form**, conditions that break the rule look ordinary in review: - A condition that **clicks "Refresh stock"** before reading the stock badge sends one request per poll instead of one per test step. - A condition that **types into the quantity box** to nudge the subtotal into recalculating leaves the field holding every poll's keystrokes concatenated. - A condition that **re-selects the delivery week** and then checks whether the price changed: whichever poll ran last decides what the form holds. - A condition that **increments a test-side counter** measures how many polls happened, not how many application events happened. - A condition that **appends to a list the assertion later reads** puts one entry in that list per poll, not per observed change. - A condition that **clicks "Place order"** and waits for the confirmation banner places the order every 500 ms until the banner appears — and then passes. The last one is the reason interviewers ask. The test is green, the nursery has six orders, and the defect is invisible in the report. ## Reads that are safe, writes that are not | Safe inside a condition | Unsafe inside a condition | |---|---| | `driver.findElements(By.cssSelector(...))` | `element.click()` | | `element.getText()`, `isDisplayed()`, `isEnabled()` | `element.sendKeys(...)` | | `element.getDomAttribute(...)`, `getDomProperty(...)` | `element.clear()`, `element.submit()` | | `driver.getCurrentUrl()`, `driver.getTitle()` | `driver.navigate().refresh()` | | a script that only reads page state | a script that mutates the DOM or posts to an API | The dividing line is not "cheap versus expensive" — it is **observation versus mutation**. A read that is slow makes the poll slow; a write inside the poll makes the test lie. ## The act-wait-act shape The correct structure keeps every action outside the loop: ```java driver.findElement(By.id("place-order")).click(); WebElement banner = new WebDriverWait(driver, Duration.ofSeconds(10)) .until(d -> { WebElement el = d.findElement(By.cssSelector("#order-confirmation")); return el.isDisplayed() ? el : null; }); banner.findElement(By.cssSelector(".order-number")).getText(); ``` Act, then wait on a read-only condition, then act on what the wait handed back. If the retry itself is genuinely part of the behaviour under test — a flaky "Add plant" button that legitimately needs a second press — that belongs in an explicit, deliberate retry the reader can see, not smuggled into a readiness predicate. ## What an interviewer is listening for - That you know the body runs **on every poll**, not once, and can say roughly how many times for a given timeout. - That you name **idempotence** as the contract rather than describing it vaguely as "don't do weird things". - That you can spot the click-inside-the-condition bug in a snippet and explain why the test still passes. - That you put the action **before** `until` and use the value `until` returns for the next step, rather than re-finding it afterwards. - That you treat a counter or a log written inside the condition as measuring polls, which makes any assertion over it meaningless.
- Your test genuinely needs to press a flaky button until it takes effect. Where does that retry belong?In an explicit retry loop the reader can see, not inside a readiness predicate. Write a loop that presses the button, then calls a short read-only wait for the effect, and repeats a bounded number of times. Keeping the action visible means the count of attempts is a deliberate choice rather than an accident of the polling interval.
- Is a slow read inside the condition, such as fetching every row of the order table, also a defect?It is a cost, not a correctness bug. A read that takes longer than the polling interval stretches the effective gap between evaluations, because the interval is added after the condition returns rather than being a fixed cadence. The test can then overshoot its timeout, but nothing in the application changes, so the result stays trustworthy.
- How would you catch a side-effecting condition in code review?Scan the lambda body for anything that is not a read: `click`, `sendKeys`, `clear`, `submit`, `navigate`, a script that assigns rather than returns, or a write to a variable outside the lambda. Any of those means the number of times it happens is decided by the polling loop, which is never what the author intended.
A wait condition is a gardener checking whether a seedling has come up. Lifting it out of the soil each morning to look at the roots answers the question and ruins the answer at the same time.
saying these in an interview costs you the question
- Thinks the condition body runs only once per until call
- Puts a click inside the condition so the wait retries it
- Believes Selenium deduplicates repeated actions during a poll
- Counts polls in a variable and asserts on that count
- Assumes a green test proves nothing extra was submitted