// 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
 */