com.vaadin.tests.tb3.AbstractTB3Test.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.tests.tb3.AbstractTB3Test.java

Source

/*
 * Copyright 2000-2016 Vaadin Ltd.
 *
 * 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.vaadin.tests.tb3;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.interactions.HasInputDevices;
import org.openqa.selenium.interactions.Keyboard;
import org.openqa.selenium.interactions.Mouse;
import org.openqa.selenium.interactions.internal.Coordinates;
import org.openqa.selenium.internal.Locatable;
import org.openqa.selenium.internal.WrapsElement;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.HttpCommandExecutor;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import com.vaadin.server.LegacyApplication;
import com.vaadin.server.UIProvider;
import com.vaadin.testbench.TestBenchDriverProxy;
import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.annotations.BrowserConfiguration;
import com.vaadin.testbench.elements.CheckBoxElement;
import com.vaadin.testbench.elements.LabelElement;
import com.vaadin.testbench.elements.TableElement;
import com.vaadin.testbench.elements.VerticalLayoutElement;
import com.vaadin.testbench.parallel.Browser;
import com.vaadin.testbench.parallel.BrowserUtil;
import com.vaadin.testbench.parallel.ParallelTest;
import com.vaadin.ui.UI;

import elemental.json.JsonObject;
import elemental.json.impl.JsonUtil;

/**
 * Base class for TestBench 3+ tests. All TB3+ tests in the project should
 * extend this class.
 *
 * Provides:
 * <ul>
 * <li>Helpers for browser selection</li>
 * <li>Hub connection setup and teardown</li>
 * <li>Automatic generation of URL for a given test on the development server
 * using {@link #getUIClass()} or by automatically finding an enclosing UI class
 * and based on requested features, e.g. {@link #isDebug()},
 * {@link #isPush()}</li>
 * <li>Generic helpers for creating TB3+ tests</li>
 * </ul>
 *
 * @author Vaadin Ltd
 */
@RunWith(TB3Runner.class)
public abstract class AbstractTB3Test extends ParallelTest {

    @Rule
    public TestName testName = new TestName();

    @Rule
    public RetryOnFail retry = new RetryOnFail();

    /**
     * Height of the screenshots we want to capture
     */
    private static final int SCREENSHOT_HEIGHT = 850;

    /**
     * Width of the screenshots we want to capture
     */
    private static final int SCREENSHOT_WIDTH = 1500;

    /**
     * Timeout used by the TB grid
     */
    private static final int BROWSER_TIMEOUT_IN_MS = 30 * 1000;

    protected static DesiredCapabilities PHANTOMJS2() {
        DesiredCapabilities phantomjs2 = new VaadinBrowserFactory().create(Browser.PHANTOMJS, "2");
        // Hack for the test cluster
        phantomjs2.setCapability("phantomjs.binary.path", "/usr/bin/phantomjs2");
        return phantomjs2;
    }

    private boolean debug = false;

    private boolean push = false;

    static {
        com.vaadin.testbench.Parameters.setScreenshotComparisonCursorDetection(true);
    }

    /**
     * Connect to the hub using a remote web driver, set the canvas size and
     * opens the initial URL as specified by {@link #getTestUrl()}
     *
     * @throws Exception
     */
    @Override
    public void setup() throws Exception {
        super.setup();

        int w = SCREENSHOT_WIDTH;
        int h = SCREENSHOT_HEIGHT;

        try {
            testBench().resizeViewPortTo(w, h);
        } catch (UnsupportedOperationException e) {
            // Opera does not support this...
        }
    }

