com.zutubi.pulse.acceptance.SeleniumBrowser.java Source code

Java tutorial

Introduction

Here is the source code for com.zutubi.pulse.acceptance.SeleniumBrowser.java

Source

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

package com.zutubi.pulse.acceptance;

import com.google.common.io.Files;
import com.thoughtworks.selenium.Selenium;
import com.thoughtworks.selenium.SeleniumException;
import com.thoughtworks.selenium.webdriven.WebDriverBackedSelenium;
import com.zutubi.pulse.acceptance.forms.LoginForm;
import com.zutubi.pulse.acceptance.forms.SeleniumForm;
import com.zutubi.pulse.acceptance.pages.LoginPage;
import com.zutubi.pulse.acceptance.pages.SeleniumPage;
import com.zutubi.pulse.core.test.TestUtils;
import com.zutubi.pulse.core.test.TimeoutException;
import com.zutubi.pulse.master.webwork.Urls;
import com.zutubi.util.Condition;
import com.zutubi.util.StringUtils;
import com.zutubi.util.SystemUtils;
import com.zutubi.util.WebUtils;
import freemarker.template.utility.StringUtil;
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxProfile;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Wait;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.nio.charset.Charset;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.zutubi.pulse.acceptance.AcceptanceTestUtils.ADMIN_CREDENTIALS;
import static com.zutubi.pulse.acceptance.AcceptanceTestUtils.getWorkingDirectory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;

/**
 * A utility class for managing and interacting with the selenium instance.
 *
 * See {@link com.thoughtworks.selenium.Selenium} for details on the locator formats
 * available.
 */
public class SeleniumBrowser {
    public static final long DEFAULT_TIMEOUT = 60000;
    public static final long WAITFOR_TIMEOUT = 60000;
    public static final long REFRESH_TIMEOUT = 60000;

    public static final long WAITFOR_INTERVAL = 3000;
    public static final long REFRESH_INTERVAL = 1000;

    private Selenium selenium;
    private int pulsePort;
    private WebDriver webDriver;
    private Urls urls;

    private static WebDriver createWebDriver() {
        if (SystemUtils.IS_WINDOWS) {
            return new InternetExplorerDriver();
        } else {
            FirefoxProfile profile = new FirefoxProfile();
            String logFile = System.getProperty("selenium.firefox.log");
            if (logFile != null) {
                profile.setPreference("webdriver.log.file", logFile);
            }

            profile.setEnableNativeEvents(true);
            return new FirefoxDriver(profile);
        }
    }

    /**
     * Create a new instance of the selenium browser, using the browser and port
     * configured in the environment.  This constructor should be used by the
     * acceptance tests to ensure the correct configurations are used.
     */
    public SeleniumBrowser() {
        this(AcceptanceTestUtils.getPulsePort(), createWebDriver());
    }

    public SeleniumBrowser(int port) {
        this(port, createWebDriver());
    }

