    /*

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

    */
    /**
    @class Siesta.Test.Browser
    @extends Siesta.Test
    @mixin Siesta.Test.TextSelection
    @mixin Siesta.Test.Element

    @mixin Siesta.Test.UserAgent.Mouse
    @mixin Siesta.Test.UserAgent.Keyboard
    @mixin Siesta.Test.UserAgent.Touch

    @mixin Siesta.Test.Browser.Role.CanGetElementFromPoint

    A base class for testing a generic browser functionality. It has various DOM-related assertions, and is not optimized for any framework.

    */
    Class('Siesta.Test.Browser', {

        isa         : Siesta.Test,

        does        : [
            Siesta.Util.Role.CanParseBrowser,
            Siesta.Util.Role.CanCalculatePageScroll,
            Siesta.Util.Role.Dom,
            Siesta.Test.Browser.Role.CanGetElementFromPoint,
            Siesta.Test.Browser.Role.CanWorkWithKeyboard,
            Siesta.Test.Browser.Role.CanRebindJQueryContext,

            Siesta.Test.UserAgent.Mouse,
            Siesta.Test.UserAgent.Keyboard,
            Siesta.Test.UserAgent.Touch,

            Siesta.Test.Element,
            Siesta.Test.TextSelection,
            Siesta.Test.Observable
        ],

        has : {
            /**
             * @property {Object} bowser An instance of browser detection library - [Bowser](https://github.com/ded/bowser).
             * Please refer to the provided link for the detailed documentation, here we just provide some examples how
             * it can be used in the test file:
             *

        // Browser detection
        if (t.bowser.chrome && t.bowser.version > 50) { .. do something .. }
        if (t.bowser.msie && t.bowser.version >= 10) { .. do something .. }

        // OS detection
        if (t.bowser.mac) { .. do something .. }

        // Rendering engine detection
        if (t.bowser.gecko) { .. do something .. }

        // Rendering engine detection
        if (t.bowser.webkit || t.bowser.blink) { .. do something .. }

             *
             * This property has an alias - {@link #browser}.
             */
            bowser                  : null,

            /**
             * @property {Object} bowser Alias for {@link #bowser}
             */
            browser                 : null,

            forceDOMVisible         : false,
            isDOMForced             : false,

            browserInfo             : {
                lazy    : function () {
                    return this.parseBrowser(window.navigator.userAgent)
                }
            },

            nextConfirmValue        : null,
            nextPromptReturnValue   : null,

            realAlert               : null,
            realConfirm             : null,
            realPrompt              : null,
            realPrint               : null,
            realOpen                : null,
            previousConfirm         : null,
            previousPrompt          : null,

            blurListener            : null,
            restartOnBlur           : false,

            blurWindow              : null,

            mouseVisualizer         : null,

            popups                  : Joose.I.Array,

            simulator               : null,
            simulatorConfig         : null,

            isHandlingThrowAsync    : false,

            initialCursorPosition   : null
        },

        override : {

            cleanup : function () {
                if (this.mouseVisualizer) {
                    this.mouseVisualizer.destroy()
                    this.mouseVisualizer = null
                }

                this.SUPERARG(arguments)

                this.bowser     = this.browser = null

                this._global    = null

                this.realAlert  = this.realConfirm = this.realPrompt = this.realPrint = this.realOpen = null

                this.previousPrompt = this.previousConfirm = null

                this.blurListener   = null

                this.blurWindow && this.blurWindow.close();

                this.blurWindow     = null;

                Joose.A.each(this.popups, function (handle) {
                    if (!handle.popup.closed) handle.popup.close()
                })

                this.popups.length  = 0
                this.popups         = null

                this.simulator.cleanup()
            },

            attachSimulator : function () {
                var me              = this
                var simulator       = this.simulator
                var isRoot          = !this.parent

                // this will "attach" simulator to a test window, by setting up a "global" property
                if (isRoot) simulator.onTestLaunch(this)

                if (simulator.type == 'synthetic') {
                    // for synthetic simulator we can just update the current position directly (move to simulator class?)
                    if (isRoot && me.initialCursorPosition) {
                        simulator.currentPosition[ 0 ] = me.initialCursorPosition[ 0 ]
                        simulator.currentPosition[ 1 ] = me.initialCursorPosition[ 1 ]
                    }

                    return
                }

                var cont            = Promise.resolve()

                return cont.then(function () {
                    if (isRoot) {
                        var x       = me.initialCursorPosition ? me.initialCursorPosition[ 0 ] : 0
                        var y       = me.initialCursorPosition ? me.initialCursorPosition[ 1 ] : 0

                        // for native simulator we need to actually move the cursor
                        return simulator.simulateMouseMove(x, y, null, { moveKind : 'instant' })
                    }
                }).then(function () {
                    var hasFocus        = me.project.browserWindowHasFocus()

                    // browser window has lost focus for some reason, trying to re-focus by clicking
                    // in -1, -1 point
                    if (!hasFocus) {
                        var currentPosition     = simulator.currentPosition.slice()

                        return simulator.simulateMouseMove(-1, -1, null, { moveKind : 'instant' }).then(function () {
                            return simulator.simulateMouseClick({ globalXY : [] })
                        }).then(function () {
                            return simulator.simulateMouseMove(currentPosition[ 0 ], currentPosition[ 1 ], null, { moveKind : 'instant' })
                        })
                    }
                })
            },

            launch : function (errorMessage) {
                var me              = this
                var SUPER           = this.SUPER

                var cont            = this.attachSimulator()

                // "attachSimulator" completed synchronously - call SUPER immediately
                // this is required for "subTest" call to be synchronous on what some tests relies (subject for change)
                if (!cont) {
                    this.SUPER(errorMessage)
                } else {
                    // "attachSimulator" returned promise
                    cont.then(function () {
                        SUPER.call(me, errorMessage)
                    }, function (reason) {
                        SUPER.call(me, reason)
                    })
                }
            }
        },

        methods : {

            initialize : function () {
                if (!this.simulator) this.simulator = new (this.getSimulatorClass())(this.simulatorConfig || {})

                // copy the "currentPosition" to the test instance for backward compatibility
                this.currentPosition    = this.simulator.currentPosition

                this.SUPERARG(arguments)
            },

            onBeforeTestFinalize : function () {
                var global          = this.global

                // If expectAlertMessage(which overwrites the alert method) was called but no alert() call happened - fail the test
                if (global.alert.__EXPECTED_ALERT__) {
                    this.fail(Siesta.Resource('Siesta.Test.Browser', 'alertMethodNotCalled'))
                }

                this.SUPERARG(arguments)
            },

            getSimulatorClass : function () {
                return Siesta.Test.Simulator
            },

            // setup : function (callback, errback) {
            //     var simulator           = this.simulator
            //
            //     // this will "attach" simulator to a test window, by setting up a "global" property
            //     simulator.onTestLaunch(this)
            //
            //     if (simulator instanceof Siesta.Test.Simulator) {
            //         // synthetic events start with [ 0, 0 ] point anyway, so avoid extra mouseover/mousemove event
            //         callback()
            //     } else {
            //         simulator.simulateMouseMove(0, 0, null, { moveKind : 'instant' }).then(callback, errback)
            //     }
            // },

            earlySetup : function (callback, errback) {
                var simulator           = this.simulator

                if (simulator.type == 'synthetic') {
                    // synthetic events start with [ 0, 0 ] point anyway, so avoid extra mouseover/mousemove event
                    callback()
                } else
                    // for native events need to reset the simulation state *before* the test starts
                    // all keys up, mouse buttons up, cursor in 0, 0
                    simulator.doFullSimulationReset().then(callback, errback)
            },

            launch : function (errorMessage) {
                var me          = this
                var win         = this.global

                // top test
                if (!me.parent) {
                    me.realAlert        = win.alert
                    me.realConfirm      = win.confirm
                    me.realPrompt       = win.prompt
                    me.realPrint        = win.print
                    me.realOpen         = win.open

                    this.maintainScrollPositionDuring(function () {
                        if (!me.project.browserWindowHasFocus() && !bowser.safari) me.onWindowBlur()

                        // trying to focus the window (hopefully fixes the tab key issues)
                        win.focus && win.focus()
                    })

    //                win.addEventListener && win.addEventListener('blur', me.blurListener = function () {
    //                    if (bowser.gecko && win.document.getElementsByTagName('iframe').length > 0)
    //                        // this "waitFor" can be interrupted, but only by forceful test finalization, which
    //                        // happens when test throws exception for example, so it fails anyway
    //                        me.waitFor({
    //                            method              : 0,
    //                            suppressAssertion   : true,
    //                            callback            : function () { me.onWindowBlur() }
    //                        })
    //                    else
    //                        me.onWindowBlur()
    //                })

                    // 1. IE can't show cursor, since it's IE.
                    // 2. Clients can opt-in for cursor display in automation mode
                    // 3. Currently we don't show cursor for tests running in popups, since that would require
                    // injecting cursor element on the test page and we don't want that
                    if (!bowser.msie && me.project.showCursor && !win.opener) {
                        me.mouseVisualizer = new Siesta.Project.Browser.UI.MouseVisualizer({
                            currentContainer    : me.global.frameElement.parentElement
                        });
                    }
                }

                // WARN: behavior when several sub-tests are running at the same time is not well-defined
                me.previousConfirm      = win.confirm
                me.previousPrompt       = win.prompt

                var emptyFn             = function () {};

                win.alert               = win.print = emptyFn;

                win.confirm = function () {
                    var retVal                  = typeof me.nextConfirmValue === 'boolean' ? me.nextConfirmValue : true;

                    me.nextConfirmValue         = null;

                    return retVal;
                };

                win.prompt = function () {
                    var retVal                  = me.nextPromptReturnValue || '';

                    me.nextPromptReturnValue    = null;

                    return retVal;
                };

                win.open = function (url) {
                    var popup   = me.realOpen.apply(win, arguments)

                    if (!popup)
                        me.fail(Siesta.Resource('Siesta.Test.Browser','popupsDisabled', { url : url }))
                    else {
                        me.popups.push({ url : url, popup : popup })
                    }

                    return popup
                }

                this.SUPERARG(arguments)
            },

            onTestFinalize : function () {
                var win         = this.global

                if (win) {
                    if (!this.parent) {
                        win.confirm = this.previousConfirm;
                        win.prompt  = this.previousPrompt;

                        win.print   = this.realPrint
                        win.alert   = this.realAlert
                        win.open    = this.realOpen
                    } else {
                        win.confirm = this.realConfirm;
                        win.prompt  = this.realPrompt;

                        win.alert   = win.print = function () {}
                    }
                }

    //            this.blurListener && win.removeEventListener('blur', this.blurListener)
    //
    //            this.blurListener   = null

                this.SUPERARG(arguments)
            },

            onWindowBlur : function (arg1, arg2) {
    //            var doc             = this.global.document
    //
    //            // ignore the case when focus is moved inside of the child iframe
    //            // IGNORE
    //            if (!doc.hasFocus && doc.hasFocus()) return
    //
    //            var slice           = Array.prototype.slice
    //
    //            //                  convert from HTMLCollection to Array
    //            var iframes         = slice.apply(doc.getElementsByTagName('iframe'))
    //
    //            while (iframes.length) {
    //                try {
    //                    var innerDoc    = iframes[ 0 ].contentWindow.document
    //
    //                    if (innerDoc.hasFocus()) return
    //
    //                    iframes.push.apply(iframes, slice.apply(innerDoc.getElementsByTagName('iframe')))
    //                } catch (e) {
    //                }
    //
    //                iframes.shift()
    //            }
    //            // EOF IGNORE

                if (this.restartOnBlur)
                    this.fireEvent('focuslost')
                else
                    this.warn(Siesta.Resource('Siesta.Test.Browser').get('focusLostWarning', { url : this.url }))
            },

            sizzle : function (selector, root) {
                return Siesta.Sizzle(selector, root || this.global.document)
            },

            isEventPrevented : function (event) {
                // our custom property - takes highest priority
                if (event.preventDefault && this.typeOf(event.preventDefault.$prevented) == 'Boolean') return event.preventDefault.$prevented

                // W3C standards property
                if (this.typeOf(event.defaultPrevented) == 'Boolean') return event.defaultPrevented

                return event.returnValue === false
            },

            // only called for the re-used contexts
            cleanupContextBeforeStart : function () {
                this.cleanupContextBeforeStartDom()

                this.SUPER()
            },

            cleanupContextBeforeStartDom : function () {
                var doc                 = this.global.document

                doc.body.innerHTML      = ''
            },

            getElementPageRect : function (el, $el) {
                $el             = $el || this.$(el)

                var offset      = $el.offset()

                return new Siesta.Util.Rect({
                    left        : offset.left,
                    top         : offset.top,
                    width       : $el.outerWidth(),
                    height      : $el.outerHeight()
                })
            },

    //        TODO no longer used, remove after some time
    //        elementHasScroller : function (el, $el) {
    //            $el             = $el || this.$(el)
    //
    //            var overflowX   = $el.css('overflow-x')
    //            var overflowY   = $el.css('overflow-y')
    //
    //            var hasX        = el.scrollWidth != el.clientWidth && overflowX != 'visible' && overflowX != 'hidden'
    //            var hasY        = el.scrollHeight != el.clientHeight && overflowY != 'visible' && overflowY != 'hidden'
    //
    //            return hasX || hasY ? { x : hasX, y : hasY } : false
    //        },

            hasForcedIframe : function () {
                return Boolean(
                    (this.isDOMForced || this.forceDOMVisible) && (this.scopeProvider instanceof Scope.Provider.IFrame) && this.scopeProvider.iframe
                )
            },

            getDivBox : function (doc, left, top, width, height) {
                var div             = doc.createElement('div')

                div.style.cssText   =
                    'position: absolute !important; left: ' + left + 'px !important; top: ' + top + 'px !important;' +
                    'border-width: 0 !important; padding: 0 !important; margin: 0 !important;' +
                    'width: ' + width + 'px !important; height: ' + height + 'px !important;'

                return div
            },

            elementIsScrolledOut : function (el, offset) {
                var doc                 = el.ownerDocument
                var win                 = doc.defaultView || doc.parentWindow
                var body                = doc.body

                var parents             = []

                for (var parent = el; parent && parent != body; parent = parent.parentNode) parents.push(parent)

                var currentRect         = new Siesta.Util.Rect({
                    left        : this.getPageScrollX(win),
                    top         : this.getPageScrollY(win),

                    // using height / width of the *whole viewport*, BODY tag may have 0 height in some cases
                    width       : this.$(win).width(),
                    height      : this.$(win).height()
                })

                for (var i = parents.length - 1; i >= 0; i--) {
                    var parent          = parents[ i ]

                    var overflowX       = this.$(parent).css('overflow-x')
                    var overflowY       = this.$(parent).css('overflow-y')

                    if (overflowX != 'visible' || overflowY != 'visible') {
                        // only get the bounding rect if we need it
                        // in IE10 a series of call to `getBoundingClientRect` of the parent elements
                        // were making the el itself hidden (offsetWidth = offsetHeight = 0)
                        var parentRect  = this.getElementPageRect(parent)

                        if (overflowX != 'visible') {
                            currentRect = currentRect.cropLeftRight(parentRect)
                            if (currentRect.isEmpty()) return true
                        }

                        if (overflowY != 'visible') {
                            currentRect     = currentRect.cropTopBottom(parentRect)

                            if (currentRect.isEmpty()) return true
                        }
                    }
                }

                var $el                 = this.$(el)

                var elPageRect          = this.getElementPageRect($el[ 0 ], $el)

                offset                  = this.normalizeOffset(offset, $el)

                return !currentRect.contains(elPageRect.left + offset[ 0 ], elPageRect.top + offset[ 1 ])
            },

            // returns "true" if scrolling has actually occured
            scrollTargetIntoView : function (target, offset) {
                var win             = this.global
                var doc             = win.document

                if (this.typeOf(target) != 'Array') {
                    target          = this.normalizeElement(target, true, null, false);
                    offset          = this.normalizeOffset(offset, this.$(target))

                    var isInside    = this.isOffsetInsideElementBox(target, offset);

                    if (
                        target && this.isElementVisible(target) &&
                        // If element isn't visible, try to bring it into view
                        isInside && this.elementIsScrolledOut(target, offset)
                    ) {
                        this.maintainScrollPositionDuring(function () {
                            // Required to handle the case where the body is scrolled
                            target.scrollIntoView();
                        })

    //                    No longer use jQuery "scrollIntoView" plugin - tests passes w/o it
    //                    and it does not take the offset point into account anyway
    //                    we still need it for ":scrollable" pseudo (which it does kind of ok)
    //                    this.$(target).scrollintoview({ duration : 0 });

                        // If element is still out of view, try manually scrolling first scrollable parent found
                        if (this.elementIsScrolledOut(target, offset)) {

                            var scrollableParent = this.$(target).closest(':scrollable')[ 0 ];

                            if (scrollableParent) {
                                var parentBox       = this.getBoundingClientRect(scrollableParent)
                                var targetBox       = this.getBoundingClientRect(target)

                                scrollableParent.scrollLeft = Math.max(0, scrollableParent.scrollLeft + targetBox.left - parentBox.left + offset[ 0 ] - 1)
                                scrollableParent.scrollTop  = Math.max(0, scrollableParent.scrollTop + targetBox.top - parentBox.top + offset[ 1 ] - 1)
                            }
                        }

                        return true
                    }
                } else {
                    var leftVisible     = this.getPageScrollX()
                    var rightVisible    = leftVisible + this.$(win).width()

                    var topVisible      = this.getPageScrollY()
                    var bottomVisible   = topVisible + this.$(win).height()

                    if (
                        leftVisible <= target[ 0 ] && target[ 0 ] <= rightVisible
                        && topVisible <= target[ 1 ] && target[ 1 ] <= bottomVisible
                    ) {
                        // no need to scroll, target point is within visible viewport area
                        return false
                    }

                    var div             = this.getDivBox(doc, target[ 0 ], target[ 1 ], 1, 1)

                    doc.body.appendChild(div)

                    this.maintainScrollPositionDuring(function () {
                        div.scrollIntoView()
                    })

                    doc.body.removeChild(div)

                    return true
                }
            },

            processSubTestConfig : function (config) {
                var res             = this.SUPER(config)
                var me              = this

                Joose.A.each([
                    'currentPosition',
                    'mouseVisualizer',
                    'simulator',
                    'simulatorConfig',
                    'bowser', 'browser',
                    'moveCursorBetweenPoints',
                    'realAlert', 'realConfirm', 'realPrompt', 'realPrint', 'realOpen', 'popups'
                ], function (name) {
                    res[ name ]     = me[ name ]
                })

                return res
            },

            // Normalizes the element to an HTML element. Every 'framework layer' will need to provide its own implementation
            // This implementation accepts either a CSS selector or an Array with xy coordinates.
            normalizeElement : function (el, allowMissing, shallow, detailed) {
                // Quick exit if already an element
                if (el && el.nodeName) return el;

                var matchingMultiple = false

                if (this.typeOf(el) === 'String') {
                    // DOM query
                    var origEl  = el;

                    var wasAdjusted = this.adjustScope(el);

                    var query   = this.$(el.indexOf('->') >= 0 ? el.split('->')[1] : el);

                    if (wasAdjusted) this.resetScope();

                    el          = query[ 0 ];
                    matchingMultiple = query.length > 1

                    if (!allowMissing && !el) {
                        var warning = Siesta.Resource('Siesta.Test.Browser','noDomElementFound') + ': ' + origEl

                        this.warn(warning);
                        throw new Error(warning);
                    }
                }

                if (this.typeOf(el) == 'Array') {
                    var x, y

                    if (el.length < 2) {
                        x       = this.simulator.currentPosition[ 0 ]
                        y       = this.simulator.currentPosition[ 1 ]
                    } else {
                        x       = this.pageXtoViewportX(Math.round(el[ 0 ]))
                        y       = this.pageYtoViewportY(Math.round(el[ 1 ]))
                    }

                    el          = this.elementFromPoint(x, y);
                }

                return detailed ? { el : el, matchingMultiple : matchingMultiple } : el;
            },

            // this method generally has the same semantic as the "normalizeElement", its being used in
            // Siesta.Test.Action.Role.HasTarget to determine what to pass to next step
            //
            // on the browser level the only possibility is DOM element
            // but on ExtJS level user can also use ComponentQuery and next step need to receive the
            // component instance
            normalizeActionTarget : function (el, allowMissing) {
                return this.normalizeElement(el, allowMissing);
            },

            randomBetween : function (min, max) {
                return Math.floor(min + (Math.random() * (max - min + 1)));
            },

            /**
             * This method uses native `document.elementFromPoint()` and returns the DOM element under the current logical cursor
             * position in the test. Note, that this method may work not 100% reliable in IE due to its bugs. In cases
             * when `document.elementFromPoint()` can't find any element this method returns the &lt;body&gt; element.
             *
             * @return {HTMLElement}
             */
            getElementAtCursor : function() {
                var xy          = this.simulator.currentPosition;

                return this.elementFromPoint(xy[ 0 ], xy[ 1 ])
            },

            addListenerToObservable : function (observable, event, listener, isSingle) {
                this.$(observable).bind(event, listener)
            },

            removeListenerFromObservable : function (observable, event, listener) {
                this.$(observable).unbind(event, listener)
            },

            // This method accepts actionTargets as input (Dom node, string, CQ etc) and does a first
            // normalization pass to get a DOM element.
            // After initial normalization it also tries to locate, the 'top' DOM node at the center of
            // the first pass resulting DOM node.
            // This is the only element we can truly interact with in a real browser.
            // returns an object containing the element plus viewport coordinates
            getNormalizedTopElementInfo : function (actionTarget, skipWarning, actionName, offset) {
                var localXY, globalXY, el;

                // support empty array as a synonym for empty target
                if (this.typeOf(actionTarget) == 'Array' && actionTarget.length == 0) { actionTarget = null }

                var targetIsPoint   = !actionTarget || this.typeOf(actionTarget) == 'Array'

                // First lets get a normal DOM element to work with
                if (targetIsPoint) {
                    // viewport coords
                    var x, y

                    if (actionTarget) {
                        x       = this.pageXtoViewportX(Math.round(actionTarget[ 0 ]))
                        y       = this.pageYtoViewportY(Math.round(actionTarget[ 1 ]))
                    } else {
                        x       = this.simulator.currentPosition[ 0 ]
                        y       = this.simulator.currentPosition[ 1 ]
                    }

                    globalXY    = [ x, y ]

                    var info    = this.elementFromPoint(x, y, false, null, true)

                    el          = info.el
                    localXY     = info.localXY
                } else {
                    el          = this.normalizeElement(actionTarget, skipWarning)
                }

                if (!el && skipWarning) {
                    return;
                }

                // 1. If this element is not visible, something is wrong
                // 2. If element is visible but not reachable (scrolled out of view) this is also an invalid scenario (this check is skipped for IE)
                //    TODO needs further investigation, conflicting with starting a drag operation on an element that isn't visible until the cursor is above it

                // we don't need to this check if target is a coordinate point, because in this case element is reachable by definition
                if (!targetIsPoint) {
                    var R       = Siesta.Resource('Siesta.Test.Browser');
                    var message = 'getNormalizedTopElementInfo: ' + (actionName ? R.get('targetElementOfAction') + " [" + actionName + "]" : R.get('targetElementOfSomeAction')) +
                        " " + R.get('isNotVisible') + ": " + (el.id ? '#' + el.id : el)

                    if (!this.isElementVisible(el)) {
                        this.fail(message)
                        return;
                    }
                    else if (!skipWarning && this.isOffsetInsideElementBox(el, offset) && !this.elementIsTop(el, true, offset)) {
                        this.warn(message)
                    }
                }

                var isOption    = el && el.nodeName.toLowerCase() === 'option';

                if (isOption) {
                    localXY     = this.simulator.currentPosition.slice();
                    globalXY    = this.simulator.currentPosition.slice();
                } else if (!targetIsPoint) {
                    var doc     = el.ownerDocument;
                    var R       = Siesta.Resource('Siesta.Test.Browser');

                    localXY     = this.getTargetCoordinate(el, true, offset)
                    globalXY    = this.getTargetCoordinate(el, false, offset)

                    // trying 2 times for IE
                    el          = doc.elementFromPoint(localXY[ 0 ], localXY[ 1 ]) || doc.elementFromPoint(localXY[ 0 ], localXY[ 1 ]) || doc.body;

                    if (!el) {
                        this.fail('getNormalizedTopElementInfo: ' + R.get('noElementFound') + ' [' + localXY + ']');
                        return; // No point going further
                    }
                }

                return {
                    el          : el,

                    // both are viewport coords
                    localXY     : localXY,
                    globalXY    : globalXY,

                    offset      : isOption ? [ 0, 0 ] : this.getOffsetRelativeToEl(el, localXY)
                }
            },

            // point should be in page coords
            getOffsetRelativeToEl : function(el, point) {
                var box = this.getElementPageRect(el);

                return [ point[0] - box.left, point[1] - box.top ];
            },

            /**
             * This method will wait for the presence of the passed string.
             *
             * @param {String} text The text to wait for
             * @param {Function} callback The callback to call
             * @param {Object} scope The scope for the callback
             * @param {Number} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
             */
            waitForTextPresent : function (text, callback, scope, timeout) {
                var R               = Siesta.Resource('Siesta.Test.Browser');

                return this.waitFor({
                    method          : function () {
                        var body        = this.global.document.body
                        var selector    = ':contains(' + text + ')'

                        return this.$(selector, body).length > 0 || this.$(body).is(selector);
                    },
                    callback        : callback,
                    scope           : scope,
                    timeout         : timeout,
                    assertionName   : 'waitForTextPresent',
                    description     : ' ' + R.get('text') + ' "' + text + '" ' + R.get('toBePresent')
                });
            },

            /**
             * This method will wait for the absence of the passed string.
             *
             * @param {String} text The text to wait for
             * @param {Function} callback The callback to call
             * @param {Object} scope The scope for the callback
             * @param {Number} timeout The maximum amount of time to wait for the condition to be fulfilled. Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value.
             */
            waitForTextNotPresent : function (text, callback, scope, timeout) {
                var R               = Siesta.Resource('Siesta.Test.Browser');

                return this.waitFor({
                    method          : function () {
                        var body        = this.global.document.body
                        var selector    = ':contains(' + text + ')'

                        return this.$(selector, body).length === 0 && !this.$(body).is(selector);
                    },
                    callback        : callback,
                    scope           : scope,
                    timeout         : timeout,
                    assertionName   : 'waitForTextNotPresent',
                    description     : ' ' + R.get('text') + ' "' + text + '" ' + R.get('toNotBePresent')
                });
            },

            /**
             * Waits until the passed action target is detected. This can be a string such as a component query, CSS query or a composite query.
             *
             * @param {String/Siesta.Test.ActionTarget} target The target presence to wait for
             * @param {Function} callback The callback to call after the target 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.
             */
            waitForTarget : function(target, callback, scope, timeout, offset) {
                var me = this;
                var R  = Siesta.Resource('Siesta.Test.Browser');

                return this.waitFor({
                    method          : function () {
                        var el      = me.normalizeElement(target, true, true, false, { offset : offset })

                        // If user is aiming outside the target, we'll *not* use the offset while
                        // detecting target presence since having a visible sized box will suffice
                        if (el && offset && me.isElementVisible(el) && !me.isOffsetInsideElementBox(el, offset)) {
                            return true;
                        }

                        return el && me.elementIsTop(el, true, offset)
                    },
                    callback        : callback,
                    scope           : scope,
                    timeout         : timeout,
                    assertionName   : 'waitForTarget',
                    description     : ' ' + R.get('target') + ' "' + target + '" ' + R.get('toAppear')
                });
            },

            /**
             * Sets a new size for the test iframe
             *
             * @param {Int} width The new width
             * @param {Int} height The new height
             */
            setWindowSize : function(width, height, callback) {
                this.scopeProvider.setViewportSize(width, height);

                callback && callback.call(this);
            },

            getJUnitClass : function () {
                var browserInfo         = this.getBrowserInfo()

                browserInfo             = browserInfo.name + browserInfo.version

                return browserInfo + ':' + this.SUPER()
            },

            // Returns true if the scope was adjusted to another frame for the target string
            adjustScope : function(target) {

                if (this.typeOf(target) == 'String') {
                    var mainParts  = target.split('->');

                    if (mainParts.length === 2) {
                        var frameEl = this.$(this.trimString(mainParts[ 0 ]))[ 0 ];

                        if (!frameEl || !frameEl.contentWindow) {
                            return false;
                        }

                        this._global    = this.global
                        this.setGlobal(frameEl.contentWindow)

                        return true;
                    }
                }

                return false;
            },

            resetScope : function() {
                this.setGlobal(this._global || this.global)

                this._global    = null
            },

            // a stub method for the Lite package
            screenshot : function (options, callback) {
                this.diag("Command: `screenshot` skipped - not running in Standard Package")

                this.processCallbackFromTest(callback, [ 'skipped' ], this)
            },

            // a stub method for the Lite package
            screenshotElement : function (target, fileName, callback) {
                this.diag("Command: `screenshot` skipped - not running in Standard Package")

                this.processCallbackFromTest(callback, [ 'skipped' ], this)
            },

            /**
             * setUrl Opens the url provided (make sure you use the {@link Siesta.Project.Browser#enablePageRedirect} option on the Harness when using this API method)
             *
             * @param {String} url The new url for current page
             * @param {Function} callback The callback to call after the page has been loaded
             * @param {Object} scope The scope for the callback
             */
            setUrl : function(url, callback, scope) {
                if (!url) throw new Error('Must provide a valid URL');

                var me = this;

                if (me.global.location.href !== url) {
                    var baseUrl = this.scopeProvider.sourceURL || this.project.baseUrl;
                    var absURl  = this.project.absolutizeURL(url, baseUrl);

                    me.waitForPageLoad(callback, scope);
                    me.global.location.href = absURl;
                } else {
                    callback.call(scope || me);
                }
            },

            /**
             * Sets the hash value of the location object
             *
             * @param {String} url The new hash
             * @param {Function} callback
             * @param {Object} scope The scope for the callback
             */
            setHash : function(hash, callback, scope) {
                var me = this;

                me.global.location.hash = hash;

                callback && callback(scope || me);
            },

            /**
             * Expects an alert message with the specified text to be shown during the test. If no alert is called,
             * or the text doesn't match, a failed assertion will be added.
             *
             * @param {String/RegExp} message The expected alert message or a regular expression to match
             * @param callback Only used internally when this method is called in a t.chain command
             */
            expectAlertMessage : function (message, callback) {
                var me          = this
                var global      = this.global
                var prevAlert   = global.alert

                global.alert = function (msg) {
                    var passed      = me.typeOf(message) == 'RegExp' ? message.test(msg) : message == msg

                    if (passed)
                        me.pass("Expected alert message has been shown")
                    else
                        me.fail("Wrong alert message has been shown", {
                            assertionName       : 'expectAlertMessage',
                            got                 : msg,
                            gotDesc             : "Message shown",
                            need                : message,
                            needDesc            : "Expected message"
                        })

                    global.alert = prevAlert
                };

                global.alert.__EXPECTED_ALERT__ = true

                this.processCallbackFromTest(callback, null, this)
            },

            /**
             * Sets the confirm dialog return value for the next window.confirm() call.
             *
             * @param {Boolean} value The confirm dialog return value (true or false)
             * @param callback Only used internally when this method is called in a t.chain command
             * */
            setNextConfirmReturnValue : function (value, callback) {
                this.nextConfirmValue = value;

                this.processCallbackFromTest(callback, null, this)
            },

            /**
             * Sets the prompt dialog return value for the next window.prompt() call.
             *
             * @param {String} value The confirm dialog return value
             * @param callback Only used internally when this method is called in a t.chain command
             */
            setNextPromptReturnValue : function (value, callback) {
                this.nextPromptReturnValue = value;

                this.processCallbackFromTest(callback, null, this)
            },

            waitForAnimations : function(callback) {
                callback.call(this);
            },

            popupHasStartedLoading : function (popup, initialUrl) {
                if (String(initialUrl).toLowerCase() != 'about:blank' && popup.location.href == 'about:blank') return false

                return true
            },

            /**
             * Switches the target of all Siesta interactive commands (like "click/type" etc) to a different
             * window (usually a popup). You can use {@link #switchToMain} method to switch back to main window.
             *
             * @param {String/RegExp/Object/Window/HTMLIFrameElement} [win] A new window which should be a target for all interactive commands.
             * If this argument is specified as `null` a first opened popup is used.
             * Can be specified as the:
             *
             * - Window - A global window instance
             * - Object - Object with the following properties
             *      - url   : String/RegExp - The first popup, opened with matching url will be used
             *      - title : String/RegExp - The first popup, opened with matching title will be used
             * - String - corresponds to the `title` property of the Object branch
             *
             * @param {Function} callback Function to call once the switch has complete (will also wait until the target page
             * completes loading)
             *
             * @return {Window} Previously active window reference
             */
            switchTo : function (win, callback) {
                var me          = this

                // In Chrome, when popup for some url is just created, it has "url" set to "about:blank"
                // after some time the url is set to the original value and load process begins
                // this opens a race condition - one can not reliably predict when the popup has completed loading
                // doing our best
                this.waitFor({
                    method              : function () {
                        for (var i = 0; i < me.popups.length; i++)
                            if (!me.popupHasStartedLoading(me.popups[ i ].popup, me.popups[ i ].url)) return false

                        return true
                    },
                    suppressAssertion   : true,

                    callback            : function () {
                        var found

                        if (!win) {
                            Joose.A.each(this.popups, function (handle) {
                                if (!handle.popup.closed) { found = handle.popup;  return false }
                            })

                            win  = found
                        }

                        if (this.typeOf(win) == 'String') win = { title : win }

                        if (this.typeOf(win) == 'Object') {
                            found           = null
                            var regexp      = win.title || win.url

                            if (this.typeOf(regexp) == 'String') regexp = new RegExp('^' + this.escapeRegExp(regexp) + '$')

                            Joose.A.each(this.popups, function (handle) {
                                var popup       = handle.popup

                                if (!popup.closed)
                                    if (
                                        win.url && regexp.test(popup.location.href)
                                        ||
                                        win.title && regexp.test(popup.document && popup.document.title || '')
                                    ) {
                                        found = popup
                                        return false
                                    }
                            })

                            win  = found
                        }

                        if (!win || win.self != win) {
                            this.fail("Can't resolve target win: " + win)

                            this.processCallbackFromTest(callback, null, this)

                            return
                        }

                        this.setGlobal(win)
    //                    This has to be revised properly in the "context" branch, idea is, that we switch to popup's implementation
    //                    of `setTimeout` for waiting, asyncing etc, because thats what really user expect
    //                    however in IE test just hangs
    //                    this.originalSetTimeout     = win.setTimeout
    //                    this.originalClearTimeout   = win.clearTimeout

                        this.waitFor({
                            suppressAssertion   : true,
                            method      :  function () {
                                return win.document && win.document.readyState == 'complete'
                            },
                            callback    : callback
                        })
                    }
                })

                return this.global
            },

            setGlobal : function (newGlobal) {
                this.global             = newGlobal
                this.simulator.global   = newGlobal
            },

            /**
             * Switches all interactive commands back to main test window.
             *
             * @param {Function} callback Function to call once the switch has complete.
             */
            switchToMain : function (callback) {
                this.switchTo(this.scopeProvider.scope, callback)
            },

            /**
             * Only useful along with {@link Siesta.Project.Browser.enablePageRedirect enablePageRedirect} option
             *
             * Wait for the page load to occur and runs the callback. The callback will receive a "window" object.
             * Should be used when you are doing a redirect / refresh of the test page:
             *
             *      t.waitForPageLoad(function (window) {
             *          ...
             *      })
             *
             * Note, that method obviously must be called before the new page has completed loading, otherwise it will
             * wait indefinitely and fail (since there will be no page load). So, to avoid the race conditions, one
             * should always start waiting for page load *before* the action, that causes it.
             *
             * Consider the following example (where click on the `>> #loginPanel button` trigger a page redirect):

             // this code does not reliably - it contains a race condition
             // in Chrome, page refresh may happen too fast (even synchronously),
             // so, by the time the `waitForPageLoad` action will start, the page load event will already happen
             // and `waitForPageLoad` will wait indefinitely
             { click : '>> #loginPanel button' },
             { waitFor : 'PageLoad'}
             * &nbsp;

             // Need to start waiting first, and only then - click
             // we'll use "trigger" config of the `wait` action for that
             {
                 waitFor     : 'PageLoad',
                 trigger     : {
                     click : '>> #loginPanel button'
                 }
             }
             // or, same action using function step:
             function (next) {
            t.waitForPageLoad(next)

            t.click('>> #loginPanel button', function () {})
        }

             *
             * @method
             * @member Siesta.Test.Browser
             */
            waitForPageLoad : function (callback, scope) {
                var me              = this

                var global          = this.global
                var unloaded        = false
                var description     = Siesta.Resource('Siesta.Test.More').get('pageToLoad');
                var onUnloadHandler = function () {
                    global.removeEventListener('unload', onUnloadHandler)

                    unloaded        = true
                }

                global.addEventListener('unload', onUnloadHandler)

                this.chain(
                    {
                        description    : description,
                        waitFor : function () {
                            return unloaded || me.global.document.readyState != 'complete'
                        }},
                    function (next) {
                        global.removeEventListener('unload', onUnloadHandler)

                        global          = null
                        onUnloadHandler = null

                        next()
                    },
                    {
                        description    : description,
                        waitFor : function () {
                            return me.global.document.readyState == 'complete'
                        }
                    },
                    { waitFor : 50 },
                    function () {
                        me.processCallbackFromTest(callback, [ me.global ], scope || me)
                    }
                )
            },

            /**
             * This method will just call the `setTimeout` method from the scope of the test page.
             *
             * Usually you don't need to use it - you can just call `setTimeout`, but if your test scripts resides in the
             * separate context, you need to use this method for `setTimeout` functionality. See documentation for {@link Siesta.Project.Browser#enablePageRedirect enablePageRedirect}
             * option and <a href="#!/guide/cross_page_testing">Cross page testing</a> guide.
             *
             * @param {Function} func The function to call after specified `delay`
             * @param {Number} delay The time to wait (in ms) before calling the `func`
             * @return {Number} timeoutId The id of the timeout, can be passed to {@link #clearTimeout} to cancel the function execution.
             *
             * @method
             * @member Siesta.Test.Browser
             */
            setTimeout : function (func, delay) {
                var pageSetTimeout = this.global.setTimeout

                pageSetTimeout(func, delay)
            },

            /**
             * This method will just call the `clearTimeout` method from the scope of the test page.
             *
             * Usually you don't need to use it - you can just call `clearTimeout`, but if your test scripts resides in the
             * separate context, you need to use this method for `clearTimeout` functionality. See documentation for {@link Siesta.Project.Browser#enablePageRedirect enablePageRedirect}
             * option and <a href="#!/guide/cross_page_testing">Cross page testing</a> guide.
             *
             * @param {Number} timeoutId The id of the timeout, recevied from the {@link #setTimeout} call
             *
             * @method
             * @member Siesta.Test.Browser
             */
            clearTimeout : function (id) {
                var pageClearTimeout = this.global.clearTimeout

                pageClearTimeout(id)
            },

            /**
             * This method mimics the deactivation of a browser window by opening a new window. Useful if you want to test
             * how your application behaves when your application is not active.
             *
             * @method
             * @member Siesta.Test.Browser
             */
            simulateDeactivateWindow : function(callback) {
                this.blurWindow = this.global.open('about:blank');

                callback && callback.call(this);
            },

            /**
             * This method activates the main browser window reverts focus to the window object of the test.
             *
             * @method
             * @member Siesta.Test.Browser
             */
            simulateActivateWindow : function(callback) {
                this.blurWindow && this.blurWindow.close();

                this.global.focus();

                this.blurWindow = null;

                callback && callback.call(this);
            },

            /**
             * This method resolves a query string. At the generic browser level, this is supposed to be a CSS selector
             * (other framework specific layers, like ExtJS adds additional semantic).
             *
             * @param {String} selector
             * @return {Array[Element]}
             */
            query : function (selector) {
                return this.sizzle(selector)
            },

            /**
             * This assertion passes if an exception with certain message is thrown, during the time since this method has been called
             * and until the `done` function has been called. The `done` function is returned from this method. It should be called once
             * the code, that is expected to throw exception, has completed. `done` function can be called asynchronously.
             *
             * If `done` is not called for more than `timeout` time, this assertions finalizes forcefully.
             *
             * For example:
             *

        var done

        t.chain(
            function (next) {
                done = t.throwsOkAsync('oops', 'Correct exception thrown', 25000)
                next()
            },
            // the exception is expected from the click handler
            { click : '.some-button' },
            function (next) {
                done()
                next()
            },
        )

             *
             * See also {@link Siesta.Test.Browser#livesOkAsync} method.
             *
             * @param {String/RegExp} expected The regex against which to test the stringified exception, can be also a plain string
             * @param {String} [desc] The description of the assertion
             * @param {Number} [timeout] The timeout after which this assertion will be finalized forcefully. Default value is {@link Siesta.Project#defaultTimeout}
             *
             * @return {Function} A function which should be called when the code block, which is expected to throw the exception, is completed
             */
            throwsOkAsync : function (expected, desc, timeout) {
                var R                   = Siesta.Resource('Siesta.Test.More')
                var me                  = this

                var exceptionThrown     = false
                var doneCalled          = false
                var prevOnError         = this.global.onerror

                var done        = function () {
                    if (doneCalled) return
                    doneCalled                  = true

                    me.endAsync(async)

                    if (!exceptionThrown) {
                        me.fail(desc, {
                            assertionName       : 'throwsOkAsync',
                            annotation          : R.get('didntThrow')
                        })
                    }

                    me.isHandlingThrowAsync     = false

                    me.global.onerror           = prevOnError

                    // return true to suppress possible timeout failure, since `done` is also used as a errback for `beginAsync`
                    return true
                }

                this.on('beforetestfinalize', function () {
                    if (!doneCalled) done()
                }, null, { single : true })

                var async       = this.beginAsync(timeout, done)

                this.isHandlingThrowAsync = true

                this.global.onerror = function (msg, url, lineNumber, col, error) {
                    exceptionThrown     = true

                    var errorText       = String(error ? error : msg)

                    if (me.typeOf(expected) == "RegExp")

                        if (errorText.match(expected))
                            me.pass(desc, {
                                descTpl             : R.get('exMatchesRe'),
                                expected            : expected
                            })
                        else
                            me.fail(desc, {
                                assertionName       : 'throwsOkAsync',
                                got                 : errorText,
                                gotDesc             : R.get('exceptionStringifiesTo'),
                                need                : expected,
                                needDesc            : R.get('needStringMatching')
                            })
                    else

                        if (errorText.indexOf(expected) != -1)
                            me.pass(desc, {
                                descTpl             : R.get('exContainsSubstring'),
                                expected            : expected
                            })
                        else
                            me.fail(desc, {
                                assertionName       : 'throwsOkAsync',
                                got                 : errorText,
                                gotDesc             : R.get('exceptionStringifiesTo'),
                                need                : expected,
                                needDesc            : R.get('needStringContaining')
                            })

                    // "hide" the exception
                    return true
                }

                return done
            },

            /**
             * This assertion passes if no exceptions are thrown, during the time since this method has been called
             * and until the `done` function has been called. The `done` function is returned from this method. It should be called once
             * the code, that is expected to not throw exceptions, has completed. `done` function can be called asynchronously.
             *
             * If `done` is not called for more than `timeout` time, this assertions finalizes forcefully.
             *
             * For example:
             *

        var done

        t.chain(
            function (next) {
                done = t.livesOkAsync('No exception thrown')
                next()
            },
            // the exception is expected from the click handler
            { click : '.some-button' },
            function (next) {
                done()
                next()
            }
        )

             *
             * See also {@link Siesta.Test.Browser#throwsOkAsync} method.
             *
             * @param {String} [desc] The description of the assertion
             * @param {Number} [timeout] The timeout after which this assertion will be finalized forcefully. Default value is {@link Siesta.Project#defaultTimeout}
             *
             * @return {Function} A function which should be called when the code block, which is expected to now throw exceptions, is completed
             */
            livesOkAsync : function (desc, timeout) {
                var R                   = Siesta.Resource('Siesta.Test.More')
                var me                  = this

                var exceptionThrown     = false
                var doneCalled          = false
                var prevOnError         = this.global.onerror

                var done        = function () {
                    if (doneCalled) return
                    doneCalled                  = true

                    me.endAsync(async)

                    if (!exceptionThrown) {
                        me.pass(desc, {
                            descTpl             : R.get('fnDoesntThrow')
                        })
                    }

                    me.isHandlingThrowAsync     = false

                    me.global.onerror           = prevOnError

                    // return true to suppress possible timeout failure, since `done` is also used as a errback for `beginAsync`
                    return true
                }

                this.on('beforetestfinalize', function () {
                    if (!doneCalled) done()
                }, null, { single : true })

                var async       = this.beginAsync(timeout, done)

                this.isHandlingThrowAsync = true

                this.global.onerror = function (msg, url, lineNumber, col, error) {
                    exceptionThrown     = true

                    var errorText       = String(error ? error : msg)

                    me.fail(desc, {
                        assertionName       : 'livesOk',
                        annotation          : R.get('fnThrew') + ': ' + errorText
                    })

                    // "hide" the exception
                    return true
                }

                return done
            },

            onException : function () {
                if (this.isHandlingThrowAsync) return true
            }
        }
    });