Skip to main content
SeleniumDecoded

Wait for Text to Change and Stabilise

Wait until an element's text differs from its previous value and stops changing, for counters, totals and live-updating fields.

Stable Verified against Selenium 4.48.0

Problem

After applying a coupon, the total animates from $99.00 down to $89.00 over half a second. Waiting for “$89.00” works if you know the value; often you do not, and reading immediately after the click returns the old value or a mid-animation one.

Solution

Capture the value before the action, wait for it to change, then wait for two consecutive reads to match.

Changed-then-stable wait
Selenium 4 Stable
public static String waitForTextChange(WebDriver driver, By by, String previous, Duration timeout) {
WebDriverWait wait = new WebDriverWait(driver, timeout);
wait.ignoring(StaleElementReferenceException.class);
// 1. Wait until it differs from the previous value
wait.until(d -> !d.findElement(by).getText().equals(previous));
// 2. Wait until two consecutive polls agree
String[] last = { null };
return wait.until(d -> {
String now = d.findElement(by).getText();
boolean stable = now.equals(last[0]);
last[0] = now;
return stable ? now : null;
});
}
String before = driver.findElement(By.id("total")).getText();
driver.findElement(By.id("apply-coupon")).click();
String after = waitForTextChange(driver, By.id("total"), before, Duration.ofSeconds(10));
assertNotEquals(before, after);
def wait_for_text_change(driver, locator, previous, timeout=10):
wait = WebDriverWait(driver, timeout, poll_frequency=0.2,
ignored_exceptions=(StaleElementReferenceException,))
wait.until(lambda d: d.find_element(*locator).text != previous)
last = [None]
def stable(d):
now = d.find_element(*locator).text
ok = now == last[0]
last[0] = now
return now if ok else False
return wait.until(stable)
before = driver.find_element(By.ID, "total").text
driver.find_element(By.ID, "apply-coupon").click()
after = wait_for_text_change(driver, (By.ID, "total"), before)
assert after != before
async function waitForTextChange(driver, locator, previous, timeout = 10000) {
const read = async () => driver.findElement(locator).getText();
await driver.wait(async () => (await read()) !== previous, timeout);
let last;
return driver.wait(async () => {
const now = await read();
const stable = now === last;
last = now;
return stable ? now : null;
}, timeout);
}
const before = await driver.findElement(By.id('total')).getText();
await driver.findElement(By.id('apply-coupon')).click();
const after = await waitForTextChange(driver, By.id('total'), before);
public static string WaitForTextChange(IWebDriver driver, By by, string previous, int seconds = 10)
{
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(seconds)) { PollingInterval = TimeSpan.FromMilliseconds(200) };
wait.IgnoreExceptionTypes(typeof(StaleElementReferenceException));
wait.Until(d => d.FindElement(by).Text != previous);
string? last = null;
return wait.Until(d =>
{
var now = d.FindElement(by).Text;
var stable = now == last;
last = now;
return stable ? now : null;
})!;
}
var before = driver.FindElement(By.Id("total")).Text;
driver.FindElement(By.Id("apply-coupon")).Click();
var after = WaitForTextChange(driver, By.Id("total"), before);

Why It Works

The first wait guarantees the action took effect. The second guarantees the animation or streaming update has finished, because two polls 200 ms apart returned the same value. Together they cover unknown target values and multi-step updates.

Gotchas

  • If the new value can legitimately equal the old one (applying a zero-discount coupon), wait on a different signal, such as a success message.
  • Very slow animations (over the polling interval) may produce two equal reads mid-animation. Increase the polling interval or add a third read for those.

Learn the theory