org.zanata.page.WebDriverFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.zanata.page.WebDriverFactory.java

Source

/*
 * Copyright 2010, Red Hat, Inc. and individual contributors as indicated by the
 * @author tags. See the copyright.txt file 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.zanata.page;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.function.Supplier;
import java.util.logging.Level;

import com.google.common.reflect.AbstractInvocationHandler;
import net.lightbody.bmp.BrowserMobProxy;
import net.lightbody.bmp.BrowserMobProxyServer;
import net.lightbody.bmp.client.ClientUtil;
import net.lightbody.bmp.filters.ResponseFilterAdapter;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Proxy;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxBinary;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxProfile;
import org.openqa.selenium.logging.LogEntries;
import org.openqa.selenium.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
import org.openqa.selenium.logging.LoggingPreferences;
import org.openqa.selenium.remote.Augmenter;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.service.DriverService;
import org.openqa.selenium.support.events.EventFiringWebDriver;
import org.openqa.selenium.support.events.WebDriverEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zanata.util.PropertiesHolder;
import com.google.common.base.Strings;
import lombok.extern.slf4j.Slf4j;
import org.zanata.util.ScreenshotDirForTest;
import org.zanata.util.TestEventForScreenshotListener;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

import static java.lang.reflect.Proxy.newProxyInstance;
import static org.zanata.util.Constants.webDriverType;
import static org.zanata.util.Constants.webDriverWait;
import static org.zanata.util.Constants.zanataInstance;

@Slf4j
public enum WebDriverFactory {
    INSTANCE;

    private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("HH:mm:ss.SSS");
        }
    };
    // can reuse, share globally
    private static ObjectMapper mapper = new ObjectMapper();

    private volatile EventFiringWebDriver driver = createDriver();
    private @Nonnull DswidParamChecker dswidParamChecker;
    private DriverService driverService;
    private TestEventForScreenshotListener screenshotListener;
    private int webdriverWait;

    private final String[] ignoredLogPatterns = {
            ".*/org.richfaces/jquery.js .* " + "'webkit(?:Cancel)?RequestAnimationFrame' is vendor-specific. "
                    + "Please use the standard " + "'(?:request|cancel)AnimationFrame' instead.",
            ".*/org.richfaces/jquery.js .* " + "'webkitMovement[XY]' is deprecated. "
                    + "Please use 'movement[XY]' instead.",
            "http://example.com/piwik/piwik.js .*", };

    @Nullable
    private WebDriverEventListener logListener;

    public WebDriver getDriver() {
        return driver;
    }

    private JavascriptExecutor getExecutor() {
        return (JavascriptExecutor) getDriver();
    }

    private EventFiringWebDriver createDriver() {
        String driverType = PropertiesHolder.getProperty(webDriverType.value());
        EventFiringWebDriver newDriver;
        switch (driverType.toLowerCase()) {
        case "chrome":
            newDriver = configureChromeDriver();
            break;
        case "firefox":
            newDriver = configureFirefoxDriver();
            break;
        default:
            throw new UnsupportedOperationException("only support chrome " + "and firefox driver");
        }
        Runtime.getRuntime().addShutdownHook(new ShutdownHook());
        webdriverWait = Integer.parseInt(PropertiesHolder.getProperty(webDriverWait.value()));
        dswidParamChecker = new DswidParamChecker(newDriver);
        newDriver.register(dswidParamChecker.getEventListener());
        return newDriver;
    }

    /**
     * List the WebDriver log types
     *
     * LogType.CLIENT doesn't seem to log anything
     * LogType.DRIVER, LogType.PERFORMANCE are too verbose
     * LogType PROFILER and LogType.SERVER don't seem to work
     *
     * @return String array of log types
     */
    private String[] getLogTypes() {
        return new String[] { LogType.BROWSER };
    }

    /**
     * Retrieves all the outstanding WebDriver logs of the specified type.
     * @param type a log type from org.openqa.selenium.logging.LogType
     *             (but they don't all seem to work)
     */
    public LogEntries getLogs(String type) {
        return getDriver().manage().logs().get(type);
    }

    private String toString(long timestamp, String text, @Nullable String json) {
        String time = TIME_FORMAT.get().format(new Date(timestamp));
        return time + " " + text + (json != null ? ": " + json : "");
    }

    private boolean ignorable(String message) {
        for (String ignorable : ignoredLogPatterns) {
            if (message.matches(ignorable)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Logs all the outstanding WebDriver logs of the specified type.
     * @param type a log type from org.openqa.selenium.logging.LogType
     *             (but they don't all seem to work)
     * @throws WebDriverLogException exception containing the first warning/error message, if any
     */
    private void logLogs(String type, boolean throwIfWarn) {
        @Nullable
        WebDriverLogException firstException = null;
        String logName = WebDriverFactory.class.getName() + "." + type;
        Logger log = LoggerFactory.getLogger(logName);
        int logCount = 0;
        for (LogEntry logEntry : getLogs(type)) {
            ++logCount;
            Level level;
            long time = logEntry.getTimestamp();
            String text, json;
            String msg = logEntry.getMessage();
            if (msg.startsWith("{")) {
                // looks like it might be json
                json = msg;
                try {
                    JsonNode message = mapper.readValue(json, ObjectNode.class).path("message");
                    String levelString = message.path("level").asText();
                    level = toLogLevel(levelString);
                    text = message.path("text").asText();
                } catch (Exception e) {
                    log.warn("unable to parse as json: " + json, e);
                    level = logEntry.getLevel();
                    text = msg;
                    json = null;
                }
            } else {
                level = logEntry.getLevel();
                text = msg;
                json = null;
            }
            String logString = toString(time, text, json);
            if (level.intValue() >= Level.SEVERE.intValue()) {
                log.error(logString);
                // If firstException was a warning, replace it with this error.
                if ((firstException == null || !firstException.isErrorLog()) && !ignorable(msg)) {
                    // We only throw this if throwIfWarn is true
                    firstException = new WebDriverLogException(level, logString, driver.getPageSource());
                }
            } else if (level.intValue() >= Level.WARNING.intValue()) {
                log.warn(logString);
                if ((firstException == null) && !ignorable(msg)) {
                    // We only throw this if throwIfWarn is true
                    firstException = new WebDriverLogException(logEntry.getLevel(), logString,
                            driver.getPageSource());
                }
            } else if (level.intValue() >= Level.INFO.intValue()) {
                log.info(logString);
            } else {
                log.debug(logString);
            }
        }
        if (throwIfWarn && firstException != null) {
            throw firstException;
        }
    }

    private Level toLogLevel(String jsLevel) {
        String upperCase = jsLevel.toUpperCase();
        if (upperCase.equals("WARN")) {
            return Level.WARNING;
        }
        return Level.parse(upperCase);
    }

    /**
     * Dump any outstanding browser logs to the main log.
     *
     * @throws WebDriverLogException exception containing the first error message, if any
     */
    public void logLogs() {
        // DeltaSpike's LAZY mode uses client-side redirects, which cause
        // other scripts to abort loading in strange ways when dswid is
        // missing/wrong. However, the server code currently preserves dswid
        // whenever possible, and DswidParamChecker aims to make sure it
        // continues to, so we can treat JS warnings/errors as failures.
        logLogs(true);
    }

    /**
     * Dump any outstanding browser logs to the main log.
     *
     * @throws WebDriverLogException exception containing the first warning/error message, if any
     */
    public void logLogs(boolean throwIfWarn) {
        for (String type : getLogTypes()) {
            logLogs(type, throwIfWarn);
        }
    }

    public String getHostUrl() {
        return PropertiesHolder.getProperty(zanataInstance.value());
    }

    public int getWebDriverWait() {
        return webdriverWait;
    }

    public void registerScreenshotListener(String testName) {
        log.info("Enabling screenshot module...");
        if (screenshotListener == null && ScreenshotDirForTest.isScreenshotEnabled()) {
            screenshotListener = new TestEventForScreenshotListener(driver);
        }
        driver.register(screenshotListener);
        screenshotListener.updateTestID(testName);
    }

    @ParametersAreNonnullByDefault
    public void registerLogListener() {
        if (logListener == null) {
            logListener = (WebDriverEventListener) newProxyInstance(WebDriverEventListener.class.getClassLoader(),
                    new Class<?>[] { WebDriverEventListener.class }, new AbstractInvocationHandler() {
                        @Override
                        protected Object handleInvocation(Object proxy, Method method, Object[] args)
                                throws Throwable {
                            logLogs();
                            return null;
                        }
                    });
        }
        driver.register(logListener);
    }

    public void unregisterLogListener() {
        driver.unregister(logListener);
    }

    public void unregisterScreenshotListener() {
        log.info("Deregistering screenshot module...");
        driver.unregister(screenshotListener);
    }

    public void injectScreenshot(String tag) {
        if (null != screenshotListener) {
            screenshotListener.customEvent(tag);
        }
    }

    private EventFiringWebDriver configureChromeDriver() {
        System.setProperty(ChromeDriverService.CHROME_DRIVER_LOG_PROPERTY,
                PropertiesHolder.getProperty("webdriver.log"));
        driverService = ChromeDriverService.createDefaultService();
        DesiredCapabilities capabilities = DesiredCapabilities.chrome();
        capabilities.setCapability("chrome.binary",
                PropertiesHolder.properties.getProperty("webdriver.chrome.bin"));

        ChromeOptions options = new ChromeOptions();
        URL url = Thread.currentThread().getContextClassLoader()
                .getResource("zanata-testing-extension/chrome/manifest.json");
        assert url != null : "can't find extension (check testResource config in pom.xml)";
        File file = new File(url.getPath()).getParentFile();
        options.addArguments("load-extension=" + file.getAbsolutePath());
        capabilities.setCapability(ChromeOptions.CAPABILITY, options);

        enableLogging(capabilities);

        // start the proxy
        BrowserMobProxy proxy = new BrowserMobProxyServer();
        proxy.start(0);

        proxy.addFirstHttpFilterFactory(
                new ResponseFilterAdapter.FilterSource((response, contents, messageInfo) -> {
                    // TODO fail test if response >= 500?
                    if (response.getStatus().code() >= 400) {
                        log.warn("Response {} for URI {}", response.getStatus(),
                                messageInfo.getOriginalRequest().getUri());
                    } else {
                        log.info("Response {} for URI {}", response.getStatus(),
                                messageInfo.getOriginalRequest().getUri());
                    }
                }, 0));
        Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy);
        capabilities.setCapability(CapabilityType.PROXY, seleniumProxy);
        try {
            driverService.start();
        } catch (IOException e) {
            throw new RuntimeException("fail to start chrome driver service");
        }
        return new EventFiringWebDriver(
                new Augmenter().augment(new RemoteWebDriver(driverService.getUrl(), capabilities)));
    }

    private EventFiringWebDriver configureFirefoxDriver() {
        final String pathToFirefox = Strings.emptyToNull(PropertiesHolder.properties.getProperty("firefox.path"));

        FirefoxBinary firefoxBinary;
        if (pathToFirefox != null) {
            firefoxBinary = new FirefoxBinary(new File(pathToFirefox));
        } else {
            firefoxBinary = new FirefoxBinary();
        }
        DesiredCapabilities capabilities = DesiredCapabilities.firefox();
        enableLogging(capabilities);
        return new EventFiringWebDriver(new FirefoxDriver(firefoxBinary, makeFirefoxProfile(), capabilities));
    }

    private void enableLogging(DesiredCapabilities capabilities) {
        LoggingPreferences logs = new LoggingPreferences();
        for (String type : getLogTypes()) {
            logs.enable(type, Level.INFO);
        }
        capabilities.setCapability(CapabilityType.LOGGING_PREFS, logs);
    }

    private FirefoxProfile makeFirefoxProfile() {
        if (!Strings.isNullOrEmpty(System.getProperty("webdriver.firefox.profile"))) {
            throw new RuntimeException("webdriver.firefox.profile is ignored");
            // TODO - look at FirefoxDriver.getProfile().
        }
        final FirefoxProfile firefoxProfile = new FirefoxProfile();
        firefoxProfile.setAlwaysLoadNoFocusLib(true);
        firefoxProfile.setEnableNativeEvents(true);
        firefoxProfile.setAcceptUntrustedCertificates(true);
        // TODO port zanata-testing-extension to firefox
        //        File file = new File("extension.xpi");
        //        firefoxProfile.addExtension(file);
        return firefoxProfile;
    }

    public void testEntry() {
        clearDswid();
    }

    public void testExit() {
        clearDswid();
    }

    private void clearDswid() {
        // clear the browser's memory of the dswid
        getExecutor().executeScript("window.name = ''");
        dswidParamChecker.clear();
    }

    public <T> T ignoringDswid(Supplier<T> supplier) {
        dswidParamChecker.stopChecking();
        try {
            return supplier.get();
        } finally {
            dswidParamChecker.startChecking();
        }
    }

    public void ignoringDswid(Runnable r) {
        dswidParamChecker.stopChecking();
        try {
            r.run();
        } finally {
            dswidParamChecker.startChecking();
        }
    }

    private class ShutdownHook extends Thread {
        public void run() {
            // If webdriver is running quit.
            WebDriver driver = getDriver();
            if (driver != null) {
                try {
                    log.info("Quitting webdriver.");
                    driver.quit();
                } catch (Throwable e) {
                    // Ignoring driver tear down errors.
                }
            }
            if (driverService != null && driverService.isRunning()) {
                driverService.stop();
            }
        }
    }
}