com.mgmtp.jfunk.web.util.WebElementFinder.java Source code

Java tutorial

Introduction

Here is the source code for com.mgmtp.jfunk.web.util.WebElementFinder.java

Source

/*
 * Copyright (c) 2015 mgm technology partners GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.mgmtp.jfunk.web.util;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Lists.newArrayList;

import java.lang.reflect.Method;
import java.util.List;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.openqa.selenium.By;
import org.openqa.selenium.NotFoundException;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;

/**
 * <p>
 * Provides a fluent interface for finding elements, optionally with timeout and constraints (enabled, displayed, selected).
 * </p>
 * <p>
 * <strong>Warning: {@link WebElementFinder} instances are always immutable</strong>.; Configuration methods have no effect on the
 * instance they are invoked on! You must store and use the new {@link WebElementFinder} instance returned by these methods. This
 * makes {@link WebElementFinder}s thread-safe and safe to store as {@code static final} constants.
 * </p>
 * <p>
 * This class follows the same mechanisms as {@link FormInputHandler}.
 * </p>
 * <string>Usage Example:</strong>
 * 
 * <pre>
 * WebElement element = WebElementFinder.create().by(By.id(&quot;someId&quot;)).displayed(true).webDriver(webDriver).find();
 * </pre>
 * 
 * @see FormInputHandler
 * @author rnaegele
 */
public final class WebElementFinder {

    private static final List<Class<? extends Throwable>> IGNORED_EXCEPTIONS = ImmutableList
            .<Class<? extends Throwable>>of(NotFoundException.class, WebElementException.class,
                    StaleElementReferenceException.class);

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final WebDriver webDriver;
    private final By by;
    private final long timeoutSeconds;
    private final long sleepMillis;
    private final Boolean enabled;
    private final Boolean displayed;
    private final Boolean selected;
    private final Predicate<WebElement> condition;
    private final boolean noLogging;

    private WebElementFinder(final WebDriver webDriver, final By by, final long timeoutSeconds,
            final long sleepMillis, final Boolean enabled, final Boolean displayed, final Boolean selected,
            final Predicate<WebElement> condition, final boolean noLogging) {
        this.webDriver = webDriver;
        this.by = by;
        this.timeoutSeconds = timeoutSeconds;
        this.sleepMillis = sleepMillis;
        this.enabled = enabled;
        this.displayed = displayed;
        this.selected = selected;
        this.condition = condition;
        this.noLogging = noLogging;
    }

    private WebElementFinder(final Fields fields) {
        this(fields.webDriver, fields.by, fields.timeoutSeconds, fields.sleepMillis, fields.enabled,
                fields.displayed, fields.selected, fields.condition, fields.noLogging);
    }

    /**
     * Creates a {@link WebElementFinder}.
     * 
     * @return the new {@link WebElementFinder} instance
     */
    public static WebElementFinder create() {
        return new WebElementFinder(null, null, 0L, 0L, null, null, null, null, false);
    }

    /**
     * Creates an new {@link WebElementFinder} based on this {@link WebElementFinder} using the specified timeout.
     * 
     * @param theTimeoutSeconds
     *            the timeout in seconds for the internal {@link WebDriverWait}
     * @return the new {@link WebElementFinder} instance
     */
    public WebElementFinder timeout(final long theTimeoutSeconds) {
        checkArgument(theTimeoutSeconds >= 0, "'theTimeoutSeconds' must be greater than or equal to zero");
        Fields fields = new Fields(this);
        fields.timeoutSeconds = theTimeoutSeconds;
        return new WebElementFinder(fields);
    }

    /**
     * Creates an new {@link WebElementFinder} based on this {@link WebElementFinder} using the specified timeout.
     * 
     * @param theTimeoutSeconds
     *            the timeout in seconds for the internal {@link WebDriverWait}
     * @param theSleepMillis
     *            the sleep time in milliseconds for the internal {@link WebDriverWait}
     * @return the {@link WebElementFinder} instance
     */
    public WebElementFinder timeout(final long theTimeoutSeconds, final long theSleepMillis) {
        checkArgument(theTimeoutSeconds >= 0, "'theTimeoutSeconds' must be greater than or equal to zero");
        checkArgument(theSleepMillis >= 0, "'theSleepMillis' must be greater than or equal to zero");
        Fields fields = new Fields(this);
        fields.timeoutSeconds = theTimeoutSeconds;
        fields.sleepMillis = theSleepMillis;
        return new WebElementFinder(fields);
    }

    /**
     * Creates an new {@link WebElementFinder} based on this {@link WebElementFinder} restricting the enabled status of elements.
     * 
     * @param theEnabled
     *            {@code true} if elements must be enabled, {@code false} if elements must not be enabled
     * @return the new {@link WebElementFinder} instance
     * @see WebElement#isEnabled()
     */
    public WebElementFinder enabled(final Boolean theEnabled) {
        Fields fields = new Fields(this);
        fields.enabled = theEnabled;
        return new WebElementFinder(fields);
    }

    /**
     * Creates an new {@link WebElementFinder} based on this {@link WebElementFinder} restricting the displayed status of
     * elements.
     * 
     * @param theDisplayed
     *            {@code true} if elements must be displayed, {@code false} if elements must not be displayed
     * @return the new {@link WebElementFinder} instance
     * @see WebElement#isDisplayed()
     */
    public WebElementFinder displayed(final Boolean theDisplayed) {
        Fields fields = new Fields(this);
        fields.displayed = theDisplayed;
        return new WebElementFinder(fields);
    }

    /**
     * Creates an new {@link WebElementFinder} based on this {@link WebElementFinder} restricting the selected status of elements.
     * This method should only be called if elements are indeed selectable, such as checkboxes, options in a select, and radio
     * buttons.
     * 
     * @param theSelected
     *            {@code true} if elements must be selected, {@code false} if elements must not be selected
     * @return the new {@link WebElementFinder} instance
     * @see WebElement#isSelected()
     */
    public WebElementFinder selected(final Boolean theSelected) {
        Fields fields = new Fields(this);
        fields.selected = theSelected;
        return new WebElementFinder(fields);
    }

    /**
     * Creates an new {@link WebElementFinder} based on this {@link WebElementFinder} specifying a condition elements must meet.
     * 
     * @param theCondition
     *            the condition
     * @return the new {@link WebElementFinder} instance
     */
    public WebElementFinder condition(final Predicate<WebElement> theCondition) {
        Fields fields = new Fields(this);
        fields.condition = theCondition;
        return new WebElementFinder(fields);
    }

    /**
     * Creates a new {@link WebElementFinder} based on this {@link WebElementFinder} using the specified element locator.
     * 
     * @param theBy
     *            locates the element to operate on
     * @return the new {@link FormInputHandler} instance
     */
    public WebElementFinder by(final By theBy) {
        Fields fields = new Fields(this);
        fields.by = theBy;
        return new WebElementFinder(fields);
    }

    /**
     * Creates a new {@link WebElementFinder} based on this {@link WebElementFinder} using the specified {@link WebDriver}.
     * 
     * @param theWebDriver
     *            the {@link WebDriver} to use
     * @return the new {@link FormInputHandler} instance
     */
    public WebElementFinder webDriver(final WebDriver theWebDriver) {
        Fields fields = new Fields(this);
        fields.webDriver = theWebDriver;
        return new WebElementFinder(fields);
    }

    /**
     * Creates a new {@link WebElementFinder} based on this {@link WebElementFinder} with logging disabled. This is useful when a
     * {@link WebElementFinder} is using by a {@link FormInputHandler} which logs on its own.
     * 
     * @param theNoLogging
     *            the {@link WebDriver} to use
     * @return the new {@link FormInputHandler} instance
     */
    public WebElementFinder noLogging(final boolean theNoLogging) {
        Fields fields = new Fields(this);
        fields.noLogging = theNoLogging;
        return new WebElementFinder(fields);
    }

    /**
     * Finds the first element.
     * 
     * @return the element
     */
    public WebElement find() {
        checkState(webDriver != null, "No WebDriver specified.");
        checkState(by != null, "No By instance for locating elements specified.");

        if (!noLogging) {
            log.info(toString());
        }

        WebElement element;

        if (timeoutSeconds > 0L) {
            WebDriverWait wait = createWebDriverWait();
            element = wait.until(new Function<WebDriver, WebElement>() {
                @Override
                public WebElement apply(final WebDriver input) {
                    WebElement el = input.findElement(by);
                    checkElement(el);
                    if (condition != null && !condition.apply(el)) {
                        throw new WebElementException(
                                String.format("Condition not met for element %s: %s", el, condition));
                    }
                    return el;
                }

                @Override
                public String toString() {
                    return WebElementFinder.this.toString();
                }
            });
        } else {
            element = webDriver.findElement(by);
            checkElement(element);
            if (condition != null && !condition.apply(element)) {
                throw new WebElementException(
                        String.format("Condition not met for element %s: %s", element, condition));
            }
        }

        return element;
    }

    /**
     * Finds all elements.
     * 
     * @return the list of elements
     */
    public List<WebElement> findAll() {
        checkState(webDriver != null, "No WebDriver specified.");
        checkState(by != null, "No By instance for locating elements specified.");

        log.info(toString());

        final List<WebElement> result = newArrayList();

        try {
            if (timeoutSeconds > 0L) {
                WebDriverWait wait = createWebDriverWait();
                wait.until(new Function<WebDriver, List<WebElement>>() {
                    @Override
                    public List<WebElement> apply(final WebDriver input) {
                        doFindElements(result, input);
                        if (result.isEmpty()) {
                            // this means, we try again until the timeout occurs
                            throw new WebElementException("No matching element found.");
                        }
                        return result;
                    }

                    @Override
                    public String toString() {
                        return WebElementFinder.this.toString();
                    }
                });
            } else {
                doFindElements(result, webDriver);
            }
            return result;
        } catch (TimeoutException ex) {
            Throwable cause = ex.getCause();
            for (Class<? extends Throwable> thClass : IGNORED_EXCEPTIONS) {
                if (thClass.isInstance(cause)) {
                    return ImmutableList.of();
                }
            }
            throw new WebElementException(ex);
        }
    }

