![JAR search and dependency download from the Maven repository](/logo.png)
org.bithill.selenium.resolving.Resolvable Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aport Show documentation
Show all versions of aport Show documentation
web UI testing made easier
package org.bithill.selenium.resolving;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bithill.selenium.driver.WebDriverHandle;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
import static java.util.regex.Pattern.quote;
import static org.bithill.selenium.resolving.FieldOfResolvable.isContainerResolvable;
import static org.bithill.selenium.resolving.FieldOfResolvable.isGroupMember;
import static org.bithill.selenium.resolving.FieldOfResolvable.isList;
import static org.bithill.selenium.resolving.FieldOfResolvable.isNullableAnnotated;
import static org.bithill.selenium.resolving.FieldOfResolvable.isResolvable;
import static org.bithill.selenium.resolving.FieldOfResolvable.isResolvableList;
import static org.bithill.selenium.resolving.FieldOfResolvable.isResolvedByAnnotated;
import static org.bithill.selenium.resolving.FieldOfResolvable.isSelect;
import static org.bithill.selenium.resolving.FieldOfResolvable.isWebElement;
import static org.bithill.selenium.resolving.Reflection.createAndSetField;
import static org.bithill.selenium.resolving.Reflection.getClassFields;
import static org.bithill.selenium.resolving.Reflection.getFieldTypeArguments;
import static org.bithill.selenium.resolving.Reflection.getFieldValue;
import static org.bithill.selenium.resolving.Reflection.setFieldValue;
import static org.bithill.selenium.condition.ElementConditions.visibilityOfAllNestedElementsLocatedBy;
import static org.bithill.selenium.condition.ElementConditions.visibilityOfNestedElement;
import static org.openqa.selenium.support.ui.ExpectedConditions.visibilityOfAllElementsLocatedBy;
import static org.openqa.selenium.support.ui.ExpectedConditions.visibilityOfElementLocated;
/**
* Shared denominator of all resolvable components of a web UI.
* Provides means for resolving page objects and filling forms with data.
*/
public class Resolvable
{
private static final Logger LOG = LogManager.getLogger(Resolvable.class);
// delimiter between locator type and specification
// all ResolveBy values have the pattern 'key=value'
private static final String SELECTOR_DELIMITER = "=";
/** Reference to itself, used in the case when Resolvable is annotated with {@link ResolveBy}. */
private WebElement self;
public WebElement getSelf() { return self; }
protected void setSelf(WebElement self) { this.self = self; }
/** WebDriverHandle used for the web page. */
private WebDriverHandle driverHandle;
protected WebDriverHandle getDriverHandle() { return driverHandle; }
protected void setDriverHandle(WebDriverHandle driverHandle) { this.driverHandle = driverHandle; }
private boolean isContainer = false;
public boolean isContainer() { return isContainer; }
public void setIsContainer(boolean isContainer) { this.isContainer = isContainer; }
/** Convenience getter for WebDriver.
*
* @return {@link WebDriver} from the currently used {@link WebDriverHandle}.
*/
public WebDriver getDriver()
{
if (getDriverHandle() == null) { throw new IllegalStateException("WebDriverHandle is null."); }
return getDriverHandle().getDriver();
}
/** Convenience getter for the default WebDriverWait.
*
* @return {@link FluentWait}, usually {@link WebDriverWait} used for {@link #resolve()} by default.
*/
public FluentWait getWait()
{
return getWait(WebDriverHandle.DEFAULT_WAIT);
}
/** Convenience getter for a named wait.
*
* @param waitName name of the wait
* @return {@link FluentWait} of the given name, or null if no such wait exists.
*/
public FluentWait getWait(String waitName)
{
if (getDriverHandle() == null) { throw new IllegalStateException("WebDriverHandle is null."); }
if (getDriverHandle().getWaits() == null) { throw new IllegalStateException("Map of WebDriverHandle's waits is null."); }
return getDriverHandle().getWaits().get(waitName);
}
/** Convenience setter for the default FluentWait.
*
* @param wait {@link FluentWait} to set and use for {@link #resolve()} by default.
*/
public void setWait(FluentWait wait)
{
setWait(WebDriverHandle.DEFAULT_WAIT, wait);
}
/** Convenience setter for a named WebDriverWait.
*
* @param waitName name of the wait
* @param wait {@link FluentWait} to set the named wait to.
*/
public void setWait(String waitName, FluentWait wait)
{
if (getDriverHandle() == null) { throw new IllegalStateException("WebDriverHandle is null."); }
if (getDriverHandle().getWaits() == null) { throw new IllegalStateException("Map of WebDriverHandle's waits is null."); }
getDriverHandle().getWaits().put(waitName, wait);
}
/** Mapping of placeholders (in ResolveBy annotation value) to real values. */
private Properties placeholders;
public Properties getPlaceholders() { return placeholders; }
public Resolvable setPlaceholders(Properties placeholders)
{
this.placeholders = placeholders;
return this;
}
/** Parent resolvable - should be set when we nest Resolvables.
* Always null for detached or Resolvables that represent a root of a hierarchy of nested Resolvables.
*/
@Nullable
private Resolvable parent;
public Resolvable getParent() { return parent; }
public void setParent(Resolvable parent)
{
this.parent = parent;
if (getDriverHandle() == null && parent != null)
{
// get driver handle from parent
WebDriverHandle parentsDriverHandle = getParent().getDriverHandle();
if (parentsDriverHandle != null)
{
setDriverHandle(parentsDriverHandle);
}
else
{
throw new NullPointerException("WebDriverHandle is null. Parent's WebDriverHandle is null too");
}
}
}
public Resolvable() { }
public Resolvable(WebDriverHandle driverHandle)
{
prepareForResolving(driverHandle);
}
/**
* Initializes necessary properties of Resolvable, allows deferred initialization.
*
* @param driverHandle handle keeping the instance of WebDriver the Resolvable should use
* @return reference to itself
*/
public Resolvable prepareForResolving(WebDriverHandle driverHandle)
{
if (driverHandle == null)
{
throw new NullPointerException("WebDriverHandle is null.");
}
setDriverHandle(driverHandle);
return this;
}
private final Set resolvableClasses = new HashSet<>(Arrays.asList(Resolvable.class, ResolvableList.class));
/**
* Resolves all resolvable elements, i.e. {@link Resolvable} and {@link ResolveBy} elements, included in this Resolvable.
*
* @param groups resolvable elements group names - only resolvable elements from listed groups will be resolved
* @return resolvable element (marked by {@link ResolveBy} annotation)
*/
public Resolvable resolve(String... groups)
{
Field[] fields = getClassFields(this, Resolvable.class);
resolveResolveBys(fields,groups);
resolveResolvables(fields,groups);
return this;
}
/**
* Resolves all resolvable elements, i.e. {@link Resolvable} and {@link ResolveBy} elements, included in this Resolvable
* and belonging to default group.
*
* @return resolvable element (marked by {@link ResolveBy} annotation)
*/
public Resolvable resolve()
{
return resolve(ResolveBy.DEFAULT_GROUP);
}
/** Resolves all included {@link Resolvable resolvables} */
private void resolveResolvables(Field[] fields, String... groups)
{
for (Field field : fields)
{
try
{
Object fieldValue = createAndSetField(this, field, resolvableClasses);
// the Resolvable field should be non-empty at this moment, initialize it
if (Resolvable.class.isInstance(fieldValue))
{
initAndResolveResolvable((Resolvable) fieldValue, isContainerResolvable(field), groups);
}
else if (ResolvableList.class.isInstance(fieldValue))
{
ResolvableList resolvable = (ResolvableList) fieldValue;
// mark the ResolvableList as container
resolvable.setIsContainer(isContainerResolvable(field));
}
}
catch (IllegalAccessException ex)
{
throw new RuntimeException(String.format("Field %s cannot be set.", field.getName()), ex);
}
}
}
private void initAndResolveResolvable(Resolvable resolvable, boolean container, String... groups)
{
resolvable.setParent(this);
// set the isContainer flag to the Resolvable
resolvable.setIsContainer(container);
resolvable.prepareForResolving(getDriverHandle());
resolvable.resolve(groups);
}
/** Resolves all fields annotated with {@link ResolveBy}. */
private void resolveResolveBys(Field[] fields, String... groups)
{
Class resolvableClass = getClass();
for (Field field : fields)
{
if (isResolvedByAnnotated(field))
{
ResolveBy resolveBy = field.getAnnotation(ResolveBy.class);
String locatorString = resolveBy.value();
String explicitWaitName = resolveBy.explicitWait();
FluentWait explicitWait = getDriverHandle().getWaits().get(explicitWaitName);
if (explicitWait == null)
{
// wait of requested name does not exist, so log warning and use default wait
LOG.warn("No wait '{}' is registered in WebDriverHandle, default wait will be used.", explicitWaitName);
}
ResolveContext resolveContext;
if (isGroupMember(field, groups))
{
if (isList(field))
{
if (explicitWait != null)
{
resolveContext = new ResolveContext(resolvableClass, field, locatorString, explicitWait);
}
else
{
// use default for lists
resolveContext = new ResolveContext(resolvableClass, field, locatorString, getWait(WebDriverHandle.DEFAULT_LIST_WAIT));
}
if (isResolvableList(field))
{
// groups are passed around for recursive resolving -- Resolvable may contain another Resolvables
resolveResolvableList(resolveContext, groups);
}
else
{
resolveWebElementsList(resolveContext);
}
}
else
{
if (explicitWait != null)
{
resolveContext = new ResolveContext(resolvableClass, field, locatorString, explicitWait);
}
else
{
// use default
resolveContext = new ResolveContext(resolvableClass, field, locatorString, getWait(WebDriverHandle.DEFAULT_WAIT));
}
resolveWebElement(resolveContext);
}
}
}
}
}
/**
* Finds and assigns single-element field.
* The WebElement must be present and visible -- if not, either a WebDriverException is thrown or
* (for fields annotated by @Nullable) the field is set to null.
*
* @param resolveContext context for {@link #resolve()}
*/
private void resolveWebElement(ResolveContext resolveContext)
{
try
{
setWebElementField(resolveContext.getField(), getSingleWebElementForContext(resolveContext));
}
catch (WebDriverException ex)
{
// @Nullable fields can be null and are set to null when the element is not found
if (isNullableAnnotated(resolveContext.getField()))
{
setWebElementField(resolveContext.getField(), null);
}
else
{
// for easier bug-fixing: add to the exception info about the source class and field
ex.addInfo("field", resolveContext.getResolvableClass().getName() + " -> " + resolveContext.getField() .getName());
throw ex;
}
}
}
/**
* Finds and assigns list-of-elements field.
* The list must have at least one item and all matching WebElements must be visible -- if not,
* either a WebDriverException is thrown or (for fields annotated by @Nullable) the field is set to empty list.
*
* @param resolveContext context for {@link #resolve()}
*/
@SuppressWarnings("unchecked")
private void resolveWebElementsList(ResolveContext resolveContext)
{
try
{
setWebElementListField(resolveContext.getField(), getWebElementsList(resolveContext));
}
catch (WebDriverException ex)
{
// @Nullable list fields can be 'null' and are set to empty list when the elements are not found
if (isNullableAnnotated(resolveContext.getField()))
{
setWebElementListField(resolveContext.getField(), Collections.EMPTY_LIST);
}
else throw ex;
}
}
/**
* Finds and assigns ResolvableList field.
*
* @param resolveContext context for {@link #resolve()}
*/
@SuppressWarnings("unchecked")
private void resolveResolvableList(ResolveContext resolveContext, String... groups)
{
List rootElements = Collections.EMPTY_LIST;
try
{
rootElements = getWebElementsList(resolveContext);
}
catch (WebDriverException ex)
{
// @Nullable list fields can be 'null' and are set to empty list when the elements are not found
if (isNullableAnnotated(resolveContext.getField()))
{
setResolvableListField(resolveContext.getField(), new ResolvableList<>(0));
}
else throw ex;
}
try
{
Class resolvableType = (Class) getFieldTypeArguments(resolveContext.getField())[0];
ResolvableList result = new ResolvableList();
for (WebElement element : rootElements)
{
// create, init and resolve Resolvable
Constructor resolvableConstructor = resolvableType.getConstructor(WebDriverHandle.class);
Resolvable newItem = (Resolvable)resolvableConstructor.newInstance((WebDriverHandle)getDriverHandle());
newItem.setSelf(element);
initAndResolveResolvable(newItem, isContainerResolvable(resolveContext.getField()), groups);
// add to the resulting collection
result.add(newItem);
}
// set the collection to the field
setFieldValue(this, resolveContext.getField(), result);
}
catch (ReflectiveOperationException ex)
{
throw new RuntimeException(ex);
}
}
/** Finds collection of WebElements for given context,
* adding additional information to {@link WebDriverException} when thrown.
*
* @param resolveContext context for {@link #resolve()}
* @return list of {@link WebElement}s matching the context
*/
private List getWebElementsList(ResolveContext resolveContext)
{
List rootElements;
// get elements that a root for further resolving, e.g. table row for table cells
try
{
rootElements = getMultipleWebElementsForContext(resolveContext);
}
catch (WebDriverException ex)
{
if (!isNullableAnnotated(resolveContext.getField()))
{
// for easier bug-fixing: add to the exception info about the source class and field
ex.addInfo("field", resolveContext.getResolvableClass().getName()
+ " -> "
+ resolveContext.getField().getName());
}
throw ex;
}
return rootElements;
}
/**
* Replaces placeholders in locator specification wit real values.
* Used for parametrization of locators.
*/
private String replacePlaceholders(String locatorSpecification)
{
if (getPlaceholders() != null)
{
for (String propertyName : getPlaceholders().stringPropertyNames())
{
locatorSpecification = locatorSpecification.replaceAll
(quote(propertyName), getPlaceholders().getProperty(propertyName));
}
}
return locatorSpecification;
}
/**
* Retrieves a {@link WebElement} for given context,
* i.e. matching context locator string and using context wait.
*
* @param resolveContext context for {@link #resolve()}
*/
private WebElement getSingleWebElementForContext(ResolveContext resolveContext)
{
WebElement result;
By locator = locatorFromString(resolveContext.getLocatorString());
if (isContainer() && getSelf() != null)
{
result = resolveContext.getWait().until(visibilityOfNestedElement(getSelf(), locator));
}
else
{
result = resolveContext.getWait().until(visibilityOfElementLocated(locator));
}
return result;
}
/**
* Retrieves a List of {@link WebElement}s for given context,
* i.e. matching context locator string and using context wait.
*
* @param resolveContext context for {@link #resolve()}
*/
private List getMultipleWebElementsForContext(ResolveContext resolveContext)
{
List result;
By locator = locatorFromString(resolveContext.getLocatorString());
if (isContainer() && getSelf() != null)
{
result = resolveContext.getWait().until(visibilityOfAllNestedElementsLocatedBy(getSelf(), locator));
}
else
{
result = resolveContext.getWait().until(visibilityOfAllElementsLocatedBy(locator));
}
return result;
}
/**
* Converts locator string to a {@link By} locator.
*/
private By locatorFromString(String locatorString)
{
// split locator string to type and specification
String[] locatorParts = locatorString.split(SELECTOR_DELIMITER, 2);
if (locatorParts.length != 2)
{
throw new InvalidLocatorFormatException(
String.format("Locator has invalid format: '%s'.", locatorString) );
}
String locatorType = locatorParts[0].trim();
String locatorSpecification = replacePlaceholders(locatorParts[1].trim());
By locator;
switch (locatorType)
{
case "id" : locator = By.id(locatorSpecification); break;
case "xpath" : locator = By.xpath(locatorSpecification); break;
case "css" : locator = By.cssSelector(locatorSpecification); break;
case "name" : locator = By.name(locatorSpecification); break;
case "tag" : locator = By.tagName(locatorSpecification); break;
case "link" : locator = By.linkText(locatorSpecification); break;
default:
throw new UnknownLocatorTypeException(String.format("Unknown locator type: '%s'.", locatorType) );
}
return locator;
}
/**
* Sets web element (resolved by the resolve() method) to given Resolvable field.
*/
private void setWebElementField(@Nonnull Field field, @Nullable WebElement webElement)
{
try
{
if (webElement != null)
{
if (isResolvable(field))
{
// for Resolvable fields annotated with @ResolveBy (because called only from resolveResolveBys())
// 1. create a new instance if no such exists
createAndSetField(this, field, Collections.singleton(Resolvable.class));
// 2. set self-reference to the resolved "container" objects
Resolvable resolvableField = (Resolvable) getFieldValue(this, field);
resolvableField.setSelf(webElement);
}
else if (isWebElement(field))
{
setFieldValue(this, field, webElement);
}
else if (isSelect(field))
{
setFieldValue(this, field, new Select(webElement));
}
}
else
{
if (isNullableAnnotated(field))
{
setFieldValue(this, field, null);
}
else
{
throw new NullPointerException
( String.format("Field %s is not @Nullable but the WebElement is null.", field.getName()) );
}
}
}
catch (IllegalAccessException ex)
{
throw new RuntimeException(String.format("Field %s cannot be set.", field.getName()), ex);
}
}
/**
* Sets list of web elements (resolved by the resolve() method) to given Resolvable field.
*/
private void setWebElementListField(Field field, List webElementList)
{
boolean isWebElementList = WebElement.class.isAssignableFrom((Class) getFieldTypeArguments(field)[0]);
setFieldValue(this, field, webElementList, aField -> isList(aField) && isWebElementList);
}
/**
* Sets ResolvableList field.
*/
private void setResolvableListField(Field field, ResolvableList extends Resolvable> resolvableList)
{
setFieldValue( this, field, resolvableList, aField -> isList(aField) && isResolvableList(aField) );
}
/**
* Loads data into form inputs.
*
* @param formValues mapping of input fields (class field of type WebElement representing
* HTMl form elements) to input values
*/
public void loadData(Properties formValues)
{
for (Field field : getClassFields(this, Resolvable.class) )
{
if (isResolvedByAnnotated(field))
{
String fieldName = field.getName();
if (formValues.containsKey(fieldName))
{
String valueSpecification = formValues.getProperty(fieldName);
if (isWebElement(field))
{
// probably some input field ..
WebElement webElement = (WebElement) getFieldValue(this, field);
webElement.clear();
webElement.sendKeys(valueSpecification);
}
else
{
if (isSelect(field))
{
Select select = (Select) (getFieldValue(this, field));
// select - by default we select using visible text
if (select.isMultiple()) { select.deselectAll(); }
select.selectByVisibleText(valueSpecification);
}
}
}
}
}
}
private static class ResolveContext
{
private final Class resolvableClass;
public Class getResolvableClass() { return resolvableClass; }
private final Field field;
public Field getField() { return field; }
/** string in format locator_type=locator_specification, e.g. "id=headline", or "xpath=//div[@id='headline']" */
private final String locatorString;
public String getLocatorString() { return locatorString; }
private final FluentWait wait;
public FluentWait getWait() { return wait; }
/**
* @param resolvableClass class extending {@link Resolvable} and containing the resolvable field
* @param field resolvable field, i.e. {@link ResolveBy}-annotated field in a resolvable class
* @param locatorString locator from the {@link ResolveBy} annotation, namely {@link ResolveBy#value()}
* @param wait explicit wait used by for resolving of the field
*/
private ResolveContext(Class resolvableClass, Field field, String locatorString, FluentWait wait)
{
this.resolvableClass = resolvableClass;
this.field = field;
this.locatorString = locatorString;
this.wait = wait;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy