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

nl.hsac.fitnesse.fixture.util.selenium.SeleniumHelper Maven / Gradle / Ivy

package nl.hsac.fitnesse.fixture.util.selenium;

import nl.hsac.fitnesse.fixture.slim.StopTestException;
import nl.hsac.fitnesse.fixture.util.FileUtil;
import org.openqa.selenium.*;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.internal.Base64Encoder;
import org.openqa.selenium.remote.Augmenter;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.ScreenshotException;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Helper to work with Selenium.
 */
public class SeleniumHelper {
    /** Default time in seconds the wait web driver waits unit throwing TimeOutException. */
    private static final int DEFAULT_TIMEOUT_SECONDS = 10;

    private static final String ELEMENT_ON_SCREEN_JS =
            "if (arguments[0].getBoundingClientRect) {\n" +
                    "var rect = arguments[0].getBoundingClientRect();\n" +
                    "return (\n" +
                    "  rect.top >= 0 &&\n" +
                    "  rect.left >= 0 &&\n" +
                    "  rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n" +
                    "  rect.right <= (window.innerWidth || document.documentElement.clientWidth));\n" +
            "} else { return null; }";

    private static final String TOP_ELEMENT_AT =
            "if (arguments[0].getBoundingClientRect) {\n" +
                    "  var rect = arguments[0].getBoundingClientRect();\n" +
                    "  var x = (rect.left + rect.right)/2;\n" +
                    "  var y = (rect.top + rect.bottom)/2;\n" +
                    "  return document.elementFromPoint(x,y);\n" +
                    "} else { return null; }";

    private final List currentIFramePath = new ArrayList(4);
    private int frameDepthOnLastAlertError;
    private DriverFactory factory;
    private WebDriver webDriver;
    private WebDriverWait webDriverWait;
    private boolean shutdownHookEnabled = false;
    private int defaultTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS;

