UNPKG

leadfoot

Version:

Leadfoot. A JavaScript client library that brings cross-platform consistency to the Selenium WebDriver API.

735 lines (684 loc) 33.9 kB
/** * @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;