org.openqa.selenium.chrome.ChromeCommandExecutor Maven / Gradle / Ivy
The newest version!
package org.openqa.selenium.chrome;
import java.awt.Dimension;
import java.awt.Point;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.ElementNotVisibleException;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.NoSuchFrameException;
import org.openqa.selenium.NoSuchWindowException;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.XPathLookupException;
import org.openqa.selenium.remote.Command;
public class ChromeCommandExecutor {
private final ServerSocket serverSocket;
//Whether the listening thread should listen
private volatile boolean listen = false;
//Whether a client is currently connected
private boolean hadClient = false;
//Whether a client has ever been connected
private boolean hasClient = false;
ListeningThread listeningThread;
private Map nameToJson = new HashMap();
/**
* Creates a new ChromeCommandExecutor which listens on a TCP port.
* Doesn't return until the TCP port is connected to.
* @param port port on which to listen for the initial connection,
* and dispatch commands
* @throws IOException if could not bind to port
* TODO(danielwh): Bind to a random port (blocked on crbug.com 11547)
*/
public ChromeCommandExecutor(int port) {
nameToJson.put("close", new JsonCommand("{request: 'close'}"));
nameToJson.put("quit", new JsonCommand("QUIT"));
nameToJson.put("get", new JsonCommand("{request: 'url', url: ?url}"));
nameToJson.put("goBack", new JsonCommand("{request: 'goBack'}"));
nameToJson.put("goForward", new JsonCommand("{request: 'goForward'}"));
nameToJson.put("refresh", new JsonCommand("{request: 'refresh'}"));
nameToJson.put("addCookie", new JsonCommand("{request: 'addCookie', cookie: ?cookie}"));
nameToJson.put("getCookies", new JsonCommand("{request: 'getCookies'}"));
nameToJson.put("getCookieNamed", new JsonCommand("{request: 'getCookieNamed', name: ?name}"));
nameToJson.put("deleteAllCookies", new JsonCommand("{request: 'deleteAllCookies'}"));
nameToJson.put("deleteCookie", new JsonCommand("{request: 'deleteCookie', name: ?name}"));
nameToJson.put("findElement", new JsonCommand("{request: 'getElement', by: [?using, ?value]}"));
nameToJson.put("findElements", new JsonCommand("{request: 'getElements', by: [?using, ?value]}"));
nameToJson.put("findChildElement", new JsonCommand("{request: 'getElement', by: [{id: ?element, using: ?using, value: ?value}]}"));
nameToJson.put("findChildElements", new JsonCommand("{request: 'getElements', by: [{id: ?element, using: ?using, value: ?value}]}"));
nameToJson.put("clearElement", new JsonCommand("{request: 'clearElement', elementId: ?elementId}"));
nameToJson.put("clickElement", new JsonCommand("{request: 'clickElement', elementId: ?elementId}"));
nameToJson.put("hoverElement", new JsonCommand("{request: 'hoverElement', elementId: ?elementId}"));
nameToJson.put("sendElementKeys", new JsonCommand("{request: 'sendElementKeys', elementId: ?elementId, keys: ?keys}"));
nameToJson.put("submitElement", new JsonCommand("{request: 'submitElement', elementId: ?elementId}"));
nameToJson.put("toggleElement", new JsonCommand("{request: 'toggleElement', elementId: ?elementId}"));
nameToJson.put("getElementAttribute", new JsonCommand("{request: 'getElementAttribute', elementId: ?elementId, attribute: ?attribute}"));
nameToJson.put("getElementLocationOnceScrolledIntoView", new JsonCommand("{request: 'getElementLocationOnceScrolledIntoView', elementId: ?elementId}"));
nameToJson.put("getElementLocation", new JsonCommand("{request: 'getElementLocation', elementId: ?elementId}"));
nameToJson.put("getElementSize", new JsonCommand("{request: 'getElementSize', elementId: ?elementId}"));
nameToJson.put("getElementTagName", new JsonCommand("{request: 'getElementTagName', elementId: ?elementId}"));
nameToJson.put("getElementText", new JsonCommand("{request: 'getElementText', elementId: ?elementId}"));
nameToJson.put("getElementValue", new JsonCommand("{request: 'getElementValue', elementId: ?elementId}"));
nameToJson.put("getElementValueOfCssProperty", new JsonCommand("{request: 'getElementValueOfCssProperty', elementId: ?elementId, css: ?property}"));
nameToJson.put("isElementDisplayed", new JsonCommand("{request: 'isElementDisplayed', elementId: ?elementId}"));
nameToJson.put("isElementEnabled", new JsonCommand("{request: 'isElementEnabled', elementId: ?elementId}"));
nameToJson.put("isElementSelected", new JsonCommand("{request: 'isElementSelected', elementId: ?elementId}"));
nameToJson.put("setElementSelected", new JsonCommand("{request: 'setElementSelected', elementId: ?elementId}"));
nameToJson.put("switchToActiveElement", new JsonCommand("{request: 'switchToActiveElement'}"));
nameToJson.put("switchToFrameByIndex", new JsonCommand("{request: 'switchToFrame', using: {index: ?index}}"));
nameToJson.put("switchToFrameByName", new JsonCommand("{request: 'switchToFrame', using: {name: ?name}}"));
nameToJson.put("switchToDefaultContent", new JsonCommand("{request: 'switchToDefaultContent'}"));
nameToJson.put("getWindowHandle", new JsonCommand("{request: 'getWindowHandle'}"));
nameToJson.put("getWindowHandles", new JsonCommand("{request: 'getWindowHandles'}"));
nameToJson.put("switchToWindow", new JsonCommand("{request: 'switchToWindow', windowName: ?name}"));
nameToJson.put("execute", new JsonCommand("EXECUTE")); //Dealt with specially
nameToJson.put("getCurrentUrl", new JsonCommand("{request: 'getCurrentUrl'}"));
nameToJson.put("getPageSource", new JsonCommand("{request: 'getPageSource'}"));
nameToJson.put("getTitle", new JsonCommand("{request: 'getTitle'}"));
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
throw new WebDriverException(e);
}
listen = true;
listeningThread = new ListeningThread(serverSocket);
listeningThread.start();
}
/**
* Returns whether an instance of Chrome is currently connected
* @return whether an instance of Chrome is currently connected
*/
boolean hasClient() {
return hasClient;
}
/**
* Executes the passed command
* @param command command to execute
* @return response to command
* @throws IllegalStateException if no socket was present
*/
public ChromeResponse execute(Command command) throws IOException {
sendCommand(command);
return handleResponse(command);
}
/**
* Sends the passed command to the Chrome extension on the
* longest-time accepted socket. Removes the socket from the queue when done
* @param command command to send
* @throws IOException if couldn't write command to socket
*/
private void sendCommand(Command command) throws IOException {
Socket socket;
//Peek, rather than poll, so that if it all goes horribly wrong,
//we can just close all sockets in the queue,
//not having to worry about the current ones
while ((socket = listeningThread.sockets.peek()) == null) {
if (hadClient && !hasClient) {
throw new IllegalStateException("Cannot execute command without a client");
}
Thread.yield();
}
try {
//Respond to request with the command
JsonCommand commandToPopulate =
nameToJson.get(command.getMethodName());
if (commandToPopulate == null) {
throw new UnsupportedOperationException("Didn't know how to execute: " +
command.getMethodName());
}
String commandStringToSend = commandToPopulate.populate(command.getParameters());
socket.getOutputStream().write(fillTwoHundredWithJson(commandStringToSend));
socket.getOutputStream().flush();
} finally {
socket.close();
listeningThread.sockets.remove(socket);
}
}
/**
* Wraps the passed message up in an HTTP 200 response, with the Content-type
* header set to application/json
* @param message message to wrap up as the response
* @return The passed message, wrapped up in an HTTP 200 response,
* encoded in UTF-8
*/
private byte[] fillTwoHundredWithJson(String message) {
return fillTwoHundred(message, "application/json; charset=UTF-8");
}
/**
* Fills in an HTTP 200 response with the passed message and content type.
* @param message Response
* @param contentType HTTP Content-type header
* @return The HTTP 200 message encoded in UTF-8 as an array of bytes
*/
private byte[] fillTwoHundred(String message, String contentType) {
String httpMessage = "HTTP/1.1 200 OK" +
"\r\nContent-Length: " + message.length() +
"\r\nContent-Type: " + contentType +
"\r\n\r\n" + message;
try {
return httpMessage.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
//Should never happen - Java ships with UTF-8
throw new WebDriverException("Your environment doesn't support UTF-8");
}
}
/**
* Listens for the response to a command on the oldest socket in the queue
* and parses it.
* Expects the response to be an HTTP request, which ends in the line:
* EOResponse
* Responds by sending a 200 response containing QUIT
* @param command command we are expecting a response to
* @return response to the command.
* @throws IOException if there are errors with the socket being used
*/
private ChromeResponse handleResponse(Command command) throws IOException {
Socket socket;
//Peek, rather than poll, so that if it all goes horribly wrong,
//we can just close all sockets in the queue,
//not having to worry about the current ones
while ((socket = listeningThread.sockets.peek()) == null) {
Thread.yield();
}
StringBuilder resultBuilder = new StringBuilder();
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String line;
boolean hasSeenDoubleCRLF = false; //Whether we are out of headers yet
while ((line = reader.readLine()) != null && !line.equals("EOResponse")) {
if (hasSeenDoubleCRLF) {
if (resultBuilder.length() > 0) {
//Out of headers, and not the first line, so append a newline
resultBuilder.append("\n");
}
resultBuilder.append(line);
}
if (line.equals("")) {
hasSeenDoubleCRLF = true;
}
}
return parseResponse(resultBuilder.toString());
}
/**
* Parses a raw json string into a response.
* @param rawJsonString JSON string encapsulating the response.
* @return the parsed response.
*/
private ChromeResponse parseResponse(String rawJsonString) {
if (rawJsonString.length() == 0) {
return new ChromeResponse(0, null);
}
try {
JSONObject jsonObject = new JSONObject(rawJsonString);
if (!jsonObject.has("statusCode")) {
throw new WebDriverException("Response had no status code. Response was: " + rawJsonString);
}
if (jsonObject.getInt("statusCode") == 0) {
//Success! Parse value
if (!jsonObject.has("value") || jsonObject.isNull("value")) {
return new ChromeResponse(0, null);
}
Object value = jsonObject.get("value");
Object parsedValue = parseJsonToObject(value);
if (parsedValue instanceof ChromeWebElement) {
return new ChromeResponse(-1, ((ChromeWebElement)parsedValue).getElementId());
} else {
return new ChromeResponse(0, parsedValue);
}
} else {
String message = "";
if (jsonObject.has("value") &&
jsonObject.get("value") instanceof JSONObject &&
jsonObject.getJSONObject("value").has("message") &&
jsonObject.getJSONObject("value").get("message") instanceof String) {
message = jsonObject.getJSONObject("value").getString("message");
}
switch (jsonObject.getInt("statusCode")) {
//Error codes are loosely based on native exception codes,
//see common/src/cpp/webdriver-interactions/errorcodes.h
case 2:
//Cookie error
throw new WebDriverException(message);
case 3:
throw new NoSuchWindowException(message);
case 7:
throw new NoSuchElementException(message);
case 8:
throw new NoSuchFrameException(message);
case 9:
//Unknown command
throw new UnsupportedOperationException(message);
case 10:
throw new StaleElementReferenceException(message);
case 11:
throw new ElementNotVisibleException(message);
case 12:
//Invalid element state (e.g. disabled)
throw new UnsupportedOperationException(message);
case 17:
//Bad javascript
throw new WebDriverException(message);
case 19:
//Bad xpath
throw new XPathLookupException(message);
case 99:
throw new WebDriverException("An error occured when sending a native event");
case 500:
if (message.equals("")) {
message = "An error occured due to the internals of Chrome. " +
"This does not mean your test failed. " +
"Try running your test again in isolation.";
}
throw new FatalChromeException(message);
}
throw new WebDriverException("An error occured in the page");
}
} catch (JSONException e) {
throw new WebDriverException(e);
}
}
private Object parseJsonToObject(Object value) throws JSONException {
if (value instanceof String) {
return value;
} else if (value instanceof Boolean) {
return value;
} else if (value instanceof Number) {
//We return all numbers as longs
return ((Number)value).longValue();
} else if (value instanceof JSONArray) {
JSONArray jsonArray = (JSONArray)(value);
List