Waiting For Element Changes
ElementQuery repeatedly evaluates selectors and filters, including when you
need all matching elements to disappear. ElementWaiter watches an element you
already have until it reaches a particular state — visible, clickable, stale,
with certain text, and so on. Reach for it whenever you’ve clicked something
and need the held element to change before you continue.
#![allow(unused)]
fn main() {
let button = driver.query(By::Css(".save")).single().await?;
button.click().await?;
button.wait_until().not_displayed().await?;
}
wait_until() is available on every WebElement. It returns an
ElementWaiter that polls the element until either a predicate
matches or the timeout elapses.
Keep Interactions Explicit
For a fresh target, put readiness on a described query, choose cardinality, perform the action, and then query for the user-visible outcome:
#![allow(unused)]
fn main() {
use thirtyfour::prelude::*;
async fn save_settings(driver: &WebDriver) -> WebDriverResult<()> {
let save = driver
.query(By::Testid("settings-save"))
.and_clickable()
.desc("settings save button")
.single()
.await?;
save.click().await?;
driver
.query(By::Testid("settings-saved"))
.and_displayed()
.with_text("Settings saved")
.desc("settings saved confirmation")
.single()
.await?;
Ok(())
}
}
For an element you already resolved and intentionally want to keep, wait on that element before acting:
#![allow(unused)]
fn main() {
use thirtyfour::prelude::*;
async fn submit(save: WebElement) -> WebDriverResult<()> {
save.click_when_ready().await?;
Ok(())
}
}
click_when_ready() is the common two-step sequence
wait_until().clickable().await? followed by click().await. Keeping the
readiness helper on a resolved object preserves the selector and cardinality
choices made when that object was found.
Text entry has application-specific replacement semantics, so make the choice to clear or append visible in the code:
#![allow(unused)]
fn main() {
use thirtyfour::prelude::*;
async fn replace_email(driver: &WebDriver) -> WebDriverResult<()> {
let email = driver
.query(By::Testid("account-email"))
.and_displayed()
.and_enabled()
.desc("account email input")
.single()
.await?;
email.clear().await?;
email.send_keys("ada@example.test").await?;
Ok(())
}
}
Here, “clickable” means displayed and enabled. It cannot guarantee that an
overlay will not intercept the click, that the DOM will not replace the
element, or that page state will not change between the readiness check and the
action. A held element can become stale; re-query when re-rendering is expected,
or keep the selector in an ElementResolver
inside a Component.
Selector-taking helpers such as driver.click(selector) or
clear_and_type(selector, text) would hide cardinality, descriptions, timeout,
clearing, stale-element handling, and the expected outcome. A builder exposing
those choices would duplicate ElementQuery without providing a stronger
safety guarantee, so there is no selector-taking generic interaction API.
Resolved objects can still provide concise helpers for unambiguous common
sequences: WebElement::click_when_ready() and the same-named
ElementResolver<WebElement> methods keep their resolution behavior explicit
in the receiver type. Retrying clicks automatically can repeat a non-idempotent
action, so these helpers do not retry the click itself. Put repeated application
behavior in a Component intent method such as save_settings() or
submit_credentials(), where those choices and the outcome are known.
Built-In Predicates
State predicates polled directly via WebDriver:
| Method | Waits until the element is… |
|---|---|
.displayed().await? | rendered (isDisplayed returns true) |
.not_displayed().await? | hidden |
.enabled().await? | not disabled |
.not_enabled().await? | disabled |
.selected().await? | selected (checkboxes, options, radios) |
.not_selected().await? | deselected |
.clickable().await? | both displayed and enabled |
.not_clickable().await? | hidden or disabled |
.stale().await? | detached from the DOM |
.stale() is especially useful right after a click: it waits for the concrete
remote element you acted on to detach. It does not prove that navigation or the
next page has finished loading.
Text, Class, Attribute, Property Waits
Each of these takes a Needle (from the
stringmatch crate) — a plain
&str for exact match, or a StringMatch for partial /
case-insensitive / word-boundary matches.
| Method | Waits until… |
|---|---|
.has_text(needle) | the element’s text matches |
.lacks_text(needle) | the element’s text does not match |
.has_class("name") | the element’s class list contains it |
.lacks_class("name") | the class is no longer present |
.has_value(needle) | the input’s value matches |
.lacks_value(needle) | the input’s value no longer matches |
.has_attribute(name, needle) | a single attribute matches |
.lacks_attribute(name, needle) | a single attribute no longer matches |
.has_attributes([...]) | several attributes match together |
.lacks_attributes([...]) | none of those attributes match |
.has_property(name, needle) | a JS property matches |
.lacks_property(name, needle) | a JS property does not match |
.has_properties([...]) | several properties match together |
.lacks_properties([...]) | none of those properties match |
.has_css_property(name, needle) | a computed CSS property matches |
.lacks_css_property(name, needle) | a computed CSS property does not match |
.has_css_properties([...]) | several CSS properties match together |
.lacks_css_properties([...]) | none of those CSS properties match |
#![allow(unused)]
fn main() {
use thirtyfour::stringmatch::StringMatchable;
elem.wait_until()
.has_text("Order received".match_partial().case_insensitive())
.await?;
}
Custom Timeouts And Error Messages
Override the poll cadence on a single wait:
#![allow(unused)]
fn main() {
use std::time::Duration;
elem.wait_until()
.wait(Duration::from_secs(60), Duration::from_secs(1))
.clickable()
.await?;
}
Attach a custom error message so a timeout reads in plain English:
#![allow(unused)]
fn main() {
elem.wait_until()
.error("Timed out waiting for the spinner to disappear")
.stale()
.await?;
}
Custom Predicates
For anything the built-ins don’t cover, pass your own predicate. It
gets a &WebElement and returns WebDriverResult<bool>:
#![allow(unused)]
fn main() {
elem.wait_until()
.condition(|elem| async move {
let value = elem.value().await?.unwrap_or_default();
Ok(value.parse::<u32>().map_or(false, |n| n > 100))
})
.await?;
}
Pre-built predicate constructors live in the
thirtyfour::extensions::query::conditions
module. They share the same shape, so you can compose several into
one wait:
#![allow(unused)]
fn main() {
use thirtyfour::extensions::query::conditions;
elem.wait_until()
.conditions(vec![
conditions::element_is_displayed(true),
conditions::element_is_clickable(true),
])
.await?;
}
The conditions module is also useful as a source of filter functions
for ElementQuery::with_filter().
When To Reach For Which
- Looking for an element on the page? Use
ElementQuery. - Already have an element and waiting for it to change? Use
ElementWaiter(this chapter). - Waiting for an element to disappear? Use
query(...).wait_until_gone()when no element may match the complete query; useelem.wait_until().stale()when that specific resolved element must be detached. Usenot_exists()when you want the same query polling as a boolean instead of a timeout error.
The distinction matters during re-rendering. stale() succeeds when its one
remote element ID detaches, even if a replacement matches the old selector.
wait_until_gone() re-runs every selector and filter and keeps waiting while a
replacement matches. Neither wait implies that a navigation or the next page
has completed.
API Reference
For the full method lists, see
ElementQuery
and
ElementWaiter
on docs.rs.