jj.webdriver.WebDriverRule.java Source code

Java tutorial

Introduction

Here is the source code for jj.webdriver.WebDriverRule.java

Source

/*
 *    Copyright 2012 Jason Miller
 *
 * 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 jj.webdriver;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import javax.inject.Singleton;

import jj.webdriver.finder.ImpatientWebElementFinder;
import jj.webdriver.generator.PanelMethodGeneratorsModule;
import jj.webdriver.panel.PanelBase;
import jj.webdriver.panel.PanelFactory;
import jj.webdriver.panel.URLBase.BaseURL;
import jj.webdriver.provider.PhantomJSWebDriverProvider;

import org.apache.commons.codec.binary.Base64;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.ScreenshotException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.TypeLiteral;

/**
 * <p>
 * Manages a WebDriver to connect to a configured browser instance,
 * producing Page/Panel instances to drive the browser for testing
 * purposes
 * 
 * <p>
 * Usage example:<pre class="brush:java">
 * 
 * public class SomeBrowserDrivenTest {
 * 
 *    {@literal @}Rule
 *    public WebDriverRule webDriverRule = new WebDriverRule()
 *       .baseUrl(... defaults to "http://localhost:8080" ...)
 *       .driverProvider(... required! ...);
 * 
 *    {@literal @}Test
 *    public void test() {
 *       SomePage page = webDriverRule.get(SomePage.class);
 *       // drive the page!
 *    }
 * }
 * </pre>
 * 
 * @author jason
 *
 * @see Panel
 * @see Page
 *
 */
public class WebDriverRule implements TestRule {

    private static final String SEPARATOR = "*************************************************************************************";

    // TODO is it reasonable even having a default here?
    private String baseUrl = "http://localhost:8080";

    private Class<? extends WebDriverProvider> webDriverProvider = null;

    private Class<? extends WebElementFinder> webElementFinder = ImpatientWebElementFinder.class;

    private Class<? extends PanelBase> panelBaseClass = PanelBase.class;

    private Description currentDescription = null;

    private Path screenshotDir = Paths.get("build");

    private boolean screenshotOnError = true;

    private Logger logger = null;

    private Injector injector = null;

    private WebDriver webDriver = null;

    @Override
    public Statement apply(final Statement base, final Description description) {

        return new Statement() {

            @Override
            public void evaluate() throws Throwable {

                assert webDriverProvider != null : "you must supply a WebDriverProvider";

                currentDescription = description;

                logger = LoggerFactory.getLogger("test runner");

                injector = Guice.createInjector(new AbstractModule() {

                    @Override
                    protected void configure() {
                        bind(new TypeLiteral<Class<? extends PanelBase>>() {
                        }).toInstance(panelBaseClass);
                        bind(String.class).annotatedWith(BaseURL.class).toInstance(baseUrl);
                        bind(WebDriver.class).toProvider(webDriverProvider).in(Singleton.class);
                        bind(WebElementFinder.class).to(webElementFinder);
                        bind(Logger.class).toInstance(logger);
                    }
                }, new PanelMethodGeneratorsModule());

                webDriver = injector.getInstance(WebDriver.class);

                try {

                    logger.info(SEPARATOR);
                    logger.info("beginning {}.{}", description.getClassName(), description.getMethodName());
                    logger.info("using driver {}", webDriver);
                    base.evaluate();

                } catch (Throwable t) {

                    logger.error("TEST ENDED IN ERROR", t);

                    if (screenshotOnError && !saveScreenshotIfFound(t)) {
                        takeScreenshot(makeScreenShotName("error-screenshot"));
                    }

                    throw t;

                } finally {

                    logger.info(SEPARATOR + "\n");
                    webDriver.quit();
                    currentDescription = null;
                    webDriver = null;
                    injector = null;
                    logger = null;
                }
            }
        };
    }

    private boolean saveScreenshotIfFound(Throwable t) {
        boolean hasScreenshot = false;
        if (t.getCause() instanceof ScreenshotException) {

            try {
                hasScreenshot = true;
                String screenshotBase64 = ((ScreenshotException) t.getCause()).getBase64EncodedScreenshot();

                byte[] screenshot = Base64.decodeBase64(screenshotBase64);

                Path screenshotFile = screenshotDir.resolve(makeScreenShotName("error-screenshot"));

                Files.write(screenshotFile, screenshot);
                logger.info("saved error state screenshot {}", screenshotFile);

            } catch (Exception ioe) {
                logger.error("couldn't save the error screenshot", ioe);
            }
        }

        return hasScreenshot;
    }

    private void assertUnstarted() {
        assert currentDescription == null : "rule configuration must be before test runs begin";
    }

    /**
     * <p>
     * Configure the base URL for the test run.  URLs are determined with simple
     * concatenation - the URL configured for a Page interface is appended to
     * the value configured here. Default is "http://localhost:8080"
     */
    public WebDriverRule baseUrl(final String baseUrl) {
        assertUnstarted();

        this.baseUrl = baseUrl;
        return this;
    }

