/*

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

*/
/**
@class Siesta.Test.More

A mixin with additional generic assertion methods, which can work cross-platform between browsers and NodeJS.
Is being consumed by {@link Siesta.Test}, so all of them are available in all tests.

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

    requires        : [ 'isFailed', 'typeOf', 'on' ],


    has : {
        autoCheckGlobals        : false,
        expectedGlobals         : Joose.I.Array,

        disableGlobalsCheck     : false,

        browserGlobals : {
            init : [
                'console',
                'getInterface',
                'ExtBox1',
                '__IE_DEVTOOLBAR_CONSOLE_COMMAND_LINE',
                /__BROWSERTOOLS/, // IE11 with console open
                'seleniumAlert',
                'onload',
                'onerror',
                'StartTest',
                'startTest',
                '__loaderInstrumentationHookInstalled__',
                'describe',
                // will be reported in IE8 after overriding
                'setTimeout',
                'clearTimeout',
                'requestAnimationFrame',
                'cancelAnimationFrame',
                '__coverage__',
                /cov_\w+/
            ]
        },

        /**
         * @cfg {Number} waitForTimeout Default timeout for `waitFor` (in milliseconds). Default value is 10000.
         */
        waitForTimeout                  : 10000,

        waitForPollInterval             : 100,

        suppressPassedWaitForAssertion  : false
    },


    methods : {

        /**
         * This assertion passes, when the comparison of 1st with 2nd, using `>` operator will return `true` and fails otherwise.
         *
         * @param {Number/Date} value1 The 1st value to compare
         * @param {Number/Date} value2 The 2nd value to compare
         * @param {String} [desc] The description of the assertion
         */
        isGreater : function (value1, value2, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (value1 > value2)
                this.pass(desc, {
                    descTpl             : R.get('isGreaterPassTpl'),
                    value1              : value1,
                    value2              : value2
                })
            else
                this.fail(desc, {
                    assertionName       : 'isGreater',

                    got                 : value1,
                    need                : value2,

                    needDesc            : R.get('needGreaterThan')
                })
        },


        /**
         * This assertion passes, when the comparison of 1st with 2nd, using `<` operator will return `true` and fails otherwise.
         *
         * @param {Number/Date} value1 The 1st value to compare
         * @param {Number/Date} value2 The 2nd value to compare
         * @param {String} [desc] The description of the assertion
         */
        isLess : function (value1, value2, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (value1 < value2)
                this.pass(desc, {
                    descTpl             : R.get('isLessPassTpl'),
                    value1              : value1,
                    value2              : value2
                })
            else
                this.fail(desc, {
                    assertionName       : 'isLess',

                    got                 : value1,
                    need                : value2,

                    needDesc            : R.get('needLessThan')
                })
        },


        isGE : function () {
            this.isGreaterOrEqual.apply(this, arguments)
        },

        /**
         * This assertion passes, when the comparison of 1st with 2nd, using `>=` operator will return `true` and fails otherwise.
         *
         * It has a synonym - `isGE`.
         *
         * @param {Number/Date} value1 The 1st value to compare
         * @param {Number/Date} value2 The 2nd value to compare
         * @param {String} [desc] The description of the assertion
         */
        isGreaterOrEqual : function (value1, value2, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (value1 >= value2)
                this.pass(desc, {
                    descTpl             : R.get('isGreaterEqualPassTpl'),
                    value1              : value1,
                    value2              : value2
                })
            else
                this.fail(desc, {
                    assertionName       : 'isGreaterOrEqual',

                    got                 : value1,
                    need                : value2,

                    needDesc            : R.get('needGreaterEqualTo')
                })
        },



        isLE : function () {
            this.isLessOrEqual.apply(this, arguments)
        },

        /**
         * This assertion passes, when the comparison of 1st with 2nd, using `<=` operator will return `true` and fails otherwise.
         *
         * It has a synonym - `isLE`.
         *
         * @param {Number/Date} value1 The 1st value to compare
         * @param {Number/Date} value2 The 2nd value to compare
         * @param {String} [desc] The description of the assertion
         */
        isLessOrEqual : function (value1, value2, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (value1 <= value2)
                this.pass(desc, {
                    descTpl             : R.get('isLessEqualPassTpl'),
                    value1              : value1,
                    value2              : value2
                })
            else
                this.fail(desc, {
                    assertionName       : 'isLessOrEqual',

                    got                 : value1,
                    need                : value2,

                    needDesc            : R.get('needLessEqualTo')
                })
        },


        /**
         * This assertion suppose to compare the numeric values. It passes when the passed values are approximately the same (the difference
         * is withing a threshold). A threshold can be provided explicitly (when assertion is called with 4 arguments),
         * or it will be set to 5% from the 1st value (when calling assertion with 3 arguments).
         *
         * @param {Number} value1 The 1st value to compare
         * @param {Number} value2 The 2nd value to compare
         * @param {Number} threshold The maximum allowed difference between values. This argument can be omitted.
         * @param {String} [desc] The description of the assertion
         */
        isApprox : function (value1, value2, threshold, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (arguments.length == 2) threshold  = Math.abs(value1 * 0.05)

            if (arguments.length == 3) {
                if (this.typeOf(threshold) == 'String') {
                    desc            = threshold
                    threshold      = Math.abs(value1 * 0.05)
                }
            }

            // this function normalizes the fractional numbers to fixed point presentation
            // for example in JS: 1.05 - 1 = 0.050000000000000044
            // so what we do is: (1.05 * 10^2 - 1 * 10^2) / 10^2 = (105 - 100) / 100 = 0.05
            var subtract    = function (value1, value2) {
                var fractionalLength    = function (v) {
                    var afterPointPart = (v + '').split('.')[ 1 ]

                    return afterPointPart && afterPointPart.length || 0
                }

                var maxLength           = Math.max(fractionalLength(value1), fractionalLength(value2))
                var k                   = Math.pow(10, maxLength);

                return (value1 * k - value2 * k) / k;
            };

            if (Math.abs(subtract(value2, value1)) <= threshold)
                this.pass(desc, {
                    descTpl             : R.get('isApproxToPassTpl'),
                    value1              : value1,
                    value2              : value2,
                    annotation          : value2 == value1 ? R.get('exactMatch') : (R.get('withinThreshold') + ': ' + threshold)
                })
            else
                this.fail(desc, {
                    assertionName       : 'isApprox',
                    got                 : value1,
                    need                : value2,
                    needDesc            : R.get('needApprox'),
                    annotation          : R.get('thresholdIs') + ': ' + threshold
                })
        },


        /**
         * This assertion passes when the passed `string` matches to a regular expression `regex`. When `regex` is a string,
         * assertion will check that it is a substring of `string`
         *
         * @param {String} string The string to check for "likeness"
         * @param {String/RegExp} regex The regex against which to test the string, can be also a plain string
         * @param {String} [desc] The description of the assertion
         */
        like : function (string, regex, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(regex) == "RegExp")

                if (string.match(regex))
                    this.pass(desc, {
                        descTpl             : R.get('stringMatchesRe'),
                        string              : string,
                        regex               : regex
                    })
                else
                    this.fail(desc, {
                        assertionName       : 'like',
                        got                 : string,
                        need                : regex,
                        needDesc            : R.get('needStringMatching')
                    })
            else

                if (string.indexOf(regex) != -1)
                    this.pass(desc, {
                        descTpl             : R.get('stringHasSubstring'),
                        string              : string,
                        regex               : regex
                    })
                else
                    this.fail(desc, {
                        assertionName       : 'like',
                        got                 : string,
                        need                : regex,
                        needDesc            : R.get('needStringContaining')
                    })
        },

        /**
         * This method is the opposite of 'like', it adds failed assertion, when the string matches the passed regex.
         *
         * @param {String} string The string to check for "unlikeness"
         * @param {String/RegExp} regex The regex against which to test the string, can be also a plain string
         * @param {String} [desc] The description of the assertion
         */
        unlike : function(string, regex, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(regex) == "RegExp")

                if (!string.match(regex))
                    this.pass(desc, {
                        descTpl             : R.get('stringNotMatchesRe'),
                        string              : string,
                        regex               : regex
                    })
                else
                    this.fail(desc, {
                        assertionName       : 'unlike',
                        got                 : string,
                        need                : regex,
                        needDesc            : R.get('needStringNotMatching')
                    })
            else

                if (string.indexOf(regex) == -1)
                    this.pass(desc, {
                        descTpl             : R.get('stringHasNoSubstring'),
                        string              : string,
                        regex               : regex
                    })
                else
                    this.fail(desc, {
                        assertionName       : 'unlike',
                        got                 : string,
                        need                : regex,
                        needDesc            : R.get('needStringNotContaining')
                    })
        },


        "throws" : function () {
            this.throwsOk.apply(this, arguments)
        },

        throws_ok : function () {
            this.throwsOk.apply(this, arguments)
        },

        /**
         * This assertion passes if the `func` function throws an exception during executing, and the
         * stringified exception passes the 'like' assertion (with 'expected' parameter).
         *
         * It has synonyms - `throws_ok` and `throws`.
         *
         *      t.throwsOk(function(){
         *          throw "oopsie";
         *      }, 'oopsie', 'Some description text');
         *
         * See also {@link Siesta.Test#livesOk} method.
         *
         * @param {Function} func The function which should throw an exception
         * @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
         */
        throwsOk : function (func, expected, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(func) != 'Function') throw new Error(R.get('throwsOkInvalid'))

            var e = this.getExceptionCatcher()(func)

            // assuming no one will throw undefined exception..
            if (e === undefined) {
                this.fail(desc, {
                    assertionName       : 'throws_ok',
                    annotation          : R.get('didntThrow')
                })

                return
            }

            if (e instanceof this.getTestErrorClass())
                //IE uses non-standard 'description' property for error msg
                e = e.message || e.description

            e = '' + e

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

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

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



        lives_ok : function () {
            this.livesOk.apply(this, arguments)
        },

        lives : function () {
            this.livesOk.apply(this, arguments)
        },

        /**
         * This assertion passes, when the supplied `func` function doesn't throw an exception during execution.
         *
         * See also {@link Siesta.Test#throwsOk} method.
         *
         * This method has two synonyms: `lives_ok` and `lives`
         *
         * @param {Function} func The function which is not supposed to throw an exception
         * @param {String} [desc] The description of the assertion
         */
        livesOk : function (func, desc) {
            if (this.typeOf(func) != 'Function') {
                func = [ desc, desc = func ][ 0 ]
            }

            var R       = Siesta.Resource('Siesta.Test.More');
            var e       = this.getExceptionCatcher()(func)

            if (e === undefined)
                this.pass(desc, {
                    descTpl             : R.get('fnDoesntThrow')
                })
            else
                this.fail(desc, {
                    assertionName       : 'lives_ok',
                    annotation          : R.get('fnThrew') + ': ' + e
                })
        },


        isa_ok : function (value, className, desc) {
            this.isInstanceOf(value, className, desc)
        },


        isaOk : function (value, className, desc) {
            this.isInstanceOf(value, className, desc)
        },

        /**
         * This assertion passes, when the supplied `value` is the instance of the `className`. The check is performed with
         * `instanceof` operator. The `className` parameter can be supplied as class constructor or as string, representing the class
         * name. In the latter case the `class` will eval'ed to receive the class constructor.
         *
         * This method has synonyms: `isaOk`, `isa_ok`
         *
         * @param {Mixed} value The value to check for 'isa' relationship
         * @param {Class/String} className The class to check for 'isa' relationship with `value`
         * @param {String} [desc] The description of the assertion
         */
        isInstanceOf : function (value, className, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            try {
                if (this.typeOf(className) == 'String') className = this.global.eval(className)
            } catch (e) {
                this.fail(desc, {
                    assertionName       : 'isa_ok',
                    annotation          : Siesta.Resource('Siesta.Test.Function', 'exceptionEvalutingClass')
                })

                return
            }

            if (value instanceof className)
                this.pass(desc, {
                    descTpl             : R.get('isInstanceOfPass')
                })
            else
                this.fail(desc, {
                    assertionName       : 'isa_ok',
                    got                 : value,
                    need                : String(className),
                    needDesc            : R.get('needInstanceOf')
                })
        },


        /**
         * This assertion passes, if supplied value is a String.
         *
         * @param {Mixed} value The value to check.
         * @param {String} [desc] The description of the assertion
         */
        isString : function (value, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(value) == 'String')
                this.pass(desc, {
                    descTpl     : R.get('isAString'),
                    value       : value
                })
            else
                this.fail(desc, {
                    got         : value,
                    need        : R.get('aStringValue')
                })
        },


        /**
         * This assertion passes, if supplied value is an Object
         *
         * @param {Mixed} value The value to check.
         * @param {String} [desc] The description of the assertion
         */
        isObject : function (value, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(value) == 'Object')
                this.pass(desc, {
                    descTpl     : R.get('isAnObject'),
                    value       : value
                })
            else
                this.fail(desc, {
                    got         : value,
                    need        : R.get('anObject')
                })
        },


        /**
         * This assertion passes, if supplied value is an Array
         *
         * @param {Mixed} value The value to check.
         * @param {String} [desc] The description of the assertion
         */
        isArray : function (value, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(value) == 'Array')
                this.pass(desc, {
                    descTpl     : R.get('isAnArray'),
                    value       : value
                })
            else
                this.fail(desc, {
                    got         : value,
                    need        : R.get('anArrayValue')
                })
        },


        /**
         * This assertion passes, if supplied value is a Number.
         *
         * @param {Mixed} value The value to check.
         * @param {String} [desc] The description of the assertion
         */
        isNumber : function (value, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(value) == 'Number')
                this.pass(desc, {
                    descTpl     : R.get('isANumber'),
                    value       : value
                })
            else
                this.fail(desc, {
                    got         : value,
                    need        : R.get('aNumberValue')
                })
        },


        /**
         * This assertion passes, if supplied value is a Boolean.
         *
         * @param {Mixed} value The value to check.
         * @param {String} [desc] The description of the assertion
         */
        isBoolean : function (value, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(value) == 'Boolean')
                this.pass(desc, {
                    descTpl     : R.get('isABoolean'),
                    value       : value
                })
            else
                this.fail(desc, {
                    got         : value,
                    need        : R.get('aBooleanValue')
                })
        },


        /**
         * This assertion passes, if supplied value is a Date.
         *
         * @param {Mixed} value The value to check.
         * @param {String} [desc] The description of the assertion
         */
        isDate : function (value, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(value) == 'Date')
                this.pass(desc, {
                    descTpl     : R.get('isADate'),
                    value       : value
                })
            else
                this.fail(desc, {
                    got         : value,
                    need        : R.get('aDateValue')
                })
        },


        /**
         * This assertion passes, if supplied value is a RegExp.
         *
         * @param {Mixed} value The value to check.
         * @param {String} [desc] The description of the assertion
         */
        isRegExp : function (value, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(value) == 'RegExp')
                this.pass(desc, {
                    descTpl     : R.get('isARe'),
                    value       : value
                })
            else
                this.fail(desc, {
                    got         : value,
                    need        : R.get('aReValue')
                })
        },


        /**
         * This assertion passes, if supplied value is a Function.
         *
         * @param {Mixed} value The value to check.
         * @param {String} [desc] The description of the assertion
         */
        isFunction : function (value, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.typeOf(value) == 'Function' || this.typeOf(value) == 'AsyncFunction')
                this.pass(desc, {
                    descTpl     : R.get('isAFunction'),
                    value       : value
                })
            else
                this.fail(desc, {
                    got         : value,
                    need        : R.get('aFunctionValue')
                })
        },


        is_deeply : function (obj1, obj2, desc) {
            this.isDeeply.apply(this, arguments)
        },

        /**
         * This assertion passes when in-depth comparison of 1st and 2nd arguments (which are assumed to be JSON objects) shows that they are equal.
         * Comparison is performed with '==' operator, so `[ 1 ]` and `[ "1" ] objects will be equal. The objects should not contain cyclic references.
         *
         * This method works correctly with the *placeholders* generated with method {@link #any}.
         *
         * This method has a synonym: `is_deeply`
         *
         * @param {Object} obj1 The 1st object to compare
         * @param {Object} obj2 The 2nd object to compare
         * @param {String} [desc] The description of the assertion
         */
        isDeeply : function (obj1, obj2, desc) {
            var R       = Siesta.Resource('Siesta.Test.More');

            var diff

            if (this.typeOf(obj1) === this.typeOf(obj2) && this.compareObjects(obj1, obj2)) {

                this.pass(desc, {
                    descTpl             : R.get('isDeeplyPassTpl'),
                    obj1                : obj1,
                    obj2                : obj2
                })
            }
            // DeepDiff Not supported in IE8
            else if (typeof DeepDiff != 'undefined' && (diff = DeepDiff(obj1, obj2))) {

                if (diff.length > 5) {
                    this.diag(R.get('tooManyDifferences', { num : 5, total : diff.length}))
                }

                for (var i = 0; i < Math.min(diff.length, 5); i++) {
                    var diffItem    = diff[i];
                    var path        = (diffItem.path || []).join('.');
                    var saw         = path ? (path + ': ' + diffItem.lhs) : obj1;
                    var expected    = path ? (path + ': ' + diffItem.rhs) : obj2;

                    this.fail(desc, {
                        assertionName       : 'isDeeply',
                        got                 : saw,
                        need                : expected
                    })

                    // Also log it to console for easy inspection
                    window.console && console.log('DIFF RESULT:', diffItem);
                }

            } else {
                this.fail(desc, {
                    assertionName       : 'isDeeply',
                    got                 : obj1,
                    need                : obj2
                })
            }
        },


        /**
         * This assertion passes when in-depth comparison of 1st and 2nd arguments (which are assumed to be JSON objects) shows that they are equal.
         * Comparison is performed with '===' operator, so `[ 1 ]` and `[ "1" ] objects will be different. The objects should not contain cyclic references.
         *
         * This method works correctly with the *placeholders* generated with method {@link #any}.
         *
         * @param {Object} obj1 The 1st object to compare
         * @param {Object} obj2 The 2nd object to compare
         * @param {String} [desc] The description of the assertion
         */
        isDeeplyStrict : function (obj1, obj2, desc) {
            if (this.typeOf(obj1) === this.typeOf(obj2) && this.compareObjects(obj1, obj2, true)) {

                var R       = Siesta.Resource('Siesta.Test.More');

                this.pass(desc, {
                    descTpl             : R.get('isDeeplyStrictPassTpl'),
                    obj1                : obj1,
                    obj2                : obj2
                })
            }
            else
                this.fail(desc, {
                    assertionName       : 'isDeeplyStrict',
                    got                 : obj1,
                    need                : obj2
                })
        },

        expectGlobal : function () {
            this.expectGlobals.apply(this, arguments)
        },


        /**
         * This method accepts a variable number of names of expected properties in the global scope. When verifying the globals with {@link #verifyGlobals}
         * assertions, the expected gloabls will not be counted as failed assertions.
         *
         * This method has a synonym with singular name: `expectGlobal`
         *
         * @param {String/RegExp} name1 The name of global property or the regular expression to match several properties
         * @param {String/RegExp} name2 The name of global property or the regular expression to match several properties
         * @param {String/RegExp} nameN The name of global property or the regular expression to match several properties
         */
        expectGlobals : function () {
            this.expectedGlobals.push.apply(this.expectedGlobals, arguments)
        },


        isGlobalExpected : function (name, index) {
            var me                  = this

            if (!index || index && !index.expectedStrings) {
                if (!index) index   = {}

                Joose.O.extend(index, {
                    expectedStrings     : {},
                    expectedRegExps     : []
                })

                Joose.A.each(this.expectedGlobals.concat(this.browserGlobals), function (value) {
                    if (me.typeOf(value) == 'RegExp')
                        index.expectedRegExps.push(value)
                    else
                        index.expectedStrings[ value ] = true
                })
            }

            if (index.expectedStrings[ name ]) return true

            var imageWithIdCreatesGlobalEnumerable  = Siesta.Project.Browser.FeatureSupport().supports.imageWithIdCreatesGlobalEnumerable;

            // remove after https://bugzilla.mozilla.org/show_bug.cgi?id=959992 will be fixed
            if (imageWithIdCreatesGlobalEnumerable) {
                var domEl       = this.global.document.getElementById(name)

                if (domEl && domEl.tagName.toLowerCase() == 'img') return true;
            }

            for (var i = 0; i < index.expectedRegExps.length; i++)
                if (index.expectedRegExps[ i ].test(name)) return true

            return false
        },


        forEachUnexpectedGlobal : function (func, scope) {
            scope                   = scope || this

            var index               = {}

            for (var name in this.global)
                if (!this.isGlobalExpected(name, index)) {
                    if (func.call(scope, name) === false) {
                        break;
                    }
                }
        },


        /**
         * This method accepts a variable number of names of expected properties in the global scope and then performs a globals check.
         *
         * It will scan all globals properties in the scope of test and compare them with the list of expected globals. Expected globals can be provided with:
         * {@link #expectGlobals} method or {@link Siesta.Project#expectedGlobals expectedGlobals} configuration option of project.
         *
         * You can enable this assertion to automatically happen at the end of each test, using {@link Siesta.Project#autoCheckGlobals autoCheckGlobals} option of the project.
         *
         * @param {String/RegExp} name1 The name of global property or the regular expression to match several properties
         * @param {String/RegExp} name2 The name of global property or the regular expression to match several properties
         * @param {String/RegExp} nameN The name of global property or the regular expression to match several properties
         */
        verifyGlobals : function () {
            var R       = Siesta.Resource('Siesta.Test.More');

            if (this.disableGlobalsCheck) {
                this.diag(R.get('globalCheckNotSupported'));

                return
            }

            this.expectGlobals.apply(this, arguments)

            this.diag(R.get('globalVariables'))

            var failed          = false
            var i               = 0
            this.forEachUnexpectedGlobal(function (name) {
                this.fail(
                    R.get('globalFound'),
                    R.get('globalName') + ': ' + name + ', ' + R.get('value') + ': ' + Siesta.Util.Serializer.stringify(this.global[ name ])
                )

                failed      = true
                return i++ < 50 // Only report first 50 globals to protect against legacy apps with thousands of globals
            })

            if (!failed) this.pass(R.get('noGlobalsFound'))
        },


        // will create a half-realized, "phantom", "isWaitFor" assertion, which is only purposed
        // for user to get the instant feedback about "waitFor" actions
        // this assertion will be "finalized" and added to the test results in the "finalizeWaiting"
        startWaiting : function (description, sourceLine) {
            var result = new Siesta.Result.Assertion({
                description     : description,
                isWaitFor       : true,
                sourceLine      : sourceLine
            });

            this.fireEvent('testupdate', this, result, this.getResults())

            return result;
        },


        finalizeWaiting : function (result, passed, desc, annotation, errback, suppressPassedWaitForAssertion) {
            // Treat this is an ordinary assertion from now on
            result.completed = true;

            if (passed) {
                if (this.suppressPassedWaitForAssertion || suppressPassedWaitForAssertion) {
                    // Make sure UI is updated and the "noise" is removed
                    this.fireEvent('assertiondiscard', this, result)
                } else {
                    this.pass(desc, annotation, result)
                }
            }
            else {
                this.fail(desc, annotation, result);

                errback && errback()
            }
        },


        conditionCheckerToString : function (checker) {
            if (this.typeOf(checker) !== 'Function') return ''

            var sources                     = checker.toString().split('\n')
            var minCommonLeadingWhitespace  = Infinity

            Joose.A.each(sources, function (line, index) {
                // ignore first line, which won't have the common leading whitespace
                if (index === 0) return

                var leadingWhitespaceMatch  = /^(\s*)/.exec(line)

                if (leadingWhitespaceMatch) {
                    var leadingWhitespace   = leadingWhitespaceMatch[ 1 ]

                    // ignore whitespace-only lines
                    if (leadingWhitespace === line) return

                    if (leadingWhitespace.length < minCommonLeadingWhitespace) minCommonLeadingWhitespace  = leadingWhitespace.length
                }
            })

            if (minCommonLeadingWhitespace < Infinity) Joose.A.each(sources, function (line, index) {
                // ignore first line, which won't have the common leading whitespace
                if (index === 0) return

                sources[ index ]    = line.slice(minCommonLeadingWhitespace)
            })

            return '[code]' + sources.join('\n') + '[/code]'
        },


        /**
         * Waits for passed checker method to return true (or any non-false value, like for example DOM element or array), and calls the callback when this happens.
         * As an additional feature, the callback will receive the result from the checker method as the 1st argument.
         *

    t.waitFor(
        function () { return document.getElementById('someEl') },
        function (el) {
            // waited for element #someEl to appear
            // element will be available in the callback as 1st argument "el"
        }
    )

         * You can also call this method with a single Object having the following properties: `method`, `callback`, `scope`, `timeout`, `interval`, `description`:

    t.waitFor({
        method      : function () { return document.getElementById('someEl') },
        callback    : function (el) {
            // waited for element #someEl to appear
            // element will be available in the callback as 1st argument "el"
        }
    })

         *
         * @param {Function/Number/Object} condition Either a function which should return true (or any other "truthy" value) when a certain condition has been fulfilled,
         * or a number of ms to wait before calling the callback. Can be also an object with the following properties:
         * @param {Function} condition.callback A function to call when the condition has been met. Will receive a result from checker function.
         * @param {Function} condition.method A condition checker function.
         * @param {Object} condition.scope The scope for the callback.
         * @param {Number} condition.timeout The maximum amount of time (in milliseconds) to wait for the condition to be fulfilled.
         * @param {Number} condition.interval The polling interval (in milliseconds)
         * @param {String} condition.description The assertion description
         *
         * @param {Function} callback A function to call when the condition has been met. Will receive a result from checker function.
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time (in milliseconds) to wait for the condition to be fulfilled.
         * Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. If condition is not fullfilled within this time, a failed assertion will be added to the test.
         * @param {Int} [interval=100] The polling interval (in milliseconds)
         *
         * @return {Promise} A promise which will be resolved when wait completes (either successfully or by timeout). In case of successfull resolution
         * promise will be resolved to the result from the checker function. Additionally it has a `force` property as noted below.
         * @return {Function} return.force A function, that will force this wait operation to immediately complete (and call the callback).
         * No call to checker will be performed and callback will not receive a result from it.
         */
        waitFor : function (method, callback, scope, timeout, interval)  {
            var R                       = Siesta.Resource('Siesta.Test.More');
            var description             = ' ' + R.get('conditionToBeFulfilled');
            var assertionName           = 'waitFor';
            var me                      = this;
            var sourceLine              = me.getSourceLine();
            var originalSetTimeout      = me.originalSetTimeout;
            var originalClearTimeout    = me.originalClearTimeout;
            var errback;
            var suppressAssertion;

            if (arguments.length === 1 && this.typeOf(method) == 'Object') {
                var options         = method;

                method              = options.method;
                callback            = options.callback;
                scope               = options.scope;
                timeout             = options.timeout;
                interval            = options.interval

                description         = options.description || description;
                assertionName       = options.assertionName || assertionName;
                suppressAssertion   = options.suppressAssertion;

                // errback is called in case "waitFor" has failed
                errback             = options.errback
            } else
                options             = {}

            var isWaitingForTime        = this.typeOf(method) == 'Number'

            callback                    = callback || function () {}
            description                 = isWaitingForTime ? (method + ' ' + R.get('ms')) : description;

            var pollTimeout

            // early notification about the started "waitFor" operation
            var waitAssertion           = me.startWaiting(R.get('waitingFor') + ' ' + description, sourceLine);

            interval                    = interval || this.waitForPollInterval
            timeout                     = timeout || this.waitForTimeout

            var resolve

            var res = new Promise(function (resolution) {
                resolve = resolution
            })

            // this async frame is not supposed to fail, because it's delayed to `timeout + 3 * interval`
            // failure supposed to be generated in the "pollFunc" and this async frame to be closed
            // however, in IE the async frame may end earlier than failure from "pollFunc"
            // in such case we report the same error as in "pollFunc"
            var async                   = this.beginAsync((isWaitingForTime ? method : timeout) + 3 * interval, function () {
                isDone      = true

                originalClearTimeout(pollTimeout)

                me.finalizeWaiting(waitAssertion, false, R.get('waitedTooLong') + ': ' + description, {
                    assertionName       : assertionName,
                    annotation          : me.typeOf(options.annotation) === 'Function' ? options.annotation() : R.get('conditionNotFulfilled') + ' ' + timeout + R.get('ms') + '. \n\n' + me.conditionCheckerToString(method)
                }, errback, suppressAssertion)

                resolve()

                return true
            })

            var isDone      = false
            var beforeFinalizeListener

            // stop polling, if this test instance has finalized (probably because of exception)
            this.on('beforetestfinalize', beforeFinalizeListener = function () {
                if (!isDone) {
                    isDone      = true

                    me.finalizeWaiting(waitAssertion, false, R.get('waitingAborted'), null, null, suppressAssertion);

                    me.endAsync(async)

                    originalClearTimeout(pollTimeout)
                }
            }, null, { single : true })

            if (isWaitingForTime) {
                if (method < 0) {
                    throw new Error('Cannot wait for a negative amount of time');
                }
                pollTimeout = originalSetTimeout(function() {
                    isDone      = true

                    me.un('beforetestfinalize', beforeFinalizeListener)
                    me.finalizeWaiting(waitAssertion, true, R.get('Waited') + ' ' + method + ' ' + R.get('ms'), null, null, suppressAssertion || method === 0);
                    me.endAsync(async);
                    me.processCallbackFromTest(callback, [], scope || me)

                    resolve()
                }, method);

            } else {

                var result;
                var startDate   = new Date()

                var pollFunc    = function () {
                    var time = new Date() - startDate;

                    if (time > timeout) {
                        me.endAsync(async);

                        isDone      = true

                        try {
                            me.un('beforetestfinalize', beforeFinalizeListener)
                            me.finalizeWaiting(waitAssertion, false, R.get('waitedTooLong') + ': ' + description, {
                                assertionName       : assertionName,
                                annotation          : me.typeOf(options.annotation) === 'Function' ? options.annotation() : R.get('conditionNotFulfilled') + ' ' + timeout + R.get('ms') + '. \n\n' + me.conditionCheckerToString(method)
                            }, errback, suppressAssertion)
                        } catch (e) {
                            if (!/__SIESTA_TEST_EXIT_EXCEPTION__/.test(String(e))) throw e
                        }

                        resolve()

                        return
                    }

                    try {
                        result = method.call(scope || me);
                    } catch (e) {
                        me.endAsync(async);

                        try {
                            me.un('beforetestfinalize', beforeFinalizeListener)
                            me.finalizeWaiting(waitAssertion, false, assertionName + ' ' + R.get('checkerException'), {
                                assertionName   : assertionName,
                                annotation      : me.stringifyException(e)
                            }, errback, suppressAssertion)
                        } catch (e) {
                            if (!/__SIESTA_TEST_EXIT_EXCEPTION__/.test(String(e))) throw e
                        }

                        isDone      = true

                        resolve()

                        return
                    }

                    if (result != null && result !== false) {
                        me.endAsync(async);

                        isDone      = true
                        me.un('beforetestfinalize', beforeFinalizeListener)
                        me.finalizeWaiting(
                            waitAssertion,
                            true,
                            R.get('Waited') + ' ' + time + ' ' + R.get('msFor') + ' ' + description,
                            me.typeOf(options.annotation) === 'Function' ? options.annotation() : null,
                            null,
                            // always add assertion (set "suppress" to false), if user has provided description
                            (suppressAssertion || time === 0) && !options.description
                        );

                        me.processCallbackFromTest(callback, [ result ], scope || me)

                        resolve(result)
                    } else
                        pollTimeout = originalSetTimeout(pollFunc, interval)
                }

                pollFunc()
            }

            res.force = function () {
                // wait operation already completed
                if (isDone) return

                isDone      = true

                originalClearTimeout(pollTimeout)

                me.endAsync(async);

                me.un('beforetestfinalize', beforeFinalizeListener)
                me.finalizeWaiting(waitAssertion, true, R.get('forcedWaitFinalization') + ' ' + description, null, null, suppressAssertion);

                me.processCallbackFromTest(callback, [], scope || me)

                resolve()
            }

            return res
        },

        /**
         * Waits for the number of a number millseconds and calls the callback when after waiting. This is just a convenience synonym for the {@link #waitFor} method.

         t.waitForMs(1500, callback)

         *
         * @param {Number} method The number of ms to wait before calling the callback.
         * @param {Function} callback A function to call when the condition has been met. Will receive a result from checker function.
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time (in milliseconds) to wait for the condition to be fulfilled.
         * Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. If condition is not fullfilled within this time, a failed assertion will be added to the test.
         * @param {Int} [interval=100] The polling interval (in milliseconds)
         *
         * @return {Object} An object with the following properties:
         * @return {Function} return.force A function, that will force this wait operation to immediately complete (and call the callback).
         * No call to checker will be performed and callback will not receive a result from it.
         */
        waitForMs : function() {
            return this.waitFor.apply(this, arguments);
        },


        /**
         * Waits for the passed checker method to return true (or any non-false value, like for example DOM element or array), and calls the callback when this happens.
         * This is just a convenience synonym for the {@link #waitFor} method.
         *

         t.waitForFn(function() { return true; }, callback)

         *
         * @param {Function} fn The checker function.
         * @param {Function} callback A function to call when the condition has been met. Will receive a result from checker function.
         * @param {Object} scope The scope for the callback
         * @param {Int} timeout The maximum amount of time (in milliseconds) to wait for the condition to be fulfilled.
         * Defaults to the {@link Siesta.Test.ExtJS#waitForTimeout} value. If condition is not fullfilled within this time, a failed assertion will be added to the test.
         * @param {Int} [interval=100] The polling interval (in milliseconds)
         *
         * @return {Object} An object with the following properties:
         * @return {Function} return.force A function, that will force this wait operation to immediately complete (and call the callback).
         * No call to checker will be performed and callback will not receive a result from it.
         */
        waitForFn : function() {
            return this.waitFor.apply(this, arguments);
        },

        // takes the step function and tries to analyze if it is missing the call to "next"
        // returns "true" if "next" is used,
        analyzeChainStep : function (func) {
            var sources         = func.toString()

            var isArrow         = !sources.match(/^function/)
            var firstArg

            if (isArrow) {
                if (sources.match(/^\(/))
                    // args wrapper in ()
                    firstArg    = sources.match(/\(\s*(.*?)\s*(?:,|\))/)[ 1 ]
                else
                    // single argument not wrapped in ()
                    firstArg    = sources.match(/(.*?)\s*=>/)[ 1 ]
            } else {
                firstArg        = sources.match(/function\s*[^(]*\(\s*(.*?)\s*(?:,|\))/)[ 1 ]
            }

            if (!firstArg) return false

            var body

            if (isArrow)
                body            = sources.match(/=>\s*([\s\S]*)/)[ 1 ]
            else
                body            = sources.match(/\{([\s\S]*)\}/)[ 1 ]

            return body.indexOf(firstArg) != -1
        },


        stringifyChainStep : function (step) {
            return this.typeOf(step).match(/^(Async)?Function$/) ? step.toString() : Siesta.Util.Serializer.stringify(step)
        },


        /**
         * This method accepts a variable number of steps, either as individual arguments or as a single array containing them. Steps and arrays
         * of steps are handled just fine, and any step-arrays passed will be flattened. Each step should be either a function or configuration
         * object for {@link Siesta.Test.Action test actions}. These functions / actions will be executed in order.
         *
         * 1) For a function step, it will receive a callback as the 1st argument, to call when the step is completed.
         * As the 2nd and further arguments, the step function will receive the arguments passed to the previous callback.
         *
         * If a function step returns a promise, it should not call the callback, but instead Siesta will wait until the promise returned is
         * resolved / rejected. This plays nicely with the `async/await` functions:
         *

    let someAsyncOperation =
        t => new Promise((resolve, reject) => {
            setTimeout(() => { resolve("someValue") }, 300)
        })

    t.it('Doing async stuff', t => {
        t.chain(
            // function step, that returns a promise (sugared with async/await)
            async () => {
                return await someAsyncOperation(t)
            },
            // desugared version
            function (next, result) {
                return someAsyncOperation(t)
            }
        )
    })

         *
         * The last step will receive a no-op callback, which can be ignored or still called. **Note**, that last step is assumed to
         * complete synchronously! If you need to launch some asynchronous process in the last step, you may need to add another empty function step
         * to the end of the chain.
         *
         * 2) For `Siesta.Test.Action` objects, the callback will be called by the action class automatically,
         * there's no need to provide any callback manually. The configuration object should contain an "action" property, specifying the action class
         * along with other config options depending on the action class. For brevity, instead of using the "action" property, the configuration
         * object can contain the property corresponding to the action name itself, with the action's target (or even a test method with arguments).
         * See the following examples and also refer to the documentation of the action classes.
         *
         * If the configuration object will contain a `desc` property, a passing assertion with its value will be added to the test, after this step has completed.
         *
         * 3) If a step is a sub test instance, created with {@link #getSubTest} method, then the step will launch it.
         *
         * It's better to see how it works in action. For example, when using using only functions:

    t.chain(
        // function receives a callback as 1st argument
        function (next) {
            // we pass that callback to the "click" method
            t.click(buttonEl, next)
        },
        function (next) {
            t.type(fieldEl, 'Something', next)
        },
        function (next) {
            t.is(fieldEl.value == 'Something', 'Correct value in the field')

            // call the callback with some arguments
            next('foo', 'bar')
        },
        // those arguments are now available as arguments of next step
        function (next, value1, value2) {
            t.is(value1, 'foo', 'The arguments for the callback are translated to the arguments of the step')
            t.is(value2, 'bar', 'The arguments for the callback are translated to the arguments of the step')
        }
    )

         *
         * The same example, using action configuration objects for first 2 steps. For the list of available actions
         * please refer to the classes in the `Siesta.Test.Action` namespace.

    t.chain(
        {
            action      : 'click',
            target      : buttonEl,
            desc        : "Clicked on the button"
        },
        // or
        {
            click       : buttonEl,
            desc        : "Clicked on the button"
        },

        {
            action      : 'type',
            target      : fieldEl,
            text        : 'Something',
            desc        : "Typed in the field"
        },
        // or
        {
            type        : 'Something',
            target      : fieldEl,
            desc        : "Typed in the field"
        },

        {
            waitFor     : 'Selector',
            args        : '.selector'
        }
        // or, using Siesta.Test.Action.MethodCall notation:
        {
            waitForSelector : '.selector'
        }

        function (next) {
            t.is(fieldEl.value == 'Something', 'Correct value in the field')

            next('foo', 'bar')
        },
        ...
    )

         * Please note, that by default, each step is expected to complete within the {@link Siesta.Project#defaultTimeout} time.
         * You can change this with the `timeout` property of the step configuration object, allowing some steps to last longer.
         * Steps with sub-tests are expected to complete within {@link Siesta.Project#subTestTimeout}.
         *
         * In a special case, `action` property of the step configuration object can be a function. In this case you can also
         * provide a `timeout` property, otherwise this case is identical to using functions:
         *

    t.chain(
        {
            action      : function (next) { ... },
            // allow 50s for the function to call "next" before step will be considered timed-out
            timeout     : 50000
        },
        ...
    )

         *  **Tip**:
         *
         *  If a step is presented with a `null` or `undefined` value it will be ignored. Additionally, a step can be
         *  an array of steps - all arrays passed to t.chain will be flattened.
         *
         *  These tips allows us to implement conditional steps processing, like this:
         *

    var el1IsInDom          = t.$('.some-class1')[ 0 ]
    var el2IsInDom          = t.$('.some-class2')[ 0 ]

    t.chain(
        { click : '.some-other-el' },

        el1IsInDom ? [
            { click : el1IsInDom },

            el2IsInDom ? [
                { click : el1IsInDom }
            ] : null,
        ] : null,

        ...
    )

         *
         *  See also : {@link #chainForArray}.
         *
         *  @param {Function/Object/Array} step1 The function to execute or action configuration, or an array of steps
         *  @param {Function/Object} step2 The function to execute or action configuration
         *  @param {Function/Object} stepN The function to execute or action configuration
         */
        chain : function () {
            // inline any arrays in the arguments into one array
            var steps       = this.flattenArray(arguments)
            var R           = Siesta.Resource('Siesta.Test.More');

            var nonEmpty    = []
            Joose.A.each(steps, function (step) { if (step) nonEmpty.push(step) })

            steps           = nonEmpty

            var len         = steps.length

            // do nothing
            if (!len) return;

            var me          = this
            var self        = arguments.callee

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

                // some dirt - "chain" is generic method and does not know about `simulator`, which appears on browser level only
                interval        : self.hasOwnProperty('actionDelay') ? self.actionDelay : (this.simulator ? this.simulator.actionDelay : 1),

                observeTest     : this
            })

            // hack to allow configuration of `actionDelay`...
            delete self.actionDelay

            var sourceLine  = me.getSourceLine();

            var args        = []

            Joose.A.each(steps, function (step, index) {

                var isLast      = index == len - 1

                queue.addAsyncStep({
                    processor : function (data) {
                        var hasReturnedPromise      = false

                        var initStep = function (stepHasOwnAsyncFrame) {

                            if (!stepHasOwnAsyncFrame) {
                                var timeout     = step.timeout || me.defaultTimeout

                                // + 100 to allow `waitFor` steps (which will be waiting the `timeout` time) to
                                // generate their own failures
                                var async       = me.beginAsync(timeout + 100, function () {
                                    me.fail(
                                        R.get('chainStepNotCompleted'),
                                        {
                                            sourceLine      : sourceLine,
                                            annotation      : R.get('stepNumber') + ': ' + (index + 1) + ' ' + R.get('oneBased')
                                                + (sourceLine ? ('\n' + R.get('atLine') + ': ' + sourceLine) : '')
                                                + '\nStep source: ' + me.stringifyChainStep(step),
                                            ownTextOnly     : true
                                        }
                                    )

                                    return true
                                })
                            }

                            return {
                                next    : function () {
                                    var self        = arguments.callee
                                    var isExtraCall = false

                                    if (hasReturnedPromise) {
                                        // ignore the 1st "manual" call to callback if promise was returned
                                        if (self.__CALLED__ === 1) { self.__CALLED__ = 2; return }
                                        // but not the 2nd
                                        if (self.__CALLED__ === 2) isExtraCall = true
                                    } else {
                                        if (self.__CALLED__ === 1) isExtraCall = true
                                    }

                                    if (isExtraCall) me.fail(R.get('calledMoreThanOnce', { num : index + 1, line : sourceLine }))

                                    self.__CALLED__ = 1

                                    if (!stepHasOwnAsyncFrame) me.endAsync(async)

                                    args        =  Array.prototype.slice.call(arguments)

                                    if (step.desc) me.pass(step.desc)

                                    data.next && data.next()
                                },
                                async   : async
                            }
                        }

                        if (step instanceof Siesta.Test) {
                            // do not try to launch sub-test if finalization has started
                            if (!(me.finalizationStarted || me.endDate)) me.launchSubTest(step, initStep(true).next)

                        } else if (me.typeOf(step).match(/^(Async)?Function$/) || me.typeOf(step.action).match(/^(Async)?Function$/)) {
                            var func            = me.typeOf(step).match(/^(Async)?Function$/) ? step : step.action

                            var stepInitData    = initStep(false)
                            // if the last step is a function - then provide empty function as the "next" callback for it
                            var next            = isLast ? function () {} : stepInitData.next

                            args.unshift(next)

                            var result

                            if (me.transparentEx)
                                result = func.apply(me, args)
                            else {
                                var e = me.getExceptionCatcher()(function () {
                                    result = func.apply(me, args)
                                })

                                if (e !== undefined) {
                                    // the "me.endAsync()" here is not enough, because for the t.beforeEach()
                                    // hook, we have a nested chain - one in the "launchSpecs",
                                    // 2nd in the "runBeforeSpecHooks", thats why in the following statement
                                    // we finalize the whole test, to avoid extra waiting for the
                                    // "chain step can not proceed" message
                                    // for `isLast` case `endAsync` will be done below
                                    if (!isLast) me.endAsync(stepInitData.async)

                                    me.failWithException(e, R.get('chainStepEx'))
                                }
                            }

                            // if there's no returning value from the step, then it must call `next` to continue
                            if (!result && !isLast && !me.analyzeChainStep(func)) me.fail(R.get('stepFn') + ' [' + func.toString() + '] ' + R.get('notUsingNext'))

                            if (result) {
                                hasReturnedPromise = true

                                me.handleReturnedPromise(
                                    result,
                                    next,
                                    me.formatString(
                                        'The promise returned from the chain step [' + me.stringifyChainStep(step) + '] did not resolve within {time}ms',
                                        { time : me.defaultTimeout }
                                    )
                                )
                            }

                            // and finalize the async frame manually, as the "nextFunc" for last step will never be called
                            if (isLast) {
                                me.endAsync(stepInitData.async)

                                if (step.desc) me.pass(step.desc)
                            }

                        } else if (me.typeOf(step) == 'String') {
                            var action      = new Siesta.Test.Action.Eval({
                                actionString        : step,
                                next                : initStep(false).next,
                                test                : me
                            })

                            action.process()

                        } else {
                            var action      = Siesta.Test.ActionRegistry().create(step, me, args, initStep)

                            action.process()
                        }
                    }
                })
            })

            queue.run()
        },


        /**
         * This is a wrapper around the {@link #chain} method, which allows you to run the chain over the steps, generated from the elements
         * of some array. For example, if in some step of outer chain, we need to click the elements with ids, given as the array, we can do:
         *

    function (next) {
        var ids     = [ 'button-1', 'button-2', 'button-3' ]

        t.chainForArray(ids, function (elId) {
            return { click : '#' + elId }
        }, next)
    }
         *
         * @param {Array} array An array with arbitrary elements
         * @param {Function} generator A function, which will be called for every element of the `array`. It should return
         * a chain step, generated from that element. This function can return an array of steps as well. If generator will return `null` or
         * `undefined` nothing will be added to the chain.
         * @param {Function} generator.el An element of the `array`
         * @param {Function} generator.index An index of the element
         * @param {Function} [callback] A function to call, once the chain is completed.
         */
        chainForArray : function (array, generator, callback, reverse) {
            var me          = this
            var steps       = []

            Joose.A[ reverse ? 'eachR' : 'each' ](array, function (el, index) {
                var res     = generator.call(me, el, index)

                if (me.typeOf(res) == 'Array')
                    steps.push.apply(steps, res)
                else
                    if (res) steps.push(res)
            })

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

            this.chain(steps)
        },


        verifyExpectedNumber : function (actual, expected) {
            var operator        = '=='

            if (this.typeOf(expected) == 'String') {
                var match       = /([<>=]=?)\s*(\d+)/.exec(expected)
                var R               = Siesta.Resource('Siesta.Test.Browser');

                if (!match) throw new Error(R.get('wrongFormat')  + ": " + expected)

                operator        = match[ 1 ]
                expected        = Number(match[ 2 ])
            }

            switch (operator) {
                case '==' : return actual == expected
                case '<=' : return actual <= expected
                case '>=' : return actual >= expected
                case '<' : return actual < expected
                case '>' : return actual > expected
            }
        },


        getMaximalTimeout : function () {
            return Math.max(this.waitForTimeout, this.defaultTimeout, this.subTestTimeout, this.timeout || 0, this.isReadyTimeout)
        }
    },


    after : {

        onBeforeTestFinalize : function () {
            if (this.autoCheckGlobals && !this.isFailed() && !this.parent) this.verifyGlobals()
        }
    }
})
//eof Siesta.Test.More