All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bithill.selenium.resolving.Resolvable Maven / Gradle / Ivy

There is a newer version: 1.0
Show newest version
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 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