    /**
     * <p>
     * Configure the class that provides the {@link WebDriver} implementation for the 
     * test run.  Default is {@link PhantomJSWebDriverProvider}.
     * 
     * <p>
     * The provider will be bound as a singleton
     */
    public WebDriverRule driverProvider(Class<? extends WebDriverProvider> webDriverProvider) {
        assertUnstarted();
        assert webDriverProvider != null : "don't give me null!";

        this.webDriverProvider = webDriverProvider;
        return this;
    }

    public WebDriverRule webElementFinder(Class<? extends WebElementFinder> webElementFinder) {
        assertUnstarted();
        assert webElementFinder != null : "don't give me null!";

        this.webElementFinder = webElementFinder;
        return this;
    }

    public WebDriverRule panelBaseClass(Class<? extends PanelBase> panelBaseClass) {
        assertUnstarted();
        assert panelBaseClass != null : "don't give me null!";

        this.panelBaseClass = panelBaseClass;
        return this;
    }

    public WebDriverRule screenShotDir(Path screenshotDir) {
        assertUnstarted();
        assert screenshotDir != null : "don't give me null!";
        assert Files.isDirectory(screenshotDir) : "must be a directory!";

        this.screenshotDir = screenshotDir;
        return this;
    }

    /**
     * Takes a screenshot of the current state of the browser, if possible according to the
     * current driver, and stores it in the current directory, which is dependent upon
     * test invocation
     */
    public void takeScreenshot() throws IOException {
        takeScreenshot(makeScreenShotName("screenshot"));
    }

    /**
     * <p>
     * Takes a screenshot of the current state of the browser, if possible according to the
     * current driver, and stores it in the screenshot directory using the given name.
     * 
     * <p>
     * If the file already exists, it is overwritten
     */
    public void takeScreenshot(String screenshotName) throws IOException {

        assert webDriver != null : "cannot take a screenshot outside of a test";

        if (webDriver instanceof TakesScreenshot) {

            byte[] screenshot = ((TakesScreenshot) webDriver).getScreenshotAs(OutputType.BYTES);
            Path restingPlace = screenshotDir.resolve(screenshotName);
            Files.write(restingPlace, screenshot);

            logger.info("saved {}", restingPlace);
        }
    }

    /**
     * Helper method to make a screenshot name in the format:
     * <pre>
     * ${base}-${test class name}.${test method name}[${year}.${month}.${day}.${hour}.${minute}.${second}.${millisecond}].png
     * </pre>
     */
    public String makeScreenShotName(String base) {

        Calendar now = Calendar.getInstance();

        return String.format("%s-%s.%s[%d.%d.%d.%d.%d.%d.%d].png", base, currentDescription.getClassName(),
                currentDescription.getMethodName(), now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, // ANNOYING
                now.get(Calendar.DATE), now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE),
                now.get(Calendar.SECOND), now.get(Calendar.MILLISECOND));
    }

    private String makeURL(String inputURL, Object... queryObjects) {

        List<Object> formatArgs = new ArrayList<>();
        QueryParams queryParams = null;
        if (queryObjects != null) {
            for (Object queryObject : queryObjects) {
                if (queryObject instanceof QueryParams) {
                    queryParams = queryParams == null ? (QueryParams) queryObject
                            : queryParams.and((QueryParams) queryObject);
                } else if (queryObject instanceof String) {
                    try {
                        formatArgs.add(URLEncoder.encode((String) queryObject, "UTF-8"));
                    } catch (UnsupportedEncodingException e) {
                        /* can't happen */ }
                } else if (queryObject instanceof Number) {
                    formatArgs.add(queryObject);
                } else {
                    logger.error("got a querystring argument that makes no sense, {}", queryObject);
                }
            }
        }
        String url = String.format(inputURL, formatArgs.toArray());

        if (queryParams != null) {
            url = url + (url.contains("?") ? "&" : "?") + queryParams;
        }

        return url;
    }

    public <T extends Page> T get(final Class<T> pageInterface) {
        return get(pageInterface, new Object[0]);
    }

    /**
     * <p>
     * Directs the underlying WebDriver to make a request, using the URL defined on the
     * given page interface combined with any query args, then returns a object implementing
     * the interface that can be used to drive the browser.
     * 
     * <p>
     * The page interface must have a {@link URL} annotation.  The value of this annotation is
     * concatenated to the configured base URL in this rule, then the result is used as a format
     * string.
     * 
     * <p>
     * The queryArgs parameter can be a mix of String, Number, and QueryParam objects.  All
     * String and Number arguments are passed to the String.format call in the order they appear.
     * All QueryParam objects are rendered to the end of the query string for the URL.
     * 
     * <p>
     * it's probably best to not mix the two styles.  this API is under... consideration.  So
     * don't expect it to be stable.
     * 
     */
    public <T extends Page> T get(final Class<T> pageInterface, final Object... queryArgs) {

        assert webDriver != null : "cannot get a page outside of a test";

        assert pageInterface.getAnnotation(URL.class) != null : "page declarations must have a URL annotation";

        webDriver.get(makeURL(baseUrl + pageInterface.getAnnotation(URL.class).value(), queryArgs));

        return injector.getInstance(PanelFactory.class).create(pageInterface);
    }

    public WebDriverRule screenshotOnError(boolean screenshotOnError) {
        this.screenshotOnError = screenshotOnError;
        return this;
    }
}