Source: webElement.js

'use strict';

var utils = require('./utils'),
	errors = require('./errors');


/**
 * Element constructor
 * @class WebElement
 * @param {String} id Element id (e.g. returned by {@link WebDriver#get})
 * @param {WebDriver} driver
 */
function WebElement(id, driver) {
	//TODO: remove `this.ELEMENT` if it is not required
	this.id = this.ELEMENT = id;
	this.driver = driver;
	this.logMethodCalls = this.driver.logMethodCalls;

	/**
	 * element object works like {@link WebDriver#element}.
	 * @name element
	 * @type {Object}
	 * @instance
	 * @memberOf WebElement
	 */
	this.driver._initElement.call(this);
}

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

// returns index of first function from `args`
function indexOfFunctionInArgs(args) {
	var index = 0;
	while (index < args.length && !utils.isFunction(args[index])) index++;
	return utils.isFunction(args[index]) ? index : -1;
}

// replace first function from `args` on `returnSelf`
function repalceArgsCallbackOnReturnSelf(args, self) {
	var index = indexOfFunctionInArgs(args);
	if (index !== -1) args[index] = returnSelf(args[index], self);
	return args;
}

/**
 * Send a sequence of key strokes to an element.
 * @param {String} value
 * @param {Object} [params]
 * @param {Boolean} [params.clear] If true then 'clear' will be called before
 * sending keys.
 * @param {Function} callback(err:Error,element:WebElement)
 */
WebElement.prototype.sendKeys = function(value, params, callback) {
	var self = this;
	callback = utils.isFunction(params) ? params : callback;
	params = !utils.isFunction(params) ? params : {};
	function sendKeys() {
		self.driver._cmd({
			path: '/element/' + self.id + '/value',
			method: 'POST',
			data: {value: utils.replaceKeyStrokesWithCodes(value).split('')}
		}, returnSelf(callback, self));
	}
	if (params.clear) {
		self.clear(function(err) {
			if (err) return callback(err);
			sendKeys();
		});
	} else {
		sendKeys();
	}
};

/**
 * Clear a TEXTAREA or text INPUT element's value.
 * @param {Function} callback(err:Error,element:WebElement)
 */
WebElement.prototype.clear = function(callback) {
	this.driver._cmd({
		path: '/element/' + this.id + '/clear',
		method: 'POST'
	}, returnSelf(callback, this));
};

/**
 * Get value of current element (alias to getAttr('value')).
 * @param {Function} callback(err:Error,value:String)
 */
WebElement.prototype.getValue = function(callback) {
	this.getAttr('value', callback);
};

/**
 * Get the value of an element's attribute.
 * @param {String} name Attribute's name
 * @param {Function} callback(err:Error,value:String)
 */
WebElement.prototype.getAttr = function(name, callback) {
	this.driver._cmd({
		path: '/element/' + this.id + '/attribute/' + name,
		method: 'GET'
	}, callback);
};

/**
 * Get the visible text (if element is invisible the result will be empty
 * string) of the element.
 * @param {Function} callback(err:Error,text:String)
 */
WebElement.prototype.getText = function(callback) {
	this.driver._cmd({
		path: '/element/' + this.id + '/text',
		method: 'GET'
	}, callback);
};

var createJqueryNameMethod = function(method, methodArgsCount) {
	return function() {
		/*
		 * Copy only known jquery method args and callback, don't use
		 * standart strategy (last parameter is callback) for correct
		 * support of nwd wrappers (e.g. co-nwd)
		 */
		var args = Array.prototype.slice.call(
			arguments,
			0,
			methodArgsCount + 1
		);
		var callback = args.pop();
		var methodParams = [];
		if (args.length) {
			for (var i in args) {
				var val = String(args[i]);
				if (typeof val === 'string') val = '"' + val + '"';
				methodParams.push(val);
			}
		}
		this.driver.execute(
			'return window.___nwdJquery(arguments[0]).' +
				method + '(' + methodParams.join(',') + ');',
			[{ELEMENT: this.id}],
			false,
			callback
		);
	};
};

/**
 * Get the value of an element's attribute via jQuery.attr method.
 * See also selenium {@link WebElement#getAttr} method.
 * @param {String} name Attribute's name
 * @param {Function} callback(err:Error,value:String)
 */
WebElement.prototype.attr = createJqueryNameMethod('attr', 1);

/**
 * Get the value of an element's property via jQuery.prop method.
 * @param {String} name Property name
 * @param {Function} callback(err:Error,value:String|Boolean)
 */
WebElement.prototype.prop = createJqueryNameMethod('prop', 1);

/**
 * Get the value of an element's css property via jQuery.css method.
 * See also selenium {@link WebElement#getCssProp} method.
 * @param {String} name Css property name
 * @param {Function} callback(err:Error,value:String)
 */
WebElement.prototype.css = createJqueryNameMethod('css', 1);

/**
 * Get the text of an element's attribute via jQuery.text method.
 * See also selenium {@link WebElement#getText} method.
 * @param {Function} callback(err:Error,text:String)
 */
WebElement.prototype.text = createJqueryNameMethod('text', 0);

/**
 * Get element's tag name.
 * @param {Function} callback(err:Error,tagName:String)
 */
WebElement.prototype.getTagName = function(callback) {
	this.driver._cmd({
		path: '/element/' + this.id + '/name',
		method: 'GET'
	}, callback);
};

/**
 * Click on an element.
 * @param {Function} callback(err:Error,element:WebElement)
 */
WebElement.prototype.click = function(callback) {
	this.driver._cmd({
		path: '/element/' + this.id + '/click',
		method: 'POST'
	}, returnSelf(callback, this));
};

/**
 * Move the mouse by an offset of the current element.
 * If the element is not visible, it will be scrolled into view.
 * @param {Object} offset Object with x, y offsets (relative to the top-left 
 * corner of the element) keys. If offset is not set the mouse will be moved 
 * to the center of the element.
 * @param {Function} callback(err:Error,element:WebElement)
 */
WebElement.prototype.moveTo = function(offset, callback) {
	callback = utils.isFunction(offset) ? offset : callback;
	offset = !utils.isFunction(offset) ? offset : {};
	this.driver._cmd({
		path: '/moveto',
		method: 'POST',
		data: {
			element: this.id,
			xoffset: offset.x,
			yoffset: offset.y
		}
	}, returnSelf(callback, this));
};

/**
 * Move to the current element and down mouse button. 
 * `button` accepts same values as at {@link WebDriver#mouseDown}.
 * @param {String} [button]
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebElement.prototype.mouseDown = function() {
	var self = this,
		args = repalceArgsCallbackOnReturnSelf(arguments, this),
		callback = arguments[arguments.length - 1];
	this.moveTo(function(err) {
		if (err) return callback(err);
		self.driver.mouseDown.apply(self.driver, args);
	});
};

/**
 * Move to the current element and up mouse button. 
 * `button` accepts same values as at {@link WebDriver#mouseDown}.
 * @param {String} [button]
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebElement.prototype.mouseUp = function() {
	var self = this,
		args = repalceArgsCallbackOnReturnSelf(arguments, this),
		callback = arguments[arguments.length - 1];
	this.moveTo(function(err) {
		if (err) return callback(err);
		self.driver.mouseUp.apply(self.driver, args);
	});
};

/**
 * Find element inside current element.
 * `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)
 */
WebElement.prototype.get = function(selector, params, callback) {
	callback = utils.isFunction(params) ? params : callback;
	params = !utils.isFunction(params) ? params : {};
	params.parent = this;
	this.driver.get(selector, params, callback);
};

/**
 * Find elements inside current element.
 * `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[])
 */
WebElement.prototype.getList = function(selector, params, callback) {
	callback = utils.isFunction(params) ? params : callback;
	params = !utils.isFunction(params) ? params : {};
	params.parent = this;
	this.driver.getList(selector, params, callback);
};

/**
 * Wait for element appear inside current element.
 * `selector` and `params` could accept same values as
 * at {@link WebDriver#waitForElement}.
 * @param {String} selector
 * @param {Object} [params]
 * @param {Function} callback(err:Error,element:WebDriver)
 */
WebElement.prototype.waitForElement = function(selector, params, callback) {
	callback = utils.isFunction(params) ? params : callback;
	params = !utils.isFunction(params) ? params : {};
	params.parent = this;
	this.driver.waitForElement(selector, params, callback);
};

/**
 * Determine if an element is currently enabled.
 * This will generally return true for everything but disabled input elements.
 * NOTE: therefore that native selenium function works properly only for
 * inputs. See {@link WebElement#isDisabled} method which works properly for
 * all native controls.
 * @param {Function} callback(err:Error,enabled:Boolean)
 */
WebElement.prototype.isEnabled = function(callback) {
	this.driver._cmd({
		path: '/element/' + this.id + '/enabled',
		method: 'GET'
	}, callback);
};

/**
 * Determine if an element is currently disabled using DOM Element 'disabled'
 * property (works properly for any native controls).
 * @param {Function} callback(err:Error,disabled:Boolean)
 */
WebElement.prototype.isDisabled = function(callback) {
	this.driver.execute(
		'return arguments[0].disabled;',
		[{ELEMENT: this.id}],
		false,
		callback
	);
};

/**
 * Describe the identified element.
 * This command is reserved for future use;
 * its return type is currently undefined.
 * @param {Function} callback(err:Error,description:String)
 */
WebElement.prototype.describe = function(callback) {
	this.driver._cmd({
		path: '/element/' + this.id,
		method: 'GET'
	}, callback);
};

/**
 * Get the value of an element's computed CSS property.
 * @param {String} propName The CSS property name, not the JavaScript property
 * name (e.g. background-color instead of backgroundColor).
 * @param {Function} callback(err:Error,value:String)
 */
WebElement.prototype.getCssProp = function(propName, callback) {
	this.driver._cmd({
		path: '/element/' + this.id + '/css/' + propName,
		method: 'GET'
	}, callback);
};

/**
 * Determine if an element is currently displayed.
 * This method avoids the problem of having to parse an element's 'style'
 * attribute.
 * @param {Function} callback(err:Error,displayed:Boolean)
 */
WebElement.prototype.isDisplayed = function(callback) {
	this.driver._cmd({
		path: '/element/' + this.id + '/displayed',
		method: 'GET'
	}, callback);
};

/**
 * Determine if a checkbox or radiobutton is currently selected.
 * @param {Function} callback(err:Error,selected:Boolean)
 */
WebElement.prototype.isSelected = function(callback) {
	this.driver._cmd({
		path: '/element/' + this.id + '/selected',
		method: 'GET'
	}, callback);
};

/*jshint ignore:start*/
var isVisibleInjection = utils.getInjectionSource(function() {
	function ___nwdIsVisible(element) {
		if (!element) return false;
		return (
			element.style.display !== 'none' ?
				(element.offsetWidth > 0 || element.offsetHeight > 0) :
				false
		);
	}
});

/*
 * Possible should be removed coz isDisplayed do the job right. 
 */
WebElement.prototype.isVisible = function(callback) {
	var self = this;
	function execute(withFunc) {
		self.driver.execute(
			(withFunc ? isVisibleInjection : '') + utils.getInjectionSource(function() {
				if (typeof window.___nwdIsVisible !== 'function') {
					if (typeof ___nwdIsVisible !== 'function') return 'needFunc';
					window.___nwdIsVisible = ___nwdIsVisible;
				}
				return ___nwdIsVisible(arguments[0]);
			}),
			[{ELEMENT: self.id}],
			false,
			function(err, result) {
				if (err) return callback(err);
				result === 'needFunc' ? execute(true) : callback(null, result);
			}
		);
	}
	execute();
};
/*jshint ignore:end*/

/**
 * Wait for element disappear from the page.
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebElement.prototype.waitForDisappear = function(callback) {
	var self = this;
	self.driver.waitFor(
		function(waitCallback) {
			self.isVisible(function(err, isVisible) {
				// Stale error is ok because it will occur when element is removed
				if (err && err instanceof errors.StaleElementReferenceError) err = null;
				waitCallback(err, !isVisible);
			});
		}, {
			errorMessage: 'waiting for element ' + self.id + ' disappear',
			timeout: self.driver.timeouts.waitForElementDisappear
		},
		returnSelf(callback, self.driver)
	);
};

/**
 * Wait untill element will be detached from page.
 * @param {Function} callback(err:Error,driver:WebDriver)
 */
WebElement.prototype.waitForDetach = function(callback) {
	var self = this;
	self.driver.waitFor(
		function(waitCallback) {
			self.getTagName(function(err) {
				waitCallback(null, err instanceof errors.StaleElementReferenceError);
			});
		}, {
			errorMessage: 'waiting for element ' + self.id + ' detach',
			timeout: self.driver.timeouts.waitForDetach
		},
		returnSelf(callback, self.driver)
	);
};

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

exports.WebElement = WebElement;