org.richfaces.tests.metamer.ftest.AbstractWebDriverTest.java Source code

Java tutorial

Introduction

Here is the source code for org.richfaces.tests.metamer.ftest.AbstractWebDriverTest.java

Source

/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010-2016, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.tests.metamer.ftest;

import static java.text.MessageFormat.format;

import static org.jboss.test.selenium.support.url.URLUtils.buildUrl;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.Graphene;
import org.jboss.arquillian.graphene.condition.element.WebElementConditionFactory;
import org.jboss.arquillian.graphene.javascript.JavaScript;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.Point;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.interactions.Action;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.FindBy;
import org.richfaces.component.Positioning;
import org.richfaces.component.SwitchType;
import org.richfaces.fragment.common.AdvancedVisibleComponentIteractions;
import org.richfaces.fragment.common.Event;
import org.richfaces.fragment.common.Locations;
import org.richfaces.fragment.common.TextInputComponentImpl;
import org.richfaces.fragment.common.Utils;
import org.richfaces.fragment.common.VisibleComponent;
import org.richfaces.fragment.popupPanel.TextualRichFacesPopupPanel;
import org.richfaces.tests.configurator.unstable.UnstableTestConfigurator;
import org.richfaces.tests.metamer.Template;
import org.richfaces.tests.metamer.ftest.AbstractWebDriverTest.JSErrorStorage;
import org.richfaces.tests.metamer.ftest.attributes.AttributeEnum;
import org.richfaces.tests.metamer.ftest.extension.configurator.Configurator;
import org.richfaces.tests.metamer.ftest.extension.configurator.config.Config;
import org.richfaces.tests.metamer.ftest.extension.configurator.transformer.DataProviderTestTransformer;
import org.richfaces.tests.metamer.ftest.extension.multipleEventFiring.MultipleEventFirerer;
import org.richfaces.tests.metamer.ftest.extension.multipleEventFiring.MultipleEventFirererImpl;
import org.richfaces.tests.metamer.ftest.extension.tester.attributes.AttributeNotSetException;
import org.richfaces.tests.metamer.ftest.extension.tester.attributes.AttributesHandler;
import org.richfaces.tests.metamer.ftest.extension.tester.attributes.MultipleAttributesSetter;
import org.richfaces.tests.metamer.ftest.extension.tester.basic.TestResourcesProvider;
import org.richfaces.tests.metamer.ftest.webdriver.Attributes;
import org.richfaces.tests.metamer.ftest.webdriver.AttributesImpl;
import org.richfaces.tests.metamer.ftest.webdriver.MetamerPage;
import org.richfaces.tests.metamer.ftest.webdriver.MetamerPage.WaitRequestType;
import org.richfaces.tests.metamer.ftest.webdriver.UnsafeAttributes;
import org.richfaces.tests.metamer.ftest.webdriver.utils.MetamerJavascriptUtils;
import org.richfaces.tests.metamer.ftest.webdriver.utils.StopWatch;
import org.richfaces.tests.metamer.ftest.webdriver.utils.StringEqualsWrapper;
import org.testng.IHookCallBack;
import org.testng.ITestResult;
import org.testng.SkipException;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;

import com.google.common.base.Predicate;
import com.google.common.collect.Lists;

public abstract class AbstractWebDriverTest extends AbstractMetamerTest {

    protected static final int WAIT_TIME = 5;// s
    protected static final int MINOR_WAIT_TIME = 50;// ms
    protected static final int TRIES = 20;// for guardListSize and expectedReturnJS
    private static final String FACES_COMPONENTS_PATH = "faces/components/";

    @Drone
    protected WebDriver driver;

    @ArquillianResource
    protected JavascriptExecutor executor;

    @JavaScript
    protected MetamerJavascriptUtils jsUtils;

    @JavaScript
    protected JSErrorStorage jsErrorStorage;

    @FindBy(css = "input[id$=statusInput]")
    protected TextInputComponentImpl statusInput;

    @FindBy(css = "[id$=containerPopupPanel]")
    protected TextualRichFacesPopupPanel popupTemplate;

    @Page
    private MetamerPage metamerPage;

    protected DriverType driverType;

    private final Boolean[] booleans = { false, true };

    private Positioning positioning;// used for testJointPoint, testDirection
    protected static final EnumSet<Positioning> STRICT_POSITIONING = EnumSet.of(Positioning.bottomLeft,
            Positioning.bottomRight, Positioning.topLeft, Positioning.topRight);

    // this field is used by MetamerTestInfo to gather information about actual test method configuration
    private Config currentConfiguration;

    /**
     * @return method should return a String representing 'component/page' (case sensitive). E.g.:
     * <code>a4jActionListener/all.xhtml</code>, <code>a4jAjax/hCommandButton.xhtml</code>
     */
    public abstract String getComponentTestPagePath();

    @Override
    public URL getTestUrl() {
        return buildUrl(contextPath, FACES_COMPONENTS_PATH + getComponentTestPagePath());
    }

    protected void blur(WaitRequestType g) {
        getMetamerPage().blur(g);
    }

    public enum DriverType {

        FireFox(FirefoxDriver.class), InternetExplorer(InternetExplorerDriver.class), Chrome(ChromeDriver.class);

        private final Class<?> clazz;

        private DriverType(Class<?> clazz) {
            this.clazz = clazz;
        }

        public static DriverType getCurrentType(WebDriver wd) {
            for (DriverType type : values()) {
                if (type.clazz.isInstance(wd)) {
                    return type;
                }
            }
            throw new IllegalArgumentException("Unknown Driver");
        }
    }

    private final Configurator c = new Configurator();

    @DataProvider(name = DataProviderTestTransformer.DATAPROVIDER_NAME)
    public Object[][] provide(Method m) {
        return c.prepareConfigurationsForMethod(m, this);
    }

    @BeforeMethod(alwaysRun = true)
    public void configure() {
        currentConfiguration = c.configureNextStep();
    }

    /**
     * Overriding method from Arquillian to introduce new test execution behavior
     */
    @Override
    public void run(final IHookCallBack callBack, final ITestResult testResult) {
        super.run(UnstableTestConfigurator.getGuardedCallback(callBack), testResult);
    }

    @AfterMethod(alwaysRun = true)
    public void unconfigure() {
        if (currentConfiguration != null) {
            currentConfiguration.unconfigure();
        }
    }

    /**
     * Opens the tested page. If templates is not empty nor null, it appends url parameter with templates.
     */
    @BeforeMethod(alwaysRun = true, dependsOnMethods = "configure")
    public void loadPage() {
        if (driver == null) {
            throw new SkipException("webDriver isn't initialized");
        }
        // delete session
        driver.manage().deleteAllCookies();
        // move mouse to upper left corner of window so it will not stay over tested component
        moveMouseToUpperLeftCorner();
        // try to load the page
        for (int i = 0; i < 3; i++) {
            openPageWithCurrentConfiguration();
            // check page is correctlu loaded or repeat
            if (checkPageIsLoadedCorrectly()) {
                break;
            }
        }
        driverType = DriverType.getCurrentType(driver);
        // resize browser window to 1280x1024 or full screen
        driver.manage().window().setSize(new Dimension(1920, 1080));
    }