    /**
     * Method for closing the tested application.
     */
    protected void closeApplication() {
        if (driver != null) {
            try {
                openTestURL("closeApplication");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    protected WebElement getTooltipErrorElement() {
        WebElement tooltip = getDriver().findElement(com.vaadin.testbench.By.className("v-tooltip"));
        return tooltip.findElement(By.className("v-errormessage"));
    }

    protected WebElement getTooltipElement() {
        return getDriver().findElement(com.vaadin.testbench.By.className("v-tooltip-text"));
    }

    protected Coordinates getCoordinates(TestBenchElement element) {
        return ((Locatable) element.getWrappedElement()).getCoordinates();
    }

    private boolean hasDebugMessage(String message) {
        return getDebugMessage(message) != null;
    }

    private WebElement getDebugMessage(String message) {
        return driver.findElement(
                By.xpath(String.format("//span[@class='v-debugwindow-message' and text()='%s']", message)));
    }

    protected void waitForDebugMessage(final String expectedMessage) {
        waitForDebugMessage(expectedMessage, 30);
    }

    protected void waitForDebugMessage(final String expectedMessage, int timeout) {
        waitUntil(new ExpectedCondition<Boolean>() {

            @Override
            public Boolean apply(WebDriver input) {
                return hasDebugMessage(expectedMessage);
            }
        }, timeout);
    }

    protected void clearDebugMessages() {
        driver.findElement(By.xpath("//button[@class='v-debugwindow-button' and @title='Clear log']")).click();
    }

    protected void waitUntilRowIsVisible(final TableElement table, final int row) {
        waitUntil(new ExpectedCondition<Object>() {
            @Override
            public Object apply(WebDriver input) {
                try {
                    return table.getCell(row, 0) != null;
                } catch (NoSuchElementException e) {
                    return false;
                }
            }
        });
    }

    protected void scrollTable(TableElement table, int rows, int rowToWait) {
        testBenchElement(table.findElement(By.className("v-scrollable"))).scroll(rows * 30);

        waitUntilRowIsVisible(table, rowToWait);
    }

    /**
     * Opens the given test (defined by {@link #getTestUrl()}, optionally with
     * debug window and/or push (depending on {@link #isDebug()} and
     * {@link #isPush()}.
     */
    protected void openTestURL() {
        openTestURL(new String[0]);
    }

    /**
     * Opens the given test (defined by {@link #getTestUrl()}, optionally with
     * debug window and/or push (depending on {@link #isDebug()} and
     * {@link #isPush()}.
     */
    protected void openTestURL(String... parameters) {
        openTestURL(getUIClass(), parameters);
    }

    /**
     * Opens the given test (defined by {@link #getTestUrl()}, optionally with
     * debug window and/or push (depending on {@link #isDebug()} and
     * {@link #isPush()}.
     */
    protected void openTestURL(Class<?> uiClass, String... parameters) {
        openTestURL(uiClass, new HashSet<>(Arrays.asList(parameters)));
    }

    private void openTestURL(Class<?> uiClass, Set<String> parameters) {
        String url = getTestURL(uiClass);

        if (isDebug()) {
            parameters.add("debug");
        }

        if (LegacyApplication.class.isAssignableFrom(uiClass)) {
            parameters.add("restartApplication");
        }

        if (parameters.size() > 0) {
            url += "?" + StringUtils.join(parameters, "&");
        }

        driver.get(url);
    }

    /**
     * Returns the full URL to be used for the test
     *
     * @return the full URL for the test
     */
    protected String getTestUrl() {
        return StringUtils.strip(getBaseURL(), "/") + getDeploymentPath();
    }

    /**
     * Returns the full URL to be used for the test for the provided UI class.
     *
     * @return the full URL for the test
     */
    protected String getTestURL(Class<?> uiClass) {
        return StringUtils.strip(getBaseURL(), "/") + getDeploymentPath(uiClass);
    }

    /**
     * Used to determine what URL to initially open for the test
     *
     * @return the host name of development server
     */
    protected abstract String getDeploymentHostname();

    /**
     * Used to determine what port the test is running on
     *
     * @return The port teh test is running on, by default 8888
     */
    protected abstract int getDeploymentPort();

    /**
     * Produces a collection of browsers to run the test on. This method is
     * executed by the test runner when determining how many test methods to
     * invoke and with what parameters. For each returned value a test method is
     * ran and before running that,
     * {@link #setDesiredCapabilities(DesiredCapabilities)} is invoked with the
     * value returned by this method.
     *
     * This method is not static to allow overriding it in sub classes. By
     * default runs the test only on Firefox
     *
     * @return The browsers to run the test on
     */
    @BrowserConfiguration
    public List<DesiredCapabilities> getBrowsersToTest() {
        return Collections.singletonList(Browser.FIREFOX.getDesiredCapabilities());
    }

    /**
     * Finds an element based on the part of a TB2 style locator following the
     * :: (e.g. vaadin=runLabelModes::PID_Scheckboxaction-Enabled/domChild[0] ->
     * PID_Scheckboxaction-Enabled/domChild[0]).
     *
     * @param vaadinLocator
     *            The part following :: of the vaadin locator string
     * @return
     */
    protected WebElement vaadinElement(String vaadinLocator) {
        return driver.findElement(vaadinLocator(vaadinLocator));
    }

    /**
     * Uses JavaScript to determine the currently focused element.
     *
     * @return Focused element or null
     */
    protected WebElement getFocusedElement() {
        Object focusedElement = executeScript("return document.activeElement");
        if (null != focusedElement) {
            return (WebElement) focusedElement;
        } else {
            return null;
        }
    }

    /**
     * Executes the given Javascript
     *
     * @param script
     *            the script to execute
     * @return whatever
     *         {@link org.openqa.selenium.JavascriptExecutor#executeScript(String, Object...)}
     *         returns
     */
    protected Object executeScript(String script, Object... args) {
        return ((JavascriptExecutor) getDriver()).executeScript(script, args);
    }

    /**
     * Find a Vaadin element based on its id given using Component.setId
     *
     * @param id
     *            The id to locate
     * @return
     */
    public WebElement vaadinElementById(String id) {
        return driver.findElement(By.id(id));
    }

    /**
     * Finds a {@link By} locator based on the part of a TB2 style locator
     * following the :: (e.g.
     * vaadin=runLabelModes::PID_Scheckboxaction-Enabled/domChild[0] ->
     * PID_Scheckboxaction-Enabled/domChild[0]).
     *
     * @param vaadinLocator
     *            The part following :: of the vaadin locator string
     * @return
     */
    public org.openqa.selenium.By vaadinLocator(String vaadinLocator) {
        String base = getApplicationId(getDeploymentPath());

        base += "::";
        return com.vaadin.testbench.By.vaadin(base + vaadinLocator);
    }

    /**
     * Constructs a {@link By} locator for the id given using Component.setId
     *
     * @param id
     *            The id to locate
     * @return a locator for the given id
     */
    public By vaadinLocatorById(String id) {
        return vaadinLocator("PID_S" + id);
    }

    /**
     * Waits up to 10s for the given condition to become true. Use e.g. as
     * {@link #waitUntil(ExpectedConditions.textToBePresentInElement(by, text))}
     *
     * @param condition
     *            the condition to wait for to become true
     */
    protected <T> void waitUntil(ExpectedCondition<T> condition) {
        waitUntil(condition, 10);
    }

    /**
     * Waits the given number of seconds for the given condition to become true.
     * Use e.g. as
     * {@link #waitUntil(ExpectedConditions.textToBePresentInElement(by, text))}
     *
     * @param condition
     *            the condition to wait for to become true
     */
    protected <T> void waitUntil(ExpectedCondition<T> condition, long timeoutInSeconds) {
        new WebDriverWait(driver, timeoutInSeconds).until(condition);
    }

    /**
     * Waits up to 10s for the given condition to become false. Use e.g. as
     * {@link #waitUntilNot(ExpectedConditions.textToBePresentInElement(by,
     * text))}
     *
     * @param condition
     *            the condition to wait for to become false
     */
    protected <T> void waitUntilNot(ExpectedCondition<T> condition) {
        waitUntilNot(condition, 10);
    }

    /**
     * Waits the given number of seconds for the given condition to become
     * false. Use e.g. as
     * {@link #waitUntilNot(ExpectedConditions.textToBePresentInElement(by,
     * text))}
     *
     * @param condition
     *            the condition to wait for to become false
     */
    protected <T> void waitUntilNot(ExpectedCondition<T> condition, long timeoutInSeconds) {
        waitUntil(ExpectedConditions.not(condition), timeoutInSeconds);
    }

    protected void waitForElementPresent(final By by) {
        waitUntil(ExpectedConditions.presenceOfElementLocated(by));
    }

    protected void waitForElementNotPresent(final By by) {
        waitUntil(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(WebDriver input) {
                return input.findElements(by).isEmpty();
            }
        });
    }

    protected void waitForElementVisible(final By by) {
        waitUntil(ExpectedConditions.visibilityOfElementLocated(by));
    }

    /**
     * Checks if the given element has the given class name.
     *
     * Matches only full class names, i.e. has ("foo") does not match
     * class="foobar"
     *
     * @param element
     * @param className
     * @return
     */
    protected boolean hasCssClass(WebElement element, String className) {
        String classes = element.getAttribute("class");
        if (classes == null || classes.isEmpty()) {
            return (className == null || className.isEmpty());
        }

        for (String cls : classes.split(" ")) {
            if (className.equals(cls)) {
                return true;
            }
        }

        return false;
    }

    /**
     * For tests extending AbstractTestUIWithLog, returns the element for the
     * Nth log row
     *
     * @param rowNr
     *            The log row to retrieve
     * @return the Nth log row
     */
    protected WebElement getLogRowElement(int rowNr) {
        return vaadinElementById("Log_row_" + rowNr);
    }

    /**
     * For tests extending AbstractTestUIWithLog, returns the text in the Nth
     * log row
     *
     * @param rowNr
     *            The log row to retrieve text for
     * @return the text in the log row
     */
    protected String getLogRow(int rowNr) {
        return getLogRowElement(rowNr).getText();
    }

    /**
     * Asserts that {@literal a} is &gt;= {@literal b}
     *
     * @param message
     *            The message to include in the {@link AssertionError}
     * @param a
     * @param b
     * @throws AssertionError
     *             If comparison fails
     */
    public static final <T> void assertGreaterOrEqual(String message, Comparable<T> a, T b) throws AssertionError {
        if (a.compareTo(b) >= 0) {
            return;
        }

        throw new AssertionError(decorate(message, a, b));
    }

    /**
     * Asserts that {@literal a} is &gt; {@literal b}
     *
     * @param message
     *            The message to include in the {@link AssertionError}
     * @param a
     * @param b
     * @throws AssertionError
     *             If comparison fails
     */
    public static final <T> void assertGreater(String message, Comparable<T> a, T b) throws AssertionError {
        if (a.compareTo(b) > 0) {
            return;
        }
        throw new AssertionError(decorate(message, a, b));
    }

    /**
     * Asserts that {@literal a} is &lt;= {@literal b}
     *
     * @param message
     *            The message to include in the {@link AssertionError}
     * @param a
     * @param b
     * @throws AssertionError
     *             If comparison fails
     */
    public static final <T> void assertLessThanOrEqual(String message, Comparable<T> a, T b) throws AssertionError {
        if (a.compareTo(b) <= 0) {
            return;
        }

        throw new AssertionError(decorate(message, a, b));
    }

    /**
     * Asserts that {@literal a} is &lt; {@literal b}
     *
     * @param message
     *            The message to include in the {@link AssertionError}
     * @param a
     * @param b
     * @throws AssertionError
     *             If comparison fails
     */
    public static final <T> void assertLessThan(String message, Comparable<T> a, T b) throws AssertionError {
        if (a.compareTo(b) < 0) {
            return;
        }
        throw new AssertionError(decorate(message, a, b));
    }

    private static <T> String decorate(String message, Comparable<T> a, T b) {
        message = message.replace("{0}", a.toString());
        message = message.replace("{1}", b.toString());
        return message;
    }

    /**
     * Returns the path that should be used for the test. The path contains the
     * full path (appended to hostname+port) and must start with a slash.
     *
     * @param push
     *            true if "?debug" should be added
     * @param debug
     *            true if /run-push should be used instead of /run
     *
     * @return The URL path to the UI class to test
     */
    protected String getDeploymentPath() {
        Class<?> uiClass = getUIClass();
        if (uiClass != null) {
            return getDeploymentPath(uiClass);
        }
        throw new IllegalArgumentException("Unable to determine path for " + getClass().getCanonicalName());

    }

    /**
     * Returns the UI class the current test is connected to (or in special
     * cases UIProvider or LegacyApplication). Uses the enclosing class if the
     * test class is a static inner class to a UI class.
     *
     * Test which are not enclosed by a UI class must implement this method and
     * return the UI class they want to test.
     *
     * Note that this method will update the test name to the enclosing class to
     * be compatible with TB2 screenshot naming
     *
     * @return the UI class the current test is connected to
     */
    protected Class<?> getUIClass() {
        try {
            // Convention: SomeUITest uses the SomeUI UI class
            String uiClassName = getClass().getName().replaceFirst("Test$", "");
            Class<?> cls = Class.forName(uiClassName);
            if (isSupportedRunnerClass(cls)) {
                return cls;
            }
        } catch (Exception e) {
        }
        throw new RuntimeException(
                "Could not determine UI class. Ensure the test is named UIClassTest and is in the same package as the UIClass");
    }

    /**
     * @return true if the given class is supported by ApplicationServletRunner
     */
    @SuppressWarnings("deprecation")
    private boolean isSupportedRunnerClass(Class<?> cls) {
        if (UI.class.isAssignableFrom(cls)) {
            return true;
        }
        if (UIProvider.class.isAssignableFrom(cls)) {
            return true;
        }
        if (LegacyApplication.class.isAssignableFrom(cls)) {
            return true;
        }

        return false;
    }

    /**
     * Returns whether to run the test in debug mode (with the debug console
     * open) or not
     *
     * @return true to run with the debug window open, false by default
     */
    protected final boolean isDebug() {
        return debug;
    }

    /**
     * Sets whether to run the test in debug mode (with the debug console open)
     * or not.
     *
     * @param debug
     *            true to open debug window, false otherwise
     */
    protected final void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * Returns whether to run the test with push enabled (using /run-push) or
     * not. Note that push tests can and should typically be created using @Push
     * on the UI instead of overriding this method
     *
     * @return true if /run-push is used, false otherwise
     */
    protected final boolean isPush() {
        return push;
    }

    /**
     * Sets whether to run the test with push enabled (using /run-push) or not.
     * Note that push tests can and should typically be created using @Push on
     * the UI instead of overriding this method
     *
     * @param push
     *            true to use /run-push in the test, false otherwise
     */
    protected final void setPush(boolean push) {
        this.push = push;
    }

    /**
     * Returns the path for the given UI class when deployed on the test server.
     * The path contains the full path (appended to hostname+port) and must
     * start with a slash.
     *
     * This method takes into account {@link #isPush()} and {@link #isDebug()}
     * when the path is generated.
     *
     * @param uiClass
     * @param push
     *            true if "?debug" should be added
     * @param debug
     *            true if /run-push should be used instead of /run
     * @return The path to the given UI class
     */
    protected String getDeploymentPath(Class<?> uiClass) {
        String runPath = "/run";
        if (isPush()) {
            runPath = "/run-push";
        }

        if (UI.class.isAssignableFrom(uiClass) || UIProvider.class.isAssignableFrom(uiClass)
                || LegacyApplication.class.isAssignableFrom(uiClass)) {
            return runPath + "/" + uiClass.getCanonicalName();
        } else {
            throw new IllegalArgumentException(
                    "Unable to determine path for enclosing class " + uiClass.getCanonicalName());
        }
    }

    /**
     * Used to determine what URL to initially open for the test
     *
     * @return The base URL for the test. Does not include a trailing slash.
     */
    protected String getBaseURL() {
        return "http://" + getDeploymentHostname() + ":" + getDeploymentPort();
    }

    /**
     * Generates the application id based on the URL in a way compatible with
     * VaadinServletService.
     *
     * @param pathWithQueryParameters
     *            The path part of the URL, possibly still containing query
     *            parameters
     * @return The application ID string used in Vaadin locators
     */
    private String getApplicationId(String pathWithQueryParameters) {
        // Remove any possible URL parameters
        String pathWithoutQueryParameters = pathWithQueryParameters.replaceAll("\\?.*", "");
        if ("".equals(pathWithoutQueryParameters)) {
            return "ROOT";
        }

        // Retain only a-z and numbers
        return pathWithoutQueryParameters.replaceAll("[^a-zA-Z0-9]", "");
    }

    /**
     * Sleeps for the given number of ms but ensures that the browser connection
     * does not time out.
     *
     * @param timeoutMillis
     *            Number of ms to wait
     */
    protected void sleep(int timeoutMillis) {
        while (timeoutMillis > 0) {
            int d = Math.min(BROWSER_TIMEOUT_IN_MS, timeoutMillis);
            try {
                Thread.sleep(d);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            timeoutMillis -= d;

            // Do something to keep the connection alive
            getDriver().getTitle();
        }
    }

    /**
     * Called by the test runner whenever there is an exception in the test that
     * will cause termination of the test
     *
     * @param t
     *            the throwable which caused the termination
     */
    public void onUncaughtException(Throwable t) {
        // Do nothing by default

    }

    /**
     * Returns the mouse object for doing mouse commands
     *
     * @return Returns the mouse
     */
    public Mouse getMouse() {
        return ((HasInputDevices) getDriver()).getMouse();
    }

    /**
     * Returns the keyboard object for controlling keyboard events
     *
     * @return Return the keyboard
     */
    public Keyboard getKeyboard() {
        return ((HasInputDevices) getDriver()).getKeyboard();
    }

    public void hitButton(String id) {
        driver.findElement(By.id(id)).click();
    }

    protected void openDebugLogTab() {

        waitUntil(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(WebDriver input) {
                WebElement element = getDebugLogButton();
                return element != null;
            }
        }, 15);
        getDebugLogButton().click();
    }

    private WebElement getDebugLogButton() {
        return findElement(By.xpath("//button[@title='Debug message log']"));
    }

    protected void assertNoDebugMessage(Level level) {
        // class="v-debugwindow-row Level.getName()"
        List<WebElement> logElements = driver.findElements(By.xpath(String.format(
                "//div[@class='v-debugwindow-row %s']/span[@class='v-debugwindow-message']", level.getName())));
        if (!logElements.isEmpty()) {
            String logRows = "";
            for (WebElement e : logElements) {
                logRows += "\n" + e.getText();
            }
            Assert.fail("Found debug messages with level " + level.getName() + ": " + logRows);
        }
    }

    /**
     * Should the "require window focus" be enabled for Internet Explorer.
     * RequireWindowFocus makes tests more stable but seems to be broken with
     * certain commands such as sendKeys. Therefore it is not enabled by default
     * for all tests
     *
     * @return true, to use the "require window focus" feature, false otherwise
     */
    protected boolean requireWindowFocusForIE() {
        return false;
    }

    /**
     * Should the "enable persistent hover" be enabled for Internet Explorer.
     *
     * Persistent hovering causes continuous firing of mouse over events at the
     * last location the mouse cursor has been moved to. This is to avoid
     * problems where the real mouse cursor is inside the browser window and
     * Internet Explorer uses that location for some undefined operation
     * (http://
     * jimevansmusic.blogspot.fi/2012/06/whats-wrong-with-internet-explorer
     * .html)
     *
     * @return true, to use the "persistent hover" feature, false otherwise
     */
    protected boolean usePersistentHoverForIE() {
        return true;
    }

    /**
     * Should the "native events" be enabled for Internet Explorer.
     * <p>
     * Native events sometimes cause failure in clicking on buttons/checkboxes
     * but are possibly needed for some operations.
     *
     * @return true, to use "native events", false to use generated Javascript
     *         events
     */
    protected boolean useNativeEventsForIE() {
        return true;
    }

    // FIXME: Remove this once TB4 getRemoteControlName works properly
    private RemoteWebDriver getRemoteDriver() {
        WebDriver d = getDriver();
        if (d instanceof TestBenchDriverProxy) {
            try {
                Field f = TestBenchDriverProxy.class.getDeclaredField("actualDriver");
                f.setAccessible(true);
                return (RemoteWebDriver) f.get(d);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (d instanceof RemoteWebDriver) {
            return (RemoteWebDriver) d;
        }

        return null;

    }

    // FIXME: Remove this once TB4 getRemoteControlName works properly
    protected String getRemoteControlName() {
        try {
            RemoteWebDriver d = getRemoteDriver();
            if (d == null) {
                return null;
            }
            HttpCommandExecutor ce = (HttpCommandExecutor) d.getCommandExecutor();
            String hostName = ce.getAddressOfRemoteServer().getHost();
            int port = ce.getAddressOfRemoteServer().getPort();
            HttpHost host = new HttpHost(hostName, port);
            DefaultHttpClient client = new DefaultHttpClient();
            URL sessionURL = new URL(
                    "http://" + hostName + ":" + port + "/grid/api/testsession?session=" + d.getSessionId());
            BasicHttpEntityEnclosingRequest r = new BasicHttpEntityEnclosingRequest("POST",
                    sessionURL.toExternalForm());
            HttpResponse response = client.execute(host, r);
            JsonObject object = extractObject(response);
            URL myURL = new URL(object.getString("proxyId"));
            if ((myURL.getHost() != null) && (myURL.getPort() != -1)) {
                return myURL.getHost();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    protected boolean logContainsText(String string) {
        List<String> logs = getLogs();

        for (String text : logs) {
            if (text.contains(string)) {
                return true;
            }
        }

        return false;
    }

    protected List<String> getLogs() {
        VerticalLayoutElement log = $(VerticalLayoutElement.class).id("Log");
        List<LabelElement> logLabels = log.$(LabelElement.class).all();
        List<String> logTexts = new ArrayList<>();

        for (LabelElement label : logLabels) {
            logTexts.add(label.getText());
        }

        return logTexts;
    }

    private static JsonObject extractObject(HttpResponse resp) throws IOException {
        InputStream contents = resp.getEntity().getContent();
        StringWriter writer = new StringWriter();
        IOUtils.copy(contents, writer, "UTF8");
        return JsonUtil.parse(writer.toString());
    }

    protected void click(CheckBoxElement checkbox) {
        WebElement cb = checkbox.findElement(By.xpath("input"));
        if (BrowserUtil.isChrome(getDesiredCapabilities())) {
            testBenchElement(cb).click(0, 0);
        } else if (BrowserUtil.isFirefox(getDesiredCapabilities())) {
            // Firefox workaround
            getCommandExecutor().executeScript("arguments[0].click()", cb);
        } else {
            cb.click();
        }
    }

    protected void clickElement(WebElement element) {
        if (BrowserUtil.isFirefox(getDesiredCapabilities())) {
            // Workaround for Selenium/TB and Firefox 45 issue
            ((TestBenchElement) (element)).clickHiddenElement();
        } else {
            element.click();
        }
    }

    protected void contextClickElement(WebElement element) {
        if (BrowserUtil.isFirefox(getDesiredCapabilities())) {
            // Workaround for Selenium/TB and Firefox 45 issue
            getCommandExecutor().executeScript(
                    "var ev = document.createEvent('HTMLEvents'); ev.initEvent('contextmenu', true, false); arguments[0].dispatchEvent(ev);",
                    element);
        } else {
            new Actions(getDriver()).contextClick(element).perform();
        }
    }

    protected boolean isLoadingIndicatorVisible() {
        WebElement loadingIndicator = findElement(By.className("v-loading-indicator"));

        return loadingIndicator.isDisplayed();
    }

    protected void waitUntilLoadingIndicatorVisible() {
        waitUntil(input -> isLoadingIndicatorVisible());
    }

    protected void waitUntilLoadingIndicatorNotVisible() {
        waitUntil(input -> !isLoadingIndicatorVisible());
    }

    /**
     * Selects a menu item. By default, this will click on the menu item.
     *
     * @param menuCaption
     *            caption of the menu item
     */
    protected void selectMenu(String menuCaption) {
        selectMenu(menuCaption, true);
    }

    /**
     * Selects a menu item.
     *
     * @param menuCaption
     *            caption of the menu item
     * @param click
     *            <code>true</code> if should click the menu item;
     *            <code>false</code> if not
     */
    protected void selectMenu(String menuCaption, boolean click) {
        WebElement menuElement = getMenuElement(menuCaption);
        Dimension size = menuElement.getSize();
        new Actions(getDriver()).moveToElement(menuElement, size.width - 10, size.height / 2).perform();
        if (click) {
            new Actions(getDriver()).click().perform();
        }
    }

    /**
     * Finds the menu item from the DOM based on menu item caption.
     *
     * @param menuCaption
     *            caption of the menu item
     * @return the found menu item
     * @throws NoSuchElementException
     *             if menu item is not found
     */
    protected WebElement getMenuElement(String menuCaption) throws NoSuchElementException {
        return getDriver().findElement(By.xpath("//span[text() = '" + menuCaption + "']"));
    }

    /**
     * Selects a submenu described by a path of menus from the first MenuBar in
     * the UI.
     *
     * @param menuCaptions
     *            array of menu captions
     */
    protected void selectMenuPath(String... menuCaptions) {
        selectMenu(menuCaptions[0], true);

        // Move to the menu item opened below the menu bar.
        new Actions(getDriver()).moveByOffset(0, getMenuElement(menuCaptions[0]).getSize().getHeight()).perform();

        for (int i = 1; i < menuCaptions.length - 1; i++) {
            selectMenu(menuCaptions[i]);
            new Actions(getDriver()).moveByOffset(40, 0).build().perform();
        }
        selectMenu(menuCaptions[menuCaptions.length - 1], true);
    }

    /**
     * Asserts that an element is present
     *
     * @param by
     *            the locatore for the element
     */
    protected void assertElementPresent(By by) {
        Assert.assertTrue("Element is not present", isElementPresent(by));
    }

    /**
     * Asserts that an element is not present
     *
     * @param by
     *            the locatore for the element
     */
    protected void assertElementNotPresent(By by) {
        Assert.assertFalse("Element is present", isElementPresent(by));
    }

    /**
     * Asserts that no error notifications are shown. Requires the use of
     * "?debug" as exceptions are otherwise not shown as notifications.
     */
    protected void assertNoErrorNotifications() {
        Assert.assertFalse("Error notification with client side exception is shown",
                isNotificationPresent("error"));
    }

    /**
     * Asserts that no system notifications are shown.
     */
    protected void assertNoSystemNotifications() {
        Assert.assertFalse("Error notification with system error exception is shown",
                isNotificationPresent("system"));
    }

    /**
     * Asserts that a system notification is shown.
     */
    protected void assertSystemNotification() {
        Assert.assertTrue("Error notification with system error exception is not shown",
                isNotificationPresent("system"));
    }

    private boolean isNotificationPresent(String type) {
        if ("error".equals(type)) {
            Assert.assertTrue("Debug window must be open to be able to see error notifications",
                    isDebugWindowOpen());
        }
        return isElementPresent(By.className("v-Notification-" + type));
    }

    private boolean isDebugWindowOpen() {
        return isElementPresent(By.className("v-debugwindow"));
    }

    protected void assertNoHorizontalScrollbar(WebElement element, String errorMessage) {
        // IE rounds clientWidth/clientHeight down and scrollHeight/scrollWidth
        // up, so using clientWidth/clientHeight will fail if the element height
        // is not an integer
        int clientWidth = getClientWidth(element);
        int scrollWidth = getScrollWidth(element);
        boolean hasScrollbar = scrollWidth > clientWidth;

        Assert.assertFalse("The element should not have a horizontal scrollbar (scrollWidth: " + scrollWidth
                + ", clientWidth: " + clientWidth + "): " + errorMessage, hasScrollbar);
    }

    protected void assertNoVerticalScrollbar(WebElement element, String errorMessage) {
        // IE rounds clientWidth/clientHeight down and scrollHeight/scrollWidth
        // up, so using clientWidth/clientHeight will fail if the element height
        // is not an integer
        int clientHeight = getClientHeight(element);
        int scrollHeight = getScrollHeight(element);
        boolean hasScrollbar = scrollHeight > clientHeight;

        Assert.assertFalse("The element should not have a vertical scrollbar (scrollHeight: " + scrollHeight
                + ", clientHeight: " + clientHeight + "): " + errorMessage, hasScrollbar);
    }

    protected int getScrollHeight(WebElement element) {
        return ((Number) executeScript("return arguments[0].scrollHeight;", element)).intValue();
    }

    protected int getScrollWidth(WebElement element) {
        return ((Number) executeScript("return arguments[0].scrollWidth;", element)).intValue();
    }

    /**
     * Returns client height rounded up instead of as double because of IE9
     * issues: https://dev.vaadin.com/ticket/18469
     */
    protected int getClientHeight(WebElement e) {
        String script = "var cs = window.getComputedStyle(arguments[0]);"
                + "return Math.ceil(parseFloat(cs.height)+parseFloat(cs.paddingTop)+parseFloat(cs.paddingBottom));";
        return ((Number) executeScript(script, e)).intValue();
    }

    /**
     * Returns client width rounded up instead of as double because of IE9
     * issues: https://dev.vaadin.com/ticket/18469
     */
    protected int getClientWidth(WebElement e) {
        String script = "var cs = window.getComputedStyle(arguments[0]);"
                + "var h = parseFloat(cs.width)+parseFloat(cs.paddingLeft)+parseFloat(cs.paddingRight);"
                + "return Math.ceil(h);";

        return ((Number) executeScript(script, e)).intValue();
    }

    protected void assertElementsEquals(WebElement expectedElement, WebElement actualElement) {
        while (expectedElement instanceof WrapsElement) {
            expectedElement = ((WrapsElement) expectedElement).getWrappedElement();
        }
        while (actualElement instanceof WrapsElement) {
            actualElement = ((WrapsElement) actualElement).getWrappedElement();
        }

        Assert.assertEquals(expectedElement, actualElement);
    }

    protected WebElement getActiveElement() {
        return (WebElement) executeScript("return document.activeElement;");

    }

    protected void waitForThemeToChange(final String theme) {

        final WebElement rootDiv = findElement(By.xpath("//div[contains(@class,'v-app')]"));
        waitUntil(new ExpectedCondition<Boolean>() {

            @Override
            public Boolean apply(WebDriver input) {
                String rootClass = rootDiv.getAttribute("class").trim();

                return rootClass.contains(theme);
            }
        }, 30);
    }

}