/*

Siesta 5.1.0
Copyright(c) 2009-2018 Bryntum AB
https://bryntum.com/contact
https://bryntum.com/products/siesta/license

*/
/**
@class Siesta.Test.Element

This is a mixin, with helper methods for testing functionality relating to DOM elements. This mixin is consumed by {@link Siesta.Test}

*/
Role('Siesta.Test.Element', {

    does        : [
        Siesta.Util.Role.CanCalculatePageScroll
    ],

    requires    : [
        'typeOf',
        'chain',
        'normalizeElement'
    ],

    has : {
        allowMonkeyToClickOnAnchors     : false,

        allowedCharacters               : function () {
            return {
                // does not include TAB by purpose, because our "TAB" simulation is not perfect
                // Also exclude BACKSPACE since it navigates the page
                special     : 'ENTER/ESCAPE/PAGE-UP/PAGE-DOWN/END/HOME/UP/RIGHT/DOWN/LEFT/INSERT/DELETE',
                // does not inlcude * because Ext fails on typing it
                punctuation : '.,/()[]{}\\"\'`~!?@#$%^&_=+-',
                normal      : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
            }
        }
    },

    methods : {

        /**
         * Utility method which returns the center of a passed element. The coordinates are by default relative to the
         * containing document of the element (so for example if the element is inside of the nested iframe, coordinates
         * will be "local" to that iframe element). To get coordinates relative to the test iframe ("global" coordinates),
         * pass `local` as `false`.
         *
         * @param {Siesta.Test.ActionTarget} el The element to find the center of.
         * @param {Boolean} [local] Pass `true` means coordinates are relative to the containing document. This is the default value.
         * Pass `false` to make sure the coordinates are global to the test window.
         *
         * @return {Array} The array first element of which is the `x` coordinate and 2nd - `y`
         */
        findCenter : function (target, local) {
            return this.getTargetCoordinate(target, local);
        },


        isInDom : function (el) {
            var doc     = el.ownerDocument

            if (!doc) return false

            while (el && el != doc.body) {
                el      = el.parentNode
            }

            return Boolean(el)
        },


        normalizeOffset : function (offset, $el) {
            var parts;

            if (this.typeOf(offset) == 'Function') offset = offset.call(this)

            offset              = offset && offset.slice() || [ '50%', '50%' ];

            var rect            = this.getBoundingClientRect($el[ 0 ])

            // The rounding rules are still magical
            // one is for sure, that if we get a precise whole number w/o fractional part
            // we subtract 1 from it, since pixel counting starts from 0
            // when we have fractional part, in good browsers it seems its enough to throw it away with `floor`
            // in IE we additionally need to subtract 1
            // following the conservative path and subtracting 1 in all browsers

            var width           = Math.floor(rect.width)
//            if (bowser.msie || bowser.edge || width === rect.width) width--
            width--

            var height          = Math.floor(rect.height)
//            if (bowser.msie || bowser.edge || height === rect.height) height--
            height--

            if (typeof (offset[ 0 ]) === 'string') {
                parts           = offset[ 0 ].split('%');
                offset[ 0 ]     = parseInt(offset[ 0 ].match(/\d+/)[ 0 ], 10) * width / 100;

                if (parts[ 1 ]) {
                    offset[ 0 ] += parseInt(parts[ 1 ]);
                }
            }

            offset[ 0 ]         = Math.round(offset[ 0 ])

            if (typeof (offset[ 1 ]) === 'string') {
                parts           = offset[ 1 ].split('%');
                offset[ 1 ]     = parseInt(offset[ 1 ].match(/\d+/)[ 0 ], 10) * height / 100;

                if (parts[ 1 ]) {
                    offset[ 1 ] += parseInt(parts[ 1 ]);
                }
            }

            offset[ 1 ]         = Math.round(offset[ 1 ])

            return offset
        },


        // return viewport coordinates
        // can be simplified with just `getBoundingClientRect` ?
        getTargetCoordinate : function (target, local, offset) {
            var el              = this.normalizeElement(target),
                $el             = this.$(el),
                elOffset        = $el.offset(),
                elDoc           = el.ownerDocument,
                elWin           = elDoc.defaultView || elDoc.parentWindow,
                xy              = [ this.pageXtoViewportX(elOffset.left, elWin), this.pageYtoViewportY(elOffset.top, elWin) ]

            offset              = this.normalizeOffset(offset, $el)

            xy[ 0 ]             += offset[ 0 ];
            xy[ 1 ]             += offset[ 1 ];

            if (local === false) {
                // Potentially we're interacting with an element inside a nested frame, which means
                // the coordinates are local to that frame
                if (elWin !== this.global) {
                    var offsetsToTop    = this.$(elWin.frameElement).offset()

                    xy[ 0 ]     += this.pageXtoViewportX(offsetsToTop.left)
                    xy[ 1 ]     += this.pageYtoViewportY(offsetsToTop.top)
                }
            }

            return xy;
        },

        /**
         * Returns true if the element is visible, checking jQuery :visible selector + style visibility value.
         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @return {Boolean}
         */
        isElementVisible : function(el) {
            el          = this.normalizeElement(el);

            // Workaround for OPTION elements which don't behave like normal DOM elements. jQuery always consider them invisible.
            // Decide based on visibility of the parent SELECT node
            if (el && el.nodeName.toLowerCase() === 'option') {
                el      = this.$(el).closest('select')[ 0 ]
            }

            if (el) {
                try {
                    // Jquery :visible doesn't handle SVG/VML, so manual check
                    // accessing to `this.global.SVGElement` throws exceptions for popups in IE 9
                    if (window.SVGElement && el instanceof this.global.SVGElement)
                        return el.style.display !== 'none' && el.style.visibility !== 'hidden'
                } catch (e) {
                }

                // Jquery :visible doesn't take visibility into account
                return this.$(el).is(':visible') && (this.$(el).css('visibility') !== 'hidden')
            }

            return false
        },

        /**
         * Passes if the `innerText` property of the <body> element contains the text passed
         *
         * @param {String} text The text to match
         * @param {String} [description] The description for the assertion
         */
        assertTextPresent : function(text, description) {
            this.like(this.global.document.body.innerText, text, description);
        },

        /**
         * Passes if the innerHTML of the passed element contains the text passed
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {String} text The text to match
         * @param {String} [description] The description for the assertion
         */
        contentLike : function(el, text, description) {
            el = this.normalizeElement(el);

            this.like(el.innerHTML, text, description);
        },

        /**
         * Passes if the innerHTML of the passed element does not contain the text passed
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {String} text The text to match
         * @param {String} [description] The description for the assertion
         */
        contentNotLike : function(el, text, description) {
            el = this.normalizeElement(el);

            this.unlike(el.innerHTML, text, description);
        },

        /**
         * Waits until the innerHTML of the passed element contains the text passed
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {String} text The text to match
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForContentLike : function(el, text, callback, scope, timeout) {
            var R = Siesta.Resource('Siesta.Test.Element');

            el = this.normalizeElement(el);

            return this.waitFor({
                method          : function() { return el.innerHTML.match(text); },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForContentLike',
                description     : ' ' + R.get('elementContent') + ' "' + text + '" ' + R.get('toAppear')
            });
        },

        /**
         * Waits until the innerHTML of the passed element does not contain the text passed
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {String} text The text to match
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForContentNotLike : function(el, text, callback, scope, timeout) {
            var R = Siesta.Resource('Siesta.Test.Element');

            el = this.normalizeElement(el);

            return this.waitFor({
                method          : function() { return !el.innerHTML.match(text); },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForContentNotLike',
                description     : ' ' + R.get('elementContent') + ' "' + text + '" ' + R.get('toDisappear')
            });
        },


        getRandomTypeString : function (length) {
            var allowedCharacters   = this.allowedCharacters

            var special     = allowedCharacters.special.split('/')
            var punctuation = allowedCharacters.punctuation
            var normal      = allowedCharacters.normal

            var total       = special.length + punctuation.length + normal.length

            var str         = ''

            for (var i = 0; i < length; i++) {
                var index       = this.randomBetween(0, total - 1)

                if (index < normal.length)
                    str     += normal.substr(index, 1)
                else {
                    index   -= normal.length

                    if (index < punctuation.length)
                        str     += punctuation.substr(index, 1)
                    else {
                        index   -= punctuation.length

                        str     += '[' + special[ index ] + ']'
                    }
                }
            }

            return str
        },

        /**
         * Performs clicks, double clicks, right clicks and drags at random coordinates within the passed target.
         * While doing all these random actions it also tracks the number of exceptions thrown and reports a failure
         * if there was any. Otherwise it reports a passed assertion.
         *
         * Use this assertion to "stress-test" your component, making sure it will work correctly in various unexpected
         * interaction scenarious.
         *
         * Note that as a special case, when this method is provided with the document's &lt;body&gt; element,
         * it will test the whole browser viewport.
         *
         * @param {Siesta.Test.ActionTarget} el The element to upon which to unleash the "monkey".
         * @param {Int} nbrInteractions The number of random interactions to perform.
         * @param {String} [description] The description for the assertion
         * @param {Function} callback The callback to call after all actions are completed
         * @param {Object} scope The scope for the callback
         */
        monkeyTest : function(el, nbrInteractions, description, callback, scope, stepCallback) {
            el              = this.normalizeElement(el, false, true);

            this.suppressPassedWaitForAssertion = true;

            if (typeof nbrInteractions === 'function') {
                callback    = nbrInteractions;
                scope       = description;
                description = '';
            } else if (typeof description === 'function') {
                callback    = description;
                description = '';
            }

            nbrInteractions = typeof nbrInteractions === 'number' ? nbrInteractions : 30;

            var global      = this.global
            var isBody      = el == global.document.body

            var me          = this,
                offset      = me.$(el).offset(),
                right       = offset.left + me.$(isBody ? global : el).width(),
                bottom      = offset.top + me.$(isBody ? global : el).height();

            var actionLog   = []
            var R           = Siesta.Resource('Siesta.Test.Element');

            var queue       = new Siesta.Util.Queue({
                deferer         : me.originalSetTimeout,
                deferClearer    : me.originalClearTimeout,

                interval        : 50,

                observeTest     : this,

                processor   : function (data) {
                    if (me.nbrExceptions || me.failed) {
                        assertionChecker()

                        // do not continue if the test has detected an exception thrown
                        queue.abort()
                    } else {
                        var async       = me.beginAsync(null, function (test) {
                            test.fail(
                                description || R.get('monkeyNoExceptions'),
                                R.get('monkeyActionLog') + ":" + JSON.stringify(actionLog)
                            )

                            return true
                        });

                        var next        = data.next

                        data.next       = function () {
                            // callback to call after each monkey action
                            stepCallback && stepCallback(data)

                            me.endAsync(async)

                            next()
                        }

                        data.action(data)
                    }
                }
            });

            var dummy       = []
            dummy.length    = nbrInteractions

            var ignoreActionOnAnchor      = function (data, i) {
                var target          = me.normalizeElement(data.dragFrom || data.xy)
                // Prevent also drag target as it could be a link tag
                var dragToTarget    = data.dragTo && me.normalizeElement(data.dragTo)

                // do not click on <a> elements, unless those
                // w/o `href' or with #hash-style hrefs
                if (
                    (target && (target.tagName.toLowerCase() == 'a' || $(target).closest('a').length > 0)) ||
                    dragToTarget && (dragToTarget.tagName.toLowerCase() == 'a' || $(dragToTarget).closest('a').length > 0)
                ) {
                    data.next()

                    return true
                } else
                    return false
            }

            Joose.A.each(dummy, function (value, i) {
                // Inject { waitForSelector : 'body' } before every monkey action to make sure we always have a body
                queue.addAsyncStep({
                    action          : function (data) {
                        me.waitForSelector('body', data.next);
                    }
                });

                var xy = [ me.randomBetween(offset.left, right), me.randomBetween(offset.top, bottom) ];

                switch (me.randomBetween(0, 4)) {
                    case 0:
                        queue.addAsyncStep({
                            action          : function (data) {
                                if (!ignoreActionOnAnchor(data)) {
                                    actionLog.push({ 'click' : xy })

                                    me.click(data.xy, data.next)
                                }
                            },
                            xy              : xy
                        });
                    break;

                    case 1:
                        queue.addAsyncStep({
                            action          : function (data) {
                                if (!ignoreActionOnAnchor(data)) {
                                    actionLog.push({ 'doubleclick' : xy })

                                    me.doubleClick(data.xy, data.next)
                                }
                            },
                            xy              : xy
                        });
                    break;

                    case 2:
                        queue.addAsyncStep({
                            action          : function (data) {
                                if (me.simulator.type != 'native' && "oncontextmenu" in window) {
                                    actionLog.push({ 'rightclick' : xy })

                                    me.rightClick(data.xy, data.next)
                                } else {
                                    if (!ignoreActionOnAnchor(data)) {
                                        actionLog.push({ 'click' : xy })

                                        me.click(data.xy, data.next)
                                    }
                                }
                            },
                            xy              : xy
                        });
                    break;

                    case 3:
                        var dragTo      = [ me.randomBetween(offset.left, right), me.randomBetween(offset.top, bottom) ]

                        queue.addAsyncStep({
                            action          : function (data) {
                                if (!ignoreActionOnAnchor(data)) {
                                    actionLog.push({
                                        action  : 'drag',
                                        target  : xy,
                                        to      : dragTo
                                    })

                                    me.dragTo(data.dragFrom, data.dragTo, data.next)
                                }
                            },
                            dragFrom        : xy,
                            dragTo          : dragTo
                        });
                    break;

                    case 4:
                        var text = me.getRandomTypeString(15)

                        // First click somewhere then type
                        queue.addAsyncStep({
                            action          : function (data) {
                                if (!ignoreActionOnAnchor(data)) {
                                    actionLog.push({ 'click' : xy })

                                    me.click(data.xy, function () {
                                        me.waitForSelector('body', function () {
                                            actionLog.push({ 'type' : text.replace("'", "\\'") })

                                            me.type(null, text, data.next)
                                        });
                                    })
                                }
                            },
                            xy              : xy
                        });
                    break;
                }
            })

            var checkerActivated    = false

            var assertionChecker    = function () {
                checkerActivated    = true

                if (me.nbrExceptions || me.failed) {
                    me.fail(description || R.get('monkeyNoExceptions'), R.get('monkeyActionLog') + ":" + JSON.stringify(actionLog))
                } else
                    me.pass(description || R.get('monkeyNoExceptions'))
            }

            this.on('beforetestfinalizeearly', assertionChecker)

            queue.run(function () {
                if (!checkerActivated) {
                    me.un('beforetestfinalizeearly', assertionChecker)

                    assertionChecker()
                }

                this.suppressPassedWaitForAssertion = false;

                me.processCallbackFromTest(callback, [actionLog], scope || me)
            });
        },

        /**
         * Passes if the element has the supplied CSS classname
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {String} cls The class name to check for
         * @param {String} [description] The description for the assertion
         */
        hasCls : function (el, cls, description) {
            var R           = Siesta.Resource('Siesta.Test.Element');

            el              = this.normalizeElement(el);

            if (this.$(el).hasClass(cls)) {
                this.pass(description, {
                    descTpl     : R.get('elementHasClass') + ' {cls}',
                    cls         : cls
                });
            } else {
                this.fail(description, {
                    assertionName   : 'hasCls',

                    got             : el.className,
                    gotDesc         : R.get('elementClasses'),
                    need            : cls,
                    needDesc        : R.get('needClass')
                })
            }
        },


        /**
         * Passes if the element does not have the supplied CSS classname
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {String} cls The class name to check for
         * @param {String} [description] The description for the assertion
         */
        hasNotCls : function (el, cls, description) {
            var R           = Siesta.Resource('Siesta.Test.Element');

            el              = this.normalizeElement(el);

            if (!this.$(el).hasClass(cls)) {
                this.pass(description, {
                    descTpl         : R.get('elementHasNoClass') + ' {cls}',
                    cls             : cls
                });
            } else {
                this.fail(description, {
                    assertionName   : 'hasNotCls',
                    got             : el.className,
                    gotDesc         : R.get('elementClasses'),
                    annotation      : R.get('elementHasClass') + ' [' + cls + ']'
                })
            }
        },

        /**
         * Passes if the element has the supplied style value
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {String} property The style property to check for
         * @param {String} value The style value to check for
         * @param {String} [description] The description for the assertion
         */
        hasStyle : function (el, property, value, description) {
            var R           = Siesta.Resource('Siesta.Test.Element');

            el              = this.normalizeElement(el);

            if (this.$(el).css(property) === value) {
                this.pass(description, {
                    descTpl         : R.get('hasStyleDescTpl'),
                    value           : value,
                    property        : property
                });
            } else {
                this.fail(description, {
                    assertionName   : 'hasStyle',
                    got             : this.$(el).css(property),
                    gotDesc         : R.get('elementStyles'),
                    need            : value,
                    needDesc        :  R.get('needStyle')
                });
            }
        },


        /**
         * Passes if the element does not have the supplied style value
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {String} property The style property to check for
         * @param {String} value The style value to check for
         * @param {String} [description] The description for the assertion
         */
        hasNotStyle : function (el, property, value, description) {
            var R           = Siesta.Resource('Siesta.Test.Element');

            el              = this.normalizeElement(el);

            if (this.$(el).css(property) !== value) {
                this.pass(description, {
                    descTpl         : R.get('hasNotStyleDescTpl'),
                    value           : value,
                    property        : property
                });
            } else {
                this.fail(description, {
                    assertionName   : 'hasNotStyle',
                    got             : el.style.toString(),
                    gotDesc         : R.get('elementStyles'),
                    annotation      : R.get('hasTheStyle') + ' [' + property + ']'
                });
            }
        },

        /**
         * Waits for a certain CSS selector to be found at the passed XY coordinate, and calls the callback when found.
         * The callback will receive the element from the passed XY coordinates.
         *
         * @param {Array} xy The x and y coordinates to query
         * @param {String} selector The CSS selector to check for
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value.
         */
        waitForSelectorAt : function(xy, selector, callback, scope, timeout) {
            var R           = Siesta.Resource('Siesta.Test.Element');

            if (!selector) throw R.get('noCssSelector');

            var me      = this

            return this.waitFor({
                method          : function() {
                    var el = me.elementFromPoint(xy[0], xy[1], true);

                    if (el && me.$(el).is(selector)) return el;
                },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForSelectorAt',
                description     : ' ' + R.get('selector') + ' "' + selector + '" ' + R.get('toAppearAt') + ': [' + xy.toString() + ']'
            });
        },

        /**
         * Waits for a certain CSS selector to be found at current cursor position, and calls the callback when found.
         * The callback will receive the element found.
         *
         * @param {String} selector The CSS selector to check for
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value.
         */
        waitForSelectorAtCursor : function(selector, callback, scope, timeout) {
            return this.waitForSelectorAt(this.simulator.currentPosition, selector, callback, scope, timeout);
        },

        /**
         * Waits for a certain CSS selector to be found in the DOM, and then calls the callback supplied.
         * The callback will receive the results of jQuery selector.
         *
         * @param {String} selector The CSS selector to check for
         * @param {Siesta.Test.ActionTarget} root (optional) The root element in which to detect the selector.
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value.
         */
        waitForSelector : function(selector, root, callback, scope, timeout) {
            var R           = Siesta.Resource('Siesta.Test.Element');
            var me          = this;

            if (!selector) throw R.get('noCssSelector');

            if (this.typeOf(root) == 'Function') {
                timeout     = scope;
                scope       = callback;
                callback    = root;
                root        = null;
            }

            if (root) root  = this.normalizeElement(root);

            return this.waitFor({
                method          : function() {
                    var result = me.$(selector, root);
                    if (result.length > 0) return result;
                },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForSelector',
                description     : ' ' + R.get('selector') + ' "' + selector + '" ' + R.get('toAppear')
            });
        },


        /**
         * Waits till all the CSS selectors from the provided array to be found in the DOM, and then calls the callback supplied.
         *
         * @param {Array[String]} selectors The array of CSS selectors to check for
         * @param {Siesta.Test.ActionTarget} root (optional) The root element in which to detect the selector.
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value.
         */
        waitForSelectors : function(selectors, root, callback, scope, timeout) {
            var R           = Siesta.Resource('Siesta.Test.Element');

            if (selectors.length < 1) throw R.get('waitForSelectorsBadInput');

            if (this.typeOf(root) == 'Function') {
                timeout     = scope;
                scope       = callback;
                callback    = root;
                root        = null;
            }

            if (root) root  = this.normalizeElement(root);

            var me          = this

            return this.waitFor({
                method          :  function () {
                    var allPresent  = true

                    Joose.A.each(selectors, function (selector) {
                        if (me.$(selector, root).length === 0) {
                            allPresent = false
                            // stop iteration
                            return false
                        }
                    })

                    return allPresent
                },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForSelectors',
                description     : ' ' + R.get('selectors') + ' "' + selectors + '" ' + R.get('toAppear')
            });
        },



        /**
         * Waits for a certain CSS selector to not be found in the DOM, and then calls the callback supplied.
         *
         * @param {String} selector The CSS selector to check for
         * @param {Siesta.Test.ActionTarget} root (optional) The root element in which to detect the selector.
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test#waitForTimeout} value.
         */
        waitForSelectorNotFound : function(selector, root, callback, scope, timeout) {
            var R           = Siesta.Resource('Siesta.Test.Element');
            var me          = this;

            if (!selector) throw 'A CSS selector must be supplied';

            if (this.typeOf(root) == 'Function') {
                timeout     = scope;
                scope       = callback;
                callback    = root;
                root        = null;
            }

            if (root) root  = this.normalizeElement(root);

            return this.waitFor({
                method          : function() { return me.$(selector, root).length === 0; },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForSelectorNotFound',
                description     : ' ' + R.get('selector') + ' "' + selector + '" ' + R.get('toDisappear')
            });
        },


        /**
         * Waits until the passed element becomes "visible" in the DOM and calls the provided callback.
         * Please note, that "visible" means element will just have a DOM node, and still may be hidden by another visible element.
         *
         * The callback will receive the passed element as the 1st argument.
         *
         * See also {@link #waitForElementTop} method.
         *
         * @param {Siesta.Test.ActionTarget} el The element to look for.
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForElementVisible : function(el, callback, scope, timeout) {
            var R       = Siesta.Resource('Siesta.Test.Element');

            return this.waitFor({
                method          : function() {
                    var normalized = this.normalizeElement(el, true);

                    if (normalized && this.isElementVisible(normalized)) return normalized;
                },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForElementVisible',
                description     : ' ' + R.get('element') + ' "' + el.toString() + '" ' + R.get('toAppear')
            });
        },

        /**
         * Waits until the passed element is becomes not "visible" in the DOM and call the provided callback.
         * Please note, that "visible" means element will just have a DOM node, and still may be hidden by another visible element.
         *
         * The callback will receive the passed element as the 1st argument.
         *
         * See also {@link #waitForElementNotTop} method.
         *
         * @param {Siesta.Test.ActionTarget} el The element to look for.
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForElementNotVisible : function(el, callback, scope, timeout) {
            el          = this.normalizeElement(el);

            var R       = Siesta.Resource('Siesta.Test.Element');
            var me      = this;

            return this.waitFor({
                method          : function() { return !me.isElementVisible(el) && el; },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForElementNotVisible',
                description     :  ' ' + R.get('element') + ' "' + el.toString() +  '" ' + R.get('toDisappear')
            });
        },


        /**
         * Waits until the passed element is the 'top' element in the DOM and call the provided callback.
         *
         * The callback will receive the passed element as the 1st argument.
         *
         * @param {Siesta.Test.ActionTarget} el The element to look for.
         * @param {Function} callback The callback to call
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForElementTop : function(el, callback, scope, timeout) {
            var R       = Siesta.Resource('Siesta.Test.Element');

            return this.waitFor({
                method          : function() {
                    var normalized = this.normalizeElement(el, true);

                    if (normalized && this.elementIsTop(normalized, true)) {
                        return normalized;
                    }
                },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForElementTop',
                description     : ' ' + R.get('element') + ' "' + el.toString() + '" ' + R.get('toBeTopEl')
            });
        },

        /**
         * Waits until the passed element is not the 'top' element in the DOM and calls the provided callback with the element found.
         *
         * The callback will receive the actual top element.
         *
         * @param {Siesta.Test.ActionTarget} el The element to look for.
         * @param {Function} callback The callback to call
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForElementNotTop : function(el, callback, scope, timeout) {
            el          = this.normalizeElement(el);

            var R       = Siesta.Resource('Siesta.Test.Element');
            var me      = this

            return this.waitFor({
                method          : function() {
                    if (!me.elementIsTop(el, true)) {
                        var center = me.findCenter(el);
                        return me.elementFromPoint(center[0], center[1], true);
                    }
                },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForElementNotTop',
                description     : ' ' + R.get('element') + ' "' + el.toString() + '" ' + R.get('toNotBeTopEl')
            });
        },

        /**
         * Passes if the element is in the DOM and visible.
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {String} [description] The description for the assertion
         */
        elementIsVisible : function(el, description) {
            el = this.normalizeElement(el, true, false, false, { ignoreNonVisible : false });

            this.ok(el && this.isElementVisible(el), description);
        },

        /**
         * Passes if the element is not visible.
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {String} [description] The description for the assertion
         */
        elementIsNotVisible : function(el, description) {
            el = this.normalizeElement(el, false, false, false, { ignoreNonVisible : false });

            this.notOk(this.isElementVisible(el), description);
        },

        /**
         * Utility method which checks if the passed method is the 'top' element at its position. By default, "top" element means,
         * that center point of the element is not covered with any other elements. You can also check any other point reachability
         * using the "offset" argument.
         *
         * @param {Siesta.Test.ActionTarget} el The element to look for.
         * @param {Boolean} allowChildren true to also include child nodes. False to strictly check for the passed element.
         * @param {Array} offset An array of 2 elements, defining "x" and "y" offset from the left-top corner of the element
         *
         * @return {Boolean} true if the element is the top element.
         */
        elementIsTop : function (el, allowChildren, offset) {
            el              = this.normalizeElement(el);

            // Workaround for OPTION elements which don't behave like normal DOM elements. jQuery always consider them invisible.
            // Decide based on visibility of the parent SELECT node
            if (el && el.nodeName.toLowerCase() === 'option') {
                el = this.$(el).closest('select')[0];
            }

            var elDoc       = el.ownerDocument

            var localPoint  = this.getTargetCoordinate(el, true, offset)
            var foundEl     = elDoc.elementFromPoint(localPoint[ 0 ], localPoint[ 1 ]);

            return foundEl && (foundEl === el || (allowChildren && this.$(foundEl).closest(el).length > 0));
        },

        // Helper method to find out if an offset is targeting a point outside its target
        // Assumes the el passed is visible
        isOffsetInsideElementBox : function (el, offset) {
            if (!offset) return true;

            var $el = this.$(this.normalizeElement(el));
            var w   = $el.outerWidth();
            var h   = $el.outerHeight();

            offset = this.normalizeOffset(offset, $el);

            return offset[0] >= 0 && offset[0] < w &&
                   offset[1] >= 0 && offset[1] < h;
        },

        /**
         * Passes if the element is found at the supplied xy coordinates.
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {Array} xy The xy coordinate to query.
         * @param {Boolean} allowChildren true to also include child nodes. False to strictly check for the passed element.
         * @param {String} [description] The description for the assertion
         */
        elementIsAt : function(el, xy, allowChildren, description) {
            el              = this.normalizeElement(el);

            var foundEl     = this.elementFromPoint(xy[0], xy[1], true);
            var R           = Siesta.Resource('Siesta.Test.Element');

            if (!foundEl) {
                this.fail(description, {
                    assertionName       : 'elementIsAt',
                    got                 : { x: xy[0], y : xy[1] },
                    gotDesc             : R.get('Position'),
                    annotation          : R.get('noElementAtPosition')
                });
            } else if (allowChildren) {
                if (foundEl === el || this.$(foundEl).closest(el).length > 0) {
                    this.pass(description, {
                        descTpl         : R.get('elementIsAtDescTpl'),
                        x               : xy[ 0 ],
                        y               : xy[ 1 ]
                    });
                } else {
                    this.fail(description, {
                        assertionName   : 'elementIsAt',
                        got             : foundEl,
                        gotDesc         : R.get('topElement'),
                        need            : el,
                        needDesc        : R.get('allowChildrenDesc'),
                        annotation      : R.get('allowChildrenAnnotation')
                    });
                }
            } else {
                if (foundEl === el) {
                    this.pass(description, {
                        descTpl         : R.get('elementIsAtPassTpl'),
                        x               : xy[ 0 ],
                        y               : xy[ 1 ]
                    });
                } else {
                    this.fail(description, {
                        assertionName   : 'elementIsAt',
                        got             : foundEl,
                        gotDesc         : R.get('topElement'),
                        need            : el,
                        needDesc        : 'Should be',
                        annotation      : R.get('noChildrenFailAnnotation')
                    });
                }
            }
        },

        /**
         * Passes if the element is the top element (using its center xy coordinates). "Top" element means,
         * that element is not covered with any other elements.
         *
         * This assertion can be used for example to test, that some element, that appears only when mouse hovers some other element is accessible by user
         * with mouse (which is not always true because of various z-index issues).
         *
         * @param {Siesta.Test.ActionTarget} el The element to look for.
         * @param {Boolean} allowChildren true to also include child nodes. False to strictly check for the passed element.
         * @param {String} [description] The description for the assertion
         * @param {Boolean} strict true to check all four corners of the element. False to only check at element center.
         */
        elementIsTopElement : function(el, allowChildren, description, strict) {
            el = this.normalizeElement(el);

            if (strict) {
                var o           = this.$(el).offset();
                var R           = Siesta.Resource('Siesta.Test.Element');
                var region      = {
                    top     : o.top,
                    right   : o.left + this.$(el).outerWidth(),
                    bottom  : o.top + this.$(el).outerHeight(),
                    left    : o.left
                };

                this.elementIsAt(el, [region.left+1, region.top+1], allowChildren, description + ' ' + R.get('topLeft'));
                this.elementIsAt(el, [region.left+1, region.bottom-1], allowChildren, description + ' ' + R.get('bottomLeft'));
                this.elementIsAt(el, [region.right-1, region.top+1], allowChildren, description + ' ' + R.get('topRight'));
                this.elementIsAt(el, [region.right-1, region.bottom-1], allowChildren, description + ' ' + R.get('bottomRight'));
            } else {
                this.elementIsAt(el, this.findCenter(el), allowChildren, description);
            }
        },

        /**
         * Passes if the element is not the top element (using its center xy coordinates).
         *
         * @param {Siesta.Test.ActionTarget} el The element to look for.
         * @param {Boolean} allowChildren true to also include child nodes. False to strictly check for the passed element.
         * @param {String} [description] The description for the assertion
         */
        elementIsNotTopElement : function(el, allowChildren, description) {
            el              = this.normalizeElement(el);
            var center      = this.findCenter(el);

            var foundEl     = this.elementFromPoint(center[ 0 ], center[ 1 ], true);

            if (!foundEl) {
                var R           = Siesta.Resource('Siesta.Test.Element');

                this.pass(description, {
                    descTpl         : R.get('elementIsNotTopElementPassTpl')
                });

                return
            }

            if (allowChildren) {
                this.ok(foundEl !== el && this.$(foundEl).closest(el).length === 0, description);
            } else {
                this.isnt(foundEl, el, description);
            }
        },

        /**
         * Passes if the element is found at the supplied xy coordinates.
         *
         * @param {String} selector The selector to query for
         * @param {Array} xy The xy coordinate to query.
         * @param {Boolean} allowChildren true to also include child nodes. False to strictly check for the passed element.
         * @param {String} [description] The description for the assertion
         */
        selectorIsAt : function(selector, xy, description) {
            var R           = Siesta.Resource('Siesta.Test.Element');

            if (!selector) throw R.get('noCssSelector');

            var foundEl = this.$(this.elementFromPoint(xy[0], xy[1], true));

            if (foundEl.has(selector).length > 0 || foundEl.closest(selector).length > 0) {
                this.pass(description, {
                    descTpl         : R.get('selectorIsAtPassTpl'),
                    selector        : selector,
                    xy              : xy
                });
            } else {
                this.fail(description, {
                    got             : foundEl[0].outerHTML ? foundEl[0].outerHTML : foundEl[0].innerHTML,
                    need            : R.get('elementMatching') + ' ' + selector,
                    assertionName   : 'selectorIsAt',
                    annotation      : R.get('selectorIsAtFailAnnotation') + ' [' + xy + ']'
                });
            }
        },

        /**
         * Passes if the selector is found in the DOM
         *
         * @param {String} selector The selector to query for
         * @param {String} [description] The description for the assertion
         */
        selectorExists : function (selector, description) {
            var R           = Siesta.Resource('Siesta.Test.Element');

            if (!selector) throw R.get('noCssSelector');

            if (this.$(selector).length <= 0) {
                this.fail(description, R.get('selectorExistsFailTpl') + ' : ' + selector);
            } else {
                this.pass(description, {
                    descTpl         : R.get('selectorExistsPassTpl'),
                    selector        : selector
                });
            }
        },

        /**
         * Passes if the selector is not found in the DOM
         *
         * @param {String} selector The selector to query for
         * @param {String} [description] The description for the assertion
         */
        selectorNotExists : function (selector, description) {
            var R           = Siesta.Resource('Siesta.Test.Element');
            var els         = this.$(selector);

            if (els.length > 0) {
                this.fail(description, {
                    descTpl     : R.get('selectorNotExistsFailTpl') + ': ' + selector,
                    annotation  : $.map(els, function (el, i) { return (i + 1) + ". " + el.cloneNode().outerHTML; }).join('\r\n')
                });
            } else {
                this.pass(description, {
                    descTpl         : R.get('selectorNotExistsPassTpl'),
                    selector        : selector
                });
            }
        },

        /**
         * Waits until the passed scroll property of the element has changed.
         *
         * The callback will receive the new `scroll` value.
         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {String} side 'left' or 'top'
         * @param {Function} callback The callback to call
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForScrollChange : function(el, side, callback, scope, timeout) {
            el                  = this.normalizeElement(el);
            var scrollProp      = 'scroll' + Joose.S.uppercaseFirst(side);
            var original        = el[ scrollProp ];
            var R               = Siesta.Resource('Siesta.Test.Element');

            return this.waitFor({
                method          : function() { if (el[ scrollProp ] !== original) return el[ scrollProp ]; },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForScrollChange',
                description     : ' ' + scrollProp + ' ' + R.get('toChangeForElement') + ' ' + el.toString()
            });
        },

        /**
         * Waits until the `scrollLeft` property of the element has changed.
         *
         * The callback will receive the new `scrollLeft` value.
         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {Function} callback The callback to call
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForScrollLeftChange : function(el, callback, scope, timeout) {
            return this.waitForScrollChange(this.normalizeElement(el), 'left', callback, scope, timeout);
        },

        /**
         * Waits until the scrollTop property of the element has changed
         *
         * The callback will receive the new `scrollTop` value.
         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {Function} callback The callback to call
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForScrollTopChange : function(el, callback, scope, timeout) {
            return this.waitForScrollChange(this.normalizeElement(el), 'top', callback, scope, timeout);
        },


        /**
         * This method changes the `scrollLeft` and `scrollTop` properties of the dom element, then waits for the "scroll" event
         * from it and calls the provided callback.
         *
         * For example:
         *

    // scroll the domEl to the 50px horizontal offset, 100px vertical, wait for "scroll" event, call the callback
    t.scrollTo(domEl, 50, 100, function () { ... })

         * Optionally it can also wait some additional time before calling the callback:
         *
    // scroll the domEl to the 50px horizontal offset, 100px vertical, wait for "scroll" event, wait 1000ms more, call the callback
    t.scrollTo(domEl, 50, 100, function () { ... }, 1000)

         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {Number} newLeft The value for the `scrollLeft` property
         * @param {Number} newTop The value for the `scrollTop` property
         * @param {Function} callback A function to call after "scroll" event has been fired and additional delay completed (if any)
         * @param {Number} [delay] Optional, additional delay before calling a callback
         *
         * @return {Object} An object with new scrolling position
         * @return {Number} return.scrollLeft The new value of the `scrollLeft` property of the dom element
         * @return {Number} return.scrollTop The new value of the `scrollTop` property of the dom element
         */
        scrollTo : function (el, scrollLeft, scrollTop, callback, delay, allowEmpty) {
            el                          = this.normalizeElement(el, allowEmpty);

            if (!el) { this.processCallbackFromTest(callback); return }

            var me                      = this
            var originalSetTimeout      = this.originalSetTimeout;

            var doc                     = el.ownerDocument

            var waiter                  = this.waitForEvent(el == this.getElForPageScroll() ? doc : el, 'scroll', function () {
                if (delay > 0) {
                    var async               = me.beginAsync(delay + 100)

                    originalSetTimeout(function () {
                        me.endAsync(async)

                        me.processCallbackFromTest(callback)
                    }, delay)
                } else
                    me.processCallbackFromTest(callback)
            })

            var prevScrollTop   = el.scrollTop
            var prevScrollLeft  = el.scrollLeft

            el.scrollLeft       = scrollLeft
            el.scrollTop        = scrollTop

            // no event will be fired in this case probably - force the waiting operation to complete
            if (el.scrollTop == prevScrollTop && el.scrollLeft == prevScrollLeft) {
                waiter.force()
            }

            return { scrollLeft : el.scrollLeft, scrollTop : el.scrollTop }
        },


        /**
         * This method changes the "scrollTop" property of the dom element, then waits for the "scroll" event from it and calls the provided callback.
         *
         * For example:
         *

    // scroll the domEl to the 100px offset, wait for "scroll" event, call the callback
    t.scrollVerticallyTo(domEl, 100, function () { ... })

         * Optionally it can also wait some additional time before calling the callback:
         *
    // scroll the domEl to the 100px offset, wait for "scroll" event, wait 1000ms more, call the callback
    t.scrollVerticallyTo(domEl, 100, 1000, function () { ... })

         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {Number} newTop The value for the "scrollTop" property
         * @param {Number} [delay] Additional delay, this argument can be omitted
         * @param {Function} callback A function to call after "scroll" event has been fired and additional delay completed (if any)
         *
         * @return {Number} The new value of the "scrollTop" property of the dom element
         */
        scrollVerticallyTo : function (el, newTop, delay, callback) {
            el                          = this.normalizeElement(el);

            if (this.typeOf(delay) != 'Number') {
                callback                = delay
                delay                   = null
            }

            var me                      = this
            var originalSetTimeout      = this.originalSetTimeout;

            var doc                     = el.ownerDocument

            var waiter                  = this.waitForEvent(el == this.getElForPageScroll() ? doc : el, 'scroll', function () {
                if (delay > 0) {
                    var async               = me.beginAsync(delay + 100)

                    originalSetTimeout(function () {
                        me.endAsync(async)

                        me.processCallbackFromTest(callback)
                    }, delay)
                } else
                    me.processCallbackFromTest(callback)
            })

            var prevScrollTop   = el.scrollTop

            el.scrollTop        = newTop

            // no event will be fired in this case probably - force the waiting operation to complete
            if (el.scrollTop == prevScrollTop) {
                waiter.force()
            }

            // re-read the scrollTop value and return it (newTop can be too big for example and will be truncated)
            return el.scrollTop
        },


        /**
         * This method changes the "scrollLeft" property of the dom element, then waits for the "scroll" event from it and calls the provided callback.
         *
         * For example:
         *

    // scroll the domEl to the 100px offset, wait for "scroll" event, call the callback
    t.scrollHorizontallyTo(domEl, 100, function () { ... })

         * Optionally it can also wait some additional time before calling the callback:
         *
    // scroll the domEl to the 100px offset, wait for "scroll" event, wait 1000ms more, call the callback
    t.scrollHorizontallyTo(domEl, 100, 1000, function () { ... })

         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {Number} newLeft The value for the "scrollLeft" property
         * @param {Number} [delay] Additional delay, this argument can be omitted
         * @param {Function} callback A function to call after "scroll" event has been fired and additional delay completed (if any)
         *
         * @return {Number} The new value of the "scrollLeft" property of the dom element
         */
        scrollHorizontallyTo : function (el, newLeft, delay, callback) {
            el                          = this.normalizeElement(el);

            if (this.typeOf(delay) != 'Number') {
                callback                = delay
                delay                   = null
            }

            var me                      = this
            var originalSetTimeout      = this.originalSetTimeout;

            var doc                     = el.ownerDocument

            var waiter                  = this.waitForEvent(el == this.getElForPageScroll() ? doc : el, 'scroll', function () {
                if (delay > 0) {
                    var async               = me.beginAsync(delay + 100)

                    originalSetTimeout(function () {
                        me.endAsync(async)

                        me.processCallbackFromTest(callback)
                    }, delay)
                } else
                    me.processCallbackFromTest(callback)
            })

            var prevScrollLeft  = el.scrollLeft

            el.scrollLeft       = newLeft

            // no event will be fired in this case probably - force the waiting operation to complete
            if (el.scrollLeft == prevScrollLeft) {
                waiter.force()
            }

            // re-read the scrollLeft value and return it (newLeft can be too big for example and will be truncated)
            return el.scrollLeft
        },



        /**
         * This method accepts an array of the DOM elements and performs a mouse click on them, in order. After that, it calls the provided callback:
         *
```javascript
t.clickAll([ el1, el2 ], function () {
    ...
})

 * the elements can be also provided inline, w/o wrapping array:

t.clickAll(el1, el2, function () {
    ...
})
```
         *
         * @param {Array[Siesta.Test.ActionTarget]} elements The array of elements to click
         * @param {Function} callback The function to call after clicking all elements
         */
        clickAll : function () {
            var args        = Array.prototype.concat.apply([], arguments)
            var callback

            if (this.typeOf(args[ args.length - 1 ]) == 'Function') callback = args.pop()

            // poor-man Array.flatten, with only 1 level of nesting support
            args            = Array.prototype.concat.apply([], args)

            var steps       = []

            Joose.A.each(args, function (arg) {
                steps.push({
                    action      : 'click',
                    target      : arg
                })
            })

            var me          = this

            if (callback) steps.push(function () {
                me.processCallbackFromTest(callback)
            })

            this.chain.apply(this, steps)
        },

        /*
        * @deprecated
        * Alias for {@link clickAll}
        * */
        chainClick : function() {
            return this.clickAll.apply(this, arguments);
        },


        /**
         * This method is a wrapper around the {@link #clickAll}, it performs a click on the every element found by the DOM query.
         *
         * You can specify the optional `root` element to start the query from:
         *
         *      t.clickSelector('.my-grid .x-grid-row', someEl, function () {})
         *
         * or omit it (query will start from the document):
         *
         *      t.clickSelector('.my-grid .x-grid-row', function () {})
         *
         * The provided callback will receive an array with DOM elements - result of query.
         *
         *
         * @param {String} selector The selector/xpath query
         * @param {Siesta.Test.ActionTarget} [root=document] The root of the query, defaults to the `document`. You can omit this parameter.
         * @param {Function} [callback]
         * @param {Object} [scope]
         */
        clickSelector : function (selector, root, callback, scope) {
            if (arguments.length > 1 && this.typeOf(arguments[ 1 ]) == 'Function') {
                scope       = callback;
                callback    = root;
                root        = null;
            }

            if (root) root = this.normalizeElement(root);

            // convert the result from jQuery dom query to a usual array
            var result      = Joose.A.map(this.sizzle(selector, root), function (el) { return el });

            this.clickAll(result, function () { callback && callback.call(scope || this, result) })
        },


        /**
         * This assertion passes when the DOM query with specified selector returns the expected number of elements
         *
         * You can specify the optional `root` element to start the query from:
         *
         *      t.selectorCountIs('.x-grid-row', grid, 5, "Grid has 5 rows")
         *
         * or omit it (query will start from the document):
         *
         *      t.selectorCountIs('.x-grid-row', 0, "No grid rows on the page")
         *
         * @param {String} selector DOM query selector
         * @param {Siesta.Test.ActionTarget} [root] An optional root element to start the query from, if omited query will start from the document
         * @param {Number} count The expected number of elements in the query result
         * @param {String} [description] The description for the assertion
         */
        selectorCountIs : function (selector, root, count, description) {
            var R               = Siesta.Resource('Siesta.Test.Element');

            if (!selector) throw R.get('noCssSelector');

            if (this.typeOf(root) == 'Number') {
                description     = count
                count           = root
                root            = null
            } else
                root            = this.normalizeElement(root)

            var inDOMCount      = this.$(selector, root).length

            if (inDOMCount != count) {
                this.fail(description, {
                    assertionName   : 'selectorCountIs',
                    descTpl         : R.get('selectorCountIsFailTpl'),
                    selector        : selector,
                    got             : inDOMCount,
                    need            : count
                });
            } else {
                this.pass(description, {
                    descTpl         : R.get('selectorCountIsPassTpl'),
                    count           : count,
                    selector        : selector
                });
            }
        },


        /**
         * Passes if the passed element is inside of the visible viewport
         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {String} [description] The description for the assertion
         */
        isInView : function (el, description) {
            if (this.elementIsInView(el)) {
                var R           = Siesta.Resource('Siesta.Test.Element');

                this.pass(description, {
                    descTpl         : R.get('isInViewPassTpl')
                })
            }
            else
                this.fail(description, {
                    assertionName   : 'isInView'
                })
        },

        /**
         * Returns true if the passed element is inside of the visible viewport
         *
         * @param {Siesta.Test.ActionTarget} el The element
         */
        elementIsInView : function(el) {
            el              = this.normalizeElement(el);

            var inView      = false;
            var offset      = this.$(el).offset();

            if (offset) {
                var docViewTop      = $(this.global).scrollTop();
                var docViewBottom   = docViewTop + $(this.global).height();

                var elemTop         = offset.top;
                var elemBottom      = elemTop + $(el).height();

                inView              = elemBottom >= docViewTop && elemTop <= docViewBottom;
            }

            return inView;
        },

        /**
         * Waits until element is inside in the visible viewport and then calls the supplied callback
         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {Function} callback The callback to call
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitUntilInView : function (el, callback, scope, timeout) {
            var me          = this;
            var R           = Siesta.Resource('Siesta.Test.Element');

            this.waitFor({
                method          : function() {
                    var normalized  = this.normalizeElement(el, true);

                    if(normalized && me.elementIsInView(normalized)) {
                        return normalized;
                    }
                },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitUntilInView',
                description     : el.toString + ' ' + R.get('toAppearInTheViewport')
            });
        },


        findScrolledParent : function(el) {
            var body   = el.ownerDocument.body;

            var parent = this.$(el);

            while (parent = parent.parent(), parent.length && parent[ 0 ] != body) {
                if (parent[0].scrollTop > 0 || parent[0].scrollLeft > 0) {
                    return parent[0];
                }
            }
        },


        focus : function (el, tryPreventScrollChange) {
            var prevIndex   = el.getAttribute('tabIndex')
            var scrolledParent;

            if (this.activeElement() === el) return;

            try {
                if (prevIndex == null) el.setAttribute('tabIndex', -1)

                if (tryPreventScrollChange) {
                    var oldScrollLeft, oldScrollTop, pageScrollX, pageScrollY;

                    // In Chrome, when calling focus() manually on an element - it's scrolled into view in its parent hierarchy
                    // Try to detect this and restore (This is far from optimal since application might have a listener triggering a desired
                    // scroll of this element. But not triggering focus() on mousedown seems like a worse situation
                    scrolledParent = this.findScrolledParent(el);

                    if (scrolledParent) {
                        oldScrollLeft   = scrolledParent.scrollLeft;
                        oldScrollTop    = scrolledParent.scrollTop;
                    }

                    pageScrollX         = this.getPageScrollX()
                    pageScrollY         = this.getPageScrollY()
                }

                el.focus({ preventScroll : tryPreventScrollChange })

                if (tryPreventScrollChange) {
                    if (scrolledParent) {
                        if (oldScrollLeft !== scrolledParent.scrollLeft)  {
                            scrolledParent.scrollLeft = oldScrollLeft;
                        }

                        if (oldScrollTop !== scrolledParent.scrollTop)  {
                            scrolledParent.scrollTop = oldScrollTop;
                        }
                    }

                    if (pageScrollX != this.getPageScrollX() || pageScrollY != this.getPageScrollY()) {
                        this.global.scrollTo(pageScrollX, pageScrollY)
                    }
                }
            } catch (e) {
            } finally {
                if (prevIndex == null)
                    el.removeAttribute('tabIndex')
                else
                    el.setAttribute('tabIndex', prevIndex)
            }
        },


        /**
         * Passes if the passed element has no content (whitespace will be trimmed)
         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {String} [description] The description for the assertion
         */
        elementIsEmpty : function (el, description) {

            el              = this.normalizeElement(el);

            if (el && this.isElementEmpty(el)) {
                var R           = Siesta.Resource('Siesta.Test.Element');

                this.pass(description, {
                    descTpl         : R.get('elementIsEmptyPassTpl')
                })
            }
            else
                this.fail(description, {
                    got             : el.innerHTML,
                    need            : '',
                    assertionName   : 'elementIsEmpty'
                })
        },

        /**
         * Passes if the passed element has some non-whitespace content
         *
         * @param {Siesta.Test.ActionTarget} el The element
         * @param {String} [description] The description for the assertion
         */
        elementIsNotEmpty : function (el, description) {
            el              = this.normalizeElement(el);

            if (el && !this.isElementEmpty(el)) {
                var R           = Siesta.Resource('Siesta.Test.Element');

                this.pass(description, {
                    descTpl         : R.get('elementIsNotEmptyPassTpl')
                })
            }
            else
                this.fail(description, {
                    assertionName   : 'elementIsNotEmpty'
                })
        },

        /**
         * Waits until the innerHTML of the passed element is empty (whitespace will be trimmed)
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForElementEmpty : function(el, callback, scope, timeout) {
            var me          = this;
            var R           = Siesta.Resource('Siesta.Test.Element');

            el              = this.normalizeElement(el);

            return this.waitFor({
                method          : function() { return me.isElementEmpty(el); },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForElementEmpty',
                description     : ' ' + R.get('elementToBeEmpty')
            });
        },

        /**
         * Waits until the innerHTML of the passed element contains some non-whitespace text.
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {Function} callback The callback to call after the CSS selector has been found
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
         */
        waitForElementNotEmpty : function(el, callback, scope, timeout) {
            var me          = this;
            var R           = Siesta.Resource('Siesta.Test.Element');

            el              = this.normalizeElement(el);

            return this.waitFor({
                method          : function() { return !me.isElementEmpty(el); },
                callback        : callback,
                scope           : scope,
                timeout         : timeout,
                assertionName   : 'waitForElementNotEmpty',
                description     : ' ' + R.get('elementToNotBeEmpty')
            });
        },

        isElementEmpty : function (el) {
            return !el.innerHTML.replace(/^\s+|\s+$/g, '');
        },

        /**
         * Passes if the target element has an attribute with the provided value.
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {String} attribute The attribute
         * @param {String} value The value
         * @param {String} [description] The description for the assertion
         */
        hasAttributeValue : function(el, attribute, value, description) {
            el              = this.normalizeElement(el);

            var foundValue = el.getAttribute(attribute);

            this.is(foundValue, value, description);
        },

        /**
         * Passes if the passed element has the expected value as its "value" property (use with SELECT, INPUT type elements).
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {Mixed} value The value to compare to.
         * @param {String} [description] The description of the assertion
         */
        hasValue : function(el, value, description) {
            el              = this.normalizeElement(el);

            var foundValue = el.value;

            this.is(foundValue, value, description);
        },

        /**
         * This assertion passes when the passed element offsetWidth matches the offsetWidth of the reference element
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {Siesta.Test.ActionTarget} referenceEl The element to compare against
         * @param {String} [description] The description of the assertion
         */
        hasSameWidth: function (el, referenceEl, description) {
            el          = this.normalizeElement(el);
            referenceEl = this.normalizeElement(referenceEl);

            this.is(el.offsetWidth, referenceEl.offsetWidth, description);
        },

        /**
         * This assertion passes when the passed element offsetHeight matches the offsetHeight of the reference element
         *
         * @param {Siesta.Test.ActionTarget} el The element to query
         * @param {Siesta.Test.ActionTarget} referenceEl The element to compare against
         * @param {String} [description] The description of the assertion
         */
        hasSameHeight: function (el, referenceEl, description) {
            el          = this.normalizeElement(el);
            referenceEl = this.normalizeElement(referenceEl);

            this.is(el.offsetHeight, referenceEl.offsetHeight, description);
        },


        isElementFocusable : function(el) {
            var disabled    = el.getAttribute('disabled') === "true";
            var nodeName    = el.nodeName.toLowerCase();
            // Other tags are covered in isTextInput
            var focusable   = { a : 1, area : 1, button : 1, object : 1, select : 1 };

            return !disabled && (
                this.isTextInput(el) || el.isContentEditable
                || (el.nodeName.toLowerCase() === 'input' && el.type === 'file')
                || (nodeName in focusable)
                || (el.getAttribute('tabIndex') != null && (!bowser.msie || String(el.getAttribute('unselectable')).toLowerCase() != 'on'))
            );
        }
    }
});