Source: webDriver.js

'use strict';

var http = require('http'),
	fs = require('fs'),
	path = require('path'),
	utils = require('./utils'),
	errors = require('./errors'),
	WebElement = require('./webElement').WebElement,
	_ = require('underscore');

function returnSelf(callback, self) {
	return function(err) {
		callback(err, self);
	};
}

/**
 * Driver constructor
 * @class WebDriver
 * @param {String} [params.host] Target selenium host
 * @param {Number} [params.port] Target selenium port
 * @param {Object} [params.desiredCapabilities] Desired capabilities which will
 * passed to the selenium server
 * @param {Object} [params.timeouts] Timeouts object which will be set using
 * `setTimeouts` method
 * @param {Object} [params.defaults] Default values for different methods
 * parameters (e.g. params.defaults.using will be used by `get`, `getList`,
 * `waitForElement`, etc)
 */
function WebDriver(params) {
	params = params || {};
	this.requestParams = {
		host: params.host || '127.0.0.1',
		port: params.port || 4444,
		method: params.method || 'POST'
	};
	this.sessionBasePath = '/wd/hub/session';
	this.desiredCapabilities = utils.extend({
		browserName: 'firefox',
		version: '',
		javascriptEnabled: true,
		platform: 'ANY'
	}, params.desiredCapabilities);
	this.defaults = utils.extend({
		using: 'css selector'
	}, params.defaults);
	this.logMethodCalls = params.logMethodCalls;
	this.timeouts = utils.extend({
		// protocol timeouts
		'page load': 3500,
		script: 1000,
		implicit: 0,
		// extended timeouts
		waitFor: 3000
	}, params.timeouts);
	this._protocolTimeoutsHash = {'page load': 1, script: 1, implicit: 1};

	this._initElement();
}

WebDriver.prototype._initElement = function() {
	var self = this;
	/**
	 * element object combines {@link WebDriver#get} with {@link WebElement}
	 * methods. So you can call any {@link WebElement} method
	 * with selector and params (optionally). Same parameters as for normal
	 * {@link WebElement} method call will be passed to the callback.
	 * Internally {@link WebDriver#get} then target method of {@link WebElement}
	 * will be called therefore if you need to call several methods on same
	 * element it make sense(due performance) to get element and then call it's
	 * methods.
	 *
	 * @example
	 * // driver is an instance of WebDriver
	 * driver.element.sendKeys(
	 *   '[name="user[login]"]',
	 *   'patrik',
	 *   function(err, element) {
	 *     // element is WebElement which was get
	 *   }
	 * );
	 *
	 * @example
	 * // driver is an instance of WebDriver
	 * driver.element.getValue('[name="user[login]"]', function(err, login) {
	 *   // login equals to 'patrik'
	 * });
	 *
	 * @name element
	 * @type {Object}
	 * @instance
	 * @memberOf WebDriver
	 */
	self.element = {};
	Object.keys(WebElement.prototype).forEach(function(method) {
		if (/^_/.test(method)) return;
		self.element[method] = function(selector, params) {
			var getArgs, methodArgs;
			if (utils.isSelectorParams(params)) {
				getArgs = [selector, params];
				methodArgs = Array.prototype.slice.call(arguments, 2);
			} else {
				getArgs = [selector];
				methodArgs = Array.prototype.slice.call(arguments, 1);
			}

			var callback = methodArgs[methodArgs.length - 1];
			getArgs.push(function(err, element) {
				if (err) return callback(err);
				element[method].apply(element, methodArgs);
			});

			self.get.apply(self, getArgs);
		};
	});
};

//strip function from https://github.com/Camme/webDriver
// strip the content from unwanted characters
WebDriver.prototype._strip = function(str) {
	var x = [],
		i = 0,
		il = str.length;

	for (i; i < il; i++) {
		if (str.charCodeAt(i)) {
			x.push(str.charAt(i));
		}
	}
	return x.join('');
};