    protected void openPageWithCurrentConfiguration() {
        if (runInPortalEnv) {
            goToTestInPortal();
        } else {
            driver.get(buildUrl(getTestUrl() + "?templates=" + template.toString()).toExternalForm());
        }
    }

    /**
     * During opening of a page with templates set up, one can face IndexOutOfBoundsException.
     */
    private boolean checkPageIsLoadedCorrectly() {
        String pageSource = driver.getPageSource();
        return !pageSource.contains("java.lang.IndexOutOfBoundsException");
    }

    protected boolean isInPopupTemplate() {
        return template.contains(Template.RICHPOPUPPANEL);
    }

    protected void waitUtilNoTimeoutsArePresent() {
        Graphene.waitAjax().pollingEvery(200, TimeUnit.MILLISECONDS).until(new Predicate<WebDriver>() {

            @Override
            public boolean apply(WebDriver t) {
                return Boolean.parseBoolean(String.valueOf(executeJS("return window.areNoTimeoutsPresent();")));
            }
        });
    }

    private void moveMouseToUpperLeftCorner() {
        new Actions(driver).moveToElement(driver.findElement(Utils.BY_BODY), 0, 0).perform();
    }

    protected Attributes<BasicAttributes> getBasicAttributes() {
        return getAttributes();
    }

    protected Attributes<MetamerAttributes> getMetamerAttributes() {
        return getAttributes();
    }

    protected MetamerPage getMetamerPage() {
        return metamerPage;
    }

    /**
     * Sets component attribute to chosen @value. Always uses the first attribute table, unless a more specific attribute
     * locator provided (e.g. @attributename="table2:onChange").
     *
     * @param attributeName name of the attribute (attach prefix of the attribute table if needed another attribute table than
     * the first one)
     * @param value value, which String representation will be set to attribute input.
     */
    protected void setAttribute(String attributeName, Object value) {
        getUnsafeAttributes("").set(attributeName, value);
    }

    /**
     * Waiting method. Waits number of milis defined by @milis
     *
     * @param milis
     */
    protected static void waiting(long milis) {
        try {
            Thread.sleep(milis);
        } catch (InterruptedException ignored) {
        }
    }

    protected void assertNotPresent(WebElement element, String msg) {
        assertTrue(new WebElementConditionFactory(element).not().isPresent().apply(driver), msg);
    }

    protected void assertNotVisible(WebElement element, String msg) {
        assertTrue(new WebElementConditionFactory(element).not().isVisible().apply(driver), msg);
    }

    protected void assertVisible(AdvancedVisibleComponentIteractions<?> o) {
        assertTrue(o.advanced().isVisible());
    }

    protected void assertVisible(AdvancedVisibleComponentIteractions<?> o, String msg) {
        assertTrue(o.advanced().isVisible(), msg);
    }

    protected void assertNotVisible(AdvancedVisibleComponentIteractions<?> o) {
        assertFalse(o.advanced().isVisible());
    }

    protected void assertNotVisible(AdvancedVisibleComponentIteractions<?> o, String msg) {
        assertFalse(o.advanced().isVisible(), msg);
    }

    protected void assertNotVisible(VisibleComponent component, String msg) {
        assertFalse(component.isVisible(), msg);
    }

    protected void assertPresent(WebElement element, String msg) {
        assertTrue(new WebElementConditionFactory(element).isPresent().apply(driver), msg);
    }

    protected void assertVisible(VisibleComponent component, String msg) {
        assertTrue(component.isVisible(), msg);
    }

    protected void assertVisible(WebElement element, String msg) {
        assertTrue(new WebElementConditionFactory(element).isVisible().apply(driver), msg);
    }

    /**
     * Executes JavaScript script.
     *
     * @param script whole command that will be executed
     * @param args
     * @return may return a value or null (if expected (non-returning script) or if returning script fails)
     */
    protected Object executeJS(String script, Object... args) {
        return executor.executeScript(script, args);
    }

    /**
     * Tries to execute JavaScript script for few times and expects a
     *
     * @expectedValue as result. Returns single trimmed String with expected value or what it found or null.
     *
     * @param expectedValue expected return value of javaScript
     * @param script whole JavaScript that will be executed
     * @param args
     * @return single and trimmed string or null
     */
    protected String expectedReturnJS(String script, String expectedValue, Object... args) {
        String result = null;
        for (int i = 0; i < TRIES; i++) {
            try {
                Object executedScriptResult = executeJS(script, args);
                if (executedScriptResult != null) {
                    result = (String.valueOf(executedScriptResult)).trim();
                    if (result.equals(expectedValue)) {
                        return result;
                    }
                }
            } catch (WebDriverException ignored) {
                // e.g. when retrieved property is not defined
            }
            waiting(100);
        }
        return result;
    }

    /**
     * Helper method for testing data.
     *
     * @param triggeringAction
     */
    protected void testData(Action triggeringAction) {
        String testedValue = "RF5";
        attsSetter().setAttribute("data").toValue(testedValue).setAttribute("oncomplete")
                .toValue("data = event.data").asSingleAction().perform();
        Graphene.guardAjax(new ActionWrapper(triggeringAction)).perform();
        assertEquals(expectedReturnJS("return window.data;", testedValue), testedValue);
    }

    /**
     * Test attribute @limitRender. Sets @render attribute to '@this renderChecker' and @limitRender to 'true' and performs an
     * ajax-guarded action.
     */
    protected void testLimitRender(Action triggeringAction) {
        attsSetter().setAttribute("limitRender").toValue(true).setAttribute("render").toValue("@this renderChecker")
                .asSingleAction().perform();
        String renderCheckerText = metamerPage.getRenderCheckerOutputElement().getText();
        String requestTime = metamerPage.getRequestTimeElement().getText();
        Graphene.guardAjax(new ActionWrapper(triggeringAction)).perform();
        Graphene.waitGui().until().element(metamerPage.getRenderCheckerOutputElement()).text().not()
                .equalTo(renderCheckerText);
        Graphene.waitGui().until().element(metamerPage.getRequestTimeElement()).text().equalTo(requestTime);
    }

    /**
     * Same as testLimitRender, but tries to set @switchType or @mode to 'ajax'
     */
    protected void testLimitRenderWithSwitchTypeOrMode(Action triggeringAction) {
        setModeOrSwitchTypeToAjax();
        testLimitRender(triggeringAction);
    }

