// NOTE we must ST.define Element instead of ST.future.define because it is // the default base class so must exist? We could pass in body.extend = // something else like ST._base??? /** * @class ST.future.Element * A future Element is a class that can be used to interact with an element that will * exist at some point. Typically that element does not exist when the future is created. * * The methods of a future (including its constructor) defer actions in an event queue * (based on {@link ST.event.Player}). A simple example would be: * * ST.element('@some-div'). * click(10, 10); * * The API of futures is based on chained method calls, sometimes called a "fluent" * API. In the above example, the `{@link ST#element}` method accepts the locator and * returns a future. The `{@link #click}` method of the future queues a click * event at offset (10, 10). * * ## Actions * Many methods of futures perform actions (such as `click`) on their target. These * methods schedule their actions so that they follow previously invoked future methods * to ensure that these actions flow in the same order as the test code that requested * them. * * Actions methods use verbs for names. * * ## States * The other main group of methods on futures are state methods. These methods do not * affect their target but rather schedule a delay in the test sequence that begins at * the proper time (following all previously schedules operations) and finishing when * the target arrives in the desired state. * * For example: * * ST.element('@some-div'). * click(10, 10). * textLike(/hello/i); * * The above test will locate our div, click on it and then wait for its `textContent` * to match the specified regular expression. The test will complete successfully if * the text matches within the default timeout (5 seconds) and will fail otherwise. * * State methods use nouns or descriptions for names. * * ## Expectations * Because operations on futures all complete asynchronously, it is important not to * mix these operations with immediate method calls. Instead, we schedule expectations * using the future's `{@link #expect}` method. This method returns a jasmine-like * Expectation object which has various matcher functions available. * * ST.element('@some-div'). * click(10, 10). * expect('id').toBe('some-div'); * * ## Inspections * Another way to add expectations is to use the `{@link #and}` method. This is most * useful when testing an in-browser Scenario because the and function has access to * the actual Ext Components and dom elements for inspection. * * ST.element('@some-div'). * click(10, 10). * and( * // Invoked after the click has played. The ST.Element wrapper for * // the target div is given as a parameter. * function (divEl) { * expect(divEl.hasCls('foo')).toBe(true); * } * ); * * With WebDriver Scenarios the and functions only have access to the test code and * the future instances. With the use of the `{@link #get}` method it is possible * to retrieve properties from the actual Component/Element for later inspection * in and functions. Properties retrieved with the get method are set on the future's * data property. * * ST.element('@some-div'). * click(10, 10). * // here 'className' will be a property on the element's dom. * get('className'). * and( * function (futureEl) { * expect(futureEl.data.className).toContain('foo'); * } * ); * * * The functions passed to `and()` are called "inspections" but there are no particular * restrictions on what these methods actually do when they are called. * * ## Waiting * There are two basic ways to control the timing of the test sequence. The first is the * `and()` method's optional second argument: * * ST.element('@some-div'). * click(10, 10). * and(function (divEl, done) { * something().then(done); * }); * * When an inspection function is declared to have a second argument, it is called with * a completion function typically named "done". If declared as an argument, this function * must be called or the test will fail. The inspection function, however, can decide when * the function should be called. Once `done` is called, the test sequence can continue. * * When there is no mechanism that can reasonably be used to determine when a condition * is satisfied, there is the `{@link ST#wait}` method. * * ST.element('@some-div'). * click(10, 10). * wait(function (divEl) { * return divEl.hasCls('foo'); * }); * * In this case, the function passed to `wait()` is called periodically and when it * eventually returns `true` the test can proceed. Obviously, the `and()` method and its * `done` function are preferrable because they won't need to poll for completion. Which * approach is more readily implemented in a given situation will typically determine the * best choice, and not this slight performance consideration. * * ## Components * * When interacting with Ext JS components, see `{@link ST#component}` or one of the * more specific methods such as `{@link ST#panel}`, `{@link ST#grid}`, etc.. * * ### Note * * This class is not created directly by user code. Instead, it is created automatically * by various helper methods, like {@link ST#element} and {@link ST#wait}. */ST.future.Element = ST.define({ valueProperty: 'el', $className: 'ST.future.Element', $futureType: 'element', $futureTypeChain: ['element'], $futureTypeMap: { element: true }, statics: { /** * if config is a function, the playable will extend ST.playable.State and the * provided function will be the "is" function. * @private */ addPlayable: function (name, config) { var me = this, playableName = ST.capitalize(name), cls, addEvent; // save original config for inheritance me.prototype.$playables = me.prototype.$playables || {}; me.prototype.$playables[name] = config; // if a function, then that fn is an "is" if (typeof config === 'function') { config = { is: config } } if (config.is) { config.extend = config.extend || ST.playable.State; } if(!config.extend) { config.extend = ST.playable.Playable; }; // inherit addEvent if needed addEvent = config.addEvent || config.extend.prototype.addEvent; cls = ST.define(config); if (!me.prototype.hasOwnProperty('playables')) { me.prototype.playables = {}; } me.prototype.playables[playableName] = cls; // glom on the playable class // element(), focus(), type() all return a future me.prototype[name] = function () { ST.logger.debug('playable ' + name + ' called, arguments=' + arguments.toString() + ', params=' + config.params); var me = this; if (addEvent) { me.params = config.params; return addEvent.apply(me, arguments); } else { var rec = me._buildRec(name, arguments, config.params || 'timeout'); me.play(rec); return me; } }; }, addPlayables: function (playables) { for (var name in playables) { this.addPlayable(name, playables[name]); } }, createGetter: function (key) { return 'get' + (key.charAt(0).toUpperCase() + key.slice(1)); }, isFutureType: function (cls, type, deep) { cls = typeof cls === 'string' ? ST.clsFromString(cls).prototype : cls; // if deep, see if this desired type exists in the futureTypeMap if (deep !== false) { return type in cls.$futureTypeMap; } // otherwise, search for strict futureType correlation on the class else { return cls.$futureType === type; } } }, constructor: function (config) { var me = this, config = config || {}; if (!config.context) { config.context = ST.defaultContext; } ST.apply(me,config); me.isFuture = true; }, /** * Schedules arbitrary actions for later execution. Often these actions are added * to the queue following {@link #click} or other interactions in order to test an * expectation. * * For example: * * ST.element('@some-div'). * click(10, 10). * and(function (el) { * // Runs after the click event. We receive the ST.Element * // wrapper for the "some-div" element. * expect(el.hasCls('foo')).toBe(true); * }); * * The future's value is passed as the first argument. For an Element future the arg * will be an {@link ST.Element}, for components it will be various things, typically * the component instance itself. If the scenario is a WebDriver scenario the arg will * be the current future such as ST.future.Element above. * * The function's scope is set to the playable which includes a reference to the future * so the code above could be re-written as: * * ST.element('@some-div'). * click(10, 10). * and(function () { * expect(this.future.el.hasCls('foo')).toBe(true); * }); * * Functions that need to perform asynchronous actions can declare a 2nd argument * (typically called "done"). * * ST.element('@some-div'). * click(10, 10). * and( * function (el, done) { * expect(el.hasCls('foo')).toBe(true); * * Ext.Ajax.request({ * ... * callback: function () { * done(); * } * }); * } * ); * * Multiple actions can be listed in a single call. Asynchronous actions can override * the {@link ST.options#timeout timeout} by specifying a number as the * previous argument. * * For example: * * ST.element('@some-div'). * click(10, 10). * and( * 1000, // timeout for following async steps in this and() * * function (el, done) { * expect(el.hasCls('foo')).toBe(true); * * Ext.Ajax.request({ * ... * callback: function () { * done(); * } * }); * }, * function (el) { * expect(el.hasCls('foo')).toBe(false); * } * ); * * @param {Number/Function...} fnOrTimeout One or more functions to invoke or timeout * values. For functions, the 1st argument will be the future value such as ST.Element * or an Ext.Component or the future itself in the case of WebDriver scenarios. * The scope of functions will be the playable event itself. To access * the future use this.future. Functions that declare a 2nd argument must call the * provided function to indicate that they are complete. Timeout values affect subsequent * asynchronous functions and override the {@link ST.options#timeout timeout}. These * timeouts only apply to functions passed in the current call. * * @return {ST.future.Element} this * @chainable */ and: function () { var me = this, events = [], timeout; // undefined so we get "player.timeout || ST.options.timeout" ST.each(arguments, function (fn) { var wrapFn; if (typeof fn === 'number') { timeout = fn; } else { if (fn.length > 1) { wrapFn = function (done) { return fn.call(this, me._value(), done); }; } else { wrapFn = function () { return fn.call(this, me._value()); }; } events.push(me._buildRec('and', { remoteable: false, fn: wrapFn, timeout: timeout })); } }); me.play(events); return me; }, /** * @method wait * @chainable * Schedules a wait a specified amount of time (in milliseconds) or until a provided * function returns a truthy value. Note that there is no practical use for using a * function in a WebDriver scenario test because these functions are executed in the * test context and not in the target browser where the application under test is. * * For example: * * ST.element('@some-div'). * click(10, 10). * * wait(100). // wait 100ms * * and(function (el) { * // Runs after the click event. We receive the ST.Element * // wrapper for the "some-div" element. * * expect(el.hasCls('foo')).toBe(true); * }); * * Sometimes the condition on which a wait is based cannot be handles via callbacks * or events and must be polled. That is, one must check and re-check at some short * interval to determine if the condition is satisfied. * * For example: * * var t = 0; * * setTimeout(function () { * t = 1; * }, 1000); * * ST.element('@some-div'). * click(10, 10). * * wait(function (el) { * // this test method ignores the el (ST.Element) argument * // for demonstration purposes. * return t; * }). * * and(function (el) { * // Runs after the click event and when t is truthy. We receive the * // ST.Element wrapper for the "some-div" element. * * expect(el.hasCls('foo')).toBe(true); * }); * * These can be combined as needed. * * ST.element('@some-div'). * click(10, 10). * * wait(200, // wait 200ms * * function (el) { * return t; // poll this one until it is truthy * }, * * 300, // wait 300ms * * 'Something interest', // message for the next fn's timeout reason * * function (el) { * return el.somethingInteresting(); * } * ). * * and(function (el) { * expect(el.hasCls('foo')).toBe(true); * }); * * @param {Number/String/Function...} delayOrPollFn One or more millisecond delays, * functions to poll for truthy return value or timeout messages for said functions. * @return {ST.future.Element} this */ wait: function () { var me = this, events = [], message; ST.each(arguments, function (delay) { var t = typeof delay, m = message; if (t === 'number') { events.push(me._buildRec('wait',{ remoteable: false, delay: delay })); } else if (t === 'string') { message = delay; } else if (t === 'function') { events.push(me._buildRec('wait',{ waitingFor: message, waitingState: 'truthy', remoteable: false, ready: function () { if (delay.call(me, me._value(), me, this)) { return this.setWaiting(false); // not "me" } return this.setWaiting(m || delay.toString(), m ? 'ready' : 'truthy'); } })); message = null; } else { throw new Error('wait() accepts millisecond delays or functions'); } }); me.play(events); return this; }, /** * @method click * @chainable * Schedules a click action at the specified relative coordinates. * * ST.element('@some-div'). * click(10, 10); * * Or for a Component: * * ST.component('#some-cmp'). * click(10, 10); * * To perform a right-click action, provide a button code of 2: * * ST.component('#some-cmp'). * click(10, 10, 2); * * This future will wait for the element to be visible before performing the click * action. * * Note that x, y and button args are not honored for WebDriver scenarios. * * @param {Number} x The number of pixels from the left edge of the element. * @param {Number} y The number of pixels from the top edge of the element. * @param {Number} [button=0] The mouse button code for the click. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ click: function () { return this.tap.apply(this,arguments); }, /** * @method doubleClick * @chainable * @since 2.2.1 * Schedules a double-click action at the specified relative coordinates. * * ST.element('@some-div'). * doubleClick(10, 10); * * Or for a Component: * * ST.component('#some-cmp'). * doubleClick(10, 10); * * This future will wait for the element to be visible before performing the double-click * action. * * @param {Number} x The number of pixels from the left edge of the element. * @param {Number} y The number of pixels from the top edge of the element. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ doubleClick: function () { return this.dblclick.apply(this,arguments); }, /** * @method rightClick * @chainable * @since 2.2.1 * Schedules a right-click action at the specified relative coordinates. * * ST.element('@some-div'). * rightClick(10, 10); * * Or for a Component: * * ST.component('#some-cmp'). * rightClick(10, 10); * * This future will wait for the element to be visible before performing the right-click * action. * * @param {Number} x The number of pixels from the left edge of the element. * @param {Number} y The number of pixels from the top edge of the element. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ rightClick: function () { return this.rightclick.apply(this,arguments); }, /** * @method down * Returns a descendant `{@link ST.future.Element future element}` that corresponds * to the specified selector. * * ST.element('@someElement'). * down('span'). * and(function (element) { * // span is now available * }); * * If the specified selector for the descendant element cannot be resolved, the request will timeout. * @param {String} selector The DOM Query selector to use to search for the descendant * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} * @chainable */ down: function (selector, timeout) { return this._createRelatedFuture('ST.future.Element', 'down', selector, timeout); }, /** * @method up * Returns an ancestor `{@link ST.future.Element future element}` that corresponds * to the specified selector. * * ST.element('@someElement'). * up('div'). * and(function (element) { * // div is now available * }); * * If the specified selector for the ancestor element cannot be resolved, the request will timeout. * @param {String} selector The DOM Query selector to use to search for the ancestor * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} * @chainable */ up: function (selector, timeout) { return this._createRelatedFuture('ST.future.Element', 'up', selector, timeout); }, /** * @method child * Returns a direct child `{@link ST.future.Element future element}` that corresponds * to the specified selector. * * ST.element('@someElement'). * child('p'). * and(function (element) { * // p is now available * }); * * If the specified selector for the child element cannot be resolved, the request will timeout. * @param {String} selector The DOM Query selector to use to search for the child component * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} * @chainable */ child: function (selector, timeout) { return this._createRelatedFuture('ST.future.Element', 'child', selector, timeout); }, isFutureType: function (type, deep) { return ST.future.Element.isFutureType(this, type, deep); }, _splits: {}, // args, params and config are all optional... // _buildRec(type,args,params,rec) // _buildRec(type,args,params) // _buildRec(type,rec) _buildRec: function (type, args, params, rec) { if (!params && !rec) { rec = args; params = null; args = null; } var me = this, rec = rec || {}, parsedArgs = {}; rec.type = type; if (args && params) { parsedArgs = me._decodeArgs(args,params,parsedArgs); } // always put args even if empty... so this.args.x is easier in fn's. rec.args = rec.args || {}; // move the timeout parameter to the record if there was one if (typeof parsedArgs.timeout !== 'undefined') { rec.timeout = parsedArgs.timeout; delete parsedArgs.timeout; } ST.apply(rec.args, parsedArgs); rec.future = rec.future || me; rec.futureClsName = rec.future.$className; return rec; }, _decodeArgs: function (args, params, rec) { var me = this, params = params || 'timeout', splits = me._splits, array = splits[params], n = args.length, o = n, a = args[0], i, value; if (n === 1 && a && a.constructor === Object) { ST.apply(rec, a); } else { rec.restParams = []; if (!array && params) { splits[params] = array = params.split(','); } n = Math.min(n, array.length); for (i = 0; i < n; ++i) { value = ST.encodeRegex(args[i]); rec[array[i]] = value; } // if the original number of args is greater than the number of expected params, // bundle the extra params into the restParams so they can be used (if needed) if (o > n) { for (i; i < o; i++) { rec.restParams.push(args[i]); } } } return rec; }, /** * @private */ _createRelatedFuture: function (maker, direction, locator, timeout) { var me = this, // locatorChain is setup automatically during construction, // so we'll just make a copy and add to it for the related playable/future locatorChain = ST.Array.slice(me.locatorChain || []), future, cls; cls = ST.clsFromString(maker); future = new cls(); return future.findInHierarchy({ target: locator, // Pass the locator chain as the root; ST.Locator can use this to walk the heirarchy and build out the correct // element chain before looking up the current element root: locatorChain, direction: direction, timeout: timeout, locatorChain: locatorChain }, timeout); }, _getContext: function () { var locator = this.locator, context = locator && locator.context || null; return context; }, /** * Called internally by and() * Will return the `valueProperty` of the currently "active" future * For in-browser tests, this could be an Ext JS Component, an ST.Element, or a full future * For WebDriver-based tests, this will always be the future * @private */ _value: function () { var me = this, valueProperty = me.valueProperty, value = me; // el/cmp are on the future, so just return them if we have a value property if (valueProperty && me[valueProperty]) { value = me[valueProperty]; } return value; }, _getFocusEl: function () { return this.el; }, // helper function to attach things to each event play: function (events) { var me = this, context = me.context; if (!ST.isArray(events)) { events = [events]; } for (var i = 0; i < events.length; i++) { events[i].future = events[i].future || me; events[i].futureClsName = events[i].future.$className; } return context.play(events)[0]; }, /** * @private * Convenience method for setting key/value pairs on the future's data object. The data object is * used by {@link #get} and {@link #expect} to store retrieved properties on the future. * * @param {String/Object} name The name of the key to set, or an object of key-value pairs to set * @param {Object} value The value to set on the data object */ setData: function (name,value) { var me = this; me.data = me.data || {}; if (typeof name === 'object') { ST.apply(me.data, name); } else { me.data[name] = value; } }, /** * @private * Convenience method for retrieving data from the future. The data object is * used by {@link #get} and {@link #expect} to store retrieved properties on the future. * * @param {String} [name] Provide the name of a key to scope the results, otherwise the full data object will be returned * @return {Object} */ getData: function (name) { var me = this; me.data = me.data || {}; if (me.data && name) { return me.data[name]; } else { return me.data; } }, /** * @private * Convenience method for creating a lasting relationship between two futures * @param {String} [name] The name of the key for the value, or an object of related futures * @param {ST.future.Component/ST.future.Element} future The future to which the relationship should be made */ setRelated: function (name, future) { var me = this; me.related = me.related || {}; if (typeof name === 'object') { ST.apply(me.related, name); } else { me.related[name] = future; } }, /** * @private * Convenience method for retrieving related futures from the future. * @param {String} [name] Provide the name of a future to scope the results, otherwise the full "related" map will be returned * @return {Object} */ getRelated: function (name) { var me = this; me.related = me.related || {}; if (name) { return me.related[name]; } else { return me.related; } }, // for contentX actions emptyRe: /^\s*$/}); ST.future.Element.addPlayables({ tap: { params: 'x,y,button,timeout', target: function () { return this.future.locator; } }, dblclick: { params: 'x,y,timeout', target: function () { return this.future.locator; } }, rightclick: { params: 'x,y,timeout', target: function () { return this.future.locator; } }, /** * @method content * @chainable * Waits for this element's `innerHTML` to match the specified value. * * ST.element('@some-div'). * click(10, 10). * content('Hello <b>world</b>'); * * @param {String} html The html to match. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this * @since 1.0.1 */ content: { addEvent: function () { var me = this, rec = me._buildRec('content', arguments, 'html,timeout', { waitingFor: 'content', waitingState: 'matching ' }); rec.waitingState += rec.args.html; me.play(rec); return me; }, ready: function () { var me = this, html = me.args.html; ST.logger.debug('content ready()? expected=' + html + ', actual=' + me.getDom().innerHTML); if (html === me.getDom().innerHTML) { return me.setWaiting(false); } return false; } }, /** * @method contentEmpty * @chainable * Waits for this element's `innerHTML` to be empty. * * ST.element('@some-div'). * click(10, 10). * contentEmpty(); * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ contentEmpty: { params: 'timeout', waitingFor: 'content', waitingState: 'empty', ready: function () { if (ST.future.Element.prototype.emptyRe.test(this.getDom().innerHTML)) { return this.setWaiting(false); } return false; } }, /** * @method contentLike * @chainable * Waits for this element's `innerHTML` to match the specified RegExp `pattern`. * * ST.element('@some-div'). * click(10, 10). * contentLike(/hello/i); * * @param {RegExp/String} pattern The pattern to match. If this is a String, it * is first promoted to a `RegExp` by called `new RegExp(pattern)`. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ contentLike: { addEvent: function () { var me = this, rec = me._buildRec('contentLike', arguments, 'pattern,timeout', { waitingFor: 'content', waitingState: 'like ' }); rec.waitingState += rec.args.pattern; me.play([rec]); return me; }, ready: function () { var pattern = ST.decodeRegex(this.args.pattern), re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern; if (re.test(this.getDom().innerHTML)) { return this.setWaiting(false); } return false; } }, /** * @method contentNotEmpty * @chainable * Waits for this element's `innerHTML` to be non-empty. * * ST.element('@some-div'). * click(10, 10). * contentNotEmpty(); * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ contentNotEmpty: { params: 'timeout', waitingFor: 'content', waitingState: 'not empty', ready: function () { ST.logger.debug('contentNotEmpty, actual=' + this.getDom().innerHTML); if (ST.future.Element.prototype.emptyRe.test(this.getDom().innerHTML)) { return false; } return this.setWaiting(false); } }, /** * @method contentNotLike * @chainable * Waits for this element's `innerHTML` to not match the specified RegExp `pattern`. * * ST.element('@some-div'). * click(10, 10). * contentNotLike(/world/i); * * @param {RegExp/String} pattern The pattern to match. If this is a String, it * is first promoted to a `RegExp` by called `new RegExp(pattern)`. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ contentNotLike: { addEvent: function () { var me = this, rec = me._buildRec('contentNotLike', arguments, 'pattern,timeout', { waitingFor: 'content', waitingState: 'not like ' }); rec.waitingState += rec.args.pattern; me.play([rec]); return me; }, ready: function () { var pattern = ST.decodeRegex(this.args.pattern), re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern; if (!re.test(this.getDom().innerHTML)) { return this.setWaiting(false); } return false; } }, /** * Waits for the document.readyState to be the specified state. * * @param {String} [state="complete"] * @method documentReady * @chainable * @return {ST.future.Element} this * @since 2.0.0 */ documentReady: { params: 'state,timeout', is: function () { var state = this.args.state || 'complete'; return document.readyState === state; }, wait: function (done) { var state = this.args.state || 'complete', listener = function () { if (document.readyState === state) { done(); } }; document.addEventListener('readystatechange', listener); return function () { document.removeEventListener('readystatechange', listener); } } }, /** * Returns a {@link ST.future.Element future element} used to queue operations for * when that element becomes available (rendered to the page). The element does not * need to be visible for this future to complete. * * Once a future is returned from this method, it is typically used to describe some * sequence of actions, wait for state transitions and perform inspections. * * ST.element('@someEl'). * click(10, 10). * textLike(/hello/i). * and(function (el) { ... }); * * @param {String} locator See {@link ST.Locator} for supported syntax. * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the element. * @return {ST.future.Element} * @method element * @chainable */ element: { addEvent: function (locator, timeout) { var me = this, locatorChain = [], comp = locator, available, root, direction; if (locator) { if (locator.constructor === Object) { timeout = locator.timeout; available = locator.available; root = locator.root; direction = locator.direction; // if locatorChain is already defined, we'll add to it locatorChain = locator.locatorChain || locatorChain; } // if the locator is an object, we don't want to add it to the chain as-is, since it's // probably a copy from a related future (like asComponent, et.al) // the locator chain will *already* have to correct path to the related future, so // we don't lose anything by skipping here! // for now, we'll add a fake locator so it doesn't get lost if (typeof locator !== 'object') { // finish locatorChain locatorChain.push({ direction: direction, locator: locator, futureClsName: me.$className, type: me.$futureType }); } if (locator.isComponent || locator.isWidget) { locator = locator.el || locator.element; // el=Ext, element=Touch } } me.timeout = timeout; if (locator) { if (locator.dom && !locator.$ST) { locator = new ST.Element(locator.dom); } if (locator.dom) { locatorChain.push({ el: locator, futureClsName: me.$className, type: me.$futureType }); } if (locatorChain) { me.locatorChain = ST.Array.slice(locatorChain); } me.locator = me.play(me._buildRec(me.$futureType || 'element', { target: locator, // TODO non-string target/locators? visible: null, // don't worry about visibility... animation: false, // ...or animations at this stage available: available, root: root, direction: direction, timeout: timeout, locatorChain: locatorChain })); } if (comp && !me.locator) { // If me.locator is not setup that means the component is not rendered. // With Sencha Touch and Ext JS 6 (Modern) this is never the case. if (comp.constructor === Object) { comp = comp.locator; } if (comp.isComponent) { me.cmp = comp; me.locator = function () { return comp.el; // no need to worry about Modern/Touch }; } } return me; }, fn: function () { var me = this, future = me.future; ST.logger.debug('future.el = this.targetEl = ', this.targetEl); future.el = this.targetEl; if (future._attach) { future._attach(); ST.logger.debug('after _attach(), future.cmp=', future.cmp); } } }, /** * @method expect * @chainable * Schedules an expectation on a getter or property of the given component or element future. The expect() call * returns a set of matchers. The matcher funtions return the current future for further chaining. * * ST.textField('@username') * .type('test-username') * .expect('value').toBe('test-username') * .textField('@password') * .type('test-password') * .expect('value').toBe('test-password'); * * The string property name is used to get properties from the future and it's underlying Ext.Component or * dom. * * new Ext.Container({ * id: 'my-container', * renderTo: Ext.getBody(), * height: 100 * }) * * ST.component('@my-container') * // component getter * .expect('XTypes').toBe('component/box/container') * * // component property * .expect('height').toBe(100) * * // dom property * .expect('innerHTML').toContain('my-container') * * // computed style * .expect('visibility').toBe('visible'); * * Custom jasmine matchers are included. "not" is supported. Other matchers such as jasmine.any(<type>), * jasmine.objectContaining and jasmine.anything() are supported as well. * * ST.textField('@futureCmp') * .expect('aString').toBe('bar') * .expect('aString').not.toBe('baz') * .expect('aString').toEqual(jasmine.any(String)) * .expect('aString').not.toEqual(jasmine.any(Number)) * .expect('aNumber').toBe(42) * .expect('aNumber').not.toBe(23) * .expect('aNumber').toEqual(jasmine.any(Number)) * .expect('aNumber').not.toEqual(jasmine.any(String)) * .expect('anObject').toEqual(jasmine.anything()) * .expect('anObject').toEqual(jasmine.objectContaining({a:1,b:2})) * .expect('anObject').not.toEqual(jasmine.objectContaining({c:3})) * .expect('notThere').not.toEqual(jasmine.anything()) * * @param {String} property name to compare against * @return Object resembling jasmine Expectation class as documented here * {@link https://jasmine.github.io/2.5/introduction#section-Included_Matchers} * @since 2.0.0 */ expect: { remotable: false, ready: function () { return true; }, addEvent: function () { var ex = new ST.Expect(this,arguments); return ex; } }, /** * @method expectFailure * * Help write negative test cases by passing when some or all of the * test results are failures. * * This function can be used in a fluent chain or by itself. It schedules * a check on the test results so far so can generally be used inside the * test unless the test itself is expected to timeout, then it should be * used in an afterEach function. * * When called with no parameters all test results which are failures * will be marked as passing and have 'Expected Failure: ' prepended * to their message. * * describe('failures', function () { * it('should pass', function () { * expect(1).toBe(2); * ST.expectFailure(); * }); * }); * * describe('no failures', function () { * it('should pass', function () { * expect(1).toBe(1); * ST.expectFailure(); * }); * }); * * Will result in the following messages: * * Expected failure: Expected 1 to be 2. * Expected at least one failing result but found none. * * It is also possible to specify one or more String or RegExp items to match * errors specifically. * * describe('match string and regexes', function () { * it('expects failure', function () { * expect(1).toBe(1); * expect(1).toBe(2); * expect('foo').toBe('bar'); * ST.expectFailure([ * /.*foo.*bar/, * 'Expected 1 to be 2' * ]); * }); * }); * * Would result in three passing results for the test: * * Expected 1 to be 1. * Expected failure: Expected 'foo' to be 'bar'. * Expected failure: Expected 1 to be 2. * * One special case where you would need to use ST.expectFailure in an afterEach is * when the test case you are expecting a failure in times out. For futures that * timeout you can use the {@link ST.future.Element#timedout} method.actions * * describe('suite', function () { * it('should timeout the entire test', function () { * ST.element().and( * 500, * function (el,done) { * setTimeout(function () { * done(); * },1000); // force a timeout * } * ); * }); * * afterEach(function () { * ST.expectFailure(); * }); * }); * * @param {String|RegExp|String[]|RegExp[]} [match] Optional set of messages to match * @member ST * @since 2.0.0 */ expectFailure : { remoteable: false, addEvent: function (match) { var me = this, rec = me._buildRec('expectFailure', arguments, 'match'); // special case... if no args that means we can expect a failure // at the level of the test... say for expecting globalCheck failures // or other things external to the it function. if (typeof match === 'undefined') { ST.Test.current.expectFailure = true; } me.play([rec]); return me; }, ready: function () { return true; }, fn: function () { var results = ST.Test.current.results, match = this.args.match, found, totalfound, markExpectedFailure = function (result) { result.message = 'Expected failure: '+result.message; result.passed = true; result.status = 'passed'; ST.Test.current.failures--; }; if (!ST.isArray(match)) { match = [match]; } for (var i=0; i < match.length; i++) { found = 0; totalfound = 0; for (var j=0; j < results.length; j++) { if (match[i].isRegex) { match[i] = ST.decodeRegex(match[i]); } if (typeof match[i] === 'object' && match[i].test) { found = match[i].test(results[j].message); } if (typeof match[i] === 'string') { found = results[j].message == match[i]; } if (found) { markExpectedFailure(results[j]); totalfound++; } } if (totalfound == 0) { ST.Test.current.addResult({ passed: false, message: 'Did not find expected failure: '+match[i] }); } } } }, /** * @method execute * Executes the provided function in the browser under test. Be aware that * with WebDriver scenarios the scope of the function is not related to the * scope of the test code. The following will not work: * * var foo = '1'; * ST.execute(function () { * expect(foo).toBe(1); * }); * * Because the bar foo is in the test scope but the function is executed * in the browser under test. * * For in-browser scenarios the code above will work. * * If the provided function returns a value it will be set on the current * future's data as the property 'executeResult'. * * ST.element('@some-div') * .execute(function () { * return 'foo'; * }) * .and(function (future) { * expect(future.data.executeResult).toBe('foo') * }); * * Similarly if any errors are thrown in the provided function the error will * be set on the future's data as the property 'executeError'. * * ST.element('@some-div') * .execute(function () { * throw 'foo'; * }) * .and(function (future) { * expect(future.data.executeError).toBe('foo') * }); * * For the function, the 1st argument will be the future value such as ST.Element * or an Ext.Component or the future itself in the case of WebDriver scenarios. * The scope of functions will be the playable event itself. To access * the future use this.future. * * @param {Function} fn the function to execute in the browser under test * @chainable * @return {ST.future.Element} this * @since 2.0.0 */ execute: { params: 'fn', remoteable: false, ready: function () { return true; }, fn: function (done) { var me = this; me.context.execute(me, me.args.fn, me.args.restParams, function (ret) { me.future.setData('executeResult',ret); me.future.setData('executeError',null); done(); }, function (err) { me.future.setData('executeResult',null); me.future.setData('executeError',err); done(); }); } }, // TODO make execute() return a Promise/Future hybrid so you can // optionally do a then(resolve,reject) which executes sandbox-run functions // to remove the need for slightly cumbersome future.data.executeResult/Error. /** * @hide */ _fnRejectsPromise: { params: 'message', ready: function () { return true; }, fn: function () { var msg = this.args.message; // here use a then-able since we might be in-browser return { then: function(resolve,reject) { reject(msg); } }; } }, /** * Schedules an event to locate a hierarchical component/element * @private */ findInHierarchy: { addEvent: function (config, timeout) { var me = this, rec = me._buildRec('element', { timeout: timeout, target: config.locator }); config.locatorChain.push({ direction: config.direction, locator: config.target, futureClsName: me.$className, type: (!me.$futureType || me.$futureType === 'element') ? 'element' : 'component' }); me.root = config.locatorChain; me.locatorChain = config.locatorChain; me.locator = me.play(ST.apply(rec, config)); return me; } }, dragAndDrop: { addEvent: function (config, timeout) { var me = this, rec = me._buildRec('dragAndDrop', { timeout: timeout, visible: true, args: { drag: { target: me.locator.target }, drop: { x: 0, y: 0 } } }); // if an object, we'll assume it' drop coordinates if (typeof config === 'object') { if (config.drag) { ST.apply(rec.args.drag, config.drag); if (typeof config.drag.target !== 'undefined') { rec.args.drag.target = me.locator.target + ' ' + config.drag.target; } } if (config.drop) { ST.apply(rec.args.drop, config.drop); if (typeof config.drop.target !== 'undefined') { rec.args.drop.target = config.drop.target; } } } // not an object, then it needs to be a drop target selector else { rec.args.drop.target = config; } me.play(rec); return me; } }, /** * @method focus * @chainable * Schedules the component to receive the focus. * * ST.element('@some-div/input'). * focus(); * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ focus: { addEvent: function () { var me = this, rec = me._buildRec('focus', arguments, 'timeout'); me.play(rec); return me.focused(); }, fn: function () { var el = this.future._getFocusEl(); if (el) { el.focus(); } } }, /** * @method get * Retrieves the values of the specified list of properties from the future's * underlying dom or computed style. These values will then be available on the future's data property * for use in expectations within an and() method. * * * This method is particularly useful for WebDriver-based tests where direct access to DOM elements * is not possible within the context of the executed spec. * * ST.element('#mydiv') * .get('className') * .and(function () { * expect(this.future.data.className).toContain('foo'); * }) * * * Each string property is tried first as a dom property and then a property of the computed style * of the dom. * * get('id') returns dom.id * get('height') returns the dom's computed style height * * See {@link ST.future.Component#get} for details on how this method works on Component futures. * * @param {String} properties A comma-delimited list of property values to retrieive * @return {ST.future.Element} this * @chainable * @since 2.0.0 */ get: { params: 'names,timeout', fn: function () { var me = this, future = me.future, names = me.args.names.split(','), el = me.getElement(), len = names.length, i, key, val; future.data = future.data || {}; if (el) { for (i = 0; i < len; i++) { key = names[i]; if (el.dom) { val = el.dom[key]; } if (val === undefined) { val = el.getStyle(key); } if (val !== undefined) { future.data[key] = val; } } } } }, /** * @method getTitle * * Returns the title of the current document as the first arg of the provided callback function. * * ST.navigate('https://duckduckgo.com') * .getTitle(function (title) { * expect(title).toContain('Duck'); * }); * * @param {Function} callback function to receive the title of the current document * @chainable * @return {ST.future.Element} this * @since 2.0.0 */ getTitle: { params: 'callback,timeout', remoteable: false, fn: function (done) { var me = this; this.context.getTitle(function(title) { me.args.callback(title); done(); }); } }, /** * @method getUrl * * Returns the current url of the target browser as the first arg of the provided callback function. * * ST.navigate('https://duckduckgo.com') * .getUrl(function (url) { * expect(url).toContain('duckduckgo'); * }); * * @param {Function} callback The callback function to execute when the url has been determined * @return {ST.future.Element} this * @chainable * @since 2.0.0 */ getUrl: { params: 'callback,timeout', remoteable: false, fn: function (done) { var me = this; this.context.getUrl(function(url) { me.args.callback(url); done(); }); } }, /** * @method navigate * * Causes the browser to load the specified URL. * * ST.navigate('https://www.google.com') * .input('input[title="Search"]'). * .navigate('https://www.sencha.com') * .input('input'); * * NOTE: Using this method to navigate away from the page in a non-WebDriver test * will halt the test execution. It is possible to use this to navigate to different anchors * on the same page: * * ST.getUrl(function (url) { * ST.navigate(url+'#first-anchor'). * element('@some-div'); * }); * * If URL begins with '#' then the page will be redirected to that anchor. * * @param {String} url The URL to navigate to. * @return {ST.future.Element} this * @chainable * @since 2.0.0 */ navigate: { remoteable: false, params: 'url', fn: function (done) { this.context.url(this.args.url,done); } }, /** * @method screenshot * @chainable * Takes a snapshot of the viewport and compares it to the associated baseline image. * * ST.element('@someEl'). * click(10, 10). * screenshot(); * * Can also be used directly from the ST namespace and chained with other futures API methods: * * ST.screenshot('first', 10, 10000). // tolerance=10 (10 pixels), timeout=10000 (10 seconds) * navigate('#somewhere'). * screenshot('second', 20); // tolerance=20 (20 pixels), timeout=30000 (default) * * It is possible to set the maximum number of different pixels the current snapshot can have * compared to the baseline image before the method will cause the test to fail, by defining a tolerance: * * ST.screenshot('first', 200); // Tolerance of 200 pixels (defaults to 0 if not explicitly set) * * To support the 1.0.x API as well as for flexibility the second parameter can be a callback function, though * the preferred usage is chaining as above. * * ST.screenshot('third', function () { * // the screenshot has been taken! * }); * * @param {String} [name] for the snapshot filename. Default is an incremental number for the current test run. * @param {Function} [callback] Optional callback for when the screenshot is complete. * @param {Number} [tolerance=0] Optional the maximum number of different pixels the current snapshot * can have compared to the baseline image before the method will cause the test to fail. * @param {Number} [timeout=30000] Optional The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ screenshot: { remoteable: false, addEvent: function (name, callback, tolerance, timeout) { var me = this, rec; if (typeof callback !== 'function') { timeout = tolerance; tolerance = callback; callback = null; } timeout = timeout || 30000; rec = me._buildRec('screenshot', { args: { name: name, callback: callback, tolerance: tolerance, }, timeout: timeout }); me.play([rec]); return me; }, fn: function (done) { var me = this, args = me.args, name = args.name, callback = args.callback, tolerance = args.tolerance; me.context.screenshot(name, tolerance, function (err, comparison) { me.future.setData('error', err); me.future.setData('comparison', comparison); if (callback) { callback(); } done(); }); } }, /** * @method scroll * @chainable * Schedules a scroll action by the amounts specified for each axis. * * ST.element('@some-div'). * scroll(10, 10); // scroll the element 10px on the x-axis and 10px on the y-axis * * Or for a Component: * * ST.component('#some-cmp'). * scroll(10, 10); // scroll the component 10px on the x-axis and 10px on the y-axis * * This future will wait for the element to be visible before performing the scroll * action. * * @param {Number} x The number of pixels to scroll from the left edge of the element. * @param {Number} y The number of pixels to scroll from the top edge of the element. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ scroll: { params: 'x,y,timeout', target: function () { return this.future.locator; } }, /** * @method setViewportSize * @chainable * Sets the size of the browser viewport. This method has no effect on in-browser tests * when the browser wasn't launched via WebDriver, and it is particulary useful to * ensure that compared screenshots have the same dimensions. * * ST.setViewportSize(1024, 768) * .screenshot(); * * @param {Number} width * @param {Number} height * @return {ST.future.Element} this */ setViewportSize: { params: 'width,height', remoteable: false, fn: function (done) { var me = this, args = me.args; me.context.setViewportSize(args.width, args.height, done); } }, /** * @method text * @chainable * Waits for this element's `textContent` to match the specified string. * * ST.element('@some-div'). * click(10, 10). * text('Hello world'); * * @param {String} text The text to match. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this * @since 1.0.1 */ text: { addEvent: function () { var me = this, rec = me._buildRec('text', arguments, 'text,timeout', { waitingFor: 'text', waitingState: 'matching ' }); rec.waitingState += rec.args.text; me.play([rec]); return me; }, ready: function () { var text = this.args.text; var t = this.getElement().getText(); if (text === t) { return this.setWaiting(false); } return false; } }, /** * @method textEmpty * @chainable * Waits for this element's `textContent` to be empty. * * ST.element('@some-div'). * click(10, 10). * textEmpty(); * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ textEmpty: { params: 'timeout', waitingFor: 'text', waitingState: 'empty', ready: function () { var text = this.getElement().getText(); if (ST.future.Element.prototype.emptyRe.test(text)) { return this.setWaiting(false); } return false; } }, /** * @method textLike * @chainable * Waits for this element's `textContent` to match the specified RegExp `pattern`. * * ST.element('@some-div'). * click(10, 10). * textLike(/hello/i); * * @param {RegExp/String} pattern The pattern to match. If this is a String, it * is first promoted to a `RegExp` by called `new RegExp(pattern)`. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ textLike: { addEvent: function () { var me = this, rec = me._buildRec('textLike', arguments, 'pattern,timeout', { waitingFor: 'text', waitingState: 'like ' }); rec.waitingState += rec.args.pattern; me.play([rec]); return me; }, ready: function () { var text = this.getElement().getText(), pattern = ST.decodeRegex(this.args.pattern), re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern; if (re.test(text)) { return this.setWaiting(false); } return false; } }, /** * @method textNotEmpty * @chainable * Waits for this element's `textContent` to be non-empty. * * ST.element('@some-div'). * click(10, 10). * textNotEmpty(200); * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ textNotEmpty: { params: 'timeout', waitingFor: 'text', waitingState: 'not empty', ready: function () { var text = this.getElement().getText(); if (ST.future.Element.prototype.emptyRe.test(text)) { return false; } return this.setWaiting(false); } }, /** * @method textNotLike * @chainable * Waits for this element's `textContent` to not match the specified RegExp `pattern`. * * ST.element('@some-div'). * click(10, 10). * textNotLike(/hello/i, 200); * * @param {RegExp/String} pattern The pattern to match. If this is a String, it * is first promoted to a `RegExp` by called `new RegExp(pattern)`. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ textNotLike: { addEvent: function () { var me = this, rec = me._buildRec('textNotLike', arguments, 'pattern,timeout', { waitingFor: 'text', waitingState: 'not like ' }); rec.waitingState += rec.args.pattern; me.play([rec]); return me; }, ready: function () { var text = this.getElement().getText(), pattern = ST.decodeRegex(this.args.pattern), re = (typeof pattern === 'string') ? new RegExp(pattern) : pattern; if (!re.test(text)) { return this.setWaiting(false); } return false; } }, /** * @method timedout * * Expects the last method to timeout and adds a test result explaining * the desired state. Such as: * * ST.element('not-there').timedout(); * * expected timeout waiting for '[input]' to be ready for element * * @return {ST.future.Element} this * @chainable * @since 2.0.0 */ timedout: { remoteable: false, addEvent: function () { this.locator.expectTimeout = true; return this; } }, /** * @method type * Schedules a "type" action at the specified relative coordinates. This method * assumes you have already achieved correct focus of the target and that the * target is visible. If the target is not visible this future will timeout. * * ST.element('@some-div/input'). * focus(). * type('Hello world'); * * If first argument is an object, it should be a {@link ST.playable.Playable playable} * config object for a `type="type"` event. In this case, all other arguments are * ignored. * * To specify a location in the input to insert text, the type object accepts a caret * property: * * ST.element('@some-div/input'). * focus(). * type('Hllo world'). * type({ * text: 'e', * caret: 1 * }); * * which would insert the 'e' after the first character. Likewise, specifying a range: * * ST.element('@some-div/input'). * focus(). * type('H1234 world'). * type({ * text: 'ello', * caret: [1,4] * }); * * would overwrite any text in the given range. * * @param {String} text The text to type. * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the * typing to finish. * @return {ST.future.Element} this * @chainable */ type: { params: 'text,timeout', target: function () { var me = this, el = me.future._getFocusEl(); return el && el.dom; } }, /** * @method hasCls * @chainable * Waits for this element to have a specified CSS class. * * ST.element('@someEl'). * hasCls('foo'). * and(function (el) { * // el now has a "foo" class * }); * * @param {String} cls The class name to test. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ hasCls: { params: 'cls,timeout', is: function () { return this.getElement().hasCls(this.args.cls); } }, /** * @method hidden * @chainable * Waits for this element to become hidden. * * ST.element('@someEl'). * hidden(). * and(function (el) { * // el is now hidden * }); * * Note that the element must still be present in order to check if it is hidden. * * ST.component('@someCmp'). * click(). * and(function (cmp) { * cmp.destroy(); * }). * hidden(); // will timeout and produce an error * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ hidden: { is: function () { return !this.getElement().isVisible(); } }, /** * @method missingCls * @chainable * Waits for this element to not have a specified CSS class. * * ST.element('@someEl'). * missingCls('foo'). * and(function (el) { * // el now does not have a "foo" class * }); * * @param {String} cls The class name to test. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ missingCls: { params: 'cls,timeout', is: function () { return !this.getElement().hasCls(this.args.cls); } }, /** * @method removed * @chainable * Waits for this element to be removed from the document. * * ST.element('@someEl'). * removed(). * and(function (el) { * // el is now removed from the document * }); * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ removed: { is: function () { return !ST.fly(document.body).contains(this.getElement()); }, available: false }, /** * @method visible * @chainable * Waits for this element to become visible. * * Event injection methods automatically wait for target visibility, however, if * using `and` sequences explicitly waiting for visibility may be necessary. * * ST.element('@someEl'). * visible(). * and(function (el) { * // el is now visible * }); * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ visible: { is: function () { return this.getElement().isVisible(); } }, /** * @method focused * @chainable * Waits for this element to become focused. * * ST.element('@someEl'). * focused(). * and(function (el) { * // el is now focused * }); * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ focused: { is: function () { return document.activeElement === this.future._getFocusEl().dom; } }, /** * @method blurred * @chainable * Waits for this element to become blurred. * * ST.element('@someEl'). * blurred(). * and(function (el) { * // el is now blurred * }); * * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.Element} this */ blurred: { is: function () { return document.activeElement !== this.future._getFocusEl().dom; } }}); /** * @method absent * @chainable * @member ST * Schedules a wait for an element to be absent or missing from the DOM. This is typically * used after some future action that should cause a removal. * * ST.button('@okButton').click(); * ST.absent('@confirmationWindow'); // the window owning the OK button * * This method is similar to `{@link ST.future.Element#removed}` but the difference * is that this method does not first wait for the specified element to be found. This * difference makes this method suitable for checking for things that should not be * present in the first place. * * @param {String} locator See {@link ST.Locator} for supported syntax. * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the element * to be removed. * @return {ST.future.Element} */ST.absent = function (locator, timeout) { return new ST.future.Element({ locator: locator, timeout: timeout, available: false });}; /** * Returns a limited-use {@link ST.future.Element future} that can be used only to * {@link ST#wait wait} and perform some * {@link ST.future.Element#method-and manual steps}. * @return {ST.future.Element} * @method wait * @member ST */ST.wait = function () { var future = new ST.future.Element(); return future.wait.apply(future, arguments);}; /** * @inheritdoc ST.future.Element#element */ST.element = function (locator, timeout) { var future = new ST.future.Element(); return future.element(locator, timeout);}; /** * @inheritdoc ST.future.Element#element */ST.context.Base.prototype.element = function (locator, timeout) { var future = new ST.future.Element({ context: this }); return future.element(locator, timeout);}; /** * @inheritdoc ST.future.Element#execute */ST.execute = function (fn) { return new ST.future.Element().execute(fn);}; /** * @inheritdoc ST.future.Element#expectFailure */ST.expectFailure = function (match) { return new ST.future.Element().expectFailure(match);}; /** * @inheritdoc ST.future.Element#navigate */ST.navigate = function (url) { var future = new ST.future.Element(); return future.navigate(url);}; /** * @inheritdoc ST.future.Element#documentReady */ST.documentReady = function (state, timeout) { var future = new ST.future.Element(); return future.documentReady(state, timeout);}; /** * @inheritdoc ST.future.Element#screenshot */ST.screenshot = function (name, callback, tolerance, timeout) { var future = new ST.future.Element(); return future.screenshot(name, callback, tolerance, timeout);} /** * @inheritdoc ST.future.Element#setViewportSize */ST.setViewportSize = function (width, height) { var future = new ST.future.Element(); return future.setViewportSize(width, height);} /** * @inheritdoc ST.future.Element#getUrl */ST.getUrl = function (callback) { ST.defaultContext.getUrl(callback);}; ST.future.classes = []; ST.future.define = function (componentName, body) { if (!body.extend) { body.extend = ST.future.Element; } var playables = body.playables; delete body.playables; var extend = body.extend, mixins = body.mixins, cls = ST.define(body), // deletes body.extend parts = componentName.split('.'), methodScope = ST, classScope = ST.future, futureType, name; while (parts.length > 1) { name = parts.shift(); if (!classScope[name]) { classScope[name] = {}; } if (!methodScope[name]) { methodScope[name] = {}; } } name = parts[0]; futureType = ST.decapitalize(name); // ensure that THIS prototype has a playables property cls.prototype.playables = {}; cls.prototype.$playables = {}; // save the config for inheritance // track the classes being defined ST.future.classes.push(cls); // add mixin playables if (mixins) { mixins = ST.isArray(mixins) ? mixins : [mixins]; for (i=0; i<mixins.length; i++) { mixin = mixins[i].prototype; if (mixin.$playables) { cls.addPlayables(mixin.$playables); } } } if (body.factoryable === false) { delete cls.prototype.factoryable; } else { ST.context.Base.prototype[futureType] = function (locator, timeout) { var future = new cls({ context: this }); return future[futureType](locator, timeout); }; // ST.button() for example methodScope[futureType] = function (locator, timeout) { var future = new cls(); return future[futureType](locator, timeout); }; var superType = cls.superclass.$futureType || 'element', superPlayableCls = extend.prototype.playables[ST.capitalize(superType)]; // Button // superType 'component' // ST.future.Component.prototype.playables.Component (really the config from 'element' playable) // Component // superType 'element' // ST.future.Element.prototype.playables.Element if (!cls.prototype[futureType]) { // no type specific method for adding event so use superclass method cls.addPlayable(futureType, { extend: superPlayableCls }); } } if (extend.prototype.$playables) { cls.addPlayables(extend.prototype.$playables); } // after any superclass playables are added, add the ones for this class... // allows for overrides such as Component.get() over Element.get() if (playables) { cls.addPlayables(playables); } // record the full class name on the prototype so we can re-build it in webdriver remote browser cls.prototype.$className = 'ST.future.' + componentName; cls.prototype.$futureType = futureType; // track futureType inheritance var futureTypeChain = cls.prototype.$futureTypeChain; var futureTypeMap = cls.prototype.$futureTypeMap; if (!cls.prototype.hasOwnProperty('$futureTypeChain')) { cls.prototype.$futureTypeChain = futureTypeChain = futureTypeChain ? futureTypeChain.slice(0) : []; cls.prototype.$futureTypeMap = futureTypeMap = ST.apply({}, futureTypeMap); } futureTypeChain.push(futureType); futureTypeMap[futureType] = true; return classScope[ST.capitalize(name)] = cls;}; /** * @class ST.future.TableCell * @extend ST.future.Element * This class provides methods to interact with a `TableCell` when it becomes * available. Instances of this class are returned by the following methods: * * * {@link ST.future.TableRow#cellAt} * * {@link ST.future.TableRow#cellBy} */ST.future.define('TableCell', { extend: ST.future.Element, factoryable: false, /** * @cfg {Number} at * The column index in the tables's `columns`. This property is set when calling the * {@link ST.future.TableRow#cellAt} method. */ /** * Returns the owning `ST.future.Table`. This method can be called at any time * to "return" to the owning future. For example: * * ST.table('@someTable') * .rowAt(3) // get a future row (ST.future.TableRow) * .cellAt(2) // cell index 2 (0-based) * .table() // operates on the ST.future.TableCell * .click(); // click on the table * * @return {ST.future.Table} */ table: function () { var table = this.getRelated('table'); // replay locator to ensure context is updated appropriately this.play([table.locator]); return table; }, /** * Returns the owning `ST.future.TableRow`. This method can be called at any time * to "return" to the owning row future. For example: * * ST.table('@someTable) * .rowAt(3) * .cellAt(2) * .value(32) * .row() * .cellAt(5) * .value(15) * * @return {ST.future.TableRow} */ row: function () { var row = this.getRelated('row'); // replay locator to ensure context is updated correctly this.play([row.locator]); return row; }, playables: { cellBy: { addEvent: function (config, timeout) { var me = this, locatorChain; me.setRelated('table', config.table); me.setRelated('row', config.row); locatorChain = ST.Array.slice(config.row.locatorChain || []); locatorChain.push({ direction: 'down', futureClsName: me.$className, type: 'cellBy', args: { cell: config.cell, row: config.row.locator.args.row } }); me.root = locatorChain; me.locatorChain = locatorChain; me.locator = me.play(me._buildRec('cellBy', { visible: null, animation: false, timeout: timeout, root: locatorChain, locatorChain: locatorChain, args: { cell: config.cell } })); return me; }, ready: function () { var me = this, node = ST.future.TableCell.findNode(me); me.future.setData(me.args); return !!node && node.isVisible(); }, fn: function () { var node = ST.future.TableCell.findNode(this); this.updateEl(node.dom); } }, /** * @method reveal * @chainable * Scrolls this table cell into view. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableCell} */ reveal: { params: 'timeout', fn: function (done) { var el = this.future.el; if (el) { el.dom.scrollIntoView(); } done(); } } }, statics: { findNode: function (instance) { var row = instance.future.getRelated('row').el, config = instance.args.cell, at = config.at, query = config.query, value = config.propertyValue, hasAt = at !== undefined, node; if (hasAt) { // get all the descendant td/th tags of this row; we'll find the right index later query = 'td, th'; } if (value) { // if we have a value, we want to first find a descendent cell of the table row that has the desired text // use .// to operate on current context (row) query = './/td[contains(text(), "' + value + '")] | .//th[contains(text(), "' + value + '")]'; // pass the row as the context and only allow a single result node = ST.Locator.find(query, true, row); } else { // this query could be for an index, so allow multiple results node = ST.Locator.find(query, true, row, 'down', true); if (ST.isArray(node)) { if (hasAt) { // if an index has been supplied, try to return it; otherwise, node is null node = node[at] || null; } else { // regular query; return first result node = node[0]; } } } return node; } } }); // TableCell /** * @class ST.future.TableRow * @extend ST.future.Element * This class provides methods to interact with a `ST.future.TableRow` when it becomes * available. Instances of this class are returned by the following methods: * * * {@link ST.future.Table#rowAt} * * {@link ST.future.Table#rowBy} */ST.future.define('TableRow', { extend: ST.future.Element, factoryable: false, /** * @cfg {Number} at * The row index in the table. * * This property is set when calling the {@link ST.future.Tabel#rowAt} method. */ /** * Returns the `{@link ST.future.TableCell future cell}` given the cell index (0-based). * @param {Number} index The index of the cell in the table (0-based). * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableCell} */ cellAt: function (index, timeout) { return this.cellBy({ at: index }, timeout); }, /** * Returns the `{@link ST.future.TableCell future cell}` given the provided dom query. * @param {String} query The query by which to locate the descendant cell. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableCell} */ cellQuery: function (query, timeout) { return this.cellBy({ query: query }, timeout); }, /** * Returns the `{@link ST.future.TableCell future cell}` that contains the provided text value. * @param {String} propertyValue The text value by which to locate the matching cell. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableCell} */ cellWith: function (propertyValue, timeout) { return this.cellBy({ propertyName: 'text', propertyValue: propertyValue }, timeout); }, /** * Returns the `{@link ST.future.TableCell future cell}` given a config object that * specified the match criteria. * @param {Object} config Configuration options for the `ST.future.TableCell`. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableCell} */ cellBy: function (config, timeout) { var me = this, cell = new ST.future.TableCell(); return cell.cellBy({ table: me.getRelated('table'), row: me, cell: config }, timeout); }, /** * Returns the owning `ST.future.Table`. This method can be called at any time * to "return" to the owning future. For example: * * ST.table('@someTable') * .row(42) // get a future row (ST.future.TableRow) * .reveal() // operates on the ST.future.TableRow * .table() // now back to the table * .click(10, 10); // click on the table * * @return {ST.future.Table} */ table: function () { var table = this.getRelated('table'); // replay locator to ensure context is updated appropriately this.play([table.locator]); return table; }, playables: { rowBy: { addEvent: function (config, timeout) { var me = this, locatorChain; me.setRelated('table', config.table); locatorChain = ST.Array.slice(config.table.locatorChain || []); locatorChain.push({ direction: 'down', futureClsName: me.$className, type: 'rowBy', args: { row: config.row } }); me.root = locatorChain; me.locatorChain = locatorChain; me.locator = me.play(me._buildRec('rowBy', { visible: null, animation: false, timeout: timeout, root: locatorChain, locatorChain: locatorChain, args: { row: config.row } })); return me; }, ready: function () { var me = this, node = ST.future.TableRow.findNode(me); me.future.setData(me.args); return !!node && node.isVisible(); }, fn: function () { var node = ST.future.TableRow.findNode(this); this.updateEl(node.dom); } }, /** * @method reveal * @chainable * Scrolls this table row into view. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableRow} */ reveal: { params: 'timeout', fn: function (done) { var el = this.future.el; if (el) { el.dom.scrollIntoView(); } done(); } } }, statics: { findNode: function (instance) { var table = instance.future.getRelated('table').el, config = instance.args.row, at = config.at, hasAt = at !== undefined, query = config.query, value = config.propertyValue; if (hasAt) { // if an index is supplied, gather all the rows; we'll get the right index later query = 'tr'; } if (value) { // if we have a value, we want to first find a descendent cell of the table that has the desired text // use .// to operate on current context (table) query = './/td[contains(text(), "' + value + '")] | .//th[contains(text(), "' + value + '")]'; // pass the table as the context, and only allow a single result node = ST.Locator.find(query, true, table); // once we find the node, we'll now go up to the owning table row if (node) { node = node.up('tr'); } } else { // for any queries, start at the table node and allow multiples node = ST.Locator.find(query, true, table, 'down', true); if (ST.isArray(node)) { if (hasAt) { // if an index is supplied, get the relevant node or return null node = node[at] || null; } else { // otherwise, for any other queries, just return first result node = node[0]; } } } return node; } }}); // TableRow /** * This class provides methods specific to interacting with HTML table elements. * * ## Examples * * // Locating a table row by index * ST.table('@mytable'). * rowAt(2). * cellAt(1). * textLike(/Sencha/i); * * // Locating a table row by value * ST.table('@mytable'). * rowWith('Sencha'). * cellAt(3). * textLike(/United States/i); * * ## Futures * * The general mechanics of futures is covered in {@link ST.future.Element}. * * ## Locators * * See {@link ST.Locator} for supported locator syntax. * * @class ST.future.Table * @extend ST.future.Element */ST.future.define('Table', { extend: ST.future.Element, valueProperty: 'el', $className: 'ST.future.Table', $futureType: 'table', $futureTypeChain: ['element', 'table'], $futureTypeMap: { element: true, table: true }, constructor: function (config) { ST.future.Table.superclass.constructor.call(this, config); }, /** * Returns the `{@link ST.future.TableRow future row}` given the row index (0-based). * @param {Number} index The index of the row in the table (0-based). * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableRow} */ rowAt: function (index, timeout) { return this.rowBy({ at: index }, timeout); }, /** * Returns the `{@link ST.future.TableRow future row}` given the dom query. * @param {String} query The dom query by which to retrieve the descendant table row. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableRow} */ rowQuery: function (query, timeout) { return this.rowBy({ query: query }, timeout); }, /** * Returns the `{@link ST.future.TableRow future row}` which has a cell containing the provided text value. * @param {String} propertyValue The text value by which to locate the matching row. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableRow} */ rowWith: function (propertyValue, timeout) { return this.rowBy({ propertyName: 'text', propertyValue: propertyValue }, timeout); }, /** * Returns the `{@link ST.future.TableRow future row}` given a config object that * specified the match criteria. * @param {Object} config Configuration options for the `ST.future.TableRow`. * @param {Number} [timeout] The maximum time (in milliseconds) to wait. * @return {ST.future.TableRow} */ rowBy: function (config, timeout) { var me = this, row = new ST.future.TableRow(); return row.rowBy({ table: me, row: config }, timeout); },}); /** * Returns a {@link ST.future.Table future Table} used to queue operations for * when that HTML table element becomes available. * @param {String} locator See {@link ST.Locator} for supported syntax. * @param {Number} [timeout] The maximum time (in milliseconds) to wait for the dataview. * @return {ST.future.Table} * @method table * @member ST */