WebDriver.prototype._cmd = function(params, callback) {
	var self = this,
		stringData = params.data ? JSON.stringify(params.data) : null,
		buferData = stringData ? new Buffer(stringData, 'utf8') : null,
		requestParams = {
			host: this.requestParams.host,
			port: this.requestParams.port,
			path: this.sessionBasePath + params.path,
			method: params.method || this.requestParams.method,
			headers: {
				'Content-Type': 'application/json; charset=UTF-8',
				'Content-Length': buferData ? buferData.length : 0
			},
			data: buferData
		};

	request(requestParams, function(err, res) {
		if (err) return callback(err);
		res.data = res.data ? self._strip(res.data.toString()) : '{}';

		try {
			res.data = JSON.parse(res.data);
		} catch(err) {
			return callback(new errors.ProtocolError(
				'Can`t parse json from response of ' + params.path +
				': ' + err.message + '\n Raw response data: ' + res.data
			));
		}
		//status 0 - success, error otherwise
		if (res.data && res.data.status) {
			var errorConstructor = errors.getProtocolErrorContructor(res.data.status);
			return callback(new errorConstructor());
		}
		// return `res` if it needed, otherwise `res.data.value`
		if (params.path === '/refresh') {
			callback(null, params.resNeeded ? res : res.data.value, true);
		} else {
			callback(null, params.resNeeded ? res : res.data.value);
		}
	});
};

function request(params, callback) {
	var req = http.request(params, function(res) {
		var data = '';
		res.setEncoding('utf8');
		res.on('data', function(chunk) { data += chunk; });
		res.on('end', function() {
			res.data = data;
			callback(null, res);
		});
	});
	req.on('error', function(err) {
		callback(err);
	});
	req.end(params.data ? params.data : null);
}

/**
 * Start driver session and return driver
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.init = function(callback) {
	var self = this;
	self._cmd({
		path: '',
		data: {desiredCapabilities: self.desiredCapabilities},
		resNeeded: true
	}, function(err, res) {
		if (err) return callback(err);
		// ghostdriver returns session id at data
		if (res.data.sessionId) {
			self.sessionId = res.data.sessionId;
		} else if (res.headers && res.headers.location) {
		// chrome, ff and maybe others return session id at location header
			var locationParts = res.headers.location.split('/');
			self.sessionId = locationParts[locationParts.length - 1];
		} else {
			return callback(new Error('Can`t determine session id'));
		}
		self.sessionBasePath = self.sessionBasePath + '/' + self.sessionId;
		self.setTimeouts(self.timeouts, returnSelf(callback, self));
	});
};

/**
 * End current driver session
 * @param {Function} callback(err:Error)
 */
WebDriver.prototype.deleteSession = function(callback) {
	this._cmd({path: '', method: 'DELETE'}, returnSelf(callback, this));
};

/**
 * Navigate to url
 * @param {String} url
 * @param {Function} callback(err:Error)
 */
WebDriver.prototype.setUrl = function(url, callback) {
	this._cmd({path: '/url', data: {url: url}}, returnSelf(callback, this));
};

/**
 * Get url of current page
 * @param {Function} callback(err:Error,url:String)
 */
WebDriver.prototype.getUrl = function(callback) {
	return this._cmd({path: '/url', method: 'GET'}, callback);
};

/**
 * Get title of current page
 * @param {Function} callback(err:Error,title:String)
 */
WebDriver.prototype.getTitle = function(callback) {
	return this._cmd({path: '/title', method: 'GET'}, callback);
};

var sweetenCssSelector = function(value) {
	value = value.replace(
		/:visible/g,
		':not([style*="display:none"]):not([style*="display: none"])'
	);
	value = value.replace(
		/:hidden/g,
		'[style*="display:none"],[style*="display: none"],' +
		'[style*="opacity: 0"],[style*="opacity:0"]'
	);
	return value;
};

var injections = {
	/*jshint ignore:start*/
	getElementsFn: utils.getInjectionSource(function() {
		//getting elements by jquery `selector`
		window.___nwdGetElements = function(selector, parent, chain, cssFilter) {
			cssFilter = cssFilter || {};
			var elements = null;
			var $ = window.___nwdJquery;
			elements = parent ? $(selector, parent) : $(selector);

			if (elements && elements.length && chain) {
				$.each(chain, function(index, object) {
					var func = null, args = null;

					//get function and arguments
					$.each(object, function(key, value) {
						func = key;
						args = value;
						return;
					});

					//wrap arguments into array if needed
					args = $.isArray(args) ? args : [args];

					if (!elements[func]) {
						throw new Error('Unknown jquery method: ' + func);
					}

					elements = elements[func].apply(elements, args);
				});
			}

			if (elements && elements.length && cssFilter) {
				elements = elements.filter(function(index, element) {
					var allFiltersMatch = true;

					for (var prop in cssFilter) {
						if ($(element).css(prop) !== cssFilter[prop]) {
							allFiltersMatch = false;
							break;
						}
					}

					return allFiltersMatch;
				});
			}

			return elements && elements.length ? elements.get() : [];
		};
	}),
	checkJqueryFn: utils.getInjectionSource(function() {
		return typeof window.___nwdJquery === 'function';
	}),
	checkGetElementsFn: utils.getInjectionSource(function() {
		return typeof window.___nwdGetElements === 'function';
	}),
	getElements: utils.getInjectionSource(function() {
		return window.___nwdGetElements.apply(null, arguments);
	}),
	waitForDocumentReady: utils.getInjectionSource(function() {
		var timeout = arguments[0],
			callback = arguments[1];

		var $ = window.___nwdJquery;

		var timeoutHandle = setTimeout(function() {
			callback(false);
		}, timeout);

		$(document).ready(function() {
			clearTimeout(timeoutHandle);
			callback(true);
		});
	}),
	/*jshint ignore:end*/
	jqueryFn: (
		'(function() { ' +
			fs.readFileSync(path.resolve(__dirname, '..', 'injections', 'jquery.js')) +
		' })();'
	)
};