    /**
     * Test attribute @render. Sets mentioned attribute to '@this renderChecker' and performs an ajax-guarded action. * .
     */
    protected void testRender(Action triggeringAction) {
        getUnsafeAttributes().set("render", "@this renderChecker");
        String renderCheckerText = metamerPage.getRenderCheckerOutputElement().getText();
        String requestTime = metamerPage.getRequestTimeElement().getText();
        Graphene.guardAjax(new ActionWrapper(triggeringAction)).perform();
        Graphene.waitGui().until().element(metamerPage.getRenderCheckerOutputElement()).text().not()
                .equalTo(renderCheckerText);
        Graphene.waitGui().until().element(metamerPage.getRequestTimeElement()).text().not().equalTo(requestTime);
    }

    /**
     * Same as testRender, but tries to set @switchType or @mode to 'ajax'
     */
    protected void testRenderWithSwitchTypeOrMode(Action triggeringAction) {
        setModeOrSwitchTypeToAjax();
        testRender(triggeringAction);
    }

    private void setModeOrSwitchTypeToAjax() {
        UnsafeAttributes attributes = getUnsafeAttributes("");
        try {
            attributes.set("mode", SwitchType.ajax);
            return;
        } catch (RuntimeException ignored) {
        }
        try {
            attributes.set("switchType", SwitchType.ajax);
            return;
        } catch (RuntimeException ignored) {
        }
        fail("Neither of @mode nor @switchType was found.");
    }

    /**
     * Testing of HTMLAttribute (e.g. type).
     *
     * E.g. testHTMLAttribute(page.link, mediaOutputAttributes, MediaOutputAttributes.type, "text/html");
     *
     * @param element WebElement which will be checked for containment of tested attribute
     * @param attributes attributes instance which will be used for setting attribute
     * @param testedAttribute attribute which will be tested
     * @param value tested value of attribute
     */
    protected <T extends AttributeEnum> void testHTMLAttribute(WebElement element, Attributes<T> attributes,
            T testedAttribute, String value) {
        attributes.set(testedAttribute, value);
        String attString = Attribute2StringDecoder.decodeAttribute(testedAttribute);
        String valueOnPage = element.getAttribute(attString);
        if (new StringEqualsWrapper(value).equalsToSomeOfThis(null, "", "null")) {
            if (new StringEqualsWrapper(valueOnPage).notEqualsToSomeOfThis(null, "", "null")) {
                fail("Attribute " + testedAttribute.toString()
                        + " does not work properly, Value of attribute on page: '" + valueOnPage
                        + "', expected value '" + value + "'.");
            }
        } else if (!valueOnPage.contains(value)) {// Attribute has not been set correctly
            fail("Attribute " + testedAttribute.toString()
                    + " does not work properly, Value of attribute on page: '" + valueOnPage + "', expected value '"
                    + value + "'.");
        }
    }

    /**
     * Testing of HTMLAttribute (e.g. type).
     *
     * E.g. testHTMLAttribute(page.link, mediaOutputAttributes, MediaOutputAttributes.type, "text/html");
     *
     * @param element FutureTarget of WebElement which will be checked for containment of tested attribute
     * @param attributes attributes instance which will be used for setting attribute
     * @param testedAttribute attribute which will be tested
     * @param value tested value of attribute
     * @param actionAfterSettingOfAttribute action which will be performed after setting the attribute(e.g. open popup), if it
     * is null then it is skipped
     */
    protected <T extends AttributeEnum> void testHTMLAttribute(FutureTarget<WebElement> element,
            Attributes<T> attributes, T testedAttribute, String value, Action actionAfterSettingOfAttribute) {
        attributes.set(testedAttribute, value);
        if (actionAfterSettingOfAttribute != null) {
            actionAfterSettingOfAttribute.perform();
        }
        String attString = Attribute2StringDecoder.decodeAttribute(testedAttribute);
        String valueOnPage = element.getTarget().getAttribute(attString);
        if (new StringEqualsWrapper(value).equalsToSomeOfThis(null, "", "null")) {
            if (new StringEqualsWrapper(valueOnPage).notEqualsToSomeOfThis(null, "", "null")) {
                fail("Attribute " + testedAttribute.toString()
                        + " does not work properly, Value of attribute on page: '" + valueOnPage
                        + "', expected value '" + value + "'.");
            }
        } else if (!valueOnPage.contains(value)) {// Attribute has not been set correctly
            fail("Attribute " + testedAttribute.toString()
                    + " does not work properly, Value of attribute on page: '" + valueOnPage + "', expected value '"
                    + value + "'.");
        }
    }

    /**
     * Testing of HTMLAttribute (e.g. type). Expects that if an attribute is set to @value, then the value will be set to
     *
     * @anotherValue (e.g. null -> submit for a4j:commandButton)
     *
     * E.g. testHTMLAttribute(page.link, mediaOutputAttributes, MediaOutputAttributes.type, "text/html");
     *
     * @param element WebElement which will be checked for containment of tested attribute
     * @param attributes attributes instance which will be used for setting attribute
     * @param testedAttribute attribute which will be tested
     * @param value tested value of attribute
     * @param anotherValue value that will replace @value
     */
    protected <T extends AttributeEnum> void testHTMLAttribute(WebElement element, Attributes<T> attributes,
            T testedAttribute, String value, String anotherValue) {
        attributes.set(testedAttribute, value);
        String attString = Attribute2StringDecoder.decodeAttribute(testedAttribute);
        String valueOnPage = element.getAttribute(attString);
        if (new StringEqualsWrapper(value).equalsToSomeOfThis(null, "", "null")) {
            if (new StringEqualsWrapper(anotherValue).isNotSimilarToSomeOfThis(valueOnPage)) {
                fail("Attribute " + testedAttribute.toString()
                        + " does not work properly, Value of attribute on page: '" + valueOnPage
                        + "', expected value '" + anotherValue + "'.");
            }
        } else if (new StringEqualsWrapper(anotherValue).isNotSimilarToSomeOfThis(value)) {// Attribute has not been set
            // correctly
            fail("Attribute " + testedAttribute.toString()
                    + " does not work properly, Value of attribute on page: '" + valueOnPage + "', expected value '"
                    + anotherValue + "'.");
        }
    }

    /**
     * Testing of HTMLAttribute. The tested value is RichFaces 4.
     *
     * @param element WebElement which will be checked for containment of tested attribute
     * @param attributes attributes instance which will be used for setting attribute
     * @param testedAttribute attribute which will be tested
     */
    protected <T extends AttributeEnum> void testHTMLAttribute(WebElement element, Attributes<T> attributes,
            T testedAttribute) {
        testHTMLAttribute(element, attributes, testedAttribute, "RichFaces 4");
    }

    /**
     * Tests lang attribute of chosen component in Metamer. Page must contain an input for this component's attribute.
     *
     * @param element WebElement representing component.
     */
    protected void testLang(WebElement element) {
        final String TESTVALUE = "cz";
        String attLang;
        // set lang to TESTVALUE
        getBasicAttributes().set(BasicAttributes.lang, TESTVALUE);
        // get attribute lang of element
        String lang1 = element.getAttribute("xml:lang");
        String lang2 = element.getAttribute("lang");

        attLang = (lang1 == null || lang1.isEmpty() ? lang2 : lang1);
        assertEquals(attLang, TESTVALUE, "Attribute xml:lang should be present.");
    }