    public SeleniumBrowser(int port, WebDriver webDriver) {
        this.webDriver = webDriver;
        webDriver.manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS);
        String baseUrl = AcceptanceTestUtils.getPulseUrl(port);
        selenium = new WebDriverBackedSelenium(webDriver, baseUrl);
        this.pulsePort = port;
        urls = new Urls(baseUrl);
    }

    /**
     * Start a fresh browser session by restarting the browser.
     */
    public synchronized void newSession() {
        webDriver.manage().deleteAllCookies();
        webDriver.get(urls.base());
    }

    public void quit() {
        webDriver.quit();
    }

    /**
     * Check if the native browser being driven by selenium is an
     * instance of firefox.
     *
     * @return true if it is firefox, false otherwise.
     */
    public boolean isFirefox() {
        return webDriver instanceof FirefoxDriver;
    }

    /**
     * Returns the Pulse port the browser is connecting to.
     * 
     * @return the Pulse port to connect to
     */
    public int getPulsePort() {
        return pulsePort;
    }

    /**
     * Create a new form instance, wiring it with the necessary resource.  The constructor
     * used is the constructor that matches provided arguments.
     *
     * @param formType      the type of the form being created.
     * @param extraArgs     the form types constructor arguments.
     * @param <T>           the form type T must extend {@link com.zutubi.pulse.acceptance.forms.SeleniumForm}
     * @return  a new instance of the form.
     */
    public <T extends SeleniumForm> T createForm(Class<T> formType, Object... extraArgs) {
        if (extraArgs == null) {
            extraArgs = new Object[0];
        }

        Object[] args = new Object[extraArgs.length + 1];
        args[0] = this;
        System.arraycopy(extraArgs, 0, args, 1, extraArgs.length);

        Class[] types = new Class[extraArgs.length + 1];
        types[0] = SeleniumBrowser.class;

        for (int i = 1; i < args.length; i++) {
            types[i] = args[i].getClass();
        }

        return createInstance(formType, types, args);
    }

    /**
     * Create a new page instance, wiring it with the necessary resource.  The constructor
     * used is the constructor that matches provided arguments.
     *
     * @param pageType      the type of the page being created.
     * @param extraArgs     the page types constructor arguments.
     * @param <T>           the page type T must extend {@link com.zutubi.pulse.acceptance.pages.SeleniumPage}
     * @return  a new instance of the form.
     */
    public <T extends SeleniumPage> T createPage(Class<T> pageType, Object... extraArgs) {
        if (extraArgs == null) {
            extraArgs = new Object[0];
        }

        Object[] args = new Object[extraArgs.length + 2];
        args[0] = this;
        args[1] = urls;
        System.arraycopy(extraArgs, 0, args, 2, extraArgs.length);

        Class[] types = new Class[extraArgs.length + 2];
        types[0] = SeleniumBrowser.class;
        types[1] = Urls.class;

        for (int i = 2; i < args.length; i++) {
            types[i] = args[i].getClass();
        }

        return createInstance(pageType, types, args);
    }

    /**
     * Create {@link #createPage(Class, Object...)} and open
     * {@link com.zutubi.pulse.acceptance.pages.SeleniumPage#open()} a new selenium page.
     *
     * @param pageType      the type of page being opened.
     * @param extraArgs     the page types constructor arguments.
     * @param <T>           the page type T must extend {@link com.zutubi.pulse.acceptance.pages.SeleniumPage}
     * @return  a new page instance that has been opened.
     */
    public <T extends SeleniumPage> T open(Class<T> pageType, Object... extraArgs) {
        T page = createPage(pageType, extraArgs);
        page.open();
        return page;
    }

    /**
     * Create {@link #createPage(Class, Object...)} and waitFor
     * {@link com.zutubi.pulse.acceptance.pages.SeleniumPage#waitFor()} a new selenium page.
     *
     * @param pageType      the type of page being opened.
     * @param extraArgs     the page types constructor arguments.
     * @param <T>           the page type T must extend {@link com.zutubi.pulse.acceptance.pages.SeleniumPage}
     * @return  a new page instance that has been waited for.
     */
    public <T extends SeleniumPage> T waitFor(Class<T> pageType, Object... extraArgs) {
        T page = createPage(pageType, extraArgs);
        page.waitFor();
        return page;
    }

    /**
     * Create {@link #createPage(Class, Object...)} open {@link com.zutubi.pulse.acceptance.pages.SeleniumPage#open()}
     * and waitFor {@link com.zutubi.pulse.acceptance.pages.SeleniumPage#waitFor()} a new selenium page.
     *
     * @param pageType      the type of page being opened.
     * @param extraArgs     the page types constructor arguments.
     * @param <T>           the page type T must extend {@link com.zutubi.pulse.acceptance.pages.SeleniumPage}
     * @return  a new page instance that has been opened and waited for.
     */
    public <T extends SeleniumPage> T openAndWaitFor(Class<T> pageType, Object... extraArgs) {
        T page = createPage(pageType, extraArgs);
        page.open();
        page.waitFor();
        return page;
    }

    private <T> T createInstance(Class<T> type, Class[] types, Object[] args) {
        try {
            // map types to primitive equivalents.
            for (int i = 0; i < types.length; i++) {
                if (types[i] == Long.class) {
                    types[i] = Long.TYPE;
                }
                if (types[i] == Integer.class) {
                    types[i] = Integer.TYPE;
                }
                if (types[i] == Boolean.class) {
                    types[i] = Boolean.TYPE;
                }
            }

            Constructor<T> c = type.getConstructor(types);
            return c.newInstance(args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Goes to the login page and logs in with the given credentials.
     *
     * @param username user to log in as
     * @param password the given user's password
     */
    public void login(String username, String password) {
        LoginPage page = openAndWaitFor(LoginPage.class);
        page.login(username, password);
    }

    /**
     * Goes to the login page, logs in with the given credentials and waits for the login to succeed.
     *
     * @param username user to log in as
     * @param password the given user's password
     */
    public void loginAndWait(String username, String password) {
        LoginPage page = openAndWaitFor(LoginPage.class);
        page.login(username, password);
        waitForElement(By.id(IDs.ID_LOGOUT));
    }

    /**
     * Goes to the login page and logs in as the admin user.
     */
    public void loginAsAdmin() {
        login(ADMIN_CREDENTIALS.getUserName(), ADMIN_CREDENTIALS.getPassword());
    }

    /**
     * Click the logout link on the browser and wait for the page to load. This
     * assumes that the logout link is available.
     */
    public void logout() {
        if (!isLoggedIn()) {
            throw new IllegalStateException("Can not logout when no logout link is available.");
        }
        waitAndClick(By.id(IDs.ID_LOGOUT));
        createForm(LoginForm.class).waitFor();
    }

    /**
     * @return true if the web browser session has an active login
     */
    public boolean isLoggedIn() {
        return isElementIdPresent(IDs.ID_LOGOUT);
    }

    /**
     * Open the selenium browser to the requested location.
     *
     * @param location  the location at which to open the browser.
     */
    public void open(String location) {
        webDriver.navigate().to(location);
    }

    /**
     * Clicks on an element.  Respects the implicit wait period.
     * 
     * @param by element locator
     */
    public void click(By by) {
        webDriver.findElement(by).click();
    }

    /**
     * Double clicks on an element.  Respects the implicit wait period.
     * 
     * @param by element locator
     */
    public void doubleClick(By by) {
        new Actions(webDriver).doubleClick(webDriver.findElement(by)).build().perform();
    }

    /**
     * Refreshes the current web page.
     */
    public void refresh() {
        webDriver.navigate().refresh();
    }

    /**
     * Types the given value into a text field or area.  Any existing text is
     * cleared first.
     * 
     * @param by    locator of the text field/area
     * @param value the text to type
     */
    public void type(By by, String value) {
        WebElement element = webDriver.findElement(by);
        element.clear();
        element.sendKeys(value);
    }

    /**
     * Adds a selection to the current value of a multi-value element.
     * 
     * @param fieldId identifier of the field
     * @param value   value to add
     */
    public void addSelection(String fieldId, String value) {
        selenium.addSelection(fieldId, value);
    }

    /**
     * Checks if the specified element is on the current page.  Respects the
     * implicit wait period.
     * 
     * @param id id identifying the element
     * @return true if the element is present, false otherwise.
     */
    public boolean isElementIdPresent(String id) {
        return isElementPresent(By.id(WebUtils.toValidHtmlName(id)));
    }

    /**
     * Checks if the specified element is on the current page.  Respects the 
     * implicit wait period.
     * 
     * @param by locator of the element to check for
     * @return true iff the given element is present (it need not be visible)
     */
    public boolean isElementPresent(By by) {
        return webDriver.findElements(by).size() > 0;
    }

    /**
     * Check if the specified text is present on the current page.
     * 
     * @param text the required text
     * @return true if the text is present, false otherwise.
     */
    public boolean isTextPresent(String text) {
        return selenium.isTextPresent(text);
    }

    public void waitForTextPresent(final String text) {
        Wait<WebDriver> wait = new WebDriverWait(webDriver, WAITFOR_TIMEOUT / 1000);
        wait.until(new ExpectedCondition<Boolean>() {
            public Boolean apply(WebDriver webDriver) {
                return selenium.isTextPresent(text);
            }
        });
    }

    /**
     * Check if the text matching the regex is present on the current page.
     * 
     * @param regex the regex
     * @return true if a match is present, false otherwise.
     */
    public boolean isRegexPresent(String regex) {
        String bodyText = selenium.getBodyText();

        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(bodyText);
        return m.find();
    }

    /**
     * Check if a link with the specified id is present in the current page.
     * 
     * @param id the link id
     * @return true if the requested link is found, false otherwise
     */
    public boolean isLinkPresent(String id) {
        return isElementPresent(By.linkText(WebUtils.toValidHtmlName(id)));
    }

    /**
     * Check if a link to the specified href is present in the current page.
     * 
     * @param href the href / target of the link
     * @return true if the requested link is found, false otherwise
     */
    public boolean isLinkToPresent(String href) {
        return isElementPresent(By.xpath("//a[@href=\"" + href + "\"]"));
    }

    /**
     * Check if the specified element is visible.
     * 
     * @param by the method by which to identify the element
     * @return  true if the element is visible, false otherwise.
     */
    public boolean isVisible(By by) {
        try {
            return webDriver.findElement(by).isDisplayed();
        } catch (NoSuchElementException e) {
            return false;
        }
    }

    public boolean isEditable(String fieldId) {
        return selenium.isEditable(fieldId);
    }

    public Object evaluateScript(String expression) {
        return ((JavascriptExecutor) webDriver).executeScript(expression);
    }

    public String getBodyText() {
        return selenium.getBodyText();
    }

    public String getText(By by) {
        WebElement element = webDriver.findElement(by);
        return element.getText().trim();
    }

    public String getAttribute(By by, String attribute) {
        return webDriver.findElement(by).getAttribute(attribute);
    }

    public String[] getAllLinks() {
        return selenium.getAllLinks();
    }

    public String getCellContents(String tableId, int row, int column) {
        return selenium.getTable(WebUtils.toValidHtmlName(tableId) + "." + row + "." + column);
    }

    public String[] getSelectOptions(String fieldId) {
        return selenium.getSelectOptions(fieldId);
    }

    public String getValue(String fieldId) {
        return selenium.getValue(fieldId);
    }

    public String getTitle() {
        return selenium.getTitle();
    }

    public boolean isCookiePresent(String name) {
        return selenium.isCookiePresent(name);
    }

    public void deleteAllCookies() {
        selenium.deleteAllVisibleCookies();
    }

    public void waitForVariable(String variable) {
        waitForVariable(variable, false);
    }

    public void waitForVariable(final String variable, final boolean inverse) {
        Wait<WebDriver> wait = new WebDriverWait(webDriver, WAITFOR_TIMEOUT / 1000);
        wait.until(new ExpectedCondition<Object>() {
            public Object apply(WebDriver webDriver) {
                JavascriptExecutor executor = (JavascriptExecutor) webDriver;
                return executor.executeScript("var r = " + variable + " !== undefined && " + variable
                        + " !== null && " + variable + " !== false; return " + (inverse ? "!" : "") + "r");
            }
        });
    }

    public WebElement waitForElement(String id) {
        return waitForElement(id, WAITFOR_TIMEOUT);
    }

    public WebElement waitForElement(final String id, long timeout) {
        return waitForElement(By.id(id), timeout);
    }

    public WebElement waitForElement(By by) {
        return waitForElement(by, WAITFOR_TIMEOUT);
    }

    public WebElement waitForElement(final By by, long timeout) {
        Wait<WebDriver> wait = new WebDriverWait(webDriver, timeout / 1000, 250).ignoring(RuntimeException.class);
        return wait.until(ExpectedConditions.presenceOfElementLocated(by));
    }

    public void waitAndClick(By by) {
        Wait<WebDriver> wait = new WebDriverWait(webDriver, WAITFOR_TIMEOUT / 1000, 250)
                .ignoring(RuntimeException.class);
        wait.until(ExpectedConditions.elementToBeClickable(by));
        click(by);
    }

    public void waitForElementToDisappear(final By by) {
        Wait<WebDriver> wait = new WebDriverWait(webDriver, WAITFOR_TIMEOUT / 1000, WAITFOR_INTERVAL)
                .ignoring(RuntimeException.class);
        wait.until(ExpectedConditions.invisibilityOfElementLocated(by));
    }

    public void waitForCondition(String condition) {
        waitForCondition(condition, WAITFOR_TIMEOUT);
    }

    public void waitForCondition(final String condition, long timeout) {
        Wait<WebDriver> wait = new WebDriverWait(webDriver, timeout / 1000);
        wait.until(new ExpectedCondition<Object>() {
            public Object apply(WebDriver webDriver) {
                return ((JavascriptExecutor) webDriver).executeScript(condition);
            }
        });
    }

    public void waitForVisible(final String locator) {
        TestUtils.waitForCondition(new Condition() {
            public boolean satisfied() {
                return selenium.isVisible(locator);
            }
        }, WAITFOR_TIMEOUT, "locator '" + locator + "' to become visible.");
    }

    /**
     * Waits for the pop-down status pane to appear with the given message.
     *
     * @param message message to wait for
     */
    public void waitForStatus(String message) {
        waitForElement(IDs.STATUS_MESSAGE, WAITFOR_TIMEOUT);
        TestUtils.waitForCondition(new Condition() {
            public boolean satisfied() {
                return StringUtils.stringSet(getText(By.id(IDs.STATUS_MESSAGE)));
            }
        }, WAITFOR_TIMEOUT, "status message to be set.");

        String text = getText(By.id(IDs.STATUS_MESSAGE));
        assertThat(text, containsString(message));
    }

    public void refreshUntilElement(String id) {
        refreshUntilElement(id, REFRESH_TIMEOUT);
    }

    public void refreshUntilElement(final String id, long timeout) {
        refreshUntil(timeout, new Condition() {
            public boolean satisfied() {
                return selenium.isElementPresent(WebUtils.toValidHtmlName(id));
            }
        }, "element '" + id + "'");
    }

    public void refreshUntil(long timeout, Condition condition, String conditionText) {
        long endTime = System.currentTimeMillis() + timeout;

        while (!condition.satisfied()) {
            long remainingTime = endTime - System.currentTimeMillis();
            if (remainingTime <= 0) {
                throw new TimeoutException(
                        "Timed out after " + Long.toString(timeout) + "ms of waiting for " + conditionText);
            }

            try {
                Thread.sleep(REFRESH_INTERVAL);
            } catch (InterruptedException e) {
                throw new SeleniumException(e);
            }

            selenium.refresh();
            try {
                selenium.waitForPageToLoad(String.valueOf(remainingTime));
            } catch (Exception e) {
                e.printStackTrace(System.err);
            }
        }
    }

    public void captureFailure(String testName) {
        try {
            captureBodyText(testName);
            captureScreenshot(testName);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void captureBodyText(String testName) throws IOException {
        String text;
        try {
            text = selenium.getHtmlSource();
        } catch (Exception e) {
            text = stackTraceAsString(e, "Unable to get HTML source using selenium:");
        }

        Files.write(text, new File(getWorkingDirectory(), testName + "-failure.html"), Charset.defaultCharset());
    }

    private void captureScreenshot(String testName) throws IOException {
        String screenshotFilename = new File(getWorkingDirectory(), testName + "-failure.png").getAbsolutePath();
        try {
            selenium.captureScreenshot(screenshotFilename);
        } catch (Exception e) {
            Files.write(stackTraceAsString(e, "Unable to capture screenshot with selenium:"),
                    new File(getWorkingDirectory(), testName + "-failure.png.txt"), Charset.defaultCharset());
        }
    }

    private String stackTraceAsString(Exception e, String prelude) {
        StringWriter stringWriter = new StringWriter();
        PrintWriter printWriter = new PrintWriter(stringWriter);
        printWriter.println(prelude);
        e.printStackTrace(printWriter);
        return stringWriter.toString();
    }

    /**
     * Sets the value of an Ext combo box with a given component id.
     *
     * @param comboId component id of the combo
     * @param value   value to set the combo to
     */
    public void setComboByValue(String comboId, String value) {
        String indexExpression;
        // Annoyingly ext stores can't find the empty string value...
        if (StringUtils.stringSet(value)) {
            indexExpression = "store.find(store.fields.first().name, '" + StringUtil.javaScriptStringEnc(value)
                    + "')";
        } else {
            indexExpression = "0";
        }

        evaluateScript("var combo = Ext.getCmp('" + comboId + "');" + "combo.setValue('"
                + StringUtil.javaScriptStringEnc(value) + "');" + "var store = combo.getStore();"
                + "combo.fireEvent('select', combo, store.getAt(" + indexExpression + "));");
    }

    /**
     * Retrieves the current value of the Ext combo box with the given
     * component id.
     * 
     * @param comboId component id of the combo
     * @return current value of the combo
     */
    public String getComboValue(String comboId) {
        return (String) evaluateScript("return Ext.getCmp('" + comboId + "').getValue()");
    }

    /**
     * Retrieves the available options in the Ext combo box with the given
     * component id.
     * 
     * @param comboId component id of the combo
     * @return available options in the combo
     */
    @SuppressWarnings("unchecked")
    public List<String> getComboOptions(String comboId) {
        String js = "return function() { " + "var combo = Ext.getCmp('" + comboId + "'); " + "var values = []; "
                + "combo.store.each(function(r) { values.push(r.get(combo.valueField)); }); " + "return values; "
                + "}();";
        return (List<String>) evaluateScript(js);
    }

    /**
     * Retrieves the displayed strings for available options in the Ext combo
     * box with the given component id.
     *
     * @param comboId component id of the combo
     * @return displayed strings for available options in the combo
     */
    @SuppressWarnings("unchecked")
    public List<String> getComboDisplays(String comboId) {
        String js = "return function() { " + "var combo = Ext.getCmp('" + comboId + "'); " + "var values = []; "
                + "combo.store.each(function(r) { values.push(r.get(combo.displayField)); }); " + "return values; "
                + "}();";
        return (List<String>) evaluateScript(js);
    }
}