WebDriver.prototype._injectJqueryIfNotExists = function(callback) {
	var self = this;

	this.execute(injections.checkJqueryFn, [], false, function(err, exists) {
		if (err) return callback(err);

		if (exists) {
			callback(null);
		} else {
			self.execute(injections.jqueryFn, [], false, callback);
		}
	});
};

WebDriver.prototype._injectGetElementsIfNotExists = function(callback) {
	var self = this;

	this._injectJqueryIfNotExists(function(err) {
		if (err) return callback(err);

		self.execute(injections.checkGetElementsFn, [], false, function(err, exists) {
			if (err) return callback(err);

			if (exists) {
				callback(null);
			} else {
				self.execute(injections.getElementsFn, [], false, callback);
			}
		});
	});
};

//strategy for getting elements via client jquery
var jqueryGetElementIds = function(selector, params, callback) {
	var self = this;

	this._injectGetElementsIfNotExists(function(err) {
		if (err) return callback(err);

		self.execute(
			injections.getElements,
			[
				selector,
				params.parentId ? {ELEMENT: params.parentId} : null,
				params.chain,
				params.cssFilter
			],
			false,
			function(err, result) {
				if (err) return callback(err);

				var elements = result;

				elements = elements.map(function(element) {
					return element.ELEMENT;
				});

				if (!elements.length) {
					if (params.isSingle) {
						callback(new errors.NoSuchElementError({
							element: selector,
							using: params.using
						}));
					} else {
						callback(null, []);
					}
				} else {
					callback(null, elements);
				}
			}
		);
	});
};

var customStrategyHash = {
	jquery: jqueryGetElementIds
};

/**
 * Get element from current page.
 * Error will be returned if no such element on the page.
 * @param {String} selector Element selector
 * @param {Object} [params]
 * @param {Boolean} [params.noError] If true then null will be passed to
 * callback instead of timeout error.
 * @param {String} [params.using] Strategy for selector (e.g. 'css selector',
 * 'jquery'), this.defaults.using will be used by default.
 * @param {Object[]} [params.chain] Traversing functions chain for jquery
 * strategy e.g. '[{closest: 'span'}, {next: 'div'}]'
 * @param {Object} [params.cssFilter] Object with css properties and values for
 * filter result elements (using jquery.css method) e.g. '{opacity: '1'}'
 * @param {Function} callback(err:Error,element:WebElement)
 */
WebDriver.prototype.get = function(selector, params, callback) {
	callback = utils.isFunction(params) ? params : callback;
	params = !utils.isFunction(params) ? params : {};
	var self = this;
	params.isSingle = true;
	this._getIds(selector, params, function(err, id) {
		callback(err, !err && id && new WebElement(id, self));
	});
};

/**
 * Get elements from current page.
 * Empty array will be returned if no such elements on the page.
 * `selector` and `params` could accept same values as at {@link WebDriver#get}.
 * @param {String} selector
 * @param {Object} [params]
 * @param {Function} callback(err:Error,element:WebElement[])
 */
WebDriver.prototype.getList = function(selector, params, callback) {
	callback = utils.isFunction(params) ? params : callback;
	params = !utils.isFunction(params) ? params : {};
	var self = this;
	params.isSingle = false;
	return this._getIds(selector, params, function(err, ids) {
		callback(err, !err && ids.map(function(id) {
			return new WebElement(id, self);
		}));
	});
};