    /**
     * Helper method for testing of delays (showDelay, hideDelay). Runs the @actionWithDelay 4 times and measure time spent in
     * it. Then count a median from these 4 values and asserts it to the @expectedDelay with 50% tolerance.
     *
     * @param actionBefore action before the measured action. Can be used for e.g. close/open menu. Can be null.
     * @param actionWithDelay the measured action. Can be e.g. open/close menu.
     * @param attributeName name of the measured attribute (e.g. hideDelay, showDelay).
     * @param expectedDelayInMillis expected delay spent in @actionWithDelay and also a value that will be set in attribute with
     * name @attributeName
     */
    protected void testDelay(final Action actionBefore, final Action actionWithDelay, String attributeName,
            long expectedDelayInMillis) {
        getUnsafeAttributes("").set(attributeName, expectedDelayInMillis);
        double tolerance = expectedDelayInMillis == 0 ? 500 : expectedDelayInMillis * 0.5;
        int cycles = 4;
        List<Long> delays = Lists.newArrayList();
        for (int i = 0; i < cycles; i++) {
            //This is debug output, to determine in which cycle test falls.
            System.out.println(format("Tested delay: {0}, cycle: {1}", expectedDelayInMillis, i));
            if (actionBefore != null) {
                actionBefore.perform();
            }
            delays.add(StopWatch.watchTimeSpentInAction(actionWithDelay).inMillis().longValue());
        }
        Number median = countMedian(delays);
        assertEquals(median.doubleValue(), expectedDelayInMillis, tolerance,
                "The delay is not in tolerance. Median of delays was " + median);
    }

    /**
     * A helper method for testing attribute "dir". It tries null, ltr and rtl.
     *
     * @param element WebElement reference of tested element
     */
    protected void testDir(WebElement element) {
        testHTMLAttribute(element, getBasicAttributes(), BasicAttributes.dir, "null");
        testHTMLAttribute(element, getBasicAttributes(), BasicAttributes.dir, "ltr");
        testHTMLAttribute(element, getBasicAttributes(), BasicAttributes.dir, "rtl");
    }

    /**
     * Use with <code>@UseWithField(field = "positioning",valuesFrom = FROM_ENUM, value = "")</code>.
     *
     * @param showAction action which will show the tested menu and will return it as a WebElement.
     */
    protected void testDirection(ShowElementAndReturnAction showAction) {
        testPositioning(showAction, 0, 0);
    }

    /**
     * Use with <code>@UseWithField(field = "positioning",valuesFrom = FROM_ENUM, value = "")</code>.
     *
     * @param maxOffSetX width of the menu item, input, etc. from which will be the menu displayed
     * @param maxOffsetY height of the menu item, input, etc. from which will be the menu displayed
     * @param showAction action which will show the tested menu and will return it as a WebElement.
     */
    protected void testJointPoint(int maxOffSetX, int maxOffsetY, ShowElementAndReturnAction showAction) {
        testPositioning(showAction, maxOffSetX, maxOffsetY);
    }

    private void testPositioning(ShowElementAndReturnAction showAction, int maxOffSetX, int maxOffsetY) {
        int tolerance = 10;
        boolean isDirectionTest = (maxOffsetY == 0 && maxOffSetX == 0);
        UnsafeAttributes atts = getUnsafeAttributes("");

        // determine tested attribute
        String testedAtt = isDirectionTest ? "direction" : "jointPoint";

        // set reference values
        if (atts.hasAttribute("jointPoint")) {
            attsSetter().setAttribute("direction").toValue(Positioning.bottomRight).setAttribute("jointPoint")
                    .toValue(Positioning.bottomRight).asSingleAction().perform();
        } else {// no @jointPoint attribute, set direction only
            atts.set("direction", Positioning.bottomRight);
        }
        // get reference locations
        Locations locationsBottomRight = Utils.getLocations(showAction.perform());

        // setup tested attribute
        atts.set(testedAtt, positioning);
        if (atts.get(testedAtt).equals(Positioning.bottomRight.toString())) {
            // reload the page to dismiss the popup
            openPageWithCurrentConfiguration();
        }
        // get new locations, with tested value of @direction/@jointPoint
        Locations locationsAfterReposition = Utils.getLocations(showAction.perform());

        int widthChange = maxOffSetX;
        int heightChange = maxOffsetY;
        if (isDirectionTest) {
            widthChange = locationsBottomRight.getWidth();
            heightChange = locationsBottomRight.getHeight();
        }

        // check
        if (STRICT_POSITIONING.contains(positioning)) {
            Utils.tolerantAssertLocationsEquals(
                    getLocationsOfBottomRightAfterPositioningChanges(locationsBottomRight, positioning, widthChange,
                            heightChange),
                    locationsAfterReposition, tolerance, tolerance, "");
        } else {// some '*auto*' option
            // cycle through all strict directions, one must be the same as the '*auto*',
            // which  one it will be depends on browser/screen resolution and actual position
            for (Positioning pos : STRICT_POSITIONING) {
                try {
                    Utils.tolerantAssertLocationsEquals(
                            getLocationsOfBottomRightAfterPositioningChanges(locationsBottomRight, pos, widthChange,
                                    heightChange),
                            locationsAfterReposition, tolerance, tolerance, "");
                    return;
                } catch (AssertionError ignored) {
                }
            }
            fail("No position was close enough for direction " + positioning.toString());
        }
    }

    private Locations getLocationsOfBottomRightAfterPositioningChanges(Locations locations, Positioning jointPoint,
            int width, int height) {
        switch (jointPoint) {
        case topLeft:
            return locations.moveAllBy(-width, -height);
        case topRight:
            return locations.moveAllBy(0, -height);
        case bottomLeft:
            return locations.moveAllBy(-width, 0);
        case bottomRight:
            return locations;
        default:
            throw new UnsupportedOperationException("Not supported positioning: " + jointPoint);
        }
    }

    /**
     * @param showAction action which will show the tested menu and will return it as a WebElement.
     */
    protected void testHorizontalOffset(ShowElementAndReturnAction showAction) {
        testOffset(false, showAction);
    }

    /**
     * @param showAction action which will show the tested menu and will return it as a WebElement.
     */
    protected void testVerticalOffset(ShowElementAndReturnAction showAction) {
        testOffset(true, showAction);
    }

    private void testOffset(boolean isVerticalOffset, ShowElementAndReturnAction showAction) {
        int tolerance = 10;
        int testedOffset = 50;

        try {
            getUnsafeAttributes("").set("direction", Positioning.bottomRight);
        } catch (AttributeNotSetException ignored) {
        }
        try {
            getUnsafeAttributes("").set("jointPoint", Positioning.bottomRight);
        } catch (AttributeNotSetException ignored) {
        }

        Locations locationsBefore = Utils.getLocations(showAction.perform());

        getUnsafeAttributes("").set((isVerticalOffset ? "verticalOffset" : "horizontalOffset"), testedOffset);
        Locations locationsAfter = Utils.getLocations(showAction.perform());

        Utils.tolerantAssertLocationsEquals(locationsAfter, locationsBefore
                .moveAllBy((isVerticalOffset ? 0 : testedOffset), (isVerticalOffset ? testedOffset : 0)), tolerance,
                tolerance, "");
    }

