compat.js

/**
 * The compat module adds support for Intern 1.x functional testing APIs based on WD.js 0.2.2 to Leadfoot.
 *
 * @deprecated Use the standard Leadfoot APIs.
 * @module leadfoot/compat
 */

var Command = require('./Command');
var Promise = require('dojo/Promise');
var pollUntil = require('./helpers/pollUntil');
var strategies = require('./lib/strategies');
var topic = require('dojo/topic');

/**
 * Deprecates `fromMethod` for `toMethod` and returns the correct call to `toMethod`.
 *
 * @private
 * @param {string} fromMethod
 * @param {string} toMethod
 * @returns {Function}
 */
function deprecate(fromMethod, toMethod) {
	return function () {
		warn('Command#' + fromMethod, 'Command#' + toMethod);
		return this[toMethod].apply(this, arguments);
	};
}

/**
 * Deprecates the element context signature of a method and returns a new command with the correct call to
 * `toMethod` on the element.
 *
 * @private
 * @param {string} fromMethod
 * The name of the old method.
 *
 * @param {string=} toMethod
 * The name of the replacement method on the element, if it is different than the name of the old method. If
 * omitted, it is assumed that the method name
 *
 * @param {Function=} fn
 * An optional function that will be invoked in lieu of a call to the original method if a non-element signature
 * is used.
 *
 * @returns {Function}
 */
function deprecateElementSig(fromMethod, toMethod, fn) {
	return function (element) {
		if (element && element.elementId) {
			warn('Command#' + fromMethod + '(element)', 'Command#find then Command#' + fromMethod + ', or ' +
				'Command#find then Command#then(function (element) { return element.' +
				(toMethod || fromMethod) + '(); }');

			var args = Array.prototype.slice.call(arguments, 1);
			return new this.constructor(this, function () {
				return element[toMethod || fromMethod].apply(this, args);
			});
		}

		return fn ? fn.apply(this, arguments) : Command.prototype[fromMethod].apply(this, arguments);
	};
}

/**
 * Deprecates the element context signature of a method as well as its non-element signature to go to `toMethod`.
 *
 * @private
 * @param {string} fromMethod
 * @param {string} toMethod
 * @returns {Function}
 */
function deprecateElementAndStandardSig(fromMethod, toMethod) {
	return deprecateElementSig(fromMethod, toMethod, deprecate(fromMethod, toMethod));
}

/**
 * Warns a user once that the method given by `name` is deprecated.
 *
 * @private
 * @method
 * @param {string} name The name of the old method.
 * @param {string=} replacement Replacement instructions, if a direct replacement for the old method exists.
 * @param {string=} extra Extra information about the deprecation.
 */
var warn = (function () {
	var warned = {};
	return function (name, replacement, extra) {
		if (warned[name]) {
			return;
		}

		warned[name] = true;
		topic.publish('/deprecated', name, replacement, extra);
	};
})();

