/**
* @module leadfoot/Command
*/
var Element = require('./Element');
var Promise = require('dojo/Promise');
var strategies = require('./lib/strategies');
var Session = require('./Session');
var util = require('./lib/util');
/**
* Creates a function that, when called, creates a new Command that retrieves elements from the parent context and
* uses them as the context for the newly created Command.
*
* @private
* @param {string} method
* @returns {Function}
*/
function createElementMethod(method) {
return function () {
var args = arguments;
return new this.constructor(this, function (setContext) {
var parentContext = this._context;
var promise;
if (parentContext.length && parentContext.isSingle) {
promise = parentContext[0][method].apply(parentContext[0], args);
}
else if (parentContext.length) {
promise = Promise.all(parentContext.map(function (element) {
return element[method].apply(element, args);
})).then(function (elements) {
// findAll against an array context will result in arrays of arrays; flatten into a single
// array of elements. It would also be possible to resort in document order but other parallel
// operations could not be sorted so we just don't do it anywhere and say not to rely in
// a particular return order for results
return Array.prototype.concat.apply([], elements);
});
}
else {
promise = this.session[method].apply(this.session, args);
}
return promise.then(function (newContext) {
setContext(newContext);
return newContext;
});
});
};
}
var TOP_CONTEXT = [];
TOP_CONTEXT.isSingle = true;
TOP_CONTEXT.depth = 0;
/**
* The Command class is a chainable, subclassable object type that can be used to execute commands serially against a
* remote WebDriver environment. The standard Command class includes methods from the {@link module:leadfoot/Session}
* and {@link module:leadfoot/Element} classes, so you can perform all standard session and element operations that
* come with Leadfoot without being forced to author long promise chains.
*
* ***Important*: Due to a documentation tool limitation, the documentation on this page currently lists return values
* of all methods as being of type `Promise`. All command methods actually return a new object of type `Command`. This
* issue will be addressed in future versions of the documentation.**
*
* In order to use the Command class, you first need to pass it a {@link module:leadfoot/Session} instance for it to
* use:
*
* ```js
* var command = new Command(session);
* ```
*
* Once you have created the Command, you can then start chaining methods, and they will execute in order one after
* another:
*
* ```js
* command.get('http://example.com')
* .findByTagName('h1')
* .getVisibleText()
* .then(function (text) {
* assert.strictEqual(text, 'Example Domain');
* });
* ```
*
* Because these operations are asynchronous, you need to use a `then` callback in order to retrieve the value from the
* last method. Command objects are Thenables, which means that they can be used with any Promises/A+ or ES6-confirmant
* Promises implementation, though there are some specific differences in the arguments and context that are provided
* to callbacks; see {@link module:leadfoot/Command#then} for more details.
*
* ---
*
* Each call on a Command generates a new Command object, which means that certain operations can be parallelised:
*
* ```js
* command = command.get('http://example.com');
* Promise.all([
* command.getPageTitle(),
* command.findByTagName('h1').getVisibleText()
* ]).then(function (results) {
* assert.strictEqual(results[0], results[1]);
* });
* ```
*
* In this example, the commands on line 3 and 4 both depend upon the `get` call completing successfully but are
* otherwise independent of each other and so execute here in parallel. This is different from commands in Intern 1
* which were always chained onto the last called method within a given test.
*
* ---
*
* Command objects actually encapsulate two different types of interaction: *session* interactions, which operate
* against the entire browser session, and *element* interactions, which operate against specific elements taken from
* the currently loaded page. Things like navigating the browser, moving the mouse cursor, and executing scripts are
* session interactions; things like getting text displayed on the page, typing into form fields, and getting element
* attributes are element interactions.
*
* Session interactions can be performed at any time, from any Command. On the other hand, to perform element
* interactions, you first need to retrieve one or more elements to interact with. This can be done using any of the
* `find` or `findAll` methods, by the `getActiveElement` method, or by returning elements from `execute` or
* `executeAsync` calls. The retrieved elements are stored internally as the *element context* of all chained
* Commands. When an element method is called on a chained Command with a single element context, the result will be
* returned as-is:
*
* ```js
* command = command.get('http://example.com')
* // finds one element -> single element context
* .findByTagName('h1')
* .getVisibleText()
* .then(function (text) {
* // `text` is the text from the element context
* assert.strictEqual(text, 'Example Domain');
* });
* ```
*
* When an element method is called on a chained Command with a multiple element context, the result will be returned
* as an array:
*
* ```js
* command = command.get('http://example.com')
* // finds multiple elements -> multiple element context
* .findAllByTagName('p')
* .getVisibleText()
* .then(function (texts) {
* // `texts` is an array of text from each of the `p` elements
* assert.deepEqual(texts, [
* 'This domain is established to be used for […]',
* 'More information...'
* ]);
* });
* ```
*
* The `find` and `findAll` methods are special and change their behaviour based on the current element filtering state
* of a given command. If a command has been filtered by element, the `find` and `findAll` commands will only find
* elements *within* the currently filtered set of elements. Otherwise, they will find elements throughout the page.
*
* Some method names, like `click`, are identical for both Session and Element APIs; in this case, the element APIs
* are suffixed with the word `Element` in order to identify them uniquely.
*
* ---
*
* Commands can be subclassed in order to add additional functionality without making direct modifications to the
* default Command prototype that might break other parts of the system:
*
* ```js
* function CustomCommand() {
* Command.apply(this, arguments);
* }
* CustomCommand.prototype = Object.create(Command.prototype);
* CustomCommand.prototype.constructor = CustomCommand;
* CustomCommand.prototype.login = function (username, password) {
* return new this.constructor(this, function () {
* return this.parent
* .findById('username')
* .click()
* .type(username)
* .end()
* .findById('password')
* .click()
* .type(password)
* .end()
* .findById('login')
* .click()
* .end();
* });
* };
* ```
*
* Note that returning `this`, or a command chain starting from `this`, from a callback or command initialiser will
* deadlock the Command, as it waits for itself to settle before settling.
*
* @constructor module:leadfoot/Command
* @param {module:leadfoot/Command|module:leadfoot/Session} parent
* The parent command that this command is chained to, or a {@link module:leadfoot/Session} object if this is the
* first command in a command chain.
*
* @param {function(setContext:Function, value:any): (any|Promise)} initialiser
* A function that will be executed when all parent commands have completed execution. This function can create a
* new context for this command by calling the passed `setContext` function any time prior to resolving the Promise
* that it returns. If no context is explicitly provided, the context from the parent command will be used.
*
* @param {(function(setContext:Function, error:Error): (any|Promise))=} errback
* A function that will be executed if any parent commands failed to complete successfully. This function can create
* a new context for the current command by calling the passed `setContext` function any time prior to resolving the
* Promise that it returns. If no context is explicitly provided, the context from the parent command will be used.
*
* @borrows module:leadfoot/Session#getTimeout as module:leadfoot/Command#getTimeout
* @borrows module:leadfoot/Session#setTimeout as module:leadfoot/Command#setTimeout
* @borrows module:leadfoot/Session#getCurrentWindowHandle as module:leadfoot/Command#getCurrentWindowHandle
* @borrows module:leadfoot/Session#getAllWindowHandles as module:leadfoot/Command#getAllWindowHandles
* @borrows module:leadfoot/Session#getCurrentUrl as module:leadfoot/Command#getCurrentUrl
* @borrows module:leadfoot/Session#get as module:leadfoot/Command#get
* @borrows module:leadfoot/Session#goForward as module:leadfoot/Command#goForward
* @borrows module:leadfoot/Session#goBack as module:leadfoot/Command#goBack
* @borrows module:leadfoot/Session#refresh as module:leadfoot/Command#refresh
* @borrows module:leadfoot/Session#execute as module:leadfoot/Command#execute
* @borrows module:leadfoot/Session#executeAsync as module:leadfoot/Command#executeAsync
* @borrows module:leadfoot/Session#takeScreenshot as module:leadfoot/Command#takeScreenshot
* @borrows module:leadfoot/Session#getAvailableImeEngines as module:leadfoot/Command#getAvailableImeEngines
* @borrows module:leadfoot/Session#getActiveImeEngine as module:leadfoot/Command#getActiveImeEngine
* @borrows module:leadfoot/Session#isImeActivated as module:leadfoot/Command#isImeActivated
* @borrows module:leadfoot/Session#deactivateIme as module:leadfoot/Command#deactivateIme
* @borrows module:leadfoot/Session#activateIme as module:leadfoot/Command#activateIme
* @borrows module:leadfoot/Session#switchToFrame as module:leadfoot/Command#switchToFrame
* @borrows module:leadfoot/Session#switchToWindow as module:leadfoot/Command#switchToWindow
* @borrows module:leadfoot/Session#switchToParentFrame as module:leadfoot/Command#switchToParentFrame
* @borrows module:leadfoot/Session#closeCurrentWindow as module:leadfoot/Command#closeCurrentWindow
* @borrows module:leadfoot/Session#setWindowSize as module:leadfoot/Command#setWindowSize
* @borrows module:leadfoot/Session#getWindowSize as module:leadfoot/Command#getWindowSize
* @borrows module:leadfoot/Session#setWindowPosition as module:leadfoot/Command#setWindowPosition
* @borrows module:leadfoot/Session#getWindowPosition as module:leadfoot/Command#getWindowPosition
* @borrows module:leadfoot/Session#maximizeWindow as module:leadfoot/Command#maximizeWindow
* @borrows module:leadfoot/Session#getCookies as module:leadfoot/Command#getCookies
* @borrows module:leadfoot/Session#setCookie as module:leadfoot/Command#setCookie
* @borrows module:leadfoot/Session#clearCookies as module:leadfoot/Command#clearCookies
* @borrows module:leadfoot/Session#deleteCookie as module:leadfoot/Command#deleteCookie
* @borrows module:leadfoot/Session#getPageSource as module:leadfoot/Command#getPageSource
* @borrows module:leadfoot/Session#getPageTitle as module:leadfoot/Command#getPageTitle
* @borrows module:leadfoot/Session#find as module:leadfoot/Command#find
* @borrows module:leadfoot/Session#findAll as module:leadfoot/Command#findAll
* @borrows module:leadfoot/Session#getActiveElement as module:leadfoot/Command#getActiveElement
* @borrows module:leadfoot/Session#pressKeys as module:leadfoot/Command#pressKeys
* @borrows module:leadfoot/Session#getOrientation as module:leadfoot/Command#getOrientation
* @borrows module:leadfoot/Session#setOrientation as module:leadfoot/Command#setOrientation
* @borrows module:leadfoot/Session#getAlertText as module:leadfoot/Command#getAlertText
* @borrows module:leadfoot/Session#typeInPrompt as module:leadfoot/Command#typeInPrompt
* @borrows module:leadfoot/Session#acceptAlert as module:leadfoot/Command#acceptAlert
* @borrows module:leadfoot/Session#dismissAlert as module:leadfoot/Command#dismissAlert
* @borrows module:leadfoot/Session#moveMouseTo as module:leadfoot/Command#moveMouseTo
* @borrows module:leadfoot/Session#clickMouseButton as module:leadfoot/Command#clickMouseButton
* @borrows module:leadfoot/Session#pressMouseButton as module:leadfoot/Command#pressMouseButton
* @borrows module:leadfoot/Session#releaseMouseButton as module:leadfoot/Command#releaseMouseButton
* @borrows module:leadfoot/Session#doubleClick as module:leadfoot/Command#doubleClick
* @borrows module:leadfoot/Session#tap as module:leadfoot/Command#tap
* @borrows module:leadfoot/Session#pressFinger as module:leadfoot/Command#pressFinger
* @borrows module:leadfoot/Session#releaseFinger as module:leadfoot/Command#releaseFinger
* @borrows module:leadfoot/Session#moveFinger as module:leadfoot/Command#moveFinger
* @borrows module:leadfoot/Session#touchScroll as module:leadfoot/Command#touchScroll
* @borrows module:leadfoot/Session#doubleTap as module:leadfoot/Command#doubleTap
* @borrows module:leadfoot/Session#longTap as module:leadfoot/Command#longTap
* @borrows module:leadfoot/Session#flickFinger as module:leadfoot/Command#flickFinger
* @borrows module:leadfoot/Session#getGeolocation as module:leadfoot/Command#getGeolocation
* @borrows module:leadfoot/Session#setGeolocation as module:leadfoot/Command#setGeolocation
* @borrows module:leadfoot/Session#getLogsFor as module:leadfoot/Command#getLogsFor
* @borrows module:leadfoot/Session#getAvailableLogTypes as module:leadfoot/Command#getAvailableLogTypes
* @borrows module:leadfoot/Session#getApplicationCacheStatus as module:leadfoot/Command#getApplicationCacheStatus
* @borrows module:leadfoot/Session#quit as module:leadfoot/Command#quit
* @borrows module:leadfoot/Session#getLocalStorageKeys as module:leadfoot/Command#getLocalStorageKeys
* @borrows module:leadfoot/Session#setLocalStorageItem as module:leadfoot/Command#setLocalStorageItem
* @borrows module:leadfoot/Session#clearLocalStorage as module:leadfoot/Command#clearLocalStorage
* @borrows module:leadfoot/Session#getLocalStorageItem as module:leadfoot/Command#getLocalStorageItem
* @borrows module:leadfoot/Session#deleteLocalStorageItem as module:leadfoot/Command#deleteLocalStorageItem
* @borrows module:leadfoot/Session#getLocalStorageLength as module:leadfoot/Command#getLocalStorageLength
* @borrows module:leadfoot/Session#getSessionStorageKeys as module:leadfoot/Command#getSessionStorageKeys
* @borrows module:leadfoot/Session#setSessionStorageItem as module:leadfoot/Command#setSessionStorageItem
* @borrows module:leadfoot/Session#clearSessionStorage as module:leadfoot/Command#clearSessionStorage
* @borrows module:leadfoot/Session#getSessionStorageItem as module:leadfoot/Command#getSessionStorageItem
* @borrows module:leadfoot/Session#deleteSessionStorageItem as module:leadfoot/Command#deleteSessionStorageItem
* @borrows module:leadfoot/Session#getSessionStorageLength as module:leadfoot/Command#getSessionStorageLength
* @borrows module:leadfoot/Session#findByClassName as module:leadfoot/Command#findByClassName
* @borrows module:leadfoot/Session#findByCssSelector as module:leadfoot/Command#findByCssSelector
* @borrows module:leadfoot/Session#findById as module:leadfoot/Command#findById
* @borrows module:leadfoot/Session#findByName as module:leadfoot/Command#findByName
* @borrows module:leadfoot/Session#findByLinkText as module:leadfoot/Command#findByLinkText
* @borrows module:leadfoot/Session#findByPartialLinkText as module:leadfoot/Command#findByPartialLinkText
* @borrows module:leadfoot/Session#findByTagName as module:leadfoot/Command#findByTagName
* @borrows module:leadfoot/Session#findByXpath as module:leadfoot/Command#findByXpath
* @borrows module:leadfoot/Session#findAllByClassName as module:leadfoot/Command#findAllByClassName
* @borrows module:leadfoot/Session#findAllByCssSelector as module:leadfoot/Command#findAllByCssSelector
* @borrows module:leadfoot/Session#findAllByName as module:leadfoot/Command#findAllByName
* @borrows module:leadfoot/Session#findAllByLinkText as module:leadfoot/Command#findAllByLinkText
* @borrows module:leadfoot/Session#findAllByPartialLinkText as module:leadfoot/Command#findAllByPartialLinkText
* @borrows module:leadfoot/Session#findAllByTagName as module:leadfoot/Command#findAllByTagName
* @borrows module:leadfoot/Session#findAllByXpath as module:leadfoot/Command#findAllByXpath
* @borrows module:leadfoot/Session#findDisplayed as module:leadfoot/Command#findDisplayed
* @borrows module:leadfoot/Session#findDisplayedByClassName as module:leadfoot/Command#findDisplayedByClassName
* @borrows module:leadfoot/Session#findDisplayedByCssSelector as module:leadfoot/Command#findDisplayedByCssSelector
* @borrows module:leadfoot/Session#findDisplayedById as module:leadfoot/Command#findDisplayedById
* @borrows module:leadfoot/Session#findDisplayedByName as module:leadfoot/Command#findDisplayedByName
* @borrows module:leadfoot/Session#findDisplayedByLinkText as module:leadfoot/Command#findDisplayedByLinkText
* @borrows module:leadfoot/Session#findDisplayedByPartialLinkText as module:leadfoot/Command#findDisplayedByPartialLinkText
* @borrows module:leadfoot/Session#findDisplayedByTagName as module:leadfoot/Command#findDisplayedByTagName
* @borrows module:leadfoot/Session#findDisplayedByXpath as module:leadfoot/Command#findDisplayedByXpath
* @borrows module:leadfoot/Session#waitForDeletedByClassName as module:leadfoot/Command#waitForDeletedByClassName
* @borrows module:leadfoot/Session#waitForDeletedByCssSelector as module:leadfoot/Command#waitForDeletedByCssSelector
* @borrows module:leadfoot/Session#waitForDeletedById as module:leadfoot/Command#waitForDeletedById
* @borrows module:leadfoot/Session#waitForDeletedByName as module:leadfoot/Command#waitForDeletedByName
* @borrows module:leadfoot/Session#waitForDeletedByLinkText as module:leadfoot/Command#waitForDeletedByLinkText
* @borrows module:leadfoot/Session#waitForDeletedByPartialLinkText as module:leadfoot/Command#waitForDeletedByPartialLinkText
* @borrows module:leadfoot/Session#waitForDeletedByTagName as module:leadfoot/Command#waitForDeletedByTagName
* @borrows module:leadfoot/Session#waitForDeletedByXpath as module:leadfoot/Command#waitForDeletedByXpath
* @borrows module:leadfoot/Session#getExecuteAsyncTimeout as module:leadfoot/Command#getExecuteAsyncTimeout
* @borrows module:leadfoot/Session#setExecuteAsyncTimeout as module:leadfoot/Command#setExecuteAsyncTimeout
* @borrows module:leadfoot/Session#getFindTimeout as module:leadfoot/Command#getFindTimeout
* @borrows module:leadfoot/Session#setFindTimeout as module:leadfoot/Command#setFindTimeout
* @borrows module:leadfoot/Session#getPageLoadTimeout as module:leadfoot/Command#getPageLoadTimeout
* @borrows module:leadfoot/Session#setPageLoadTimeout as module:leadfoot/Command#setPageLoadTimeout
* @borrows module:leadfoot/Element#click as module:leadfoot/Command#click
* @borrows module:leadfoot/Element#submit as module:leadfoot/Command#submit
* @borrows module:leadfoot/Element#getVisibleText as module:leadfoot/Command#getVisibleText
* @borrows module:leadfoot/Element#type as module:leadfoot/Command#type
* @borrows module:leadfoot/Element#getTagName as module:leadfoot/Command#getTagName
* @borrows module:leadfoot/Element#clearValue as module:leadfoot/Command#clearValue
* @borrows module:leadfoot/Element#isSelected as module:leadfoot/Command#isSelected
* @borrows module:leadfoot/Element#isEnabled as module:leadfoot/Command#isEnabled
* @borrows module:leadfoot/Element#getSpecAttribute as module:leadfoot/Command#getSpecAttribute
* @borrows module:leadfoot/Element#getAttribute as module:leadfoot/Command#getAttribute
* @borrows module:leadfoot/Element#getProperty as module:leadfoot/Command#getProperty
* @borrows module:leadfoot/Element#equals as module:leadfoot/Command#equals
* @borrows module:leadfoot/Element#isDisplayed as module:leadfoot/Command#isDisplayed
* @borrows module:leadfoot/Element#getPosition as module:leadfoot/Command#getPosition
* @borrows module:leadfoot/Element#getSize as module:leadfoot/Command#getSize
* @borrows module:leadfoot/Element#getComputedStyle as module:leadfoot/Command#getComputedStyle
*/
function Command(parent, initialiser, errback) {
var self = this;
var session;
function setContext(context) {
if (!Array.isArray(context)) {
context = [ context ];
context.isSingle = true;
}
// If the context being set has depth, then it is coming from `Command#end`,
// or someone smart knows what they are doing; do not change the depth
if (!('depth' in context)) {
context.depth = parent ? parent.context.depth + 1 : 0;
}
self._context = context;
}
function fixStack(error) {
error.stack = error.stack + util.trimStack(trace.stack);
throw error;
}
if (parent && parent.session) {
this._parent = parent;
session = this._session = parent.session;
}
else if (parent && parent.sessionId) {
session = this._session = parent;
parent = null;
}
else {
throw new Error('A parent Command or Session must be provided to a new Command');
}
// Add any custom functions from the session to this command object so they can be accessed automatically
// using the fluid interfaces
// TODO: Test
for (var key in session) {
if (session[key] !== Session.prototype[key]) {
Command.addSessionMethod(this, key, session[key]);
}
}
var trace = {};
Error.captureStackTrace(trace, Command);
this._promise = (parent ? parent.promise : Promise.resolve(undefined)).then(function (returnValue) {
self._context = parent ? parent.context : TOP_CONTEXT;
return returnValue;
}, function (error) {
self._context = parent ? parent.context : TOP_CONTEXT;
throw error;
}).then(
initialiser && function (returnValue) {
return Promise.resolve(returnValue)
.then(initialiser.bind(self, setContext))
.catch(fixStack);
},
errback && function (error) {
return Promise.reject(error)
.catch(errback.bind(self, setContext))
.catch(fixStack);
}
);
}
/**
* @lends module:leadfoot/Command#
*/
Command.prototype = {
constructor: Command,
/**
* The parent Command of the Command, if one exists.
*
* @member {module:leadfoot/Command=} parent
* @memberOf module:leadfoot/Command#
* @readonly
*/
get parent() {
return this._parent;
},
/**
* The parent Session of the Command.
*
* @member {module:leadfoot/Session} session
* @memberOf module:leadfoot/Command#
* @readonly
*/
get session() {
return this._session;
},
/**
* The filtered elements that will be used if an element-specific method is invoked. Note that this property is not
* valid until the parent Command has been settled. The context array also has two additional properties:
*
* - isSingle (boolean): If true, the context will always contain a single element. This is used to differentiate
* between methods that should still return scalar values (`find`) and methods that should return arrays of
* values even if there is only one element in the context (`findAll`).
* - depth (number): The depth of the context within the command chain. This is used to prevent traversal into
* higher filtering levels by {@link module:leadfoot/Command#end}.
*
* @member {module:leadfoot/Element[]} context
* @memberOf module:leadfoot/Command#
* @readonly
*/
get context() {
return this._context;
},
/**
* The underlying Promise for the Command.
*
* @member {Promise.<any>} promise
* @memberOf module:leadfoot/Command#
* @readonly
*/
get promise() {
return this._promise;
},
/**
* Pauses execution of the next command in the chain for `ms` milliseconds.
*
* @param {number} ms Time to delay, in milliseconds.
* @returns {module:leadfoot/Command.<void>}
*/
sleep: function (ms) {
return new this.constructor(this, function () {
return util.sleep(ms);
});
},
/**
* Ends the most recent filtering operation in the current Command chain and returns the set of matched elements
* to the previous state. This is equivalent to the `jQuery#end` method.
*
* @example
* command
* .findById('parent') // sets filter to #parent
* .findByClassName('child') // sets filter to all .child inside #parent
* .getVisibleText()
* .then(function (visibleTexts) {
* // all the visible texts from the children
* })
* .end() // resets filter to #parent
* .end(); // resets filter to nothing (the whole document)
*
* @param {number=} numCommandsToPop The number of element contexts to pop. Defaults to 1.
* @returns {module:leadfoot/Command.<void>}
*/
end: function (numCommandsToPop) {
numCommandsToPop = numCommandsToPop || 1;
return new this.constructor(this, function (setContext) {
var command = this;
var depth = this.context.depth;
while (depth && numCommandsToPop && (command = command.parent)) {
if (command.context.depth < depth) {
--numCommandsToPop;
depth = command.context.depth;
}
}
setContext(command.context);
});
},
/**
* Adds a callback to be invoked once the previously chained operation has completed.
*
* This method is compatible with the `Promise#then` API, with two important differences:
*
* 1. The context (`this`) of the callback is set to the Command object, rather than being `undefined`. This allows
* promise helpers to be created that can retrieve the appropriate session and element contexts for execution.
* 2. A second non-standard `setContext` argument is passed to the callback. This `setContext` function can be
* called at any time before the callback fulfills its return value and expects either a single
* {@link module:leadfoot/Element} or an array of Elements to be provided as its only argument. The provided
* element(s) will be used as the context for subsequent element method invocations (`click`, etc.). If
* the `setContext` method is not called, the element context from the parent will be passed through unmodified.
*
* @param {Function=} callback
* @param {Function=} errback
* @returns {module:leadfoot/Command.<any>}
*/
then: function (callback, errback) {
function runCallback(command, callback, value, setContext) {
var returnValue = callback.call(command, value, setContext);
// If someone returns `this` (or a chain starting from `this`) from the callback, it will cause a deadlock
// where the child command is waiting for the child command to resolve
if (returnValue instanceof command.constructor) {
var maybeCommand = returnValue;
do {
if (maybeCommand === command) {
throw new Error('Deadlock: do not use `return this` from a Command callback');
}
} while ((maybeCommand = maybeCommand.parent));
}
return returnValue;
}
return new this.constructor(this, callback && function (setContext, value) {
return runCallback(this, callback, value, setContext);
}, errback && function (setContext, value) {
return runCallback(this, errback, value, setContext);
});
},
/**
* Adds a callback to be invoked when any of the previously chained operations have failed.
*
* @param {Function} errback
* @returns {module:leadfoot/Command.<any>}
*/
catch: function (errback) {
return this.then(null, errback);
},
/**
* Adds a callback to be invoked once the previously chained operations have resolved.
*
* @param {Function} callback
* @returns {module:leadfoot/Command.<any>}
*/
finally: function (callback) {
return this.then(callback, callback);
},
/**
* Cancels all outstanding chained operations of the Command. Calling this method will cause this command and all
* subsequent chained commands to fail with a CancelError.
*
* @returns {module:leadfoot/Command.<void>}
*/
cancel: function () {
this._promise.cancel.apply(this._promise, arguments);
return this;
},
find: createElementMethod('find'),
findAll: createElementMethod('findAll'),
findDisplayed: createElementMethod('findDisplayed')
};
/**
* Augments `target` with a conversion of the `originalFn` method that enables its use with a Command object.
* This can be used to easily add new methods from any custom object that implements the Session API to any target
* object that implements the Command API.
*
* Functions that are copied may have the following extra properties in order to change the way that Command works
* with these functions:
*
* - `createsContext` (boolean): If this property is specified, the return value from the function will be used as
* the new context for the returned Command.
* - `usesElement` (boolean): If this property is specified, element(s) from the current context will be used as
* the first argument to the function, if the explicitly specified first argument is not already an element.
*
* @memberOf module:leadfoot/Command
* @param {module:leadfoot/Command} target
* @param {string} key
* @param {Function} originalFn
*/
Command.addSessionMethod = function (target, key, originalFn) {
// Checking for private/non-functions here deduplicates this logic; otherwise it would need to exist in both
// the Command constructor (for copying functions from sessions) as well as the Command factory below
if (key.charAt(0) !== '_' && !target[key] && typeof originalFn === 'function') {
target[key] = function () {
var args = arguments;
return new this.constructor(this, function (setContext) {
var parentContext = this._context;
var session = this._session;
// The function may have come from a session object prototype but have been overridden on the actual
// session instance; in such a case, the overridden function should be used instead of the one from
// the original source object. The original source object may still be used, however, if the
// function is being added like a mixin and does not exist on the actual session object for this
// session
var fn = session[key] || originalFn;
if (fn.usesElement && parentContext.length && (!args[0] || !args[0].elementId)) {
var promise;
// Defer converting arguments into an array until it is necessary to avoid overhead
args = Array.prototype.slice.call(args, 0);
if (parentContext.isSingle) {
promise = fn.apply(session, [ parentContext[0] ].concat(args));
}
else {
promise = Promise.all(parentContext.map(function (element) {
return fn.apply(session, [ element ].concat(args));
}));
}
}
else {
promise = fn.apply(session, args);
}
if (fn.createsContext) {
promise = promise.then(function (newContext) {
setContext(newContext);
return newContext;
});
}
return promise;
});
};
}
};
/**
* Augments `target` with a method that will call `key` on all context elements stored within `target`.
* This can be used to easily add new methods from any custom object that implements the Element API to any target
* object that implements the Command API.
*
* Functions that are copied may have the following extra properties in order to change the way that Command works
* with these functions:
*
* - `createsContext` (boolean): If this property is specified, the return value from the function will be used as
* the new context for the returned Command.
*
* @memberOf module:leadfoot/Command
* @param {module:leadfoot/Command} target
* @param {string} key
*/
Command.addElementMethod = function (target, key) {
if (key.charAt(0) !== '_') {
// some methods, like `click`, exist on both Session and Element; deduplicate these methods by appending the
// element ones with 'Element'
var targetKey = key + (target[key] ? 'Element' : '');
target[targetKey] = function () {
var args = arguments;
return new this.constructor(this, function (setContext) {
var parentContext = this._context;
var promise;
var fn = parentContext[0] && parentContext[0][key];
if (parentContext.isSingle) {
promise = fn.apply(parentContext[0], args);
}
else {
promise = Promise.all(parentContext.map(function (element) {
return element[key].apply(element, args);
}));
}
if (fn && fn.createsContext) {
promise = promise.then(function (newContext) {
setContext(newContext);
return newContext;
});
}
return promise;
});
};
}
};
// Element retrieval strategies must be applied directly to Command because it has its own custom
// find/findAll methods that operate based on the Command’s context, so can’t simply be delegated to the
// underlying session
strategies.applyTo(Command.prototype);
(function () {
var key;
for (key in Session.prototype) {
Command.addSessionMethod(Command.prototype, key, Session.prototype[key]);
}
for (key in Element.prototype) {
Command.addElementMethod(Command.prototype, key);
}
})();
try {
var chaiAsPromised = require('chai-as-promised');
}
catch (error) {}
// TODO: Add unit test
if (chaiAsPromised) {
chaiAsPromised.transferPromiseness = function (assertion, promise) {
assertion.then = promise.then.bind(promise);
for (var method in promise) {
if (typeof promise[method] === 'function') {
assertion[method] = promise[method].bind(promise);
}
}
};
}
module.exports = Command;