// returns single element or list depending on `params.isSingle`
WebDriver.prototype._getIds = function(selector, params, callback) {
	params = !utils.isFunction(params) ? params : {};
	callback = utils.isFunction(params) ? params : callback;
	// transform params.parent to params.parent.id
	if (params.parent && !(params.parent instanceof WebElement)) {
		throw new Error('Parent should be instanceof WebElement');
	}
	if (params.parent) {
		params.parentId = params.parent.id;
		delete params.parent;
	}

	params.using = params.using || this.defaults.using;
	var customStrategy = customStrategyHash[params.using];
	if (customStrategy) {
		return customStrategy.call(this, selector, params, function(err, elements) {
			if (err && params.noError && err instanceof errors.NoSuchElementError) {
				return callback(null, null);
			}

			callback(
				parametrizeError(err),
				!err && (params.isSingle ? elements[0] : elements)
			);
		});
	} else {
		var plural = params.isSingle ? '' : 's';
		var parentPath = params.parentId ? (
			'/' + params.parentId + '/element' + plural
		) : plural;
		this._cmd({
			path: '/element' + parentPath,
			method: 'POST',
			data: {
				using: params.using,
				value: sweetenCssSelector(selector)
			}
		}, function(err, value) {
			if (err && params.noError && err instanceof errors.NoSuchElementError) {
				return callback(null, null);
			}

			callback(
				parametrizeError(err),
				!err && (params.isSingle ? value.ELEMENT : value.map(function(item) {
					return item.ELEMENT;
				}))
			);
		});
	}

	function parametrizeError(err) {
		if (!err) return err;
		err.parametrize({element: selector, using: params.using});
		return err;
	}
};

/**
 * Set timeout
 * @param {String} type
 * @param {Number} timeout timeout in ms
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.setTimeout = function(type, timeout, callback) {
	var self = this;
	if (type in this._protocolTimeoutsHash) {
		this._cmd({
			path: '/timeouts',
			method: 'POST',
			data: {type: type, ms: timeout}
		}, function(err) {
			if (err) return callback(err);
			self.timeouts[type] = timeout;
			callback(null, self);
		});
	} else {
		self.timeouts[type] = timeout;
		callback(null, self);
	}
};

/**
 * Set timeouts
 * @param {Object} timeout Object key - timeout type, value - timeout
 * value in ms
 * @param {Function} callback(err:Error)
 */
WebDriver.prototype.setTimeouts = function(timeouts, callback) {
	var count = Object.keys(timeouts).length,
		current = 0;
	function callbackWrapper(err) {
		if (err) return callback(err);
		current++;
		if (current === count) callback();
	}
	for (var type in timeouts) {
		this.setTimeout(type, timeouts[type], callbackWrapper);
	}
};

/**
 * Get timeout by type
 * @param {String} type
 * @param {Function} callback(err:Error,timeout:Number)
 */
WebDriver.prototype.getTimeout = function(type, callback) {
	callback(null, this.timeouts[type]);
};

/**
 * Inject a snippet of JavaScript into the page for execution in the context of
 * the currently selected frame.
 * @param {String} script
 * @param {Any[]} args Script arguments
 * @param {Boolean} isAsync
 * @param {Function} callback(err:Error)
 */
WebDriver.prototype.execute = function(script, args, isAsync, callback) {
	if (utils.isFunction(isAsync)) {
		callback = isAsync;
		isAsync = false;
	}
	if (utils.isFunction(script)) script = utils.getInjectionSource(script);
	this._cmd({
		path: '/execute' + (isAsync ? '_async' : ''),
		method: 'POST',
		data: {script: script, args: args || []}
	}, callback);
};

/**
 * Wait for element appear on current page and return it.
 * `selector` and `params` could accept same values as at `get`.
 * @param {String} selector
 * @param {Object} [params]
 * @param {Function} callback(err:Error,element:WebDriver)
 */
WebDriver.prototype.waitForElement = function(selector, params, callback) {
	callback = utils.isFunction(params) ? params : callback;
	params = !utils.isFunction(params) ? params : {};
	var self = this;
	self.waitFor(
		function(waitCallback) {
			self.get(selector, params, function(err, element) {
				if (err && (err instanceof errors.NoSuchElementError)) err = null;
				waitCallback(err, Boolean(element));
			});
		}, {
			noError: params.noError,
			errorMessage: 'waiting for element ' + selector,
			timeout: params.timeout || self.timeouts.waitForElement
		},
		callback
	);
};

