/*

Siesta 5.6.1
Copyright(c) 2009-2022 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                = this.getBodyElement(el)

            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') {
                var query  = el;

                // DOM query
                var matches   = this.query(el);

                el = matches[0];

                matchingMultiple = matches.length > 1

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

                    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) {
            if (this.typeOf(observable) === 'String') observable = this.normalizeElement(observable)

            observable.addEventListener(event, listener)
        },


        removeListenerFromObservable : function (observable, event, listener) {
            if (this.typeOf(observable) === 'String') observable = this.normalizeElement(observable)

            observable.removeEventListener(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     = this.getQueryableContainer(el);
                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 () {
                    return this.global.document.body.innerText.indexOf(text) >= 0;
                },
                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 () {
                    return this.global.document.body.innerText.indexOf(text) < 0;
                },
                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()
        },

        // 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);
        },


        /**
         * Queries for elements using a CSS selector (optionally inside specific root element). At the generic browser level, the query is a CSS selector
         * (other framework specific layers, like Ext JS adds additional semantic).
         *
         * You can also target elements in nested iframes or web components. For that, split the query into two parts, delimited with the `->`.
         * The first part of the query should target an iframe element or a web component. The second part will be resolved inside the DOM of that iframe.
         *
         * ```
         * await t.click('iframe.frame -> button'); // Targeting element inside an iframeee
         * await t.click('todo-app -> input[type=text]'); // Targeting element inside a web component
         * ```
         *
         * The nesting level is not limited - you can target elements in the nested iframe of the nested iframe etc.
         *
         * ```
         * await t.click('iframe.frame1 -> iframe.frame2 -> button');
         * ```
         *
         * A few extra CSS pseudo selectors are also supported: `:contains()` and `:textEquals()` which makes it
         * possible to query elements by their exact (textEquals) or partial (contains) textual content.
         *
         * ```
         * await t.click('.iframe1 -> .iframe2 -> button:contains(Save)');
         * ```
         *
         * @param {String} selector A CSS selector
         * @param {Object} [root] The root element for the query (or shadow root)
         * @return {Array[Element]}
         */
        query : function (selector, root) {
            selector = selector.trim();

            root = root || this.getNestedRoot(selector);

            return root ? this.sizzle(selector.split('->').pop(), root) : [];
        },

        getGlobal : function(docRoot) {
            return docRoot.defaultView || this.global;
        },

        getNestedRoot : function(selector, root) {
            var parts = selector.split('->');

            root = root || this.global.document;

            if (parts.length > 1) {
                var newContext = this.query(parts.shift(), root)[0];

                // Go deeper?
                if (newContext) {
                    var global = newContext.contentWindow ? newContext.contentWindow : this.global;

                    // Make sure nested web component shadow root exists
                    if (!newContext.contentWindow && !newContext.shadowRoot) {
                        return null;
                    }

                    root = newContext.shadowRoot || global.document;

                    if (parts.length > 1) {
                        return this.getNestedRoot(parts.join('->'), root);
                    }
                }
                else {
                    root = null;
                }
            }

            return root;
        },


        setExceptionHandlerOverrideFlag : function (value) {
            var test    = this

            while (test) {
                test.isHandlingThrowAsync   = value

                test    = test.parent
            }
        },

        /**
         * 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.setExceptionHandlerOverrideFlag(false)

                me.global.onerror           = prevOnError

                if (supportsUnhandledRejections) {
                    me.global.removeEventListener('unhandledrejection', unhandledRejectionListener)
                }

                // 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 supportsUnhandledRejections = 'onunhandledrejection' in this.global

            var async       = this.beginAsync(timeout, done)

            this.setExceptionHandlerOverrideFlag(true)

            var verifyErrorText = function (errorText) {
                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')
                        })
            }

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

                var errorText       = String(error ? error : msg)

                verifyErrorText(errorText)

                // "hide" the exception
                return true
            }

            if (supportsUnhandledRejections) {
                var unhandledRejectionListener  = function (event) {
                    exceptionThrown     = true

                    verifyErrorText(String(event.reason))
                }

                me.global.addEventListener('unhandledrejection', unhandledRejectionListener)
            }


            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.setExceptionHandlerOverrideFlag(false)

                me.global.onerror           = prevOnError

                if (supportsUnhandledRejections) {
                    me.global.removeEventListener('unhandledrejection', unhandledRejectionListener)
                }

                // 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 supportsUnhandledRejections = 'onunhandledrejection' in this.global

            var async       = this.beginAsync(timeout, done)

            this.setExceptionHandlerOverrideFlag(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
            }

            if (supportsUnhandledRejections) {
                var unhandledRejectionListener  = function (event) {
                    exceptionThrown     = true

                    me.fail(desc, {
                        assertionName       : 'livesOk',
                        annotation          : 'Promise rejected: ' + event.reason
                    })
                }

                me.global.addEventListener('unhandledrejection', unhandledRejectionListener)
            }

            return done
        },


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