org.kurento.test.browser.Browser.java Source code

Java tutorial

Introduction

Here is the source code for org.kurento.test.browser.Browser.java

Source

/*
 * (C) Copyright 2014 Kurento (http://kurento.org/)
 *
 * 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 org.kurento.test.browser;

import static org.kurento.commons.PropertiesManager.getProperty;
import static org.kurento.test.config.TestConfiguration.SAUCELAB_COMMAND_TIMEOUT_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SAUCELAB_COMMAND_TIMEOUT_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SAUCELAB_IDLE_TIMEOUT_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SAUCELAB_IDLE_TIMEOUT_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SAUCELAB_KEY_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SAUCELAB_MAX_DURATION_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SAUCELAB_MAX_DURATION_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SAUCELAB_USER_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_MAX_DRIVER_ERROR_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SELENIUM_MAX_DRIVER_ERROR_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_REMOTEWEBDRIVER_TIME_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SELENIUM_REMOTEWEBDRIVER_TIME_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_REMOTE_HUB_URL_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_SCOPE_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_VERSION;
import static org.kurento.test.config.TestConfiguration.TEST_HOST_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_NODE_LOGIN_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_NODE_PASSWD_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_NODE_PEM_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_PATH_DEFAULT;
import static org.kurento.test.config.TestConfiguration.TEST_PATH_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_PORT_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_PROTOCOL_DEFAULT;
import static org.kurento.test.config.TestConfiguration.TEST_PROTOCOL_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_PUBLIC_IP_DEFAULT;
import static org.kurento.test.config.TestConfiguration.TEST_PUBLIC_IP_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_PUBLIC_PORT_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_SCREEN_SHARE_TITLE_DEFAULT;
import static org.kurento.test.config.TestConfiguration.TEST_SCREEN_SHARE_TITLE_DEFAULT_WIN;
import static org.kurento.test.config.TestConfiguration.TEST_SCREEN_SHARE_TITLE_PROPERTY;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.kurento.commons.exception.KurentoException;
import org.kurento.test.base.KurentoTest;
import org.kurento.test.config.AudioChannel;
import org.kurento.test.config.BrowserScope;
import org.kurento.test.config.Protocol;
import org.kurento.test.config.TestConfiguration;
import org.kurento.test.docker.Docker;
import org.kurento.test.grid.GridHandler;
import org.kurento.test.grid.GridNode;
import org.kurento.test.services.WebServerService;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Platform;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxProfile;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.github.bonigarcia.wdm.ChromeDriverManager;

/**
 * Wrapper of Selenium Webdriver for testing Kurento applications.
 *
 * @author Boni Garcia (bgarcia@gsyc.es)
 * @author Micael Gallego (micael.gallego@gmail.com)
 * @since 5.1.0
 * @see <a href="http://www.seleniumhq.org/">Selenium</a>
 */
public class Browser implements Closeable {

    public static Logger log = LoggerFactory.getLogger(Browser.class);

    private WebDriver driver;
    private String jobId;
    private Builder builder;
    private BrowserType browserType;
    private BrowserScope scope;
    private String browserVersion;
    private Platform platform;
    private String video;
    private String audio;
    private int recordAudio;
    private int audioSampleRate;
    private AudioChannel audioChannel;
    private int timeout;
    private boolean usePhysicalCam;
    private boolean enableScreenCapture;
    private String name;
    private String id;
    private double colorDistance;
    private int thresholdTime;
    private int numInstances;
    private int browserPerInstance;
    private Protocol protocol;
    private String node;
    private String host;
    private int serverPort;
    private WebPageType webPageType;
    private String webPagePath;
    private String login;
    private String passwd;
    private String pem;
    private boolean avoidProxy;
    private String parentTunnel;
    private List<Map<String, String>> extensions;
    private URI url;

    private static volatile DockerBrowserManager dockerManager;
    private static Docker docker = Docker.getSingleton();

    public Browser(Builder builder) {

        this.builder = builder;
        this.scope = builder.scope;
        this.video = builder.video;
        this.audio = builder.audio;
        this.serverPort = getProperty(TEST_PORT_PROPERTY,
                getProperty(TEST_PUBLIC_PORT_PROPERTY, builder.serverPort));
        this.webPageType = builder.webPageType;
        this.browserType = builder.browserType;
        this.usePhysicalCam = builder.usePhysicalCam;
        this.enableScreenCapture = builder.enableScreenCapture;
        this.recordAudio = builder.recordAudio;
        this.audioSampleRate = builder.audioSampleRate;
        this.audioChannel = builder.audioChannel;
        this.browserVersion = builder.browserVersion;
        this.platform = builder.platform;
        this.timeout = builder.timeout;
        this.colorDistance = builder.colorDistance;
        this.thresholdTime = builder.thresholdTime;
        this.node = builder.node;
        this.protocol = builder.protocol;
        this.numInstances = builder.numInstances;
        this.browserPerInstance = builder.browserPerInstance;
        this.login = builder.login;
        this.passwd = builder.passwd;
        this.pem = builder.pem;
        this.host = builder.host;
        this.avoidProxy = builder.avoidProxy;
        this.parentTunnel = builder.parentTunnel;
        this.extensions = builder.extensions;
        this.url = builder.url;
        this.webPagePath = builder.webPagePath;
    }

    public void init() {

        log.debug("Starting browser {}", getId());

        Class<? extends WebDriver> driverClass = browserType.getDriverClass();

        try {
            DesiredCapabilities capabilities = new DesiredCapabilities();

            if (driverClass.equals(FirefoxDriver.class)) {

                createFirefoxBrowser(capabilities);

            } else if (driverClass.equals(ChromeDriver.class)) {

                createChromeBrowser(capabilities);

            } else if (driverClass.equals(InternetExplorerDriver.class)) {

                if (scope == BrowserScope.SAUCELABS) {
                    capabilities.setBrowserName(DesiredCapabilities.internetExplorer().getBrowserName());
                    capabilities.setCapability("ignoreProtectedModeSettings", true);
                    createSaucelabsDriver(capabilities);
                }

            } else if (driverClass.equals(SafariDriver.class)) {

                if (scope == BrowserScope.SAUCELABS) {
                    capabilities.setBrowserName(DesiredCapabilities.safari().getBrowserName());
                    createSaucelabsDriver(capabilities);
                }
            }

            // Timeouts
            changeTimeout(timeout);

            log.debug("Browser {} started", getId());

            calculateUrl();

            log.debug("Browser {} loading url {}", getId(), url);

            driver.get(url.toString());

            log.debug("Browser {} initialized", getId());

        } catch (MalformedURLException e) {
            log.error("MalformedURLException in Browser.init", e);
        }

    }

    private void calculateUrl() {
        if (url == null) {
            if (protocol == Protocol.FILE) {
                String webPage = webPagePath != null ? webPagePath : webPageType.toString();
                File webPageFile = new File(
                        this.getClass().getClassLoader().getResource("static" + webPage).getFile());
                try {
                    url = new URI(protocol.toString() + webPageFile.getAbsolutePath());
                } catch (URISyntaxException e) {
                    throw new KurentoException(
                            "Exception generating URI from " + protocol + " and " + webPageFile.getAbsolutePath());
                }
            } else {

                String hostName;
                log.debug("BrowserScope is {}", scope);
                if (scope == BrowserScope.DOCKER) {
                    if (docker.isRunningInContainer()) {
                        hostName = docker.getContainerIpAddress();
                    } else {
                        hostName = docker.getHostIpForContainers();
                    }

                } else {
                    hostName = host != null ? host : node;
                }

                log.debug("Protocol: {}, Hostname: {}, Port: {}, Web page type: {}", protocol, hostName, serverPort,
                        webPageType);

                try {
                    url = new URI(protocol.toString(), null, hostName, serverPort, webPageType.toString(), null,
                            null);
                } catch (URISyntaxException e) {
                    throw new KurentoException("Exception generating URI from " + protocol + ", " + hostName
                            + ", server port " + serverPort + " and webpage type " + webPageType);
                }
            }
        }
    }