/**
 * Wait until element will not be on page.
 * `selector` and `params` could accept same values as at `get`.
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.waitForElementAbsent =
	function(selector, params, callback) {
		callback = utils.isFunction(params) ? params : callback;
		params = !utils.isFunction(params) ? params : {};
		var self = this;
		this.get(
			selector,
			utils.extend({noError: true}, params),
			function(err, element) {
				if (err) return callback(err);
				if (!element) return callback(null, self);
				element.waitForDisappear(callback);
			}
		);
	};

/**
 * Wait for url change from `oldUrl` to `newUrl`.
 * Query string is not involved in url comparation.
 * @param {String|RegExp} oldUrl Old url (will be ignored if falsy)
 * @param {String|RegExp} newUrl New url (will be ignored if falsy)
 * @param {Object} [params]
 * @param {Boolean} [params.omitQueryString=false]
 * the parameter determines whether the current and expected new URL
 * will be checked without querystring
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.waitForUrlChange =
	function(oldUrl, newUrl, params, callback) {
		if (utils.isFunction(params)) {
			callback = params;
			params = {};
		}

		params = _({
			omitQueryString: false
		}).extend(params);

		var self = this;

		if (!oldUrl && !newUrl) return callback(new Error(
			'Both of new and old url can`t be falsy.'
		));

		if (params.omitQueryString) {
			newUrl = newUrl.replace(/\?.*$/, '');
		}

		self.waitFor(
			function(waitCallback) {
				self.getUrl(function(err, url) {
					if (err) return waitCallback(err);

					if (params.omitQueryString) {
						// remove query string
						url = url.replace(/\?.*$/, '');
					}

					waitCallback(
						null,
						(utils.isRegExp(oldUrl) ? !oldUrl.test(url) : oldUrl !== url) &&
							(!newUrl || (utils.isRegExp(newUrl) ? newUrl.test(url) : newUrl === url))
					);
				});
			}, {
				errorMessage: 'waiting for url change' +
					(oldUrl ? ' from ' + oldUrl : '') +
					(newUrl ? ' to ' + newUrl : ''),
				timeout: self.timeouts.waitForUrlChange
			},
			callback
		);
	};

/**
 * Wait for redirect to `newUrl`.
 * Query string is not involved in url comparation.
 * @param {String|RegExp} newUrl New url
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.waitForRedirect = function(newUrl, callback) {
	this.waitForUrlChange({
		oldUrl: '',
		newUrl: newUrl
	}, callback);
};

/**
 * Wait for document ready (jquery).
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.waitForDocumentReady = function(callback) {
	var self = this;

	this._injectJqueryIfNotExists(function(err) {
		if (err) return callback(err);

		self.execute(
			injections.waitForDocumentReady,
			[self.timeouts.waitFor],
			true,
			function(err, result) {
				if (err) return callback(err);

				if (result === true) {
					callback(null, self);
				} else {
					callback(new Error(
						result === false ?
						'Timeout exceeded while waiting for document ready' :
						('Unexpected result while waiting for document ready: ' + result)
					));
				}
			}
		);
	});
};


/**
 * Wait until waitcb from `func(waitcb)` will be called with `true` or until
 * `params.timeout` (if not set `this.timeouts.waitFor` will be used) expired
 * (in this case error with `params.errorMessage` will be passed to callback,
 * if `params.noError` is not true).
 * Note: co version of the driver has similar method - `yieldUntil`.
 */
WebDriver.prototype.waitFor = function(func, params, callback) {
	callback = utils.isFunction(params) ? params : callback;
	params = !utils.isFunction(params) ? params : {};

	var self = this,
		timeout = params.timeout || self.timeouts.waitFor,
		delay = 20;

	var executeTimeoutHandle = null,
		waitTimeoutHandle = null;

	//TODO: refactor function that use this variable
	var isWaitingTimeExpired = false;

	var clearTimeouts = function() {
		if (executeTimeoutHandle) {
			clearTimeout(executeTimeoutHandle);
			executeTimeoutHandle = null;
		}

		if (waitTimeoutHandle) {
			clearTimeout(waitTimeoutHandle);
			waitTimeoutHandle = null;
		}
	};

	var execute = function() {
		func(function(err, done) {
			if (isWaitingTimeExpired) {
				return;
			}

			if (err) {
				clearTimeouts();
				return callback(err);
			}

			if (done) {
				clearTimeouts();
				callback(null, self);
			} else {
				executeTimeoutHandle = setTimeout(execute, delay);
			}
		});
	};

	waitTimeoutHandle = setTimeout(function() {
		isWaitingTimeExpired = true;

		callback(params.noError ? null : new Error(
			'Timeout (' + timeout + ' ms) exceeded' +
			(params.errorMessage ? ' while ' + params.errorMessage : '')
		));
	}, timeout);

	execute();
};

