Skip to content

Instantly share code, notes, and snippets.

@double16
Last active April 17, 2021 14:26
Show Gist options
  • Save double16/e3dfa7ec496264f11648 to your computer and use it in GitHub Desktop.
Save double16/e3dfa7ec496264f11648 to your computer and use it in GitHub Desktop.
Selenium document ready state settled condition

Waiting for a Redirect Chain to Settle using Selenium WebDriver

Selenium WebDriver is a great framework for automating browser usage in automated testing and scripting. Some interactions with the browser can be tricky. Recently I came across a case where the login page redirected several times before landing on an application page. This post will describe how I waited for the application to settle before continuing.

I needed to change the URL after login and the redirects were causing issues occasionally. The issue was definately related to timing. It's difficult to tell but I'm pretty sure after setting the URL using WebDriver the browser redirect took effect and nullified my URL change.

A typical solution, and simplier, than what is described here is to wait for an element on the target page to become available. Unfortunately, the application I was dealing with did not have a predictable landing page. It depends on the page the user last visited and the test data environment is such that I couldn't create a new user with known state.

My solution is to create an ExpectedCondition implementation that waits for the application to stabilize with respect to redirects. Conditions are used with Selenium's Wait interface to wait for a given condition to be "successful". A condition can return any object. If the object is a Boolean then a return value of Boolean.TRUE is considered success. Other classes will be considered a success when the return value is non-null.

The following example waits for the body element to become visible:

new FluentWait<WebDriver>(driver)
  .withTimeout(30, TimeUnit.SECONDS)
  .pollingEvery(2, TimeUnit.MILLISECONDS)
  .ignoring(WebDriverException.class)
  .until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("body"));

My DocumentSettleCondition checks two properties. The first is the document.readyState property to be complete for the stabilization period. The second is the value of WebDriver.getCurrentUrl() to not change during the stabilization period. DocumentSettleCondition wraps another condition. After the document is considered settled, the value of the wrapped condition is returned. Since we don't know what page we're going to land on, the wrapped condition will be something generic, like By.cssSelector("body") or an element common across the application, from the header, footer, etc.

DocumentSettleCondition settleCondition = new DocumentSettleCondition(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("body")));

new FluentWait<WebDriver>(driver)
  .withTimeout(30, TimeUnit.SECONDS)
  .pollingEvery(settleCondition.getSettleTime(), TimeUnit.MILLISECONDS)
  .ignoring(WebDriverException.class)
  .until(settleCondition);

In my particular case, there is a "forwarding to the application" page that can wait past the settle condition. Checking for the body tag won't work because the condition will settle on this page and send WebDriver the URL before the forwarding is done. So we can wait for this page not to be present for the settle time using the invisibilityOfElementLocated condition.

DocumentSettleCondition settleCondition = new DocumentSettleCondition(
  ExpectedConditions.invisibilityOfElementLocated(By.cssSelector("div.forwarding")));

new FluentWait<WebDriver>(driver)
  .withTimeout(30, TimeUnit.SECONDS)
  .pollingEvery(settleCondition.getSettleTime(), TimeUnit.MILLISECONDS)
  .ignoring(WebDriverException.class)
  .until(settleCondition);

If this seems like it's becoming complicated, I agree. Unfortunately that happens in web applications from time to time. If you have the ability to influence how the application is built, consider testing in the design. Sometimes you're bolting testing on after the fact. Hopefully the DocumentSettleCondition will get you a little further along the path to success.

