package net.thucydides.core.annotations.locators;
import com.google.common.collect.Lists;
import net.thucydides.core.steps.StepEventBus;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.Clock;
import org.openqa.selenium.support.ui.SlowLoadableComponent;
import org.openqa.selenium.support.ui.SystemClock;
import java.lang.reflect.Field;
import java.util.List;
public class SmartAjaxElementLocator extends SmartElementLocator {
protected final int timeOutInSeconds;
private final Clock clock;
private final Field field;
private final WebDriver driver;
/**
* Main constructor.
*
* @param driver The WebDriver to use when locating elements
* @param field The field representing this element
* @param timeOutInSeconds How long to wait for the element to appear. Measured in seconds.
*/
public SmartAjaxElementLocator(WebDriver driver, Field field, int timeOutInSeconds) {
this(new SystemClock(), driver, field, timeOutInSeconds);
}
public SmartAjaxElementLocator(Clock clock, WebDriver driver, Field field, int timeOutInSeconds) {
super(driver, field);
this.timeOutInSeconds = timeOutInSeconds;
this.clock = clock;
this.field = field;
this.driver = driver;
}
@Override
public WebElement findElement() {
if (shouldFindElementImmediately()) {
return findElementImmediately();
} else {
return ajaxFindElement();
}
}
private boolean shouldFindElementImmediately() {
return aPreviousStepHasFailed() || (calledFromAQuickMethod());
}
private boolean calledFromAQuickMethod() {
for (StackTraceElement elt : Thread.currentThread().getStackTrace()) {
//if (QUICK_METHODS.contains(elt.getMethodName())
if (elt.getMethodName().contains("Currently")) {
return true;
}
}
return false;
}
public WebElement findElementImmediately() {
SmartAnnotations annotations = new SmartAnnotations(field);
By by = annotations.buildBy();
WebElement element = driver.findElement(by);
if (element == null) {
throw new NoSuchElementException("No such element found for criteria " + by.toString());
}
return element;
}
protected boolean isElementUsable(WebElement element) {
return (element != null) && (element.isDisplayed());
}
/**
* Will poll the interface on a regular basis until the element is present.
*/
public WebElement ajaxFindElement() {
SlowLoadingElement loadingElement = new SlowLoadingElement(clock, timeOutInSeconds);
try {
return loadingElement.get().getElement();
} catch (NoSuchElementError e) {
throw new NoSuchElementException(
String.format("Timed out after %d seconds. %s", timeOutInSeconds, e.getMessage()),
e.getCause());
}
}
private final static List<WebElement> EMPTY_LIST_OF_WEBELEMENTS = Lists.newArrayList();
/**
* Will poll the interface on a regular basis until at least one element is present.
*/
public List<WebElement> findElements() {
if (aPreviousStepHasFailed()) {
return EMPTY_LIST_OF_WEBELEMENTS;
}
SlowLoadingElementList list = new SlowLoadingElementList(clock, timeOutInSeconds);
try {
return list.get().getElements();
} catch (NoSuchElementError e) {
throw new NoSuchElementException(
String.format("Timed out after %d seconds. %s", timeOutInSeconds, e.getMessage()),
e.getCause());
}
}
private boolean aPreviousStepHasFailed() {
return (StepEventBus.getEventBus().aStepInTheCurrentTestHasFailed());
}
/**
* By default, we sleep for 250ms between polls. You may override this method in order to change
* how it sleeps.
*
* @return Duration to sleep in milliseconds
*/
protected long sleepFor() {
return 250;
}
private class SlowLoadingElement extends SlowLoadableComponent<SlowLoadingElement> {
private NoSuchElementException lastException;
private WebElement element;
public SlowLoadingElement(Clock clock, int timeOutInSeconds) {
super(clock, timeOutInSeconds);
}
@Override
protected void load() {
// Does nothing
}
@Override
protected long sleepFor() {
return SmartAjaxElementLocator.this.sleepFor();
}
@Override
protected void isLoaded() throws Error {
try {
element = SmartAjaxElementLocator.super.findElement();
if (!isElementUsable(element)) {
throw new NoSuchElementException("Element is not usable " + element.toString());
}
} catch (NoSuchElementException e) {
lastException = e;
// Should use JUnit's AssertionError, but it may not be present
throw new NoSuchElementError("Unable to locate the element: " + e.getMessage(), e);
}
}
public NoSuchElementException getLastException() {
return lastException;
}
public WebElement getElement() {
return element;
}
}
private class SlowLoadingElementList extends SlowLoadableComponent<SlowLoadingElementList> {
private NoSuchElementException lastException;
private List<WebElement> elements;
public SlowLoadingElementList(Clock clock, int timeOutInSeconds) {
super(clock, timeOutInSeconds);
}
@Override
protected void load() {
// Does nothing
}
@Override
protected long sleepFor() {
return SmartAjaxElementLocator.this.sleepFor();
}
@Override
protected void isLoaded() throws Error {
try {
elements = SmartAjaxElementLocator.super.findElements();
if (elements.size() == 0) {
/*return even if empty and don't wait for them to become available.
*not sure that it is the correct approach for Ajax Element Locator that should wait for elements
*however correcting it due to https://java.net/jira/browse/THUCYDIDES-187 */
return;
}
for (WebElement element : elements) {
if (!isElementUsable(element)) {
throw new NoSuchElementException("Element is not usable");
}
}
} catch (NoSuchElementException e) {
lastException = e;
// Should use JUnit's AssertionError, but it may not be present
throw new NoSuchElementError("Unable to locate the element " + e.getMessage(), e);
}
}
public NoSuchElementException getLastException() {
return lastException;
}
public List<WebElement> getElements() {
return elements;
}
}
private static class NoSuchElementError extends Error {
private NoSuchElementError(String message, Throwable throwable) {
super(message, throwable);
}
}
@Override
public String toString() {
SmartAnnotations annotations = new SmartAnnotations(field);
By by = annotations.buildBy();
return by.toString();
}
}