WebDriver.prototype.getCookie = function(name) {
	return this._cmd({
		path: name ? '/cookie/' + name : '/cookie',
		method: 'GET'
	});
};

/**
 * Delete all cookies or cookie with given name.
 * @param {String} [name]
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.deleteCookie = function(name, callback) {
	if (typeof name === 'function') {
		callback = name;
		name = null;
	}
	this._cmd({
		path: name ? '/cookie/' + name : '/cookie',
		method: 'DELETE'
	}, returnSelf(callback, this));
};

/**
 * Make screenshot and save it to target path.
 * @param {String} path
 * @param {Function} callback(err:Error)
 */
WebDriver.prototype.makeScreenshot = function(path, callback) {
	this._cmd({
		path: '/screenshot',
		method: 'GET'
	}, function(err, res) {
		if (err) return callback(err);
		//convert base64 to binary
		var data = new Buffer(res, 'base64').toString('binary');
		fs.writeFile(path, data, 'binary', callback);
	});
};

/**
 * Maximize current window.
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.maximizeWindow = function(callback) {
	this._cmd({
		path: '/window/current/maximize',
		method: 'POST'
	}, returnSelf(callback, this));
};

/**
 * Navigate backwards in the browser history, if possible.
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.back = function(callback) {
	this._cmd({path: '/back', method: 'POST'}, returnSelf(callback, this));
};

/**
 * Navigate forwards in the browser history, if possible.
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.forward = function(callback) {
	this._cmd({path: '/forward', method: 'POST'}, returnSelf(callback, this));
};

/**
 * Refresh the current page.
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.refresh = function(callback) {
	this._cmd({path: '/refresh', method: 'POST'}, returnSelf(callback, this));
};

var mouseButtons = {
	left: 0,
	middle: 1,
	right: 2
};

/**
 * Click and hold mouse button (at the coordinates set by the last
 * moveto command). Note that the next mouse-related command that should
 * follow is buttonup . Any other mouse command (such as click or another
 * call to buttondown) will yield undefined behaviour.
 * @param {String} [button] Could be left, middle or right(left used by default)
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.mouseDown = function(button, callback) {
	callback = utils.isFunction(button) ? button : callback;
	button = !utils.isFunction(button) ? button : 'left';
	this._cmd({
		path: '/buttondown',
		method: 'POST',
		data: {button: mouseButtons[button]}
	}, returnSelf(callback, this));
};

/**
 * Releases the mouse button previously held (where the mouse is currently at).
 * Must be called once for every buttondown command issued. See the note in
 * click and buttondown about implications of out-of-order commands.
 * @param {String} [button] Could be left, middle or right(left used by default)
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.mouseUp = function(button, callback) {
	callback = utils.isFunction(button) ? button : callback;
	button = !utils.isFunction(button) ? button : 'left';
	this._cmd({
		path: '/buttonup',
		method: 'POST',
		data: {button: mouseButtons[button]}
	}, returnSelf(callback, this));
};

/**
 * Click any mouse button (at the coordinates set by the last moveto command).
 * Note that calling this command after calling buttondown and before calling
 * button up (or any out-of-order interactions sequence) will yield undefined
 * behaviour).
 * @param {String} [button] Could be left, middle or right(left used by default)
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.click = function(button, callback) {
	callback = utils.isFunction(button) ? button : callback;
	button = !utils.isFunction(button) ? button : 'left';
	this._cmd({
		path: '/click',
		method: 'POST',
		data: {button: mouseButtons[button]}
	}, returnSelf(callback, this));
};

/**
 * Send a sequence of key strokes to the active element.
 * @param {String} value
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebDriver.prototype.sendKeys = function(value, callback) {
	this._cmd({
		path: '/keys',
		method: 'POST',
		data: {value: utils.replaceKeyStrokesWithCodes(value).split('')}
	}, returnSelf(callback, this));
};

WebDriver.prototype.getLog = function(type, callback) {
	this._cmd({
		path: '/log',
		method: 'POST',
		data: {type: type}
	}, callback);
};

/**
 * Retrieve the capabilities of the current session
 * @param {Function} callback(err:Error,capabilities:Object)
 */
WebDriver.prototype.getCapabilities = function(callback) {
	this._cmd({path: '', method: 'GET'}, callback);
};

utils.loggify(WebDriver.prototype, 'WebDriver');

exports.WebDriver = WebDriver;