com.nordstrom.automation.selenium.model.RobustElementFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of selenium3-foundation Show documentation
Show all versions of selenium3-foundation Show documentation
Selenium3 Foundation is an automation framework designed to extend and enhance the capabilities provided by Selenium 3.0 (WebDriver).
package com.nordstrom.automation.selenium.model;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.WebDriver.Timeouts;
import org.openqa.selenium.internal.FindsByCssSelector;
import org.openqa.selenium.internal.FindsByXPath;
import com.nordstrom.automation.selenium.SeleniumConfig.WaitType;
import com.nordstrom.automation.selenium.core.ByType;
import com.nordstrom.automation.selenium.core.JsUtility;
import com.nordstrom.automation.selenium.core.WebDriverUtils;
import com.nordstrom.automation.selenium.exceptions.OptionalElementNotAcquiredException;
import com.nordstrom.automation.selenium.interfaces.WrapsContext;
import com.nordstrom.automation.selenium.support.Coordinator;
import com.nordstrom.common.base.UncheckedThrow;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.MethodNameEqualityResolver;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.BindingPriority;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.TargetMethodAnnotationDrivenBinder;
import net.bytebuddy.implementation.bind.annotation.This;
import static net.bytebuddy.matcher.ElementMatchers.*;
/**
* This class contains the classes, methods, and interfaces used to wrap {@link WebElement} objects in a
* reference-refreshing shell.
*/
public final class RobustElementFactory {
private static Map creatorMap = new HashMap<>();
private RobustElementFactory() {
throw new AssertionError("RobustElementFactory is a static utility class that cannot be instantiated");
}
/**
* Basic robust web element builder.
*
* @param context element search context
* @param locator element locator
* @return robust web element
*/
public static WebElement makeRobustElement(WrapsContext context, By locator) {
return makeRobustElement(null, context, locator, RobustElementWrapper.CARDINAL);
}
/**
* Builder for wrapping an existing element reference.
*
* @param element element reference to be wrapped
* @param context element search context
* @param locator element locator
* @return robust web element
*/
public static WebElement makeRobustElement(WebElement element, WrapsContext context, By locator) {
return makeRobustElement(element, context, locator, RobustElementWrapper.CARDINAL);
}
/**
* Main robust web element builder.
*
* @param element element reference to be wrapped (may be 'null')
* @param context element search context
* @param locator element locator
* @param index element index
* @return robust web element
*/
public static WebElement makeRobustElement(WebElement element, WrapsContext context, By locator, int index) {
InstanceCreator creator = getCreator(context);
RobustElementWrapper interceptor = new RobustElementWrapper(element, context, locator, index);
WebElement robust = (WebElement) creator.makeInstance();
((InterceptionAccessor) robust).setInterceptor(interceptor);
return robust;
}
/**
* Get robust web element factory for this context.
*
* @param context target context
* @return robust web element factory
*/
private static synchronized InstanceCreator getCreator(WrapsContext context) {
WebDriver driver = context.getWrappedDriver();
String driverName = driver.getClass().getName();
if (creatorMap.containsKey(driverName)) {
return creatorMap.get(driverName);
}
WebElement reference = driver.findElement(By.tagName("*"));
Class extends WebElement> refClass = reference.getClass();
Class extends WebElement> wrapperClass = new ByteBuddy()
.subclass(refClass)
.name(refClass.getPackage().getName() + ".Robust" + refClass.getSimpleName())
.method(not(isDeclaredBy(Object.class)))
.intercept(MethodDelegation.withEmptyConfiguration()
.withBinders(TargetMethodAnnotationDrivenBinder.ParameterBinder.DEFAULTS)
.withResolvers(MethodNameEqualityResolver.INSTANCE, BindingPriority.Resolver.INSTANCE)
.filter(not(isDeclaredBy(Object.class)))
.toField("interceptor"))
.implement(RobustWebElement.class)
.defineField("interceptor", RobustElementWrapper.class, Visibility.PRIVATE)
.implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty())
.make()
.load(refClass.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
InstanceCreator creator;
try {
creator = new ByteBuddy()
.subclass(InstanceCreator.class)
.method(not(isDeclaredBy(Object.class)))
.intercept(MethodDelegation.toConstructor(wrapperClass))
.make()
.load(wrapperClass.getClassLoader())
.getLoaded().newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw UncheckedThrow.throwUnchecked(e);
}
creatorMap.put(driverName, creator);
return creator;
}
/**
* This interface defines accessor and mutator methods for element method interceptor.
*/
public interface InterceptionAccessor {
/**
* Get the {@link RobustElementWrapper} interceptor.
*
* @return RobustElementWrapper object
*/
RobustElementWrapper getInterceptor();
/**
* Set the {@link RobustElementWrapper} interceptor.
*
* @param interceptor RobustElementWrapper object
*/
void setInterceptor(RobustElementWrapper interceptor);
}
/**
* This interface defines the robust web element factory builder method.
*/
public interface InstanceCreator {
/**
* Make a new robust web element instance.
*
* @return robust web element
*/
Object makeInstance();
}
/**
* This class defines the generic interceptor for the methods of wrapped web element references. It also provides
* implementations for methods that acquire web element references and recover from StaleElementReferenceException
* failures.
*/
public static class RobustElementWrapper implements ReferenceFetcher {
/** wraps 1st matched reference */
public static final int CARDINAL = -1;
/** wraps an optional reference */
public static final int OPTIONAL = -2;
private static final String LOCATE_BY_CSS = JsUtility.getScriptResource("locateByCss.js");
private static final String LOCATE_BY_XPATH = JsUtility.getScriptResource("locateByXpath.js");
private enum Strategy { LOCATOR, JS_XPATH, JS_CSS }
private final WebDriver driver;
private final WrapsContext context;
private WebElement wrapped;
private By locator;
private int index;
private String selector;
private Strategy strategy = Strategy.LOCATOR;
private long acquiredAt;
private NoSuchElementException deferredException;
private final boolean findsByCssSelector;
private final boolean findsByXPath;
/**
* Main robust web element constructor
*
* @param element element reference to be wrapped (may be 'null')
* @param context element search context
* @param locator element locator
* @param index element index
*/
public RobustElementWrapper(WebElement element, WrapsContext context, By locator, int index) {
// if specified element is already robust
if (element instanceof RobustWebElement) {
RobustElementWrapper wrapper = ((InterceptionAccessor) element).getInterceptor();
this.acquiredAt = wrapper.acquiredAt;
this.wrapped = wrapper.wrapped;
this.context = wrapper.context;
this.locator = wrapper.locator;
this.index = wrapper.index;
} else {
Objects.requireNonNull(context, "[context] must be non-null");
Objects.requireNonNull(locator, "[locator] must be non-null");
if (index < OPTIONAL) {
throw new IndexOutOfBoundsException("Specified index is invalid");
}
this.wrapped = element;
this.context = context;
this.locator = locator;
this.index = index;
}
driver = WebDriverUtils.getDriver(this.context.getWrappedContext());
findsByCssSelector = (driver instanceof FindsByCssSelector);
findsByXPath = (driver instanceof FindsByXPath);
if ((this.index == OPTIONAL) || (this.index > 0)) {
if (findsByXPath && ( ! (this.locator instanceof By.ByCssSelector))) {
selector = ByType.xpathLocatorFor(this.locator);
if (this.index > 0) {
selector += "[" + (this.index + 1) + "]";
}
strategy = Strategy.JS_XPATH;
this.locator = By.xpath(this.selector);
} else if (findsByCssSelector) {
selector = ByType.cssLocatorFor(this.locator);
if (selector != null) {
strategy = Strategy.JS_CSS;
}
}
}
if (this.wrapped == null) {
if (this.index == OPTIONAL) {
acquireReference(this);
} else {
refreshReference(null);
}
} else if (acquiredAt == 0) {
acquiredAt = System.currentTimeMillis();
}
}
/**
* This is the method that intercepts component container methods in "enhanced" model objects.
*
* @param obj "enhanced" object upon which the method was invoked
* @param method {@link Method} object for the invoked method
* @param args method invocation arguments
* @return {@code anything} (the result of invoking the intercepted method)
* @throws Exception {@code anything} (exception thrown by the intercepted method)
*/
@RuntimeType
@BindingPriority(Integer.MAX_VALUE)
public Object intercept(@This Object obj, @Origin Method method, @AllArguments Object[] args) throws Exception
{
try {
return method.invoke(getWrappedElement(), args);
} catch (InvocationTargetException ite) {
Throwable t = ite.getCause();
if (t instanceof StaleElementReferenceException) {
try {
StaleElementReferenceException sere = (StaleElementReferenceException) t;
return method.invoke(refreshReference(sere).getWrappedElement(), args);
} catch (NullPointerException npe) {
throw deferredException();
}
}
throw UncheckedThrow.throwUnchecked(t);
}
}
/**
* {@inheritDoc}
*/
@Override
public WebElement getWrappedElement() {
if (wrapped == null) {
refreshReference(null);
}
return wrapped;
}
/**
* {@inheritDoc}
*/
public WebElement findOptional(By by) {
return getElement(this, by, OPTIONAL);
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasReference() {
if ((index == OPTIONAL) && (wrapped == null)) {
acquireReference(this);
return (null != wrapped);
} else {
return true;
}
}
/**
* {@inheritDoc}
*/
@Override
public WrapsContext getContext() {
return context;
}
/**
* {@inheritDoc}
*/
@Override
public By getLocator() {
return locator;
}
/**
* {@inheritDoc}
*/
@Override
public int getIndex() {
return index;
}
/**
* {@inheritDoc}
*/
@Override
public RobustElementWrapper refreshReference(final StaleElementReferenceException refreshTrigger) {
try {
WaitType.IMPLIED.getWait((SearchContext) context).until(referenceIsRefreshed(this));
return this;
} catch (TimeoutException e) {
if (refreshTrigger == null) {
throw UncheckedThrow.throwUnchecked(e.getCause());
}
} catch (WebDriverException e) {
if (refreshTrigger == null) {
throw e;
}
}
throw refreshTrigger;
}
/**
* Returns a 'wait' proxy that refreshes the wrapped reference of the specified robust element.
*
* @param wrapper robust element wrapper
* @return wrapped element reference (refreshed)
*/
private static Coordinator referenceIsRefreshed(final RobustElementWrapper wrapper) {
return new Coordinator() {
@Override
public RobustElementWrapper apply(SearchContext context) {
try {
return acquireReference(wrapper);
} catch (StaleElementReferenceException e) {
((WrapsContext) context).refreshContext(((WrapsContext) context).acquiredAt());
return acquireReference(wrapper);
}
}
@Override
public String toString() {
return "element reference to be refreshed";
}
};
}
/**
* Acquire the element reference that's wrapped by the specified robust element wrapper.
*
* @param wrapper robust element wrapper
* @return wrapped element reference
*/
private static RobustElementWrapper acquireReference(RobustElementWrapper wrapper) {
SearchContext context = wrapper.context.getWrappedContext();
if (wrapper.strategy == Strategy.LOCATOR) {
Timeouts timeouts = wrapper.driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
try {
if (wrapper.index > 0) {
List elements = context.findElements(wrapper.locator);
if (wrapper.index < elements.size()) {
wrapper.wrapped = elements.get(wrapper.index);
} else {
throw new NoSuchElementException(
String.format("Too few elements located %s: need: %d; have: %d",
wrapper.locator, wrapper.index + 1, elements.size()));
}
} else {
wrapper.wrapped = context.findElement(wrapper.locator);
}
} catch (NoSuchElementException e) {
if (wrapper.index != OPTIONAL) {
throw e;
}
wrapper.deferredException = e;
wrapper.wrapped = null;
} finally {
timeouts.implicitlyWait(WaitType.IMPLIED.getInterval(), TimeUnit.SECONDS);
}
} else {
List