    /**
     * A helper method for testing JavaScripts events. It sets "metamerEvents += "testedAttribute" to the input field of the
     * tested attribute and fires the event @event using jQuery on the given element @element. Then it checks if the event was
     * fired. This method should only be used for attributes consistent with DOM events (e.g. (on)click, (on)change...).
     *
     * @param element WebElement on which will be the event triggered
     * @param attributes attributes instance which will be used for setting attribute
     * @param testedAttribute attribute which will be tested
     */
    protected <T extends AttributeEnum> void testFireEventWithJS(WebElement element, Attributes<T> attributes,
            final T testedAttribute) {
        attributes.set(testedAttribute, "metamerEvents += \"" + testedAttribute.toString() + " \"");
        executeJS("metamerEvents = \"\";");
        Event e = new Event(testedAttribute.toString().substring(2));// remove prefix "on"
        fireEvent(element, e);

        Graphene.waitGui().withMessage("Event " + e + " does not work.").until(new Predicate<WebDriver>() {
            @Override
            public boolean apply(WebDriver wd) {
                String metamerEvents = executor.executeScript("return metamerEvents").toString().trim();
                return testedAttribute.toString().equals(metamerEvents);
            }
        });
    }

    /**
     * A helper method for testing JavaScripts events. It sets "metamerEvents += "testedAttribute" to the input field of the
     * tested attribute and fires the event @event using jQuery on the element @element. Then it checks if the event was fired.
     *
     * @event using jQuery on the element
     * @element. Then it checks if the event was fired.
     *
     * @see testFireEventWithJS(WebElement element, Attributes<T> attributes, T testedAttribute)
     * @param element WebElement on which will be the event triggered
     * @param event event wich will be triggered
     * @param attributes attributes instance which will be used for setting attribute
     * @param testedAttribute attribute which will be tested
     */
    protected <T extends AttributeEnum> void testFireEventWithJS(WebElement element, Event event,
            Attributes<T> attributes, T testedAttribute) {
        attributes.set(testedAttribute, "metamerEvents += \"" + testedAttribute.toString() + " \"");
        executeJS("metamerEvents = \"\";");
        fireEvent(element, event);
        String returnedString = expectedReturnJS("return metamerEvents", testedAttribute.toString());
        assertEquals(returnedString, testedAttribute.toString(), "Event " + event + " does not work.");
    }

    /**
     * A helper method for testing events. It sets "metamerEvents += "@testedAttribute" to the input field and fires the event
     * using Actions. Then it checks if the event was fired.
     *
     * @param attributes attributes instance which will be used for setting attribute
     * @param testedAttribute attribute which will be tested
     * @param eventFiringAction selenium action which leads to launch the tested event,
     */
    protected <T extends AttributeEnum> void testFireEvent(Attributes<T> attributes, T testedAttribute,
            Action eventFiringAction) {
        attributes.set(testedAttribute, "metamerEvents += \"" + testedAttribute.toString() + " \"");
        executeJS("metamerEvents = \"\";");
        try {
            eventFiringAction.perform();
            String returnedString = expectedReturnJS("return metamerEvents", testedAttribute.toString());
            assertEquals(returnedString, testedAttribute.toString(),
                    "Event " + testedAttribute.toString() + " does not work.");
        } finally {
            if (testedAttribute.toString().toLowerCase().endsWith("mousedown")) {
                tryToReleaseMouseButton();
            }
        }

    }

    /**
     * A helper method for testing JavaScripts events.
     *
     * @param event JavaScript event to be tested
     * @param element element on which will be the event triggered
     */
    protected void testFireEvent(Event event, WebElement element) {
        testFireEvent(event, element, event.getEventName());
    }

    /**
     * A helper method for testing JavaScripts events.
     *
     * @param event JavaScript event to be tested
     * @param element FutureTarget of WebElement on which will be the event triggered
     * @param actionBeforeFiringTheEvent action which will be performed before firing the event
     */
    protected void testFireEvent(final Event event, final FutureTarget<WebElement> element,
            final Action actionBeforeFiringTheEvent) {
        testFireEvent(event.getEventName(), new Action() {
            @Override
            public void perform() {
                if (actionBeforeFiringTheEvent != null) {
                    actionBeforeFiringTheEvent.perform();
                }
                fireEvent(element.getTarget(), event);
            }
        });
    }

    /**
     * A helper method for testing JavaScripts events.
     *
     * @param event JavaScript event to be tested
     * @param element element on which will be the event triggered (must be present before test, or a proxy)
     * @param attributeName name of the attribute that should be set
     */
    protected void testFireEvent(final Event event, final WebElement element, String attributeName) {
        testFireEvent(attributeName, new Action() {
            @Override
            public void perform() {
                fireEvent(element, event);
            }
        });
    }

    /**
     * A helper method for testing events.
     *
     * @param attributeName name of the attribute that should be set (i.e. inputselect, onselect ; can be without the prefix
     * 'on')
     * @param eventFiringAction action which will be performed to trigger the event
     */
    protected void testFireEvent(String attributeName, Action eventFiringAction) {
        setAttribute((attributeName.startsWith("on") ? attributeName : "on" + attributeName),
                "metamerEvents += \"" + attributeName + " \"");
        // clear/init events
        executeJS("metamerEvents = \"\";");
        // trigger event
        try {
            eventFiringAction.perform();
            // check
            assertEquals(expectedReturnJS("return metamerEvents", attributeName), attributeName,
                    "Attribute " + attributeName + " does not work.");
        } finally {
            if (attributeName.toLowerCase().endsWith("mousedown")) {
                tryToReleaseMouseButton();
            }
        }
    }

    private void tryToReleaseMouseButton() {
        try {
            new Actions(driver).release().perform();
        } catch (WebDriverException ignored) {
        }
    }

    /**
     * Method for firing JavaScript events on given element using jQuery.
     *
     * @param element
     * @param event
     */
    protected void fireEvent(WebElement element, Event event) {
        Utils.triggerJQ(executor, event.getEventName(), element);
    }

    /**
     * Helper method for testing label's text changing. At first it sets "RichFaces 4" to the <code>testedAttribute</code>
     * input, then fires <code>labelChangeAction</code>(if some), then waits for the visibility of <code>element</code> and
     * finally checks if the label (<code>getText()</code> method) of <code>element</code> was changed as expected.
     *
     * @param element element which <code>getText()</code> method will be used for checking of label text
     * @param attributes attributes instance which will be used for setting attribute
     * @param testedAttribute attribute which will be tested
     * @param labelChangeAction action which will change the label (if no action needed use <code>null</code> or empty Action)
     */
    protected <T extends AttributeEnum> void testLabelChanges(WebElement element, Attributes<T> attributes,
            T testedAttribute, Action labelChangeAction) {
        testLabelChanges(FutureWebElement.of(element), attributes, testedAttribute, labelChangeAction);
    }

    protected <T extends AttributeEnum> void testLabelChanges(FutureTarget<WebElement> futureTarget,
            Attributes<T> attributes, T testedAttribute, Action labelChangeAction) {
        String rf = "RichFaces 4";
        attributes.set(testedAttribute, rf);
        if (labelChangeAction != null) {
            labelChangeAction.perform();
        }
        Graphene.waitModel().until().element(futureTarget.getTarget()).is().visible();
        Graphene.waitModel().until(testedAttribute + " does not work, label has not changed.")
                .element(futureTarget.getTarget()).text().equalTo(rf);
    }

    protected <T extends AttributeEnum> void testLabelChanges(String attributeName,
            FutureTarget<WebElement> futureTarget, Action labelChangeAction) {
        String rf = "RichFaces 4";
        setAttribute(attributeName, rf);
        if (labelChangeAction != null) {
            labelChangeAction.perform();
        }
        Graphene.waitModel().until().element(futureTarget.getTarget()).is().visible();
        Graphene.waitModel().until(attributeName + " does not work, label has not changed.")
                .element(futureTarget.getTarget()).text().equalTo(rf);
    }

    /**
     * Helper method for testing of attribute 'status'. At first it sets @status to "statusChecker", then saves Metamer's
     * 'statusCheckerOutput' time, then fires <code>statusChangingAction</code> (if not null), and finally checks if Metamer's
     * 'statusCheckerOutput' time was changed before Graphene.waitModel() interval expires.
     *
     * @param statusChangingAction action that will change the status. Can be null.
     */
    protected void testStatus(Action statusChangingAction) {
        String checker = "statusChecker";
        // set attribute
        getUnsafeAttributes("").set("status", checker);

        String statusCheckerTimeBefore = metamerPage.getStatusCheckerOutputElement().getText();
        if (statusChangingAction != null) {
            Graphene.guardAjax(new ActionWrapper(statusChangingAction)).perform();
        }
        Graphene.waitModel().until().element(metamerPage.getStatusCheckerOutputElement()).text().not()
                .equalTo(statusCheckerTimeBefore);
    }

    /**
     * A helper method for testing attribute "style" or similar. It sets "background-color: yellow; font-size: 1.5em;" to the
     * input field and checks that it was changed on the page.
     *
     * @param element WebElement reference of tested element
     * @param attribute name of the attribute that will be set (e.g. style, headerStyle, itemContentStyle)
     */
    protected void testStyle(final WebElement element, BasicAttributes attribute) {
        final String value = "background-color: yellow;";
        testHTMLAttribute(element, getBasicAttributes(), attribute, value);
    }

    /**
     * A helper method for testing attribute "style". It sets "background-color: yellow; font-size: 1.5em;" to the input field
     * and checks that it was changed on the page.
     *
     * @param element WebElement reference of tested element
     */
    protected void testStyle(final WebElement element) {
        testStyle(element, BasicAttributes.style);
    }

    /**
     * A helper method for testing attribute "class" or similar. It sets "metamer-ftest-class" to the input field and checks
     * that it was changed on the page.
     *
     * @param element WebElement reference of tested element
     * @param attribute name of the attribute that will be set (e.g. styleClass, headerClass, itemContentClass)
     */
    protected void testStyleClass(WebElement element, BasicAttributes attribute) {
        final String styleClass = "metamer-ftest-class";
        testHTMLAttribute(element, getBasicAttributes(), attribute, styleClass);
    }

    /**
     * A helper method for testing attribute "class". It sets "metamer-ftest-class" to the input field and checks that it was
     * changed on the page.
     *
     * @param element locator of tested element
     */
    protected void testStyleClass(WebElement element) {
        testStyleClass(element, BasicAttributes.styleClass);
    }

    /**
     * A helper method for testing attribute "title".
     *
     * @param element WebElement reference of tested element
     */
    protected void testTitle(WebElement element) {
        final String testTitle = "RichFaces 4";
        testHTMLAttribute(element, getBasicAttributes(), BasicAttributes.title, testTitle);
    }

    /**
     * Helper method for testing table's style class attributes (e.g. cellClass, columnFooterClass)
     *
     * @param attributeName
     * @param elementClassName
     * @param expectedCount
     */
    protected void testTableStyleClass(String attributeName, String elementClassName, int expectedCount) {
        String klass = "metamer-ftest-class";
        setAttribute(attributeName, klass);
        List<WebElement> elems = driver.findElements(By.className(elementClassName));
        assertTrue(expectedCount > 0);
        assertEquals(elems.size(), expectedCount);
        for (WebElement elem : elems) {
            assertTrue(elem.getAttribute("class").contains(klass));
        }
    }

    /**
     * Checks that there is no 404 error in browser's console.
     *
     * @param beforeCheckAction action berformed before checking. Can be null (no action will be performed).
     */
    protected void checkNoResourceErrorPresent(Action beforeCheckAction) {
        // perform action
        if (beforeCheckAction != null) {
            beforeCheckAction.perform();
        }
        final String errMsg = "failed to load resource";
        for (String msg : jsErrorStorage.getMessages()) {
            assertFalse(msg.contains(errMsg),
                    "There should be no resource error message in browser's console. Had: <" + msg + ">.");
        }
    }

    /**
     * Tries to check and wait for correct size (@size) of list. Depends on list of WebElements decorated with
     * StaleReferenceAwareFieldDecorator.
     *
     * @param list input list
     * @param size expected size of list
     * @return list with or without expected size
     */
    protected List<WebElement> guardListSize(List<WebElement> list, int size) {
        boolean lastCheckWithModifications;
        int checkedSize = list.size();
        for (int i = 0; i < TRIES; i++) {
            if (checkedSize < list.size()) {
                checkedSize = list.size();
                lastCheckWithModifications = true;
            } else {
                lastCheckWithModifications = false;
            }
            if (checkedSize >= size && !lastCheckWithModifications) {
                // last check
                waiting(MINOR_WAIT_TIME);
                list.size();
                return list;
            }
            waiting(MINOR_WAIT_TIME);
        }
        return list;
    }

    protected <T extends AttributeEnum> Attributes<T> getAttributes(String attributesTableId) {
        return AttributesImpl.<T>getAttributesFor(testResourcesProvider, attributesTableId);
    }

    protected <T extends AttributeEnum> Attributes<T> getAttributes() {
        return getAttributes("");
    }

    protected UnsafeAttributes getUnsafeAttributes(String attributesTableId) {
        return new AttributesImpl<AttributeEnum>(testResourcesProvider, attributesTableId);
    }