    /**
     * Sets up webDriver to be used.
     * @param aWebDriver web driver to use.
     */
    public void setWebDriver(WebDriver aWebDriver) {
        if (webDriver != null && !webDriver.equals(aWebDriver)) {
            webDriver.quit();
        }
        webDriver = aWebDriver;

        if (webDriver == null) {
            webDriverWait = null;
        } else {
            webDriverWait = new WebDriverWait(webDriver, getDefaultTimeoutSeconds());
        }

        if (!shutdownHookEnabled) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    close();
                }
            });
            shutdownHookEnabled = true;
        }
    }

    /**
     * Shuts down selenium web driver.
     */
    public void close() {
        setWebDriver(null);
    }

    /**
     * @return current page title.
     */
    public String getPageTitle() {
        return driver().getTitle();
    }

    /**
     * @return Selenium's navigation.
     */
    public WebDriver.Navigation navigate() {
        return driver().navigate();
    }

    /**
     * Finds element to click, by searching in multiple locations.
     * @param place identifier for element.
     * @return first interactable element found,
     *          first element found if no interactable element could be found,
     *          null if none could be found.
     */
    public WebElement getElementToClick(String place) {
        By by = placeToBy(place);
        if (by != null) {
            return findElement(by);
        } else {
            WebElement element = findElement(By.linkText(place));
            WebElement firstFound = element;
            if (!isInteractable(element)) {
                // finding by linkText does not find actual text if css text-transform is in place
                element = findByXPath("//*[normalize-space(descendant::text())='%s']/ancestor-or-self::a", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = findByXPath("//button/descendant-or-self::text()[normalize-space(.)='%s']/ancestor-or-self::button", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = findByXPath("//label/descendant-or-self::text()[normalize-space(.)='%s']/ancestor-or-self::label", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = getElementExact(place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (("Submit".equals(place) || "Reset".equals(place))
                    && !isInteractable(element)) {
                element = findElement(byCss("input[type='%s']:not([value])", place.toLowerCase()));
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = findElement(By.partialLinkText(place));
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                // finding by linkText does not find actual text if css text-transform is in place
                element = findByXPath("//*[contains(normalize-space(descendant::text()),'%s')]/ancestor-or-self::a", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = findByXPath("//button/descendant-or-self::text()[contains(normalize-space(.), '%s')]/ancestor-or-self::button", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = findByXPath("//label/descendant-or-self::text()[contains(normalize-space(.), '%s')]/ancestor-or-self::label", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = getElementPartial(place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                // find element with specified text and 'onclick' attribute
                element = findByXPath("//*[@onclick and normalize-space(text())='%s']", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = findByXPath("//*[@onclick and contains(normalize-space(text()),'%s')]", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                // find element with child with specified text and 'onclick' attribute
                element = findByXPath("//*[@onclick and normalize-space(descendant::text())='%s']", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = findByXPath("//*[@onclick and contains(normalize-space(descendant::text()),'%s')]", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                // find element with specified text
                element = findByXPath("//*[normalize-space(text())='%s']", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = findByXPath("//*[contains(normalize-space(text()),'%s')]", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                // find element with child with specified text
                element = findByXPath("//*[normalize-space(descendant::text())='%s']", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            if (!isInteractable(element)) {
                element = findByXPath("//*[contains(normalize-space(descendant::text()),'%s')]", place);
                if (firstFound == null) {
                    firstFound = element;
                }
            }
            return isInteractable(element)
                    ? element
                    : firstFound;
        }
    }

    /**
     * Finds element to retrieve content from or enter content in, by searching in multiple locations.
     * @param place identifier for element.
     * @return first interactable element found,
     *          first element found if no interactable element could be found,
     *          null if none could be found.
     */
    public WebElement getElement(String place) {
        By by = placeToBy(place);
        if (by != null) {
            return findElement(by);
        } else {
            WebElement element = getElementExact(place);
            // first element found, even if it is not (yet) interactable.
            WebElement firstElement = element;
            if (!isInteractable(element)) {
                element = getElementPartial(place);
                if (firstElement == null) {
                    firstElement = element;
                }
            }
            return isInteractable(element)
                    ? element
                    : firstElement;
        }
    }

    public By placeToBy(String place) {
        By result = null;
        if (place.startsWith("id=")) {
            result = By.id(place.substring(3));
        } else if (place.startsWith("css=")) {
            result = By.cssSelector(place.substring(4));
        } else if (place.startsWith("name=")) {
            result = By.name(place.substring(5));
        } else if (place.startsWith("link=")) {
            result = By.linkText(place.substring(5));
        } else if (place.startsWith("partialLink=")) {
            result = By.partialLinkText(place.substring(12));
        } else if (place.startsWith("xpath=")) {
            result = By.xpath(place.substring(6));
        }
        return result;
    }

    public WebElement getElementExact(String place) {
        WebElement element = getElementByLabelOccurrence(place, -1);
        // first element found, even if it is not (yet) interactable.
        WebElement firstElement = element;

        if (!isInteractable(element)) {
            element = findElement(byCss("input[placeholder='%s']", place));
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findElement(byCss("input[value='%s']:not([type='hidden'])", place));
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findElement(byCss("textarea[placeholder='%s']", place));
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findByXPath("//th/descendant-or-self::text()[normalize-space(.)='%s']/ancestor-or-self::th[1]/../td ", place);
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findByXPath("//dt/descendant-or-self::text()[normalize-space(.)='%s']/ancestor-or-self::dt[1]/following-sibling::dd[1] ", place);
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = getElementByAriaLabel(place, -1);
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findElement(byCss("[title='%s']", place));
            if (firstElement == null) {
                firstElement = element;
            }
        }
        return isInteractable(element)
                ? element
                : firstElement;
    }

    public WebElement getElementPartial(String place) {
        WebElement element = getElementByPartialLabelOccurrence(place, -1);
        // first element found, even if it is not (yet) interactable.
        WebElement firstElement = element;

        if (!isInteractable(element)) {
            element = findElement(byCss("input[placeholder*='%s']", place));
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findElement(byCss("input[value*='%s']:not([type='hidden'])", place));
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findElement(byCss("textarea[placeholder*='%s']", place));
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findByXPath("//th/descendant-or-self::text()[contains(normalize-space(.), '%s')]/ancestor-or-self::th[1]/../td ", place);
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findByXPath("//dt/descendant-or-self::text()[contains(normalize-space(.), '%s')]/ancestor-or-self::dt[1]/following-sibling::dd[1] ", place);
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = getElementByPartialAriaLabel(place, -1);
            if (firstElement == null) {
                firstElement = element;
            }
        }
        if (!isInteractable(element)) {
            element = findElement(byCss("[title*='%s']", place));
            if (firstElement == null) {
                firstElement = element;
            }
        }
        return isInteractable(element)
                ? element
                : firstElement;
    }

    /**
     * @param element element to check.
     * @return whether the element is displayed and enabled.
     */
    public boolean isInteractable(WebElement element) {
        return element != null && element.isDisplayed() && element.isEnabled();
    }

    /**
     * Finds element based on the exact (aria-)label text.
     * @param labelText text for label.
     * @param index occurrence of label (first is 1).
     * @return first interactable element found,
     *          first element found if no interactable element could be found,
     *          null if none could be found.
     */
    public WebElement getElementByLabelOccurrence(String labelText, int index) {
        return getElementByLabel(labelText, index,
                                    "//label/descendant-or-self::text()[normalize-space(.)='%s']/ancestor-or-self::label"
        );
    }

    /**
     * Finds element based on the start of the (aria-)label text.
     * @param labelText text for label.
     * @param index occurrence of label (first is 1).
     * @return first interactable element found,
     *          first element found if no interactable element could be found,
     *          null if none could be found.
     */
    public WebElement getElementByStartLabelOccurrence(String labelText, int index) {
        return getElementByLabel(labelText, index,
                "//label/descendant-or-self::text()[starts-with(normalize-space(.), '%s')]/ancestor-or-self::label"
        );
    }

    /**
     * Finds element based on part of the (aria-)label text.
     * @param labelText text for label.
     * @param index occurrence of label (first is 1).
     * @return first interactable element found,
     *          first element found if no interactable element could be found,
     *          null if none could be found.
     */
    public WebElement getElementByPartialLabelOccurrence(String labelText, int index) {
        return getElementByLabel(labelText, index,
                "//label/descendant-or-self::text()[contains(normalize-space(.), '%s')]/ancestor-or-self::label"
        );
    }

    private String indexedXPath(String xpathBase, int index) {

        String xPath = xpathBase;
        if (index > 0) {
            xPath = String.format("(%s)[%s]", xpathBase, index);
        }
        return xPath;
    }

    private WebElement getElementByLabel(String labelText, int index, String labelXPath) {
        WebElement element = null;
        String labelPattern = indexedXPath(labelXPath, index);
        WebElement label = findByXPath(labelPattern, labelText);
        if (label != null) {
            String forAttr = label.getAttribute("for");
            if (forAttr == null || "".equals(forAttr)) {
                element = getNestedElementForValue(label);
            } else {
                element = findElement(By.id(forAttr));
            }
        }
        return element;
    }

    public WebElement getElementByAriaLabel(String labelText, int index) {
        // see if there is an element with labelText as text, whose id is referenced by an aria-labelledby attribute
        String labelledByPattern = indexedXPath("//*[@aria-labelledby and @aria-labelledby=//*[@id]/descendant-or-self::text()[normalize-space(.) = '%s']/ancestor-or-self::*[@id]/@id]", index);
        WebElement element = findByXPath(labelledByPattern, labelText);
        WebElement firstFound = element;

        if (!isInteractable(element)) {
            By by = byCss("[aria-label='%s']", labelText);
            if (index > 0) {
                element = findElement(by, index - 1);
            } else {
                element = findElement(by);
            }
            if (firstFound == null) {
                firstFound = element;
            }
        }
        return isInteractable(element)
                ? element
                : firstFound;
    }

    public WebElement getElementByPartialAriaLabel(String labelText, int index) {
        String labelledByPattern = indexedXPath("//*[@aria-labelledby and @aria-labelledby=//*[@id]/descendant-or-self::text()[contains(normalize-space(.), '%s')]/ancestor-or-self::*[@id]/@id]", index);
        WebElement element = findByXPath(labelledByPattern, labelText);
        WebElement firstFound = element;

        if (!isInteractable(element)) {
            By by = byCss("[aria-label*='%s']", labelText);
            if (index > 0) {
                element = findElement(by, index - 1);
            } else {
                element = findElement(by);
            }
            if (firstFound == null) {
                firstFound = element;
            }
        }
        return isInteractable(element)
                ? element
                : firstFound;
    }

    public WebElement getNestedElementForValue(WebElement parent) {
        return findElement(parent, false, By.xpath(".//input|.//select|.//textarea"));
    }

    /**
     * Determines number displayed for item in ordered list.
     * @param element ordered list item.
     * @return number, if one could be determined.
     */
    public Integer getNumberFor(WebElement element) {
        Integer number = null;
        if ("li".equalsIgnoreCase(element.getTagName())
                && element.isDisplayed()) {
            int num;
            String ownVal = element.getAttribute("value");
            if (ownVal != null && !"0".equals(ownVal)) {
                num = toInt(ownVal, 0);
            } else {
                String start = element.findElement(By.xpath("ancestor::ol")).getAttribute("start");
                num = toInt(start, 1);

                List allItems = element.findElements(By.xpath("ancestor::ol/li"));
                int index = allItems.indexOf(element);
                for (int i = 0; i < index; i++) {
                    WebElement item = allItems.get(i);
                    if (item.isDisplayed()) {
                        num++;
                        String val = item.getAttribute("value");
                        int valNum = toInt(val, num);
                        if (valNum != 0) {
                            num = valNum + 1;
                        }
                    }
                }
            }
            number = num;
        }
        return number;
    }

    private int toInt(String attributeValue, int defaultVal) {
        int result = defaultVal;
        if (attributeValue != null) {
            try {
                result = Integer.parseInt(attributeValue);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("Unable to parse value: " + attributeValue, e);
            }
        }
        return result;
    }

    /**
     * Returns the texts of all available options for the supplied select element.
     * @param element select element to find options for.
     * @return text per option.
     */
    public ArrayList getAvailableOptions(WebElement element) {
        ArrayList result = null;
        if (isInteractable(element)
                && "select".equalsIgnoreCase(element.getTagName())) {
            result = new ArrayList();
            List options = element.findElements(By.tagName("option"));
            for (WebElement option : options) {
                if (option.isEnabled()) {
                    result.add(option.getText());
                }
            }
        }
        return result;
    }

    /**
     * Sets value of hidden input field.
     * @param idOrName id or name of input field to set.
     * @param value value to set.
     * @return whether input field was found.
     */
    public boolean setHiddenInputValue(String idOrName, String value) {
        WebElement element = findElement(By.id(idOrName));
        if (element == null) {
            element = findElement(By.name(idOrName));
            if (element != null) {
                executeJavascript("document.getElementsByName('%s')[0].value='%s'", idOrName, value);
            }
        } else {
            executeJavascript("document.getElementById('%s').value='%s'", idOrName, value);
        }
        return element != null;
    }

    /**
     * Executes Javascript in browser. If statementPattern contains the magic variable 'arguments'
     * the parameters will also be passed to the statement. In the latter case the parameters
     * must be a number, a boolean, a String, WebElement, or a List of any combination of the above.
     * @link http://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/JavascriptExecutor.html#executeScript(java.lang.String,%20java.lang.Object...)
     * @param statementPattern javascript to run, possibly with placeholders to be replaced.
     * @param parameters placeholder values that should be replaced before executing the script.
     * @return return value from statement.
     */
    public Object executeJavascript(String statementPattern, Object... parameters) {
        Object result;
        String script = String.format(statementPattern, parameters);
        if (statementPattern.contains("arguments")) {
            result = executeScript(script, parameters);
        } else {
            result = executeScript(script);
        }
        return result;
    }

    protected Object executeScript(String script, Object... parameters) {
        Object result;
        JavascriptExecutor jse = (JavascriptExecutor) driver();
        try {
            result = jse.executeScript(script, parameters);
        } catch (WebDriverException e) {
            String msg = e.getMessage();
            if (msg != null && msg.contains("Detected a page unload event; script execution does not work across page loads.")) {
                // page reloaded while script ran, retry it once
                result = jse.executeScript(script, parameters);
            } else {
                throw e;
            }
        }
        return result;
    }

    /**
     * Executes Javascript in browser and then waits for 'callback' to be invoked.
     * If statementPattern should reference the magic (function) variable 'callback' which should be
     * called to provide this method's result.
     * If the statementPattern contains the magic variable 'arguments'
     * the parameters will also be passed to the statement. In the latter case the parameters
     * must be a number, a boolean, a String, WebElement, or a List of any combination of the above.
     * @link http://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/JavascriptExecutor.html#executeAsyncScript(java.lang.String,%20java.lang.Object...)
     * @param statementPattern javascript to run, possibly with placeholders to be replaced.
     * @param parameters placeholder values that should be replaced before executing the script.
     * @return return value from statement.
     */
    public Object waitForJavascriptCallback(String statementPattern, Object... parameters) {
        Object result;
        String script = "var callback = arguments[arguments.length - 1];"
                        + String.format(statementPattern, parameters);
        JavascriptExecutor jse = (JavascriptExecutor) driver();
        if (statementPattern.contains("arguments")) {
            result = jse.executeAsyncScript(script, parameters);
        } else {
            result = jse.executeAsyncScript(script);
        }
        return result;
    }

    /**
     * Creates By based on CSS selector, supporting placeholder replacement.
     * @param pattern basic CSS selectot, possibly with placeholders.
     * @param parameters values for placeholders.
     * @return ByCssSelector.
     */
    public By byCss(String pattern, String... parameters) {
        String selector = fillPattern(pattern, parameters);
        return By.cssSelector(selector);
    }

    /**
     * Creates By based on xPath, supporting placeholder replacement.
     * @param pattern basic XPATH, possibly with placeholders.
     * @param parameters values for placeholders.
     * @return ByXPath.
     */
    public By byXpath(String pattern, String... parameters) {
        String xpath = fillPattern(pattern, parameters);
        return By.xpath(xpath);
    }

    public By byJavascript(String pattern, Object... arguments) {
        return new JavascriptBy(pattern, arguments);
    }

    /**
     * Fills in placeholders in pattern using the supplied parameters.
     * @param pattern pattern to fill (in String.format style).
     * @param parameters parameters to use.
     * @return filled in pattern.
     */
    protected String fillPattern(String pattern, String[] parameters) {
        boolean containsSingleQuote = false;
        boolean containsDoubleQuote = false;
        Object[] escapedParams = new Object[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            String param = parameters[i];
            containsSingleQuote = containsSingleQuote || param.contains("'");
            containsDoubleQuote = containsDoubleQuote || param.contains("\"");
            escapedParams[i] = param;
        }
        if (containsDoubleQuote && containsSingleQuote) {
            throw new RuntimeException("Unsupported combination of single and double quotes");
        }
        String patternToUse;
        if (containsSingleQuote) {
            patternToUse = pattern.replace("'", "\"");
        } else {
            patternToUse = pattern;
        }
        return String.format(patternToUse, escapedParams);
    }

    /**
     * Checks whether element is in browser's viewport.
     * @param element element to check
     * @return true if element is in browser's viewport, null if we could not determine whether it was in viewport.
     */
    public Boolean isElementOnScreen(WebElement element) {
        return (Boolean)executeJavascript(ELEMENT_ON_SCREEN_JS, element);
    }

    /**
     * Sets how long to wait before deciding an element does not exists.
     * @param implicitWait time in milliseconds to wait.
     */
    public void setImplicitlyWait(int implicitWait) {
        try {
            driver().manage().timeouts().implicitlyWait(implicitWait, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            // https://code.google.com/p/selenium/issues/detail?id=6015
            System.err.println("Unable to set implicit timeout (known issue for Safari): " + e.getMessage());
        }
    }

    /**
     * Sets how long to wait when executing asynchronous script calls.
     * @param scriptTimeout time in milliseconds to wait.
     */
    public void setScriptWait(int scriptTimeout) {
        try {
            driver().manage().timeouts().setScriptTimeout(scriptTimeout, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            // https://code.google.com/p/selenium/issues/detail?id=6015
            System.err.println("Unable to set script timeout (known issue for Safari): " + e.getMessage());
        }
    }

    /**
     * Sets how long to wait on opening a page.
     * @param pageLoadWait time in milliseconds to wait.
     */
    public void setPageLoadWait(int pageLoadWait) {
        try {
            driver().manage().timeouts().pageLoadTimeout(pageLoadWait, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            // https://code.google.com/p/selenium/issues/detail?id=6015
            System.err.println("Unable to set page load timeout (known issue for Safari): " + e.getMessage());
        }
    }

    /**
     * Simulates placing the mouse over the supplied element.
     * @param element element to place mouse over.
     */
    public void hoverOver(WebElement element) {
        new Actions(driver()).moveToElement(element).perform();
    }

    /**
     * @return currently active element.
     */
    public WebElement getActiveElement() {
        return getTargetLocator().activeElement();
    }

    /**
     * Finds element using xPath, supporting placeholder replacement.
     * @param pattern basic XPATH, possibly with placeholders.
     * @param parameters values for placeholders.
     * @return element if found, null if none could be found.
     */
    public WebElement findByXPath(String pattern, String... parameters) {
        By by = byXpath(pattern, parameters);
        return findElement(by);
    }

    /**
     * Finds first element matching the By supplied.
     * @param by criteria.
     * @return element if found, null if none could be found.
     */
    public WebElement findElement(By by) {
        return findElement(false, by);
    }

    /**
     * Finds element matching the By supplied.
     * @param atMostOne true indicates multiple matching elements should trigger an exception
     * @param by criteria.
     * @return element if found, null if none could be found.
     * @throws RuntimeException if atMostOne is true and multiple elements match by.
     */
    public WebElement findElement(boolean atMostOne, By by) {
        return findElement(driver(), atMostOne, by);
    }

    /**
     * Finds the nth element matching the By supplied.
     * @param by criteria.
     * @param index (zero based) matching element to return.
     * @return element if found, null if none could be found.
     */
    public WebElement findElement(By by, int index) {
        WebElement element = null;
        List elements = driver().findElements(by);
        if (elements.size() > index) {
            element = elements.get(index);
        }
        return element;
    }

    /**
     * @return the session id from the current driver (if available).
     */
    public String getSessionId() {
        String result = null;
        WebDriver d = driver();
        if (d instanceof RemoteWebDriver) {
            Object s = ((RemoteWebDriver) d).getSessionId();
            if (s != null) {
                result = s.toString();
            }
        }
        return result;
    }

    /**
     * @return true when current driver is connected to either a local or remote Internet Explorer.
     */
    public boolean connectedToInternetExplorer() {
        boolean result = false;
        WebDriver driver = driver();
        if (driver instanceof InternetExplorerDriver) {
            result = true;
        } else if (driver instanceof RemoteWebDriver) {
            result = checkRemoteBrowserName(driver, "internet explorer");
        }
        return result;
    }

    /**
     * @return true when current driver is connected to either a local or remote Safari.
     */
    public boolean connectedToSafari() {
        boolean result = false;
        WebDriver driver = driver();
        if (driver instanceof SafariDriver) {
            result = true;
        } else if (driver instanceof RemoteWebDriver) {
            result = checkRemoteBrowserName(driver, "safari");
        }
        return result;
    }

    protected boolean checkRemoteBrowserName(WebDriver driver, String expectedName) {
        RemoteWebDriver remoteWebDriver = (RemoteWebDriver) driver;
        String browserName = remoteWebDriver.getCapabilities().getBrowserName();
        return expectedName.equalsIgnoreCase(browserName);
    }

    /**
     * Allows direct access to WebDriver. If possible please use methods of this class to facilitate testing.
     * @return selenium web driver.
     */
    public WebDriver driver() {
        if (webDriver == null) {
            if (factory == null) {
                throw new StopTestException("Cannot use Selenium before a driver is started (for instance using SeleniumDriverSetup)");
            } else {
                factory.createDriver();
            }
        }
        return webDriver;
    }

    /**
     * Allows clients to wait until a certain condition is true.
     * @return wait using the driver in this helper.
     */
    public WebDriverWait waitDriver() {
        return webDriverWait;
    }

    /**
     * Executes condition until it returns a value other than null or false.
     * It does not forward StaleElementReferenceExceptions, but keeps waiting.
     * @param maxSecondsToWait number of seconds to wait at most.
     * @param condition condition to check.
     * @param  return type.
     * @return result of condition (if not null).
     * @throws TimeoutException when condition did not give a value to return after maxSecondsToWait.
     */
    public  T waitUntil(int maxSecondsToWait, ExpectedCondition condition) {
        ExpectedCondition cHandlingStale = getConditionIgnoringStaleElement(condition);
        FluentWait wait = waitDriver().withTimeout(maxSecondsToWait, TimeUnit.SECONDS);
        return wait.until(cHandlingStale);
    }

    /**
     * Wraps the supplied condition so that StaleElementReferenceExceptions (and the Safari equivalent)
     * are to thrown by waitUntil(), but just mean: try again.
     * @param condition condition to wrap.
     * @param  retrun type of condition
     * @return wrapped condition.
     */
    public  ExpectedCondition getConditionIgnoringStaleElement(final ExpectedCondition condition) {
        return new ExpectedCondition() {
            @Override
            public T apply(WebDriver webDriver) {
                try {
                    return condition.apply(webDriver);
                } catch (StaleElementReferenceException e) {
                    // try again
                    return null;
                } catch (WebDriverException e) {
                    String msg = e.getMessage();
                    if (msg != null
                            && (msg.contains("Element does not exist in cache")
                                // Safari stale element
                                || msg.contains("Error: element is not attached to the page document")
                                // Alternate Chrome stale element
                            )) {
                        return null;
                    } else {
                        throw e;
                    }
                }
            }
        };
    }

    /**
     * Finds element matching the By supplied.
     * @param context context to find element in.
     * @param atMostOne true indicates multiple matching elements (that have an id) should trigger an exception
     * @param by criteria.
     * @return element if found, null if none could be found.
     * @throws RuntimeException if atMostOne is true and multiple elements (having an id) match the by.
     */
    public WebElement findElement(SearchContext context, boolean atMostOne, By by) {
        WebElement element = null;
        List elements = context.findElements(by);
        if (elements.size() == 1) {
            element = elements.get(0);
        } else if (elements.size() > 1) {
            if (!atMostOne) {
                element = selectBestElement(elements);
            } else {
                elements = elementsWithId(elements);
                if (elements.size() == 1) {
                    element = elements.get(0);
                } else {
                    throw new RuntimeException("Multiple elements with id found for: " + by
                                                + ":\n" + elementsAsString(elements));
                }
            }
        }
        return element;
    }

    private WebElement selectBestElement(List elements) {
        // take the first displayed element without any elements on top of it,
        // if none: take first displayed
        // or if none are displayed: just take the first
        WebElement element = elements.get(0);
        WebElement firstDisplayed = null;
        WebElement firstOnTop = null;
        if (!element.isDisplayed() || !isOnTop(element)) {
            for (int i = 1; i < elements.size(); i++) {
                WebElement otherElement = elements.get(i);
                if (otherElement.isDisplayed()) {
                    if (firstDisplayed == null) {
                        firstDisplayed = otherElement;
                    }
                    if (isOnTop(otherElement)) {
                        firstOnTop = otherElement;
                        element = otherElement;
                        break;
                    }
                }
            }
            if (firstOnTop == null
                    && firstDisplayed != null
                    && !element.isDisplayed()) {
                // none displayed and on top
                // first was not displayed, but another was
                element = firstDisplayed;
            }
        }
        return element;
    }

    private boolean isOnTop(WebElement element) {
        WebElement e = (WebElement) executeJavascript(TOP_ELEMENT_AT, element);
        return element.equals(e);
    }

    private List elementsWithId(List elements) {
        List result = new ArrayList(1);
        for (WebElement e : elements) {
            String attr = e.getAttribute("id");
            if (attr != null && !attr.isEmpty()) {
                result.add(e);
            }
        }
        return result;
    }

    private String elementsAsString(Collection elements) {
        StringBuilder b = new StringBuilder();
        b.append("[");
        boolean first = true;
        for (WebElement e : elements) {
            if (first) {
                first = false;
            } else {
                b.append(", ");
            }
            b.append(e.getAttribute("id"));
        }
        b.append("]");
        return b.toString();
    }

    /**
     * Trigger scrolling of window to ensure element is in visible.
     * @param element element to scroll to.
     */
    public void scrollTo(WebElement element) {
        executeJavascript("arguments[0].scrollIntoView(true);", element);
    }

    /**
     * Takes screenshot of current page (as .png).
     * @param baseName name for file created (without extension),
     *                 if a file already exists with the supplied name an
     *                 '_index' will be added.
     * @return absolute path of file created.
     */
    public String takeScreenshot(String baseName) {
        String result = null;

        WebDriver d = driver();

        if (!(d instanceof TakesScreenshot)) {
            d = new Augmenter().augment(d);
        }
        if (d instanceof TakesScreenshot) {
            TakesScreenshot ts = (TakesScreenshot) d;
            byte[] png = ts.getScreenshotAs(OutputType.BYTES);
            result = writeScreenshot(baseName, png);
        }
        return result;
    }

    /**
     * Finds screenshot embedded in throwable, if any.
     * @param t exception to search in.
     * @return content of screenshot (if any is present), null otherwise.
     */
    public byte[] findScreenshot(Throwable t) {
        byte[] result = null;
        if (t != null) {
            if (t instanceof ScreenshotException) {
                String encodedScreenshot = ((ScreenshotException)t).getBase64EncodedScreenshot();
                result = new Base64Encoder().decode(encodedScreenshot);
            } else {
                result = findScreenshot(t.getCause());
            }
        }
        return result;
    }

    /**
     * Saves screenshot (as .png).
     * @param baseName name for file created (without extension),
     *                 if a file already exists with the supplied name an
     *                 '_index' will be added.
     * @return absolute path of file created.
     */
    public String writeScreenshot(String baseName, byte[] png) {
        return FileUtil.saveToFile(baseName, "png", png);
    }

    /**
     * @return HTML content of current page.
     */
    public String getHtml() {
        String html;
        try {
            html = (String) executeJavascript(
                    "var node = document.doctype;\n" +
                            "var docType = '';\n" +
                            "if (node) {\n" +
                            "  docType = \"'; }\n" +
                            "var html = document.documentElement.outerHTML " +
                            "|| '' + document.documentElement.innerHTML + '';\n" +
                            "return docType + html;"
            );
        } catch (RuntimeException e) {
            // unable to get via Javascript
            try {
                // this is very WebDriver implementation dependent, so we only use as fallback
                html = driver().getPageSource();
            } catch (Exception ex) {
                ex.printStackTrace();
                // throw original exception
                throw e;
            }
        }
        return html;
    }

    /**
     * @return current window's size.
     */
    public Dimension getWindowSize() {
        return getWindow().getSize();
    }

    /**
     * Sets current window's size.
     * @param newWidth new width (in pixels)
     * @param newHeight new height (in pixels)
     */
    public void setWindowSize(int newWidth, int newHeight) {
        getWindow().setSize(new Dimension(newWidth, newHeight));
    }

    /**
     * Sets current window to maximum size.
     */

    public void setWindowSizeToMaximum() {
        getWindow().maximize();
    }

    /**
     * @return current browser window.
     */
    public WebDriver.Window getWindow() {
        return driver().manage().window();
    }

    public int getCurrentTabIndex(List tabHandles) {
        try {
            String currentHandle = driver().getWindowHandle();
            return tabHandles.indexOf(currentHandle);
        } catch (NoSuchWindowException e) {
            return -1;
        }
    }

    public void goToTab(List tabHandles, int indexToGoTo) {
        getTargetLocator().window(tabHandles.get(indexToGoTo));
        switchToDefaultContent();
    }

    public List getTabHandles() {
        return new ArrayList(driver().getWindowHandles());
    }

    /**
     * Activates main/top-level iframe (i.e. makes it the current frame).
     */
    public void switchToDefaultContent() {
        getTargetLocator().defaultContent();
        currentIFramePath.clear();
    }


    /**
     * Activates specified child frame of current iframe.
     * @param iframe frame to activate.
     */
    public void switchToFrame(WebElement iframe) {
        getTargetLocator().frame(iframe);
        currentIFramePath.add(iframe);
    }

    /**
     * Activates parent frame of current iframe.
     * Does nothing if when current frame is the main/top-level one.
     */
    public void switchToParentFrame() {
        if (!currentIFramePath.isEmpty()) {
            // copy path since substring creates a view, not a deep copy
            List newPath = currentIFramePath.subList(0, currentIFramePath.size() - 1);
            newPath = new ArrayList(newPath);
            // Safari and PhantomJs don't support switchTo.parentFrame, so we do this
            // it works for Phantom, but is VERY slow there (other browsers are slow but ok)
            switchToDefaultContent();
            for (WebElement iframe : newPath) {
                switchToFrame(iframe);
            }
        }
    }

    public  ExpectedCondition conditionForAllFrames(ExpectedCondition nested) {
        return new TryAllFramesConditionDecorator(this, nested);
    }

    /**
     * @return current alert, if one is present, null otherwise.
     */
    public Alert getAlert() {
        Alert alert = null;
        try {
            alert = getTargetLocator().alert();
        } catch (NoAlertPresentException e) {
            // just leave alert null
        }
        return alert;
    }

    private WebDriver.TargetLocator getTargetLocator() {
        return driver().switchTo();
    }

    /**
     * Gets current browser's cookie with supplied name.
     * @param cookieName name of cookie to return.
     * @return cookie, if present, null otherwise.
     */
    public Cookie getCookie(String cookieName) {
        return driver().manage().getCookieNamed(cookieName);
    }

    /**
     * @return current browser's cookies.
     */
    public Set getCookies() {
        return driver().manage().getCookies();
    }

    /**
     * Deletes all of the browser's cookies (for the current domain).
     */
    public void deleteAllCookies() {
        driver().manage().deleteAllCookies();
    }

    public void setDriverFactory(DriverFactory aFactory) {
        factory = aFactory;
    }

    /**
     * @param timeoutSeconds default number of seconds to wait before throwing timeout exceptions
     */
    public void setDefaultTimeoutSeconds(int timeoutSeconds) {
        defaultTimeoutSeconds = timeoutSeconds;
    }

    /**
     * @return default time for waiting (in seconds).
     */
    public int getDefaultTimeoutSeconds() {
        return defaultTimeoutSeconds;
    }

    /**
     * @return return current depth of (i)frames.
     */
    public int getCurrentFrameDepth() {
        return currentIFramePath.size();
    }

    /**
     * Store current frame depth in case of alert error
     * @param frameDepthOnAlert frames added searching in nested frames started, this is the number of levels that
     *                          should be removed after the alert is handled.
     */
    public void storeFrameDepthOnAlertError(int frameDepthOnAlert) {
        frameDepthOnLastAlertError = frameDepthOnAlert;
    }

    /**
     * Reactivate (i)frame that was active before we encountered an alert searching in nested (i)frames.
     */
    public void resetFrameDepthOnAlertError() {
        int depthOnLastAlertError = getFrameDepthOnLastAlertError();
        for (int i = 0; i < depthOnLastAlertError; i++) {
            switchToParentFrame();
            frameDepthOnLastAlertError--;
        }
    }

    /**
     * @return number of (i)frame levels that need to be removed to get back to right frame after encountering an alert
     *         in a nested (i)frame.
     */
    public int getFrameDepthOnLastAlertError() {
        return frameDepthOnLastAlertError;
    }

    public interface DriverFactory {
        void createDriver();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy