<commit_before>/*
 * (C) Copyright 2014 Kurento (http://kurento.org/)
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-2.1.html
 *
 * This library 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.
 *
 */
package org.kurento.test.client;

import static org.kurento.commons.PropertiesManager.getProperty;
import static org.kurento.test.TestConfiguration.SAUCELAB_COMMAND_TIMEOUT_DEFAULT;
import static org.kurento.test.TestConfiguration.SAUCELAB_COMMAND_TIMEOUT_PROPERTY;
import static org.kurento.test.TestConfiguration.SAUCELAB_IDLE_TIMEOUT_DEFAULT;
import static org.kurento.test.TestConfiguration.SAUCELAB_IDLE_TIMEOUT_PROPERTY;
import static org.kurento.test.TestConfiguration.SAUCELAB_KEY_PROPERTY;
import static org.kurento.test.TestConfiguration.SAUCELAB_USER_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_HOST_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_NODE_LOGIN_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_NODE_PASSWD_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_NODE_PEM_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PATH_DEFAULT;
import static org.kurento.test.TestConfiguration.TEST_PATH_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PORT_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PROTOCOL_DEFAULT;
import static org.kurento.test.TestConfiguration.TEST_PROTOCOL_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PUBLIC_IP_DEFAULT;
import static org.kurento.test.TestConfiguration.TEST_PUBLIC_IP_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PUBLIC_PORT_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_SCREEN_SHARE_TITLE_DEFAULT;
import static org.kurento.test.TestConfiguration.TEST_SCREEN_SHARE_TITLE_DEFAULT_WIN;
import static org.kurento.test.TestConfiguration.TEST_SCREEN_SHARE_TITLE_PROPERTY;
import io.github.bonigarcia.wdm.ChromeDriverManager;

import java.io.Closeable;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.FileUtils;
import org.kurento.test.config.BrowserScope;
import org.kurento.test.config.Protocol;
import org.kurento.test.grid.GridHandler;
import org.kurento.test.grid.GridNode;
import org.kurento.test.services.AudioChannel;
import org.kurento.test.services.KurentoServicesTestHelper;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Platform;
import org.openqa.selenium.WebDriver;
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;

/**
 * Wrapper of Selenium Webdriver for testing Kurento applications.
 *
 * @author <NAME> (<EMAIL>)
 * @author <NAME> (<EMAIL>)
 * @since 5.1.0
 * @see <a href="http://www.seleniumhq.org/">Selenium</a>
 */
public class BrowserClient implements Closeable {

	public Logger log = LoggerFactory.getLogger(BrowserClient.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 Client client;
	private String login;
	private String passwd;
	private String pem;
	private boolean avoidProxy;
	private String parentTunnel;
	private List<Map<String, String>> extensions;

	public BrowserClient(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.client = builder.client;
		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 = <PASSWORD>;
		this.pem = builder.pem;
		this.host = builder.host;
		this.avoidProxy = builder.avoidProxy;
		this.parentTunnel = builder.parentTunnel;
		this.extensions = builder.extensions;
	}

	public void init() {
		Class<? extends WebDriver> driverClass = browserType.getDriverClass();

		try {
			DesiredCapabilities capabilities = new DesiredCapabilities();

			if (driverClass.equals(FirefoxDriver.class)) {
				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());
							}
						}
					}
				}

				if (scope == BrowserScope.SAUCELABS) {
					createSaucelabsDriver(capabilities);
				} else if (scope == BrowserScope.REMOTE) {
					createRemoteDriver(capabilities);
				} else {
					driver = new FirefoxDriver(profile);
				}

			} else if (driverClass.equals(ChromeDriver.class)) {
				// 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");

					if (video != null && isLocal()) {
						options.addArguments("--use-file-for-fake-video-capture="
								+ video);
					}
				}

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

				if (scope == BrowserScope.SAUCELABS) {
					createSaucelabsDriver(capabilities);
				} else if (scope == BrowserScope.REMOTE) {
					createRemoteDriver(capabilities);
				} else {
					driver = new ChromeDriver(options);
				}
			} 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);

			// Launch Browser
			String url;
			if (protocol == Protocol.FILE) {
				String clientPage = client.toString();
				File clientPageFile = new File(this.getClass().getClassLoader()
						.getResource("static" + clientPage).getFile());
				url = protocol.toString() + clientPageFile.getAbsolutePath();
			} else {
				String hostName = host != null ? host : node;
				url = protocol.toString() + hostName + ":" + serverPort
						+ client.toString();
			}
			log.info("*** Browsing URL with WebDriver: {}", url);
			driver.get(url);

		} catch (MalformedURLException e) {
			log.error("MalformedURLException in BrowserClient.initDriver", e);
		}

	}

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

		try {
			log.info("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.info("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.info("Trying to locate extension as URL ({}) ...",
						extension);
				URL url = new URL(extension);
				is = url.openStream();
				log.info("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);
		int idleTimeout = getProperty(SAUCELAB_IDLE_TIMEOUT_PROPERTY,
				SAUCELAB_IDLE_TIMEOUT_DEFAULT);
		int commandTimeout = getProperty(SAUCELAB_COMMAND_TIMEOUT_PROPERTY,
				SAUCELAB_COMMAND_TIMEOUT_DEFAULT);

		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);

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

		capabilities.setCapability("idleTimeout", idleTimeout);
		capabilities.setCapability("commandTimeout", commandTimeout);

		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.info("%%%%%%%%%%%%% Saucelabs URL job ({} {} in {}) %%%%%%%%%%%%%",
				browserType, browserVersion, platform);
		log.info("https://saucelabs.com/tests/{}", jobId);
	}

	public void createRemoteDriver(DesiredCapabilities capabilities)
			throws MalformedURLException {
		assertPublicIpNotNull();
		if (!GridHandler.getInstance().containsSimilarBrowserKey(id)) {
			GridNode gridNode = null;

			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);
			}

			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);
			}

		}

		// 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);
		}

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

		driver = new RemoteWebDriver(new URL("http://" + hubHost + ":"
				+ hubPort + "/wd/hub"), capabilities);
	}

	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() {
		if (this.getBrowserType() != BrowserType.IEXPLORER) {
			String kurentoTestJs = "var kurentoScript=window.document.createElement('script');";
			String kurentoTestJsPath = "./lib/kurento-test.js";
			if (this.getProtocol() == Protocol.FILE) {
				File clientPageFile = new File(this.getClass().getClassLoader()
						.getResource("static/lib/kurento-test.js").getFile());
				kurentoTestJsPath = this.getProtocol().toString()
						+ clientPageFile.getAbsolutePath();
			}
			kurentoTestJs += "kurentoScript.src='" + kurentoTestJsPath + "';";
			kurentoTestJs += "window.document.head.appendChild(kurentoScript);";
			kurentoTestJs += "return true;";
			this.executeScript(kurentoTestJs);
		}
	}

	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,
						KurentoServicesTestHelper.getAppHttpPort()));
		private BrowserScope scope = BrowserScope.LOCAL;
		private BrowserType browserType = BrowserType.CHROME;
		private Protocol protocol = Protocol.valueOf(getProperty(
				TEST_PROTOCOL_PROPERTY, TEST_PROTOCOL_DEFAULT).toUpperCase());
		private Client client = Client.value2Client(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;

		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) {
			this.scope = scope;
			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 client(Client client) {
			this.client = client;
			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 BrowserClient build() {
			return new BrowserClient(this);
		}
	}

	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 WebDriver getDriver() {
		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) {
				out[0] = executeScript(command);
				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 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 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 Client getClient() {
		return client;
	}

	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 timeout) {
		this.timeout = timeout;
	}

	public Protocol getProtocol() {
		return protocol;
	}

	public URL getUrl() {
		String ip = this.getHost();
		int port = this.getServerPort();
		String protocol = this.getProtocol().toString();
		String path = this.getClient().toString();
		URL url = null;
		try {
			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() {
		// Stop Selenium Grid (if necessary)
		if (GridHandler.getInstance().useRemoteNodes()) {
			GridHandler.getInstance().stopGrid();
		}

		// WebDriver
		if (driver != null) {
			try {
				driver.quit();
				driver = null;
			} catch (Throwable t) {
				log.warn("Exception closing webdriver {} : {}", t.getClass(),
						t.getMessage());
			}
		}
	}

	public String getJobId() {
		return jobId;
	}

}
<commit_msg>Set to BrowserClient.setTimeout(int timeoutSeconds) to clarify unit <commit_after>/*
 * (C) Copyright 2014 Kurento (http://kurento.org/)
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-2.1.html
 *
 * This library 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.
 *
 */
package org.kurento.test.client;

import static org.kurento.commons.PropertiesManager.getProperty;
import static org.kurento.test.TestConfiguration.SAUCELAB_COMMAND_TIMEOUT_DEFAULT;
import static org.kurento.test.TestConfiguration.SAUCELAB_COMMAND_TIMEOUT_PROPERTY;
import static org.kurento.test.TestConfiguration.SAUCELAB_IDLE_TIMEOUT_DEFAULT;
import static org.kurento.test.TestConfiguration.SAUCELAB_IDLE_TIMEOUT_PROPERTY;
import static org.kurento.test.TestConfiguration.SAUCELAB_KEY_PROPERTY;
import static org.kurento.test.TestConfiguration.SAUCELAB_USER_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_HOST_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_NODE_LOGIN_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_NODE_PASSWD_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_NODE_PEM_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PATH_DEFAULT;
import static org.kurento.test.TestConfiguration.TEST_PATH_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PORT_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PROTOCOL_DEFAULT;
import static org.kurento.test.TestConfiguration.TEST_PROTOCOL_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PUBLIC_IP_DEFAULT;
import static org.kurento.test.TestConfiguration.TEST_PUBLIC_IP_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_PUBLIC_PORT_PROPERTY;
import static org.kurento.test.TestConfiguration.TEST_SCREEN_SHARE_TITLE_DEFAULT;
import static org.kurento.test.TestConfiguration.TEST_SCREEN_SHARE_TITLE_DEFAULT_WIN;
import static org.kurento.test.TestConfiguration.TEST_SCREEN_SHARE_TITLE_PROPERTY;
import io.github.bonigarcia.wdm.ChromeDriverManager;

import java.io.Closeable;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.FileUtils;
import org.kurento.test.config.BrowserScope;
import org.kurento.test.config.Protocol;
import org.kurento.test.grid.GridHandler;
import org.kurento.test.grid.GridNode;
import org.kurento.test.services.AudioChannel;
import org.kurento.test.services.KurentoServicesTestHelper;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Platform;
import org.openqa.selenium.WebDriver;
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;

/**
 * Wrapper of Selenium Webdriver for testing Kurento applications.
 *
 * @author <NAME> (<EMAIL>)
 * @author <NAME> (<EMAIL>)
 * @since 5.1.0
 * @see <a href="http://www.seleniumhq.org/">Selenium</a>
 */
public class BrowserClient implements Closeable {

	public Logger log = LoggerFactory.getLogger(BrowserClient.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 Client client;
	private String login;
	private String passwd;
	private String pem;
	private boolean avoidProxy;
	private String parentTunnel;
	private List<Map<String, String>> extensions;

	public BrowserClient(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.client = builder.client;
		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 = <PASSWORD>;
		this.pem = builder.pem;
		this.host = builder.host;
		this.avoidProxy = builder.avoidProxy;
		this.parentTunnel = builder.parentTunnel;
		this.extensions = builder.extensions;
	}

	public void init() {
		Class<? extends WebDriver> driverClass = browserType.getDriverClass();

		try {
			DesiredCapabilities capabilities = new DesiredCapabilities();

			if (driverClass.equals(FirefoxDriver.class)) {
				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());
							}
						}
					}
				}

				if (scope == BrowserScope.SAUCELABS) {
					createSaucelabsDriver(capabilities);
				} else if (scope == BrowserScope.REMOTE) {
					createRemoteDriver(capabilities);
				} else {
					driver = new FirefoxDriver(profile);
				}

			} else if (driverClass.equals(ChromeDriver.class)) {
				// 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");

					if (video != null && isLocal()) {
						options.addArguments("--use-file-for-fake-video-capture="
								+ video);
					}
				}

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

				if (scope == BrowserScope.SAUCELABS) {
					createSaucelabsDriver(capabilities);
				} else if (scope == BrowserScope.REMOTE) {
					createRemoteDriver(capabilities);
				} else {
					driver = new ChromeDriver(options);
				}
			} 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);

			// Launch Browser
			String url;
			if (protocol == Protocol.FILE) {
				String clientPage = client.toString();
				File clientPageFile = new File(this.getClass().getClassLoader()
						.getResource("static" + clientPage).getFile());
				url = protocol.toString() + clientPageFile.getAbsolutePath();
			} else {
				String hostName = host != null ? host : node;
				url = protocol.toString() + hostName + ":" + serverPort
						+ client.toString();
			}
			log.info("*** Browsing URL with WebDriver: {}", url);
			driver.get(url);

		} catch (MalformedURLException e) {
			log.error("MalformedURLException in BrowserClient.initDriver", e);
		}

	}

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

		try {
			log.info("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.info("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.info("Trying to locate extension as URL ({}) ...",
						extension);
				URL url = new URL(extension);
				is = url.openStream();
				log.info("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);
		int idleTimeout = getProperty(SAUCELAB_IDLE_TIMEOUT_PROPERTY,
				SAUCELAB_IDLE_TIMEOUT_DEFAULT);
		int commandTimeout = getProperty(SAUCELAB_COMMAND_TIMEOUT_PROPERTY,
				SAUCELAB_COMMAND_TIMEOUT_DEFAULT);

		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);

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

		capabilities.setCapability("idleTimeout", idleTimeout);
		capabilities.setCapability("commandTimeout", commandTimeout);

		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.info("%%%%%%%%%%%%% Saucelabs URL job ({} {} in {}) %%%%%%%%%%%%%",
				browserType, browserVersion, platform);
		log.info("https://saucelabs.com/tests/{}", jobId);
	}

	public void createRemoteDriver(DesiredCapabilities capabilities)
			throws MalformedURLException {
		assertPublicIpNotNull();
		if (!GridHandler.getInstance().containsSimilarBrowserKey(id)) {
			GridNode gridNode = null;

			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);
			}

			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);
			}

		}

		// 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);
		}

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

		driver = new RemoteWebDriver(new URL("http://" + hubHost + ":"
				+ hubPort + "/wd/hub"), capabilities);
	}

	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() {
		if (this.getBrowserType() != BrowserType.IEXPLORER) {
			String kurentoTestJs = "var kurentoScript=window.document.createElement('script');";
			String kurentoTestJsPath = "./lib/kurento-test.js";
			if (this.getProtocol() == Protocol.FILE) {
				File clientPageFile = new File(this.getClass().getClassLoader()
						.getResource("static/lib/kurento-test.js").getFile());
				kurentoTestJsPath = this.getProtocol().toString()
						+ clientPageFile.getAbsolutePath();
			}
			kurentoTestJs += "kurentoScript.src='" + kurentoTestJsPath + "';";
			kurentoTestJs += "window.document.head.appendChild(kurentoScript);";
			kurentoTestJs += "return true;";
			this.executeScript(kurentoTestJs);
		}
	}

	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,
						KurentoServicesTestHelper.getAppHttpPort()));
		private BrowserScope scope = BrowserScope.LOCAL;
		private BrowserType browserType = BrowserType.CHROME;
		private Protocol protocol = Protocol.valueOf(getProperty(
				TEST_PROTOCOL_PROPERTY, TEST_PROTOCOL_DEFAULT).toUpperCase());
		private Client client = Client.value2Client(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;

		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) {
			this.scope = scope;
			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 client(Client client) {
			this.client = client;
			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 BrowserClient build() {
			return new BrowserClient(this);
		}
	}

	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 WebDriver getDriver() {
		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) {
				out[0] = executeScript(command);
				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 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 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 Client getClient() {
		return client;
	}

	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() {
		String ip = this.getHost();
		int port = this.getServerPort();
		String protocol = this.getProtocol().toString();
		String path = this.getClient().toString();
		URL url = null;
		try {
			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() {
		// Stop Selenium Grid (if necessary)
		if (GridHandler.getInstance().useRemoteNodes()) {
			GridHandler.getInstance().stopGrid();
		}

		// WebDriver
		if (driver != null) {
			try {
				driver.quit();
				driver = null;
			} catch (Throwable t) {
				log.warn("Exception closing webdriver {} : {}", t.getClass(),
						t.getMessage());
			}
		}
	}

	public String getJobId() {
		return jobId;
	}

}

{'fraction_non_alphanumeric': 0.1101465002712968, 'fraction_numerical': 0.0018086453246518358, 'mean_word_length': 3.9274185562927024, 'pattern_counts': {'":': 8, '<': 41, '<?xml version=': 0, '>': 39, 'https://': 4, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6894274', 'n_tokens_mistral': 17733, 'n_tokens_neox': 14886, 'n_words': 3443}