import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
/**
* Licensed under MIT License
* Wraps a condition so that it returns only after the document state has settled for a given time, the default being 2 seconds. The
* document is considered settled when the "document.readyState" field stays "complete" and the URL in the browser stops changing.
*/
public class DocumentSettleCondition<T> implements ExpectedCondition<T> {
private final ExpectedCondition<T> condition;
private final long settleTimeInMillis;
private long lastComplete = 0L;
private String lastUrl;
public DocumentSettleCondition(ExpectedCondition<T> condition, long settleTimeInMillis) {
this.condition = condition;
this.settleTimeInMillis = settleTimeInMillis;
}
public DocumentSettleCondition(ExpectedCondition<T> condition) {
this(condition, 2000L);
}
/**
* Get the settle time in millis.
*/
public long getSettleTime() {
return settleTimeInMillis;
}
@Override
public T apply(WebDriver driver) {
if (driver instanceof JavascriptExecutor) {
String currentUrl = driver.getCurrentUrl();
String readyState = String.valueOf(((JavascriptExecutor) driver).executeScript("return document.readyState"));
boolean complete = readyState.equalsIgnoreCase("complete");
if (!complete) {
lastComplete = 0L;
return null;
}
if (lastUrl != null && !lastUrl.equals(currentUrl)) {
lastComplete = 0L;
}
lastUrl = currentUrl;
if (lastComplete == 0L) {
lastComplete = System.currentTimeMillis();
return null;
}
long settleTime = System.currentTimeMillis() - lastComplete;
if (settleTime < this.settleTimeInMillis) {
return null;
}
}
return condition.apply(driver);
}
@Override
public String toString() {
return "Document settle @" + settleTimeInMillis + "ms for " + condition;
}
}
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
/**
* Licensed under MIT License
* Wraps a condition so that it returns only after the document state has settled for a given time, the default being 2 seconds.
*/
public class DocumentSettleCondition<T> implements ExpectedCondition<T> {
private final ExpectedCondition<T> condition;
private final long settleTimeInMillis;
private long lastComplete = 0L;
private String lastUrl;
public DocumentSettleCondition(ExpectedCondition<T> condition, long settleTimeInMillis) {
this.condition = condition;
this.settleTimeInMillis = settleTimeInMillis;
}
public DocumentSettleCondition(ExpectedCondition<T> condition) {
this(condition, 2000L);
}
/**
* Get the settle time in millis.
*/
public long getSettleTime() {
return settleTimeInMillis;
}
@Override
public T apply(WebDriver driver) {
if (driver instanceof JavascriptExecutor) {
String currentUrl = driver.getCurrentUrl();
String readyState = String.valueOf(((JavascriptExecutor) driver).executeScript("return document.readyState"));
boolean complete = readyState.equalsIgnoreCase("complete");
if (!complete) {
lastComplete = 0L;
return null;
}
if (lastUrl != null && !lastUrl.equals(currentUrl)) {
lastComplete = 0L;
}
lastUrl = currentUrl;
if (lastComplete == 0L) {
lastComplete = System.currentTimeMillis();
return null;
}
long settleTime = System.currentTimeMillis() - lastComplete;
if (settleTime < this.settleTimeInMillis) {
return null;
}
}
return condition.apply(driver);
}
@Override
public String toString() {
return "Document settle @" + settleTimeInMillis + "ms for " + condition;
}
}
new FluentWait<WebDriver>(driver)
.withTimeout(30, TimeUnit.SECONDS)
.pollingEvery(2, TimeUnit.MILLISECONDS)
.ignoring(WebDriverException.class)
.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("body"));
DocumentSettleCondition settleCondition = new DocumentSettleCondition(
ExpectedConditions.visibilityOfElementLocated(By.cssSelector("body")));
new FluentWait<WebDriver>(driver)
.withTimeout(30, TimeUnit.SECONDS)
.pollingEvery(settleCondition.getSettleTime(), TimeUnit.MILLISECONDS)
.ignoring(WebDriverException.class)
.until(settleCondition);
DocumentSettleCondition settleCondition = new DocumentSettleCondition(
ExpectedConditions.invisibilityOfElementLocated(By.cssSelector("div.forwarding")));
new FluentWait<WebDriver>(driver)
.withTimeout(30, TimeUnit.SECONDS)
.pollingEvery(settleCondition.getSettleTime(), TimeUnit.MILLISECONDS)
.ignoring(WebDriverException.class)
.until(settleCondition);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment