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").textdriver.find_element(By.ID, "apply-coupon").click()after = wait_for_text_change(driver, (By.ID, "total"), before)assert after != beforeasync 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.