var methods = {
	get sessionID() {
		warn('Command#sessionID', 'the Command#session.sessionId property');
		return this.session.sessionId;
	},
	status: function () {
		warn('Command#status');
		return new this.constructor(this, function () {
			return this.session.server.getStatus();
		});
	},
	init: function () {
		warn('Command#init');
		return this;
	},
	sessions: function () {
		warn('Command#sessions');
		return new this.constructor(this, function () {
			return this.session.server.getSessions();
		});
	},
	sessionCapabilities: function () {
		warn('Command#sessionCapabilities', 'the Command#session.capabilities property');
		return new this.constructor(this, function () {
			return this.session.capabilities;
		});
	},
	altSessionCapabilities: function () {
		warn('Command#altSessionCapabilities', 'the Command#session.capabilities property');
		return new this.constructor(this, function () {
			return this.session.capabilities;
		});
	},
	getSessionId: function () {
		warn('Command#getSessionId', 'the Command#session.sessionId property');
		return new this.constructor(this, function () {
			return this.session.sessionId;
		});
	},
	getSessionID: function () {
		warn('Command#getSessionID', 'the Command#session.sessionId property');
		return new this.constructor(this, function () {
			return this.session.sessionId;
		});
	},
	setAsyncScriptTimeout: deprecate('setAsyncScriptTimeout', 'setExecuteAsyncTimeout'),
	setWaitTimeout: deprecate('setWaitTimeout', 'setFindTimeout'),
	setImplicitWaitTimeout: deprecate('setImplicitWaitTimeout', 'setFindTimeout'),
	windowHandle: deprecate('windowHandle', 'getCurrentWindowHandle'),
	windowHandles: deprecate('windowHandles', 'getAllWindowHandles'),
	url: deprecate('url', 'getCurrentUrl'),
	forward: deprecate('forward', 'goForward'),
	back: deprecate('back', 'goBack'),
	safeExecute: deprecate('safeExecute', 'execute'),
	eval: function (code) {
		warn('Command#eval', 'Command#execute with a return call');
		return this.execute('return eval(arguments[0]);', [ code ]);
	},
	safeEval: function (code) {
		warn('Command#safeEval', 'Command#execute with a return call');
		return this.execute('return eval(arguments[0]);', [ code ]);
	},
	safeExecuteAsync: deprecate('safeExecuteAsync', 'executeAsync'),
	frame: deprecate('frame', 'switchToFrame'),
	window: deprecate('window', 'switchToWindow'),
	close: deprecate('close', 'closeCurrentWindow'),
	windowSize: deprecate('windowSize', 'setWindowSize'),
	setWindowSize: function () {
		var args = Array.prototype.slice.call(arguments, 0);

		if (args.length === 3 && typeof args[0] === 'number') {
			warn('Command#setWindowSize(width, height, handle)',
				'Command#setWindowSize(handle, width, height)');
			args.unshift(args.pop());
		}

		return Command.prototype.setWindowSize.apply(this, args);
	},
	setWindowPosition: function () {
		var args = Array.prototype.slice.call(arguments, 0);

		if (args.length === 3 && typeof args[0] === 'number') {
			warn('Command#setWindowPosition(x, y, handle)',
				'Command#setWindowPosition(handle, x, y)');
			args.unshift(args.pop());
		}

		return Command.prototype.setWindowPosition.apply(this, args);
	},
	maximize: deprecate('maximize', 'maximizeWindow'),
	allCookies: deprecate('allCookies', 'getCookies'),
	deleteAllCookies: deprecate('deleteAllCookies', 'clearCookies'),
	source: deprecate('source', 'getPageSource'),
	title: deprecate('title', 'getPageTitle'),
	element: deprecate('element', 'find'),
	elementByClassName: deprecate('elementByClassName', 'findByClassName'),
	elementByCssSelector: deprecate('elementByCssSelector', 'findByCssSelector'),
	elementById: deprecate('elementById', 'findById'),
	elementByName: deprecate('elementByName', 'findByName'),
	elementByLinkText: deprecate('elementByLinkText', 'findByLinkText'),
	elementByPartialLinkText: deprecate('elementByPartialLinkText', 'findByPartialLinkText'),
	elementByTagName: deprecate('elementByTagName', 'findByTagName'),
	elementByXPath: deprecate('elementByXPath', 'findByXpath'),
	elementByCss: deprecate('elementByCss', 'findByCssSelector'),
	elements: deprecate('elements', 'findAll'),
	elementsByClassName: deprecate('elementsByClassName', 'findAllByClassName'),
	elementsByCssSelector: deprecate('elementsByCssSelector', 'findAllByCssSelector'),
	elementsById: function (value) {
		warn('Command#elementsById', 'Command#findById');
		return this.findAll('id', value);
	},
	elementsByName: deprecate('elementsByName', 'findAllByName'),
	elementsByLinkText: deprecate('elementsByLinkText', 'findAllByLinkText'),
	elementsByPartialLinkText: deprecate('elementsByPartialLinkText', 'findAllByPartialLinkText'),
	elementsByTagName: deprecate('elementsByTagName', 'findAllByTagName'),
	elementsByXPath: deprecate('elementsByXPath', 'findAllByXpath'),
	elementsByCss: deprecate('elementsByCss', 'findAllByCssSelector'),
	elementOrNull: function (using, value) {
		warn('Command#elementOrNull', 'Command#find and Command#finally, or Command#findAll');
		return this.find(using, value).catch(function () {
			return null;
		});
	},
	elementIfExists: function (using, value) {
		warn('Command#elementIfExists', 'Command#find and Command#finally, or Command#findAll');
		return this.find(using, value).catch(function () {});
	},
	hasElement: function (using, value) {
		warn('Command#hasElement', 'Command#find and Command#then(exists, doesNotExist)');
		return this.find(using, value).then(function () {
			return true;
		}, function () {
			return false;
		});
	},
	active: deprecate('active', 'getActiveElement'),
	clickElement: deprecateElementAndStandardSig('clickElement', 'click'),
	submit: deprecateElementSig('submit'),
	text: function (element) {
		return new this.constructor(this, function () {
			if ((!element || element === 'body') && !this.context.length) {
				if (element === 'body') {
					warn('Command#text(\'body\')', 'Command#findByTagName(\'body\') then Command#getVisibleText');
				}
				else {
					warn('Command#text with no element', 'Command#findByTagName(\'body\') then Command#getVisibleText');
				}

				return this.session.findByTagName('body').then(function (body) {
					return body.getVisibleText();
				});
			}
			else if (element && element.elementId) {
				warn('Command#text(element)', 'Command#find then Command#getVisibleText, or ' +
					'Command#find then Command#then(function (element) { return element.getVisibleText(); }');
				return element.getVisibleText();
			}
			else {
				warn('Command#text', 'Command#getVisibleText');
				if (this.context.isSingle) {
					return this.context[0].getVisibleText();
				}
				else {
					return Promise.all(this.context.map(function (element) {
						return element.getVisibleText();
					}));
				}
			}
		});
	},

	// This method had a two-argument version according to the WD.js docs but they inexplicably swapped the first
	// and second arguments so it probably never would have worked properly in Intern
	textPresent: function (searchText, element) {
		warn('Command#textPresent', 'Command#getVisibleText and a promise helper');

		function test(text) {
			return text.indexOf(searchText) > -1;
		}

		if (element) {
			return new this.constructor(this, function () {
				return element.getVisibleText().then(test);
			});
		}

		return this.getVisibleText().then(test);
	},

	type: deprecateElementSig('type'),
	keys: deprecate('keys', 'pressKeys'),
	getTagName: deprecateElementSig('getTagName'),
	clear: deprecateElementAndStandardSig('clear', 'clearValue'),
	isSelected: deprecateElementSig('isSelected'),
	isEnabled: deprecateElementSig('isEnabled'),
	enabled: deprecateElementAndStandardSig('enabled', 'isEnabled'),
	getAttribute: deprecateElementSig('getAttribute'),
	getValue: function (element) {
		if (element && element.elementId) {
			warn('Command#getValue(element)', 'Command#find then Command#getProperty(\'value\'), or ' +
				'Command#find then Command#then(function (element) { ' +
				'return element.getProperty(\'value\'); }');

			return new this.constructor(this, function () {
				return element.getProperty('value');
			});
		}

		warn('Command#getValue', 'Command#find then Command#getProperty(\'value\')');
		return this.getProperty('value');
	},
	equalsElement: function (element, other) {
		if (other && other.elementId) {
			warn('Command#equalsElement(element, other)', 'element.equals(other)');
			return new this.constructor(this, function () {
				return element.equals(other);
			});
		}

		warn('Command#equalsElement', 'Command#equals');
		return this.equals(element);
	},
	isDisplayed: deprecateElementSig('isDisplayed'),
	displayed: deprecateElementAndStandardSig('displayed', 'isDisplayed'),
	getLocation: deprecateElementAndStandardSig('getLocation', 'getPosition'),
	getLocationInView: function () {
		warn(
			'Command#getLocationInView',
			'Command#getPosition',
			'This command is defined in the spec as internal and should never have been exposed to end users. ' +
			'The returned value of this command will be the same as Command#getPosition, which may not match ' +
			'prior behaviour.'
		);

		return this.getPosition.apply(this, arguments);
	},
	getSize: deprecateElementSig('getSize'),
	getComputedCss: deprecateElementAndStandardSig('getComputedCss', 'getComputedStyle'),
	getComputedCSS: deprecateElementAndStandardSig('getComputedCSS', 'getComputedStyle'),
	alertText: deprecate('alertText', 'getAlertText'),
	alertKeys: deprecate('alertKeys', 'typeInPrompt'),
	moveTo: deprecateElementAndStandardSig('moveTo', 'moveMouseTo'),
	click: function (button) {
		if (typeof button === 'number') {
			warn('Command#click(button)', 'Command#clickMouseButton(button)');
			return this.clickMouseButton(button);
		}

		return Command.prototype.click.apply(this, arguments);
	},
	buttonDown: deprecate('buttonDown', 'pressMouseButton'),
	buttonUp: deprecate('buttonUp', 'releaseMouseButton'),
	doubleclick: deprecate('doubleclick', 'doubleClick'),
	// TODO: There is no tap on elements
	tapElement: deprecateElementSig('tapElement', 'tap'),
	// TODO: There is no flick on elements
	flick: deprecate('flick', 'flickFinger'),
	setLocalStorageKey: deprecate('setLocalStorageKey', 'setLocalStorageItem'),
	getLocalStorageKey: deprecate('getLocalStorageKey', 'getLocalStorageItem'),
	removeLocalStorageKey: deprecate('removeLocalStorageKey', 'deleteLocalStorageItem'),
	log: deprecate('log', 'getLogsFor'),
	logTypes: deprecate('logTypes', 'getAvailableLogTypes'),
	newWindow: function (url, name) {
		warn('Command#newWindow', 'Command#execute');
		return this.execute('window.open(arguments[0], arguments[1]);', [ url, name ]);
	},
	windowName: function () {
		warn('Command#windowName', 'Command#execute');
		return this.execute('return window.name;');
	},
	setHTTPInactivityTimeout: function () {
		warn('Command#setHTTPInactivityTimeout');
		return this;
	},
	getPageIndex: function (element) {
		warn('Command#getPageIndex', null, 'This command is not part of any specification.');
		if (element && element.elementId) {
			return new this.constructor(this, function () {
				return element._get('pageIndex');
			});
		}

		return new this.constructor(this, function () {
			if (this.context.isSingle) {
				return this.context[0]._get('pageIndex');
			}
			else {
				return Promise.all(this.context.map(function (element) {
					return element._get('pageIndex');
				}));
			}
		});
	},
	uploadFile: function () {
		warn(
			'Command#uploadFile',
			'Command#type to type a file path into a file upload form control',
			'This command is not part of any specification. This command is a no-op.'
		);

		return this;
	},
	waitForCondition: function (expression, timeout, pollInterval) {
		timeout = timeout || 1000;
		pollInterval = pollInterval || 100;

		warn('Command#waitForCondition', 'Command#executeAsync or leadfoot/helpers/pollUntil');

		return this.then(pollUntil('return eval(arguments[0]) ? true : null;', [ expression ], timeout, pollInterval));
	},
	waitForConditionInBrowser: function (expression, timeout, pollInterval) {
		timeout = timeout || 1000;
		pollInterval = pollInterval || 100;

		warn('Command#waitForConditionInBrowser', 'Command#executeAsync or leadfoot/helpers/pollUntil');

		return this.then(pollUntil('return eval(arguments[0]) ? true : null;', [ expression ], timeout, pollInterval));
	},
	sauceJobUpdate: function () {
		warn(
			'Command#sauceJobUpdate',
			null,
			'This command is not part of any specification. This command is a no-op.'
		);

		return this;
	},
	sauceJobStatus: function () {
		warn(
			'Command#sauceJobStatus',
			null,
			'This command is not part of any specification. This command is a no-op.'
		);

		return this;
	},
	reset: function () {
		warn(
			'Command#reset',
			'a previously stored Command instance'
		);

		return this;
	},
	waitForElement: function (using, value, timeout) {
		warn(
			'Command#waitForElement',
			'Command#setFindTimeout and Command#find',
			'This command is implemented using implicit timeouts, which may not match the prior behaviour.'
		);

		// This is effectively what the WD.js code does, though there it's because the property is never validated,
		// so the end date becomes NaN; not an intentional design choice
		if (typeof timeout === 'undefined') {
			timeout = Infinity;
		}

		var command = this;
		return this.getFindTimeout().then(function (originalTimeout) {
			return command.setFindTimeout(timeout)
				.find(using, value)
				.then(function () {
					return command.setFindTimeout(originalTimeout).then(function () {
						return null;
					});
				}, function (error) {
					return command.setFindTimeout(originalTimeout).then(function () {
						throw error;
					});
				});
		});
	},
	waitForVisible: function (using, value, timeout) {
		warn(
			'Command#waitForVisible',
			null,
			'This command is partially implemented using implicit timeouts, which may not match the prior ' +
			'behaviour.'
		);

		// This is effectively what the WD.js code does, though there it's because the property is never validated,
		// so the end date becomes NaN; not an intentional design choice
		if (typeof timeout === 'undefined') {
			timeout = Infinity;
		}

		var startTime = Date.now();
		var command = this;
		return this.getFindTimeout().then(function (originalTimeout) {
			return command.setFindTimeout(timeout)
				.find(using, value)
				.then(function (element) {
					return pollUntil(/* istanbul ignore next */ function (element) {
						return element.offsetWidth && element.offsetHeight ? true : null;
					}, [ element ], timeout - (startTime - Date.now())).call(this);
				}).then(function (isVisible) {
					return command.setFindTimeout(originalTimeout).then(function () {
						if (!isVisible) {
							throw new Error('Element didn\'t become visible');
						}
					});
				}, function (error) {
					return command.setFindTimeout(originalTimeout).then(function () {
						throw error;
					});
				});
		});
	},
	isVisible: function () {
		warn(
			'Command#isVisible',
			'Command#isDisplayed',
			'This command is implemented using Command#isDisplayed, which may not match the prior behaviour.'
		);

		if (arguments.length === 2) {
			var using = arguments[0];
			var value = arguments[1];
			return this.find(using, value).isDisplayed().catch(function () {
				return false;
			});
		}
		else if (arguments.length === 1) {
			var element = arguments[0];
			if (element && element.elementId) {
				return new this.constructor(this, function () {
					return element.isDisplayed();
				});
			}
		}

		return new this.constructor(this, function () {
			if (this.context.isSingle) {
				return this.context[0].isDisplayed();
			}
			else {
				return Promise.all(this.context.map(function (element) {
					return element.isDisplayed();
				}));
			}
		});
	},
	otherwise: deprecate('otherwise', 'catch'),
	always: deprecate('always', 'finally'),
	wait: deprecate('wait', 'sleep')
};

strategies.suffixes.forEach(function (suffix, index) {
	function addStrategy(method, toMethod, suffix, wdSuffix, using) {
		methods[method + 'OrNull'] = function (value) {
			return this.elementOrNull(using, value);
		};

		methods[method + 'IfExists'] = function (value) {
			return this.elementIfExists(using, value);
		};

		methods['hasElementBy' + wdSuffix] = function (value) {
			return this.hasElement(using, value);
		};

		methods['waitForElementBy' + wdSuffix] = function (value, timeout) {
			return this.waitForElement(using, value, timeout);
		};

		methods['waitForVisibleBy' + wdSuffix] = function (value, timeout) {
			return this.waitForVisible(using, value, timeout);
		};
	}

	var wdSuffix = suffix === 'Xpath' ? 'XPath' : suffix;
	var method = 'elementBy' + wdSuffix;
	var toMethod = 'findBy' + suffix;
	var using = strategies[index];
	addStrategy(method, toMethod, suffix, wdSuffix, using);
	if (suffix === 'CssSelector') {
		addStrategy('elementByCss', toMethod, suffix, 'Css', using);
	}
});

module.exports = {
	/**
	 * Applies the methods from compat to a {@link module:leadfoot/Command} prototype or instance.
	 *
	 * @param {module:leadfoot/Command} prototype A {@link module:leadfoot/Command} prototype or instance.
	 */
	applyTo: function (prototype) {
		for (var key in methods) {
			Object.defineProperty(prototype, key, Object.getOwnPropertyDescriptor(methods, key));
		}
	}
};