    private void doFindElements(final List<WebElement> result, final WebDriver searchContext) {
        List<WebElement> elList = searchContext.findElements(by);
        for (WebElement element : elList) {
            if (!checkElementForList(element)) {
                continue;
            }
            if (condition != null && !condition.apply(element)) {
                continue;
            }
            result.add(element);
        }
    }

    private boolean checkElementForList(final WebElement element) {
        if (enabled != null) {
            if (enabled != element.isEnabled()) {
                return false;
            }
        }
        if (displayed != null) {
            if (displayed != element.isDisplayed()) {
                return false;
            }
        }
        if (selected != null) {
            if (selected != element.isSelected()) {
                return false;
            }
        }
        return true;
    }

    private void checkElement(final WebElement element) {
        if (enabled != null) {
            if (enabled) {
                checkElementState(element, element.isEnabled(),
                        "Element '%s' was expected to be enabled but is not.");
            } else {
                checkElementState(element, !element.isEnabled(),
                        "Element '%s' was expected to not be enabled but is.");
            }
        }
        if (displayed != null) {
            if (displayed) {
                checkElementState(element, element.isDisplayed(),
                        "Element '%s' was expected to be displayed but is not.");
            } else {
                checkElementState(element, !element.isDisplayed(),
                        "Element '%s' was expected to not be displayed but is.");
            }
        }
        if (selected != null) {
            if (selected) {
                checkElementState(element, element.isSelected(),
                        "Element '%s' was expected to be selected but is not.");
            } else {
                checkElementState(element, !element.isSelected(),
                        "Element '%s' was expected to not be selected but is.");
            }
        }
    }

    private static void checkElementState(final WebElement element, final boolean expression,
            final String msgTemplate) {
        if (!expression) {
            WebElement underlyingElement;
            try {
                Method m = element.getClass().getMethod("getWrappedElement");
                m.setAccessible(true);
                underlyingElement = (WebElement) m.invoke(element);
            } catch (Exception ex) {
                underlyingElement = element;
            }
            throw new WebElementException(String.format(msgTemplate, underlyingElement));
        }
    }

    private WebDriverWait createWebDriverWait() {
        WebDriverWait webDriverWait = sleepMillis > 0L ? new WebDriverWait(webDriver, timeoutSeconds, sleepMillis)
                : new WebDriverWait(webDriver, timeoutSeconds);
        webDriverWait.ignoreAll(IGNORED_EXCEPTIONS);
        return webDriverWait;
    }

    /**
     * @return the webDriver
     */
    public WebDriver getWebDriver() {
        return webDriver;
    }

    /**
     * @return the by
     */
    public By getBy() {
        return by;
    }

    /**
     * @return the timeoutSeconds
     */
    public long getTimeoutSeconds() {
        return timeoutSeconds;
    }

    /**
     * @return the sleepMillis
     */
    public long getSleepMillis() {
        return sleepMillis;
    }

    /**
     * @return the enabled
     */
    public Boolean getEnabled() {
        return enabled;
    }

    /**
     * @return the displayed
     */
    public Boolean getDisplayed() {
        return displayed;
    }

    /**
     * @return the selected
     */
    public Boolean getSelected() {
        return selected;
    }

    /**
     * @return the condition
     */
    public Predicate<WebElement> getCondition() {
        return condition;
    }

    @Override
    public String toString() {
        ToStringBuilder tsb = new ToStringBuilder(this, ShortToStringStyle.INSTANCE);
        tsb.append("by", by);
        if (timeoutSeconds > 0L) {
            tsb.append("timeoutSeconds", timeoutSeconds);
            if (sleepMillis > 0L) {
                tsb.append("sleepMillis", sleepMillis);
            }
        }
        if (enabled != null) {
            tsb.append("enabled", enabled);
        }
        if (displayed != null) {
            tsb.append("displayed", displayed);
        }
        if (selected != null) {
            tsb.append("selected", selected);
        }
        return tsb.toString();
    }

    private static class Fields {
        private WebDriver webDriver;
        private By by;
        private long timeoutSeconds;
        private long sleepMillis;
        private Boolean enabled;
        private Boolean displayed;
        private Boolean selected;
        private Predicate<WebElement> condition;
        private boolean noLogging;

        private Fields(final WebElementFinder finder) {
            this.webDriver = finder.webDriver;
            this.by = finder.by;
            this.timeoutSeconds = finder.timeoutSeconds;
            this.sleepMillis = finder.sleepMillis;
            this.enabled = finder.enabled;
            this.displayed = finder.displayed;
            this.selected = finder.selected;
            this.condition = finder.condition;
            this.noLogging = finder.noLogging;
        }
    }
}