    protected UnsafeAttributes getUnsafeAttributes() {
        return getUnsafeAttributes("");
    }

    /**
     * Method used to run selenium test in portal environment.
     */
    private void goToTestInPortal() {
        driver.get(String.format("%s://%s:%s/%s", contextPath.getProtocol(), contextPath.getHost(),
                contextPath.getPort(), "portal/classic/metamer"));
        try {
            driver.findElement(By.cssSelector("a[id$='controlsForm:goHomeLink']")).click();
            // JSF form works only on home page
        } catch (NoSuchElementException ex) {
        }
        JavascriptExecutor js = (JavascriptExecutor) driver;
        String setTextQuery = "document.querySelector(\"input[id$='linksForm:%s']\").value = '%s';";
        String testUrl = getTestUrl().toExternalForm().substring(getTestUrl().toExternalForm().indexOf("faces"));
        js.executeScript(String.format(setTextQuery, "linkToTest", testUrl));
        js.executeScript(String.format(setTextQuery, "template", template.toString()));
        js.executeScript("document.querySelector(\"a[id$='linksForm:redirectToPortlet']\").click()");
    }

    public void testRequestEventsBefore(String... events) {
        String testedEventTrueName;
        MultipleAttributesSetter attsSetter = attsSetter();
        for (String event : events) {
            testedEventTrueName = event;
            if (!testedEventTrueName.startsWith("on")) {
                testedEventTrueName = format("on{0}", testedEventTrueName);
            }
            attsSetter.setAttribute(testedEventTrueName).toValue(format(
                    "sessionStorage.setItem(\"metamerEvents\", sessionStorage.getItem(\"metamerEvents\") + \"{0} \")",
                    event));
        }
        attsSetter.asSingleAction().perform();
        cleanMetamerEventsVariable();
    }

    public void testRequestEventsAfter(final String... events) {
        Graphene.waitModel().until(new Predicate<WebDriver>() {
            private String actualEvents;
            private int lastNumberOfEvents;

            @Override
            public boolean apply(WebDriver arg0) {
                actualEvents = ((String) executeJS("return sessionStorage.getItem(\"metamerEvents\")"));
                lastNumberOfEvents = (StringUtils.isBlank(actualEvents) ? 0 : actualEvents.split(" ").length);
                return lastNumberOfEvents == events.length;
            }

            @Override
            public String toString() {
                return format("number of events is equal to {0}, found {1}. Actual events: {2}", events.length,
                        lastNumberOfEvents, actualEvents);
            }

        });
        String[] actualEvents = ((String) executeJS("return sessionStorage.getItem(\"metamerEvents\")")).split(" ");
        assertEquals(actualEvents, events, String.format("The events (%s) don't came in right order (%s)",
                Arrays.deepToString(actualEvents), Arrays.deepToString(events)));
    }

    public MultipleEventFirerer getMultipleEventsFirerer() {
        return new MultipleEventFirererImpl(executor);
    }

    public void cleanMetamerEventsVariable() {
        // since metamerEvents variable stored on session too, make sure that cleaned both of them
        executeJS("window.metamerEvents = \"\";");
        executeJS("sessionStorage.setItem('metamerEvents',\"\")");
    }

    public static <T extends Number & Comparable<T>> Number countMedian(List<T> values) {
        assertTrue(values.size() > 0);
        if (values.size() == 1) {
            return values.get(0);
        }

        final List<T> copy = Lists.newArrayList(values);
        Collections.sort(copy);

        int middleIndex = (copy.size() - 1) / 2;

        double result = copy.get(middleIndex).doubleValue();
        if (copy.size() % 2 == 0) {
            result = (result + copy.get(middleIndex + 1).doubleValue()) / 2.0;
        }
        final Double median = Double.valueOf(result);
        return new Number() {
            private static final long serialVersionUID = 1L;

            @Override
            public int intValue() {
                return median.intValue();
            }

            @Override
            public long longValue() {
                return median.longValue();
            }

            @Override
            public float floatValue() {
                return median.floatValue();

            }

            @Override
            public double doubleValue() {
                return median.doubleValue();

            }

            @Override
            public String toString() {
                return median.doubleValue() + " from values(sorted) " + copy.toString() + '.';
            }
        };
    }

    /**
     * Decoder for Attributes. Converts given Attribute to String. If Attribute ends with 'class' or 'style', then it returns
     * the correct one, when the attribute does not end with none of those, then it returns toString() method of attribute
     */
    public static class Attribute2StringDecoder {

        private static final String[] ATTRIBUTES = { "class", "classes", "style" };

        public static <T extends AttributeEnum> String decodeAttribute(T testedAttribute) {
            String testedAtt = testedAttribute.toString();
            if (testedAtt.length() > 6) {
                // get the ending
                String tmp = testedAtt.toLowerCase();
                for (String string : ATTRIBUTES) {
                    if (tmp.lastIndexOf(string) > 0) {// contains an attribute to decode
                        if (string.equalsIgnoreCase(ATTRIBUTES[0]) || string.equalsIgnoreCase(ATTRIBUTES[1])) {
                            return ATTRIBUTES[0];
                        } else if (string.equalsIgnoreCase(ATTRIBUTES[2])) {
                            return ATTRIBUTES[2];
                        } else {
                            throw new RuntimeException("Cannot decode attribute " + testedAtt);
                        }
                    }
                }
            }
            return testedAtt;
        }
    }

    /**
     * Abstract ReloadTester for testing component's state after reloading the page
     *
     * @param <T> the type of input values which will be set, sent and then verified
     */
    public abstract class ReloadTester<T> {

        public abstract void doRequest(T inputValue);

        public abstract void verifyResponse(T inputValue);

        public abstract T[] getInputValues();

        public void testRerenderAll() {
            for (T inputValue : getInputValues()) {
                doRequest(inputValue);
                verifyResponse(inputValue);
                getMetamerPage().rerenderAll();
                verifyResponse(inputValue);
            }
        }

        public void testFullPageRefresh() {
            for (T inputValue : getInputValues()) {
                doRequest(inputValue);
                verifyResponse(inputValue);
                getMetamerPage().fullPageRefresh();
                verifyResponse(inputValue);
            }
        }
    }

    protected interface ShowElementAndReturnAction {

        WebElement perform();
    }

    public interface FutureTarget<T> {

        T getTarget();
    }

    /**
     * Wrapper for anonymous actions, so it can be guarded by graphene.
     */
    public static class ActionWrapper implements Action {

        private final Action a;

        public ActionWrapper(Action a) {
            this.a = a;
        }

        @Override
        public void perform() {
            a.perform();
        }
    }

    public static final class FutureWebElement {

        private FutureWebElement() {
        }

        public static FutureTarget<WebElement> of(final WebElement element) {
            return new FutureTarget<WebElement>() {
                @Override
                public WebElement getTarget() {
                    return element;
                }
            };
        }