    private void createChromeBrowser(DesiredCapabilities capabilities) throws MalformedURLException {

        // Chrome driver
        ChromeDriverManager.getInstance().setup();

        // Chrome options
        ChromeOptions options = new ChromeOptions();

        // Chrome extensions
        if (extensions != null && !extensions.isEmpty()) {

            for (Map<String, String> extension : extensions) {
                InputStream is = getExtensionAsInputStream(extension.values().iterator().next());
                if (is != null) {
                    try {
                        File crx = File.createTempFile(extension.keySet().iterator().next(), ".crx");
                        FileUtils.copyInputStreamToFile(is, crx);
                        options.addExtensions(crx);
                    } catch (Throwable t) {
                        log.error("Error loading Chrome extension {} ({} : {})", extension, t.getClass(),
                                t.getMessage());
                    }
                }
            }
        }

        if (enableScreenCapture) {
            // This flag enables the screen sharing
            options.addArguments("--enable-usermedia-screen-capturing");

            String windowTitle = TEST_SCREEN_SHARE_TITLE_DEFAULT;
            if (platform != null && (platform == Platform.WINDOWS || platform == Platform.XP
                    || platform == Platform.VISTA || platform == Platform.WIN8 || platform == Platform.WIN8_1)) {

                windowTitle = TEST_SCREEN_SHARE_TITLE_DEFAULT_WIN;
            }
            options.addArguments("--auto-select-desktop-capture-source="
                    + getProperty(TEST_SCREEN_SHARE_TITLE_PROPERTY, windowTitle));

        } else {
            // This flag avoids grant the camera
            options.addArguments("--use-fake-ui-for-media-stream");
        }

        // This flag avoids warning in Chrome. See:
        // https://code.google.com/p/chromedriver/issues/detail?id=799
        options.addArguments("--test-type");

        if (protocol == Protocol.FILE) {
            // This flag allows reading local files in video tags
            options.addArguments("--allow-file-access-from-files");
        }

        if (!usePhysicalCam) {
            // This flag makes using a synthetic video (green with
            // spinner) in WebRTC. Or it is needed to combine with
            // use-file-for-fake-video-capture to use a file faking the
            // cam
            options.addArguments("--use-fake-device-for-media-stream=fps=30");

            if (video != null && (isLocal() || isDocker())) {

                if (!Files.exists(Paths.get(video))) {
                    throw new RuntimeException("Trying to create a browser using video file " + video
                            + ", but this file doesn't exist.");
                }

                log.debug("Using video {} in browser {}", video, id);
                options.addArguments("--use-file-for-fake-video-capture=" + video);
            }
        }

        capabilities.setCapability(ChromeOptions.CAPABILITY, options);
        capabilities.setBrowserName(DesiredCapabilities.chrome().getBrowserName());

        createDriver(capabilities, options);
    }

    private void createFirefoxBrowser(DesiredCapabilities capabilities) throws MalformedURLException {
        FirefoxProfile profile = new FirefoxProfile();
        // This flag avoids granting the access to the camera
        profile.setPreference("media.navigator.permission.disabled", true);

        capabilities.setCapability(FirefoxDriver.PROFILE, profile);
        capabilities.setBrowserName(DesiredCapabilities.firefox().getBrowserName());

        // Firefox extensions
        if (extensions != null && !extensions.isEmpty()) {
            for (Map<String, String> extension : extensions) {
                InputStream is = getExtensionAsInputStream(extension.values().iterator().next());
                if (is != null) {
                    try {
                        File xpi = File.createTempFile(extension.keySet().iterator().next(), ".xpi");
                        FileUtils.copyInputStreamToFile(is, xpi);
                        profile.addExtension(xpi);
                    } catch (Throwable t) {
                        log.error("Error loading Firefox extension {} ({} : {})", extension, t.getClass(),
                                t.getMessage());
                    }
                }
            }
        }

        createDriver(capabilities, profile);
    }

    private void createDriver(DesiredCapabilities capabilities, Object options) throws MalformedURLException {

        log.debug("Creating driver in scope {} for browser {}", scope, id);

        if (scope == BrowserScope.SAUCELABS) {
            createSaucelabsDriver(capabilities);
        } else if (scope == BrowserScope.REMOTE) {
            createRemoteDriver(capabilities);
        } else if (scope == BrowserScope.DOCKER) {
            driver = getDockerManager().createDockerDriver(id, capabilities);
        } else {
            driver = newWebDriver(options);
        }
    }

    private DockerBrowserManager getDockerManager() {
        if (dockerManager == null) {
            synchronized (Browser.class) {
                if (dockerManager == null) {
                    dockerManager = new DockerBrowserManager();
                }
            }
        }

        return dockerManager;
    }

    public static WebDriver newWebDriver(Object options) {

        WebDriver driver = null;

        int numDriverTries = 0;
        final int maxDriverError = getProperty(SELENIUM_MAX_DRIVER_ERROR_PROPERTY,
                SELENIUM_MAX_DRIVER_ERROR_DEFAULT);
        final String errMessage = "Exception creating webdriver for chrome";
        do {
            try {

                if (options instanceof ChromeOptions) {
                    driver = new ChromeDriver((ChromeOptions) options);
                } else if (options instanceof FirefoxProfile) {
                    driver = new FirefoxDriver((FirefoxProfile) options);
                }

            } catch (Throwable t) {
                driver = null;
                log.warn(errMessage + " #" + numDriverTries, t);
            } finally {
                numDriverTries++;
                if (numDriverTries > maxDriverError) {
                    throw new KurentoException(errMessage + " (" + maxDriverError + " times)");
                }
            }
        } while (driver == null);

        return driver;
    }

    public void reload() {
        if (url != null) {
            this.driver.get(url.toString());
        }
    }

    public InputStream getExtensionAsInputStream(String extension) {
        InputStream is = null;

        try {
            log.debug("Trying to locate extension in the classpath ({}) ...", extension);
            is = ClassLoader.getSystemResourceAsStream(extension);
            if (is.available() < 0) {
                log.warn("Extension {} is not located in the classpath", extension);
                is = null;
            } else {
                log.debug("Success. Loading extension {} from classpath", extension);
            }
        } catch (Throwable t) {
            log.warn("Exception reading extension {} in the classpath ({} : {})", extension, t.getClass(),
                    t.getMessage());
            is = null;
        }
        if (is == null) {
            try {
                log.debug("Trying to locate extension as URL ({}) ...", extension);
                URL url = new URL(extension);
                is = url.openStream();
                log.debug("Success. Loading extension {} from URL", extension);
            } catch (Throwable t) {
                log.warn("Exception reading extension {} as URL ({} : {})", extension, t.getClass(),
                        t.getMessage());
            }
        }
        if (is == null) {
            throw new RuntimeException(extension + " is not a valid extension (it is not located in project"
                    + " classpath neither is a valid URL)");
        }
        return is;
    }

    public void changeTimeout(int timeoutSeconds) {
        driver.manage().timeouts().implicitlyWait(timeoutSeconds, TimeUnit.SECONDS);
        driver.manage().timeouts().setScriptTimeout(timeoutSeconds, TimeUnit.SECONDS);
    }

    public void createSaucelabsDriver(DesiredCapabilities capabilities) throws MalformedURLException {
        assertPublicIpNotNull();
        String sauceLabsUser = getProperty(SAUCELAB_USER_PROPERTY);
        String sauceLabsKey = getProperty(SAUCELAB_KEY_PROPERTY);

        if (sauceLabsUser == null || sauceLabsKey == null) {
            throw new RuntimeException("Invalid Saucelabs credentials: " + SAUCELAB_USER_PROPERTY + "="
                    + sauceLabsUser + " " + SAUCELAB_KEY_PROPERTY + "=" + sauceLabsKey);
        }

        capabilities.setCapability("version", browserVersion);
        capabilities.setCapability("platform", platform);

        String seleniumVersion = getProperty(SELENIUM_VERSION);
        if (seleniumVersion != null) {
            capabilities.setCapability("seleniumVersion", seleniumVersion);
        }
        if (parentTunnel != null) {
            capabilities.setCapability("parent-tunnel", parentTunnel);
        }
        if (avoidProxy) {
            capabilities.setCapability("avoid-proxy", avoidProxy);
        }

        int idleTimeout = getProperty(SAUCELAB_IDLE_TIMEOUT_PROPERTY, SAUCELAB_IDLE_TIMEOUT_DEFAULT);
        int commandTimeout = getProperty(SAUCELAB_COMMAND_TIMEOUT_PROPERTY, SAUCELAB_COMMAND_TIMEOUT_DEFAULT);
        int maxDuration = getProperty(SAUCELAB_MAX_DURATION_PROPERTY, SAUCELAB_MAX_DURATION_DEFAULT);
        capabilities.setCapability("idleTimeout", idleTimeout);
        capabilities.setCapability("commandTimeout", commandTimeout);
        capabilities.setCapability("maxDuration", maxDuration);

        if (name != null) {
            capabilities.setCapability("name", name);
        }

        driver = new RemoteWebDriver(
                new URL("http://" + sauceLabsUser + ":" + sauceLabsKey + "@ondemand.saucelabs.com:80/wd/hub"),
                capabilities);

        jobId = ((RemoteWebDriver) driver).getSessionId().toString();
        log.debug("%%%%%%%%%%%%% Saucelabs URL job for {} ({} {} in {}) %%%%%%%%%%%%%", id, browserType,
                browserVersion, platform);
        log.debug("https://saucelabs.com/tests/{}", jobId);
    }

    public void createRemoteDriver(final DesiredCapabilities capabilities) throws MalformedURLException {

        assertPublicIpNotNull();

        String remoteHubUrl = getProperty(SELENIUM_REMOTE_HUB_URL_PROPERTY);
        GridNode gridNode = null;

        if (remoteHubUrl == null) {

            log.debug("Creating remote webdriver for {}", id);
            if (!GridHandler.getInstance().containsSimilarBrowserKey(id)) {

                if (login != null) {
                    System.setProperty(TEST_NODE_LOGIN_PROPERTY, login);
                }
                if (passwd != null) {
                    System.setProperty(TEST_NODE_PASSWD_PROPERTY, passwd);
                }
                if (pem != null) {
                    System.setProperty(TEST_NODE_PEM_PROPERTY, pem);
                }

                // Filtering valid nodes (just the first time will be effective)
                GridHandler.getInstance().filterValidNodes();

                if (!node.equals(host) && login != null && !login.isEmpty()
                        && (passwd != null && !passwd.isEmpty() || pem != null && !pem.isEmpty())) {
                    gridNode = new GridNode(node, browserType, browserPerInstance, login, passwd, pem);
                    GridHandler.getInstance().addNode(id, gridNode);
                } else {
                    gridNode = GridHandler.getInstance().getRandomNodeFromList(id, browserType, browserPerInstance);
                }

                // Start Hub (just the first time will be effective)
                GridHandler.getInstance().startHub();

                // Start node
                GridHandler.getInstance().startNode(gridNode);

                // Copy video (if necessary)
                if (video != null && browserType == BrowserType.CHROME) {
                    GridHandler.getInstance().copyRemoteVideo(gridNode, video);
                }

            } else {
                // Wait for node
                boolean started = false;
                do {
                    gridNode = GridHandler.getInstance().getNode(id);
                    if (gridNode != null) {
                        started = gridNode.isStarted();
                    }
                    if (!started) {
                        log.debug("Node {} is not started ... waiting 1 second", id);
                        waitSeconds(1);
                    }
                } while (!started);
            }

            // At this moment we are able to use the argument for remote video
            if (video != null && browserType == BrowserType.CHROME) {
                ChromeOptions options = (ChromeOptions) capabilities.getCapability(ChromeOptions.CAPABILITY);
                options.addArguments("--use-file-for-fake-video-capture="
                        + GridHandler.getInstance().getFirstNode(id).getRemoteVideo(video));
                capabilities.setCapability(ChromeOptions.CAPABILITY, options);
            }

            final int hubPort = GridHandler.getInstance().getHubPort();
            final String hubHost = GridHandler.getInstance().getHubHost();

            log.debug("Creating remote webdriver of {} ({})", id, gridNode.getHost());

            remoteHubUrl = "http://" + hubHost + ":" + hubPort + "/wd/hub";
        }

        final String remoteHub = remoteHubUrl;
        Thread t = new Thread() {
            @Override
            public void run() {
                boolean exception = false;
                do {
                    try {
                        driver = new RemoteWebDriver(new URL(remoteHub), capabilities);
                        exception = false;
                    } catch (MalformedURLException | WebDriverException e) {
                        log.error("Exception {} creating RemoteWebDriver ... retrying in 1 second", e.getClass());
                        waitSeconds(1);
                        exception = true;
                    }
                } while (exception);
            }
        };
        t.start();

        int timeout = getProperty(SELENIUM_REMOTEWEBDRIVER_TIME_PROPERTY, SELENIUM_REMOTEWEBDRIVER_TIME_DEFAULT);
        String nodeMsg = gridNode != null ? " (" + gridNode.getHost() + ")" : "";

        for (int i = 0; i < timeout; i++) {
            if (t.isAlive()) {
                log.debug("Waiting for RemoteWebDriver {}{}", id, nodeMsg);
            } else {
                log.debug("Remote webdriver of {}{} created", id, nodeMsg);
                return;
            }
            waitSeconds(1);
        }

        String exceptionMessage = "Remote webdriver of " + id + nodeMsg + " not created in " + timeout + "seconds";
        log.error(">>>>>>>>>> " + exceptionMessage);
        throw new RuntimeException(exceptionMessage);
    }

    private void waitSeconds(long timeInSeconds) {
        try {
            Thread.sleep(TimeUnit.SECONDS.toMillis(timeInSeconds));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void assertPublicIpNotNull() {
        if (host == null) {
            throw new RuntimeException("Public IP must be available to run remote test. "
                    + "You can do it by adding the paramter -D" + TEST_HOST_PROPERTY
                    + "=<public_ip> or with key 'host' in " + "the JSON configuration file.");
        }
    }

    public void injectKurentoTestJs() throws IOException {
        if (this.getBrowserType() != BrowserType.IEXPLORER) {

            String kurentoTestJsContent = "";
            String kurentoTestPath = "static/lib/kurento-test.min.js";
            try {
                File pageFile = new File(this.getClass().getClassLoader().getResource(kurentoTestPath).getFile());
                kurentoTestJsContent = new String(Files.readAllBytes(pageFile.toPath()));
            } catch (NoSuchFileException nsfe) {
                InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(kurentoTestPath);
                StringWriter writer = new StringWriter();
                IOUtils.copy(inputStream, writer, Charset.defaultCharset());
                kurentoTestJsContent = writer.toString();
            }

            String kurentoTestJs = "var kurentoScript=window.document.createElement('script');";
            kurentoTestJs += "kurentoScript.type='text/javascript';";
            kurentoTestJs += "kurentoScript.text='" + kurentoTestJsContent + "';";
            kurentoTestJs += "window.document.head.appendChild(kurentoScript);";
            kurentoTestJs += "return true;";
            this.executeScript(kurentoTestJs);

            String recordingJs = "var recScript=window.document.createElement('script');";
            recordingJs += "recScript.type='text/javascript';";
            recordingJs += "recScript.src='https://cdn.webrtc-experiment.com/RecordRTC.js';";
            recordingJs += "window.document.head.appendChild(recScript);";
            recordingJs += "return true;";
            this.executeScript(recordingJs);
        }
    }

    public int getRecordAudio() {
        return recordAudio;
    }

    public int getAudioSampleRate() {
        return audioSampleRate;
    }

    public AudioChannel getAudioChannel() {
        return audioChannel;
    }

    public String getAudio() {
        return audio;
    }

    public int getTimeout() {
        return timeout;
    }

    public long getTimeoutMs() {
        return TimeUnit.SECONDS.toMillis(timeout);
    }

    public WebDriver getWebDriver() {
        return driver;
    }

    public JavascriptExecutor getJs() {
        return (JavascriptExecutor) driver;
    }

    public Object executeScriptAndWaitOutput(final String command) {
        WebDriverWait wait = new WebDriverWait(driver, timeout);
        wait.withMessage("Timeout executing script: " + command);

        final Object[] out = new Object[1];
        wait.until(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(WebDriver d) {
                try {
                    out[0] = executeScript(command);
                } catch (WebDriverException we) {
                    log.warn("Exception executing script", we);
                    out[0] = null;
                }
                return out[0] != null;
            }
        });
        return out[0];
    }

    public Object executeScript(final String command) {
        return ((JavascriptExecutor) driver).executeScript(command);
    }

    public double getColorDistance() {
        return colorDistance;
    }

    public int getThresholdTime() {
        return thresholdTime;
    }

    public void setThresholdTime(int thresholdTime) {
        this.thresholdTime = thresholdTime;
    }

    public boolean isLocal() {
        return BrowserScope.LOCAL.equals(this.scope);
    }

    public boolean isRemote() {
        return BrowserScope.REMOTE.equals(this.scope);
    }

    public boolean isSauceLabs() {
        return BrowserScope.SAUCELABS.equals(this.scope);
    }

    public boolean isDocker() {
        return BrowserScope.DOCKER.equals(this.scope);
    }

    public BrowserType getBrowserType() {
        return browserType;
    }

    public BrowserScope getScope() {
        return scope;
    }

    public String getBrowserVersion() {
        return browserVersion;
    }

    public Platform getPlatform() {
        return platform;
    }

    public String getVideo() {
        return video;
    }

    public int getServerPort() {
        return serverPort;
    }

    public WebPageType getWebPageType() {
        return webPageType;
    }

    public boolean isUsePhysicalCam() {
        return usePhysicalCam;
    }

    public boolean isEnableScreenCapture() {
        return enableScreenCapture;
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }

    public String getNode() {
        return node;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setId(String id) {
        this.id = id;
    }

    public int getNumInstances() {
        return numInstances;
    }

    public Builder getBuilder() {
        return builder;
    }

    public String getLogin() {
        return login;
    }

    public String getPasswd() {
        return passwd;
    }

    public String getPem() {
        return pem;
    }

    public int getBrowserPerInstance() {
        return browserPerInstance;
    }

    public String getHost() {
        return host;
    }

    public void setTimeout(int timeoutSeconds) {
        this.timeout = timeoutSeconds;
    }

    public Protocol getProtocol() {
        return protocol;
    }

    public URL getUrl() {
        URL url = null;
        try {
            if (this.url != null) {
                url = this.url.toURL();
            } else {
                String ip = this.getHost();
                int port = this.getServerPort();
                String protocol = this.getProtocol().toString();
                String path = this.getWebPageType().toString();
                url = new URL(protocol, ip, port, path);
            }
        } catch (MalformedURLException e) {
            log.error("Malformed URL", e);
            throw new RuntimeException(e);
        }
        return url;
    }

    @Override
    public void close() {
        // WebDriver
        if (driver != null) {
            try {
                log.debug("Closing webdriver of {} ", id);
                driver.quit();
                driver = null;
            } catch (Throwable t) {
                log.warn("** Exception closing webdriver {} : {}", t.getClass(), t.getMessage());
            }
        }

        // Stop Selenium Grid (if necessary)
        if (GridHandler.getInstance().useRemoteNodes()) {
            log.debug("Closing Grid of {} ", id);
            GridHandler.getInstance().stopGrid();
        }

        // Stop docker containers (if necessary)
        if (scope == BrowserScope.DOCKER) {

            Path logFile = Paths.get(KurentoTest.getDefaultOutputFile(""));

            try {
                if (!Files.exists(logFile)) {
                    Files.createDirectories(logFile);
                }
                getDockerManager().setDownloadLogsPath(logFile);

            } catch (IOException e) {
                log.warn("Exception creating path {} for logs", logFile);
            }

            getDockerManager().closeDriver(id);
        }
    }

    public String getJobId() {
        return jobId;
    }

    public static class Builder {

        private int timeout = 60; // seconds
        private int thresholdTime = 10; // seconds
        private double colorDistance = 60;
        private String node = getProperty(TEST_HOST_PROPERTY,
                getProperty(TEST_PUBLIC_IP_PROPERTY, TEST_PUBLIC_IP_DEFAULT));
        private String host = node;
        private int serverPort = getProperty(TEST_PORT_PROPERTY,
                getProperty(TEST_PUBLIC_PORT_PROPERTY, WebServerService.getAppHttpsPort()));
        private BrowserScope scope = BrowserScope.LOCAL;
        private BrowserType browserType = BrowserType.CHROME;
        private Protocol protocol = Protocol
                .valueOf(getProperty(TEST_PROTOCOL_PROPERTY, TEST_PROTOCOL_DEFAULT).toUpperCase());
        private WebPageType webPageType = WebPageType
                .value2WebPageType(getProperty(TEST_PATH_PROPERTY, TEST_PATH_DEFAULT));
        private boolean usePhysicalCam = false;
        private boolean enableScreenCapture = false;
        private int recordAudio = 0; // seconds
        private int audioSampleRate; // samples per seconds (e.g. 8000, 16000)
        private AudioChannel audioChannel; // stereo, mono
        private int numInstances = 0;
        private int browserPerInstance = 1;
        private String video;
        private String audio;
        private String browserVersion;
        private Platform platform;
        private String login;
        private String passwd;
        private String pem;
        private boolean avoidProxy;
        private String parentTunnel;
        private List<Map<String, String>> extensions;
        private URI url;
        private String webPagePath;

        public Builder browserPerInstance(int browserPerInstance) {
            this.browserPerInstance = browserPerInstance;
            return this;
        }

        public Builder login(String login) {
            this.login = login;
            return this;
        }

        public Builder passwd(String passwd) {
            this.passwd = passwd;
            return this;
        }

        public Builder pem(String pem) {
            this.pem = pem;
            return this;
        }

        public Builder protocol(Protocol protocol) {
            this.protocol = protocol;
            return this;
        }

        public Builder numInstances(int numInstances) {
            this.numInstances = numInstances;
            return this;
        }

        public Builder serverPort(int serverPort) {
            this.serverPort = serverPort;
            return this;
        }

        public Builder node(String node) {
            this.node = node;
            return this;
        }

        public Builder scope(BrowserScope scope) {
            String scopeProp = getProperty(SELENIUM_SCOPE_PROPERTY);
            if (scopeProp != null) {
                scope = BrowserScope.valueOf(scopeProp.toUpperCase());
            }
            this.scope = scope;

            String appAutostart = getProperty(TestConfiguration.TEST_APP_AUTOSTART_PROPERTY,
                    TestConfiguration.TEST_APP_AUTOSTART_DEFAULT);

            if (BrowserScope.DOCKER.equals(scope)
                    && !appAutostart.equals(TestConfiguration.AUTOSTART_FALSE_VALUE)) {

                if (docker.isRunningInContainer()) {
                    this.node = docker.getContainerIpAddress();
                } else {
                    this.node = docker.getHostIpForContainers();
                }
            }

            return this;
        }

        public Builder timeout(int timeout) {
            this.timeout = timeout;
            return this;
        }

        public Builder thresholdTime(int thresholdTime) {
            this.thresholdTime = thresholdTime;
            return this;
        }

        public Builder colorDistance(double colorDistance) {
            this.colorDistance = colorDistance;
            return this;
        }

        public Builder video(String video) {
            this.video = video;
            return this;
        }

        public Builder webPageType(WebPageType webPageType) {
            this.webPageType = webPageType;
            return this;
        }

        public Builder browserType(BrowserType browser) {
            this.browserType = browser;
            return this;
        }

        public Builder usePhysicalCam() {
            this.usePhysicalCam = true;
            return this;
        }

        public Builder avoidProxy() {
            this.avoidProxy = true;
            return this;
        }

        public Builder parentTunnel(String parentTunnel) {
            this.parentTunnel = parentTunnel;
            return this;
        }

        public Builder enableScreenCapture() {
            this.enableScreenCapture = true;
            return this;
        }

        public Builder audio(String audio, int recordAudio, int audioSampleRate, AudioChannel audioChannel) {
            this.audio = audio;
            this.recordAudio = recordAudio;
            this.audioSampleRate = audioSampleRate;
            this.audioChannel = audioChannel;
            return this;
        }

        public Builder browserVersion(String browserVersion) {
            this.browserVersion = browserVersion;
            return this;
        }

        public Builder platform(Platform platform) {
            this.platform = platform;
            return this;
        }

        public Builder host(String host) {
            this.host = host;
            return this;
        }

        public Builder extensions(List<Map<String, String>> extensions) {
            this.extensions = extensions;
            return this;
        }

        public Builder url(String url) {
            try {
                this.url = new URI(url);

            } catch (URISyntaxException e) {
                throw new KurentoException("Could not parse URI " + url);
            }
            return this;
        }

        public Builder webPagePath(String webPagePath) {
            this.webPagePath = webPagePath;
            return this;
        }

        public Browser build() {
            return new Browser(this);
        }

    }
}