        public static FutureTarget<WebElement> of(final By by, final SearchContext context) {

            return new FutureTarget<WebElement>() {
                @Override
                public WebElement getTarget() {
                    return context.findElement(by);
                }
            };
        }
    }

    /**
     * Helper class for testing of popup menu's hide/show delays. Executes the measuring in browser using JavaScript, JS API of
     * the component and attributes 'onhide' and 'onshow' of the component.
     */
    protected class MenuDelayTester {

        private static final String ATTRIBUTE_TIME_NAME = "delayTime";
        private static final String ATTRIBUTE_TRIGGERED_NAME = "delayAttributeTriggered";
        private static final String HIDE_DELAY = "hideDelay";
        private static final String ON_HIDE = "onhide";
        private static final String ON_SHOW = "onshow";
        private static final String SHOW_DELAY = "showDelay";
        private final String GET_TIME_SCRIPT = format("return window.{0};", ATTRIBUTE_TIME_NAME);
        private final String MEASURING_SCRIPT_TEMPLATE = String
                .format("window.%s = false;" + "var event = jQuery.Event(\"click\");"// event type does not matter, but has to be non-empty
                        + "event.pageX = {0};" + "event.pageY = {1};" + "RichFaces.component(\"{2}\").{3}(event);"
                        + "window.%s = new Date().getTime();", ATTRIBUTE_TRIGGERED_NAME, ATTRIBUTE_TIME_NAME);
        private final String PREPARATION_SCRIPT = format(
                "window.{0}=new Date().getTime() - window.{0}; window.{1}=true;", ATTRIBUTE_TIME_NAME,
                ATTRIBUTE_TRIGGERED_NAME);

        public MenuDelayTester() {
        }

        private String getMeasuringScript(WebElement rootElement, boolean isHideDelay, Event[] triggerEvents) {
            Point location = rootElement.getLocation();
            Locations locations = Utils.getLocations(rootElement);
            int x = location.x + locations.getWidth() / 2;
            int y = location.y + locations.getHeight() / 2;
            String id = rootElement.getAttribute("id");
            String first = isHideDelay ? "show" : "hide";
            StringBuilder sb = new StringBuilder(MEASURING_SCRIPT_TEMPLATE);
            for (Event e : triggerEvents) {
                sb.append("event = jQuery.Event(\"").append(e)
                        .append("\");event.pageX={0};event.pageY = {1};jQuery(arguments[0]).trigger(event);");
            }
            return format(sb.toString(), x, y, id, first);
        }

        private void testDelay(boolean isHideDelay, final WebElement menuRootElement,
                final long expectedDelayInMillis, Event[] triggerEvent, WebElement triggerEventOnElement) {
            double tolerance = expectedDelayInMillis == 0 ? 500 : expectedDelayInMillis * 0.5;
            int cycles = 4;
            attsSetter().setAttribute(isHideDelay ? HIDE_DELAY : SHOW_DELAY).toValue(expectedDelayInMillis)// set tested attribute
                    .setAttribute(isHideDelay ? SHOW_DELAY : HIDE_DELAY).toValue(0)// reset not tested attribute
                    .setAttribute(isHideDelay ? ON_HIDE : ON_SHOW).toValue(PREPARATION_SCRIPT).asSingleAction()
                    .perform();
            List<Long> delays = new ArrayList<Long>(cycles);
            String measuringScript = getMeasuringScript(menuRootElement, isHideDelay, triggerEvent);
            for (int i = 1; i <= cycles; i++) {
                //This is debug output, to determine in which cycle test could fall.
                System.out.println(format("Tested delay: {0} [ms], cycle: {1}", expectedDelayInMillis, i));
                executor.executeScript(measuringScript, triggerEventOnElement);
                Graphene.waitGui().withTimeout(expectedDelayInMillis * 2, TimeUnit.MILLISECONDS)
                        .until(new EventTriggeredPredicate(ATTRIBUTE_TRIGGERED_NAME));
                delays.add(Long.valueOf(executor.executeScript(GET_TIME_SCRIPT).toString()));
                if (!isHideDelay) {
                    // hide menu after testing @showDelay
                    getMetamerPage().getRequestTimeElement().click();
                }
            }
            Number median = countMedian(delays);
            assertEquals(median.doubleValue(), expectedDelayInMillis, tolerance,
                    "The delay is not in tolerance. Median of" + " delays was " + median);
        }

        public void testHideDelay(final WebElement menuRootElement, final long expectedDelayInMillis,
                Event[] triggerEvents, WebElement triggerEventOnElement) {
            testDelay(Boolean.TRUE, menuRootElement, expectedDelayInMillis, triggerEvents, triggerEventOnElement);
        }

        public void testHideDelay(final WebElement menuRootElement, final long expectedDelayInMillis,
                Event triggerEvent, WebElement triggerEventOnElement) {
            testHideDelay(menuRootElement, expectedDelayInMillis, new Event[] { triggerEvent },
                    triggerEventOnElement);
        }

        public void testShowDelay(final WebElement menuRootElement, final long expectedDelayInMillis,
                Event[] triggerEvents, WebElement triggerEventOnElement) {
            testDelay(Boolean.FALSE, menuRootElement, expectedDelayInMillis, triggerEvents, triggerEventOnElement);
        }

        public void testShowDelay(final WebElement menuRootElement, final long expectedDelayInMillis,
                Event triggerEvent, WebElement triggerEventOnElement) {
            testDelay(Boolean.FALSE, menuRootElement, expectedDelayInMillis, new Event[] { triggerEvent },
                    triggerEventOnElement);
        }

        private class EventTriggeredPredicate implements Predicate<WebDriver> {

            private final String eventName;
            private String lastReturnedString;
            private final String valueToEqualTo;

            protected EventTriggeredPredicate(String eventName) {
                this(eventName, "true");
            }

            protected EventTriggeredPredicate(String eventName, String valueToEqualTo) {
                this.eventName = eventName;
                this.valueToEqualTo = valueToEqualTo.toLowerCase();
            }

            @Override
            public boolean apply(WebDriver t) {
                lastReturnedString = executor.executeScript(format("return window.{0};", eventName)).toString()
                        .toLowerCase();
                return lastReturnedString.equals(valueToEqualTo);
            }

            @Override
            public String toString() {
                return format("<{0}> to be equal to <{1}>, last returned value was <{2}>.", eventName,
                        valueToEqualTo, lastReturnedString);
            }
        }
    }

    private final TestResourcesProvider testResourcesProvider = new TestResourcesProvider() {

        @Override
        public UnsafeAttributes getAttributes(String attributeTableId) {
            return getUnsafeAttributes(attributeTableId);
        }

        @Override
        public JavascriptExecutor getJSExecutor() {
            return executor;
        }

        @Override
        public WebDriver getWebDriver() {
            return driver;
        }
    };

    public MultipleAttributesSetter attsSetter() {
        return new AttributesHandler(testResourcesProvider);
    }

    @JavaScript("window.JSErrorStorage")
    public interface JSErrorStorage {

        List<String> getMessages();
    }
}