/*

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

*/
/**
@class Siesta.Test
@mixin Siesta.Test.More
@mixin Siesta.Test.Date
@mixin Siesta.Test.Function
@mixin Siesta.Test.BDD
@mixin Siesta.Util.Role.CanCompareObjects

`Siesta.Test` is a base testing class in Siesta hierarchy. It's not supposed to be created manually, instead the project will create it for you.

This file is a reference only, for a getting start guide and manual please refer to the <a href="#!/guide/getting_started_browser">Siesta getting started in browser environment</a> guide.

SYNOPSIS
========

    StartTest(function(t) {
        t.diag("Sanity")

        t.ok($, 'jQuery is here')

        t.ok(Your.Project, 'My project is here')
        t.ok(Your.Project.Util, '.. indeed')

        setTimeout(function () {

            t.ok(true, "True is ok")

        }, 500)
    })


*/

Class('Siesta.Test', {

    does        : [
        Siesta.Util.Role.CanFormatStrings,
        Siesta.Util.Role.CanGetType,
        Siesta.Util.Role.CanCompareObjects,
        Siesta.Util.Role.CanEscapeRegExp,

        Siesta.Test.More,
        Siesta.Test.Date,
        Siesta.Test.Function,
        Siesta.Test.BDD,

        JooseX.Observable,

        // quick "id" attribute, perhaps should be changed later
        Siesta.Util.Role.HasUniqueGeneratedId
    ],


    has        : {
        name                : null,

        uniqueId            : function () {
            var holder  = Siesta.Test

            holder.__UNIQUE_ID_GEN__ = holder.__UNIQUE_ID_GEN__ || 0

            return ++holder.__UNIQUE_ID_GEN__
        },

        /**
         * @property url The url of this test, as given to the {@link Siesta.Project#start start} method. All subtests of some top-level test shares the same url.
         */
        url                 : { required : true },
        urlExtractRegex     : {
            is      : 'rwc',
            lazy    : function () {
                return new RegExp(this.url.replace(/([.*+?^${}()|[\]\/\\])/g, "\\$1") + ':(\\d+)')
            }
        },

        referenceUrl        : null,

        assertPlanned       : null,
        assertCount         : 0,

        // whether this test contains only "todo" assertions
        isTodo              : false,

        results             : {
            lazy    : function () {
                return new Siesta.Result.SubTest({ description  : this.name || 'Root', test : this })
            }
        },

        run                 : null,
        startTestAnchor     : null,
        exceptionCatcher    : null,
        testErrorClass      : null,

        // same number for the whole subtests tree
        generation          : function () {
            return Math.random()
        },

        launchId            : null,

        parent              : null,

        project             : null,

        // backward compat - alias for `project`
        harness             : null,


        /**
         * @cfg {Number} isReadyTimeout
         *
         * Timeout in milliseconds to wait for test start. Default value is 10000. See also {@link #isReady}
         */
        isReadyTimeout      : 10000,

        // indicates that a test has thrown an exception (not related to failed assertions)
        failed              : false,
        failedException     : null, // stringified exception
        failedExceptionType : null, // type of exception

        // start and end date are stored as numbers (new Date() - 0)
        // this is to allow sharing date instances between different contexts
        startDate           : null,
        endDate             : null,
        lastActivityDate    : null,
        contentManager      : null,

        // the scope provider for the context of the test page
        scopeProvider       : null,
        // the context of the test page
        global              : null,

        reusingSandbox      : false,
        sandboxCleanup      : true,
        sharedSandboxState  : null,

        // the scope provider for the context of the test script
        // usually the same as the `scopeProvider`, but may be different in case of using `enablePageRedirect` option
        scriptScopeProvider : null,

        transparentEx       : false,

        needDone            : false,
        isDone              : false,

        defaultTimeout      : 15000,
        // a default timeout for sub tests
        subTestTimeout      : null,
        // a timeout of this particular test
        timeout             : null,

        timeoutsCount       : function () {
            return { counter : 1 }
        },
        timeoutIds          : Joose.I.Object,
        idsToIndex          : Joose.I.Object,
        waitTitles          : Joose.I.Object,


        // indicates that test function has completed the execution (test may be still running due to async)
        processed           : false,
        // indicates that test has started finalization process ("tearDown" method). At this point, test is considered
        // finished, but the failing assertion (if "tearDown" fails) may still be added
        finalizationStarted : false,

        callback            : null,

        // Nbr of exceptions detected while running the test
        nbrExceptions       : 0,
        testEndReported     : false,

        // only used for testing itself, otherwise should be always `true`
        needToCleanup               : true,

        overrideSetTimeout          : false,

        overrideForSetTimeout       : null,
        overrideForClearTimeout     : null,

        originalSetTimeout          : null,
        originalClearTimeout        : null,

        sourceLineForAllAssertions  : false,

        $passCount                  : null,
        $failCount                  : null,

        actionableMethods           : {
            lazy        : 'buildActionableMethods'
        },

        jUnitClass                  : null,
        groups                      : null,
        automationElementId         : null,

        // enableCodeCoverage          : false,

        snoozeUntil                 : null,

        breakTestOnFail             : false,
        breakSubTestOnFail          : false,

        // user-provided config values
        config                      : null
    },


    methods : {

        initialize : function () {
            // backward compat - both `project` and `harness` attributes should be supported
            this.harness            = this.project = this.project || this.harness

            // suppress bubblings of some events (JooseX.Observable does not provide better mechanism for that, yet)
            this.on('teststart', function (event) {
                if (this.parent) event.stopPropagation()
            })

            this.on('testfinalize', function (event) {
                if (this.parent) event.stopPropagation()
            })

            this.on('teststop', function (event) {
                if (this.parent) event.stopPropagation()
            })

            this.on('beforetestfinalize', function (event) {
                if (this.parent) event.stopPropagation()
            })

            this.on('beforetestfinalizeearly', function (event) {
                if (this.parent) event.stopPropagation()
            })

            this.subTestTimeout     = this.subTestTimeout || 2 * this.defaultTimeout

            if (this.snoozeUntil) {
                this.snoozeUntil = new Date(this.snoozeUntil)

                if (isNaN(this.snoozeUntil - 0 )) this.snoozeUntil = null
            }

            if (this.snoozeUntil && new Date() < this.snoozeUntil) this.isTodo = true

            // Potentially may overwrite default properties and break test instance, should be used with care
            if (this.config) Joose.O.extend(this, this.config)
        },

        /**
         * This method allows you to delay the start of the test, for example for performing some asynchronous setup code (like login into an application).
         * Note, that you may want to use the {@link #setup} method instead, as it is a bit simpler to implement.
         *
         * It is supposed to be overridden in a subclass of the Siesta.Test class and should return an object with two properties: "ready" and "reason"
         * ("reason" is only meaningful for the case where "ready : false"). The Test instance will poll this method and will only launch
         * the test after this method returns "ready : true". If waiting for this condition takes longer than {@link #isReadyTimeout}, the test
         * will be launched anyway, but a failing assertion will be added to it.
         *
         * **Important** This method should always check the value returned by a `this.SUPER` call.
         *
         * A typical example of using this method can be seen below:
         *

    Class('My.Test.Class', {

        isa         : Siesta.Test.Browser,

        has         : {
            isCustomSetupDone           : false
        },

        override : {

            isReady : function () {
                var result = this.SUPERARG(arguments);

                if (!result.ready) return result;

                if (!this.isCustomSetupDone) return {
                    ready       : false,
                    reason      : "Waiting for `isCustomSetupDone` took too long - something wrong?"
                }

                return {
                    ready       : true
                }
            },


            start : function () {
                var me      = this;

                Ext.Ajax.request({
                    url     : 'do_login.php',

                    params  : { ... },

                    success : function () {
                        me.isCustomSetupDone    = true
                    }
                })

                this.SUPERARG(arguments)
            }
        },

        ....
    })

         *
         * @return {Object} Object with properties `{ ready : true/false, reason : 'description' }`
         */
        isReady: function() {
            var R = Siesta.Resource('Siesta.Test');

            // this should allow us to wait until the presense of "run" function
            // it will become available after call to StartTest method
            // which some users may call asynchronously, after some delay
            // see https://www.assembla.com/spaces/bryntum/tickets/379
            // in this case test can not be configured using object as 1st argument for StartTest
            this.run    = this.run || this.getStartTestAnchor().args && this.getStartTestAnchor().args[ 0 ]

            return {
                ready   : this.typeOf(this.run) == 'Function' || this.typeOf(this.run) == 'AsyncFunction',
                reason  : R.get('noCodeProvidedToTest')
            }
        },


        // indicates that the tests are identical or from the same tree (one is parent for another)
        isFromTheSameGeneration : function (test2) {
            return this.generation == test2.generation
        },


        toString : function() {
            return this.url
        },


        // deprecated
        plan : function (value) {
            if (this.assertPlanned != null) throw new Error("Test plan can't be changed")

            this.assertPlanned = value
        },


        addResult : function (result) {
            var isAssertion = result instanceof Siesta.Result.Assertion

            if (isAssertion) result.isTodo = this.isTodo

            // only allow to add diagnostic results and todo results after the end of test
            // and only if "needDone" is enabled
            if (isAssertion && (this.isDone || this.isFinished()) && !result.isTodo) {
                if (!this.testEndReported) {
                    this.testEndReported = true
                    var R = Siesta.Resource('Siesta.Test');

                    this.fail(R.get('addingAssertionsAfterDone'))
                }
            }

            if (isAssertion && !result.index) {
                result.index = ++this.assertCount
            }

            this.getResults().push(result)

            // clear the cache
            this.$passCount     = this.$failCount   = null

            /**
             * This event is fired when an individual test case receives a new result (assertion or diagnostic message).
             *
             * This event bubbles up to the {@link Siesta.Project project}, so you can observe it on the project as well.
             *
             * @event testupdate
             * @member Siesta.Test
             * @param {JooseX.Observable.Event} event The event instance
             * @param {Siesta.Test} test The test instance that just has started
             * @param {Siesta.Result} result The new result. Instance of Siesta.Result.Assertion or Siesta.Result.Diagnostic classes
             */
            this.fireEvent('testupdate', this, result, this.getResults())

            this.lastActivityDate = new Date();

            return result
        },


        /**
         * This method output the diagnostic message.
         * @param {String} desc The text of diagnostic message
         */
        diag : function (desc, callback) {
            this.addResult(new Siesta.Result.Diagnostic({
                // protection from user passing some arbitrary JSON object instead of string
                // (which can be circular and then test report will fail with "Converting circular structure to JSON"
                description : String(desc || '')
            }))

            callback && callback();
        },


        /**
         * This method add the passed assertion to this test.
         *
         * @param {String} desc The description of the assertion
         * @param {String/Object} [annotation] The string with additional description how exactly this assertion passes. Will be shown with monospace font.
         * Can be also an object with the following properties:
         * @param {String} annotation.annotation The actual annotation text
         * @param {String} annotation.descTpl The template for the default description text. Will be used if user did not provide any description for
         * assertion. Template can contain variables in braces. The values for variables are taken as properties of `annotation` parameters with the same name:
         *

    this.pass(desc, {
        descTpl         : '{value1} sounds like {value2}',
        value1          : '1',
        value2          : 'one
    })

         *
         */
        pass : function (desc, annotation, result) {
            if (annotation && this.typeOf(annotation) != 'String') {
                // create a default assertion description
                if (!desc && annotation.descTpl) desc = this.formatString(annotation.descTpl, annotation)

                // actual annotation
                annotation          = annotation.annotation
            }

            if (result) {
                result.passed       = true
                result.description  = String(desc || '')
                result.annotation   = annotation
            }

            this.addResult(result || new Siesta.Result.Assertion({
                passed          : true,

                // protection from user passing some arbitrary JSON object instead of string
                // (which can be circular and then test report will fail with "Converting circular structure to JSON"
                annotation      : String(annotation || ''),
                description     : String(desc || ''),
                sourceLine      : (result && result.sourceLine) || (annotation && annotation.sourceLine) || this.sourceLineForAllAssertions && this.getSourceLine() || null
            }))
        },


        /**
         * This method add the failed assertion to this test.
         *
         * @param {String} desc The description of the assertion
         * @param {String/Object} annotation The additional description how exactly this assertion fails. Will be shown with monospace font.
         *
         * Can be either string or an object with the following properties. In the latter case a string will be constructed from the properties of the object.
         *
         * - `assertionName` - the name of assertion, will be shown in the 1st line, along with originating source line (in FF and Chrome only)
         * - `got` - an arbitrary JavaScript object, when provided will be shown on the next line
         * - `need` - an arbitrary JavaScript object, when provided will be shown on the next line
         * - `gotDesc` - a prompt for "got", default value is "Got", but can be for example: "We have"
         * - `needDesc` - a prompt for "need", default value is "Need", but can be for example: "We need"
         * - `annotation` - A text to append on the last line, can contain some additional explanations
         *
         *  The "got" and "need" values will be stringified to the "not quite JSON" notation. Notably the points of circular references will be
         *  marked with `[Circular]` marks and the values at 4th (and following) level of depth will be marked with triple points: `[ [ [ ... ] ] ]`
         */
        fail : function (desc, annotation, result) {
            var sourceLine          = (result && result.sourceLine) || (annotation && annotation.sourceLine) || this.getSourceLine()
            var assertionName       = '';

            if (annotation && this.typeOf(annotation) != 'String') {
                if (!desc && annotation.descTpl) desc = this.formatString(annotation.descTpl, annotation)

                var strings             = []

                var params              = annotation
                var hasGot              = params.hasOwnProperty('got')
                var hasNeed             = params.hasOwnProperty('need')
                var gotDesc             = params.gotDesc || 'Got'
                var needDesc            = params.needDesc || 'Need'

                assertionName           = params.assertionName
                annotation              = params.annotation

                if (!params.ownTextOnly && (assertionName || sourceLine)) strings.push(
                    'Failed assertion ' + (assertionName ? '`' + assertionName + '` ' : '') + this.formatSourceLine(sourceLine)
                )

                if (hasGot && hasNeed) {
                    var max         = Math.max(gotDesc.length, needDesc.length)

                    gotDesc         = this.appendSpaces(gotDesc, max - gotDesc.length + 1)
                    needDesc        = this.appendSpaces(needDesc, max - needDesc.length + 1)
                }

                if (hasGot)     strings.push(gotDesc   + ': ' + Siesta.Util.Serializer.stringify(params.got))
                if (hasNeed)    strings.push(needDesc  + ': ' + Siesta.Util.Serializer.stringify(params.need))

                if (annotation) strings.push(annotation)

                annotation      = strings.join('\n')
            }

            if (result) {
                // Failing a pending waitFor operation
                result.name         = assertionName;
                result.passed       = false;
                result.annotation   = annotation;
                result.description  = desc;
            }

            this.addResult(result || new Siesta.Result.Assertion({
                name        : assertionName,
                passed      : false,
                sourceLine  : sourceLine,

                // protection from user passing some arbitrary JSON object instead of string
                // (which can be circular and then test report will fail with "Converting circular structure to JSON"
                annotation  : String(annotation || ''),
                description : String(desc || '')
            }))

            this.onFailedAssertion()
        },


        onFailedAssertion : function (noNeedToExit) {
            if (!this.isTodo) {
                if (this.project.debuggerOnFail) eval("debugger")

                if (this.project.breakOnFail && !this.__STOPPED__) {
                    this.__STOPPED__    = true

                    this.project.stopCurrentLaunch(this)

                    if (!noNeedToExit) this.exit()
                }

                if ((this.breakTestOnFail || this.breakSubTestOnFail) && !this.__STOPPED__) {
                    this.__STOPPED__    = true

                    if (!noNeedToExit) {
                        if (this.breakTestOnFail) {
                            this.getRootTest().exit()
                        } else {
                            this.exit()
                        }
                    }
                }
            }
        },


        /**
         * This method interrupts the test execution. You can use it if, for example, you already know the status of
         * test (failed) and further actions involves long waitings etc.
         *
         * This method accepts the same arguments as the {@link #fail} method. If at least the one argument is given,
         * a failed assertion will be added to the test before the exit.
         *
         * The interruption is performed by throwing an exception from the test. If you have the
         * {@link Siesta.Project#transparentEx transparentEx} option enabled you will observe it in the debugger/console.
         *
         * See also {@link Siesta.Project#breakTestOnFail breakTestOnFail}, {@link Siesta.Project#breakSubTestOnFail breakSubTestOnFail}
         *
         * For example:
         *

        t.chain(
            function (next) {
                // do something

                next()
            },
            function (next) {
                if (someCondition)
                    t.exit("Failure description")
                else
                    next()
            },
            { waitFor : function () { ... } }
        )


         *
         * @param {String} [desc] The description of the assertion
         * @param {String/Object} [annotation] The additional description how exactly this assertion fails. Will be shown with monospace font.
         */
        exit : function (desc, annotation) {
            if (arguments.length > 0) this.fail(desc, annotation)

            this.finalize(true)

            throw '__SIESTA_TEST_EXIT_EXCEPTION__'
        },


        getSource : function () {
            return this.contentManager.getContentOf(this.url)
        },


        getSourceLine : function () {
            var stack           = new Error().stack

            if (!stack) {
                try {
                    throw new Error()
                } catch (e) {
                    stack       = e.stack
                }
            }

            if (stack) {
                var match       = stack.match(this.urlExtractRegex())

                if (match) return match[ 1 ]
            }

            return null
        },


        getStartTestAnchor : function () {
            return this.startTestAnchor
        },


        getExceptionCatcher : function () {
            return this.exceptionCatcher
        },


        getTestErrorClass : function () {
            return this.testErrorClass
        },


        processCallbackFromTest : function (callback, args, scope) {
            var me      = this

            if (!callback) return true;

            if (this.transparentEx) {
                callback.apply(scope || this.global, args || [])
            } else {
                var e = this.getExceptionCatcher()(function () {
                    callback.apply(scope || me.global, args || [])
                })

                if (e) {
                    this.failWithException(e)

                    // flow should be interrupted - exception detected
                    return false
                }
            }

            // flow can be continued
            return true
        },


        getStackTrace : function (e) {
            if (Object(e) !== e)    return null
            if (!e.stack)           return null

            var stackLines      = (e.stack + '').split('\n')
            var message         = e + ''
            var R               = Siesta.Resource('Siesta.Test');
            var result          = []
            var match

            for (var i = 0; i < stackLines.length; i++) {
                var line        = stackLines[ i ]

                if (!line) continue

                // first line should contain exception message
                if (!i) {
                    if (line != message)
                        result.push(message)
                    else {
                        result.push(line)
                        continue;
                    }
                }

                result.push(line)
            }

            if (!result.length) return null

            return result
        },


        formatSourceLine : function (sourceLine) {
            var R               = Siesta.Resource('Siesta.Test');

            return sourceLine ? (R.get('atLine') + ' ' + sourceLine + ' ' + R.get('of') + ' ' + this.url) : ''
        },


        appendSpaces : function (str, num) {
            var spaces      = ''

            while (num--) spaces += ' '

            return str + spaces
        },


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

            this.getResults().each(function (result) {
                if (result instanceof Siesta.Result.Assertion) func.call(scope, result)
            })
        },


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

            this.getResults().each(function (result) {
                if (result instanceof Siesta.Result.SubTest)
                    if (func.call(scope, result.test) === false) return false
            })
        },


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

            this.getResults().eachChild(function (result) {
                if (result instanceof Siesta.Result.SubTest)
                    if (func.call(scope, result.test) === false) return false
            })
        },


        /**
         * This assertion passes when the supplied `value` evalutes to `true` and fails otherwise.
         *
         * @param {Mixed} value The value, indicating wheter assertions passes or fails
         * @param {String} [desc] The description of the assertion
         */
        ok : function (value, desc) {
            var R               = Siesta.Resource('Siesta.Test');

            if (value)
                this.pass(desc, {
                    descTpl             : R.get('isTruthy'),
                    value               : value
                })
            else
                this.fail(desc, {
                    assertionName       : 'ok',
                    got                 : value,
                    annotation          : R.get('needTruthy')
                })
        },


        notok : function () {
            this.notOk.apply(this, arguments)
        },

        /**
         * This assertion passes when the supplied `value` evalutes to `false` and fails otherwise.
         *
         * It has a synonym - `notok`.
         *
         * @param {Mixed} value The value, indicating wheter assertions passes or fails
         * @param {String} [desc] The description of the assertion
         */
        notOk : function (value, desc) {
            var R               = Siesta.Resource('Siesta.Test');

            if (!value)
                this.pass(desc, {
                    descTpl             : R.get('isFalsy'),
                    value               : value
                })
            else
                this.fail(desc, {
                    assertionName       : 'notOk',
                    got                 : value,
                    annotation          : R.get('needFalsy')
                })
        },


        /**
         * This assertion passes when the comparison of 1st and 2nd arguments with `==` operator returns true and fails otherwise.
         *
         * As a special case, one or both arguments can be *placeholders*, generated with method {@link #any}.
         *
         * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure
         * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure
         * @param {String} [desc] The description of the assertion
         */
        is : function (got, expected, desc) {
            var R               = Siesta.Resource('Siesta.Test');

            if (expected && got instanceof this.global.Date) {
                this.isDateEqual(got, expected, desc);
            } else if (this.compareObjects(got, expected, false, true))
                this.pass(desc, {
                    descTpl             : R.get('isEqualTo'),
                    got                 : got,
                    expected            : expected
                })
            else
                this.fail(desc, {
                    assertionName       : 'is',
                    got                 : got,
                    need                : expected
                })
        },



        isnot : function () {
            this.isNot.apply(this, arguments)
        },

        isnt : function () {
            this.isNot.apply(this, arguments)
        },


        /**
         * This assertion passes when the comparison of 1st and 2nd arguments with `!=` operator returns true and fails otherwise.
         * It has synonyms - `isnot` and `isnt`.
         *
         * As a special case, one or both arguments can be instance of {@link Siesta.Test.BDD.Placeholder} class, generated with method {@link #any}.
         *
         * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure
         * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure
         * @param {String} [desc] The description of the assertion
         */
        isNot : function (got, expected, desc) {
            var R               = Siesta.Resource('Siesta.Test');

            if (!this.compareObjects(got, expected, false, true))
                this.pass(desc, {
                    descTpl             : R.get('isNotEqualTo'),
                    got                 : got,
                    expected            : expected
                })
            else
                this.fail(desc, {
                    assertionName       : 'isnt',
                    got                 : got,
                    need                : expected,
                    needDesc            : R.get('needNot')
                })
        },


        /**
         * This assertion passes when the comparison of 1st and 2nd arguments with `===` operator returns true and fails otherwise.
         *
         * As a special case, one or both arguments can be instance of {@link Siesta.Test.BDD.Placeholder} class, generated with method {@link #any}.
         *
         * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure
         * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure
         * @param {String} [desc] The description of the assertion
         */
        exact : function (got, expected, desc) {
            var R               = Siesta.Resource('Siesta.Test');

            if (this.compareObjects(got, expected, true, true))
                this.pass(desc, {
                    descTpl             : R.get('isStrictlyEqual'),
                    got                 : got,
                    expected            : expected
                })
            else
                this.fail(desc, {
                    assertionName       : 'isStrict',
                    got                 : got,
                    need                : expected,
                    needDesc            : R.get('needStrictly')
                })
        },


        /**
         * This assertion passes when the comparison of 1st and 2nd arguments with `===` operator returns true and fails otherwise.
         *
         * As a special case, one or both arguments can be instance of {@link Siesta.Test.BDD.Placeholder} class, generated with method {@link #any}.
         *
         * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure
         * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure
         * @param {String} [desc] The description of the assertion
         */
        isStrict : function (got, expected, desc) {
            var R               = Siesta.Resource('Siesta.Test');

            if (this.compareObjects(got, expected, true, true))
                this.pass(desc, {
                    descTpl             : R.get('isStrictlyEqual'),
                    got                 : got,
                    expected            : expected
                })
            else
                this.fail(desc, {
                    assertionName       : 'isStrict',
                    got                 : got,
                    need                : expected,
                    needDesc            : R.get('needStrictly')
                })
        },


        isntStrict : function () {
            this.isNotStrict.apply(this, arguments)
        },

        /**
         * This assertion passes when the comparison of 1st and 2nd arguments with `!==` operator returns true and fails otherwise.
         * It has synonyms - `isntStrict`.
         *
         * As a special case, one or both arguments can be instance of {@link Siesta.Test.BDD.Placeholder} class, generated with method {@link #any}.
         *
         * @param {Mixed} got The value "we have" - will be shown as "Got:" in case of failure
         * @param {Mixed} expected The value "we expect" - will be shown as "Need:" in case of failure
         * @param {String} [desc] The description of the assertion
         */
        isNotStrict : function (got, expected, desc) {
            var R               = Siesta.Resource('Siesta.Test');

            if (!this.compareObjects(got, expected, true, true))
                this.pass(desc, {
                    descTpl             : R.get('isStrictlyNotEqual'),
                    got                 : got,
                    expected            : expected
                })
            else
                this.fail(desc, {
                    assertionName       : 'isntStrict',
                    got                 : got,
                    need                : expected,
                    needDesc            : R.get('needStrictlyNot')
                })
        },


        // DEPRECATED in 5.0
        wait : function (title, howLong) {
            var R               = Siesta.Resource('Siesta.Test');

            if (this.waitTitles.hasOwnProperty(title)) throw new Error(R.get('alreadyWaiting')+ " [" + title + "]")

            return this.waitTitles[ title ] = this.beginAsync(howLong)
        },


        // DEPRECATED in 5.0
        endWait : function (title) {
            var R               = Siesta.Resource('Siesta.Test');

            if (!this.waitTitles.hasOwnProperty(title)) throw new Error(R.get('noOngoingWait') + " [" + title + "]")

            this.endAsync(this.waitTitles[ title ])

            delete this.waitTitles[ title ]
        },



        /**
         * This method starts the "asynchronous frame". The test will wait for all asynchronous frames to complete before it will finalize.
         * The frame should be finished with the {@link #endAsync} call within the provided `time`, otherwise a failure will be reported.
         *
         * For example:
         *
         *      var async = t.beginAsync()
         *
         *      Ext.require('Some.Class', function () {
         *
         *          t.ok(Some.Class, 'Some class was loaded')
         *
         *          t.endAsync(async)
         *      })
         *
         *
         * Additionally, if you return a `Promise` instance from the test function itself, Siesta will wait until that promise is resolved before finalizing the test.
         * In modern browsers, this allows us to use `async/await` functions:

     StartTest(t => {
        let someAsyncOperation = () => new Promise((resolve, reject) => {
            setTimeout(() => resolve(true), 1000)
        })

        t.it('Doing async stuff', async t => {
            let res = await someAsyncOperation()

            t.ok(res, "Async stuff finished correctly")
        })
    })

         * @param {Number} time The maximum time (in ms) to wait until force the finalization of this async frame. Optional. Default time is 15000 ms.
         * @param {Function} errback Optional. The function to call in case the call to {@link #endAsync} was not detected withing `time`. If function
         * will return any "truthy" value, the failure will not be reported (you can report own failure with this errback).
         *
         * @return {Object} The frame object, which can be used in {@link #endAsync} call
         */
        beginAsync : function (time, errback) {
            time                        = time || this.defaultTimeout

            if (time > this.getMaximalTimeout()) this.fireEvent('maxtimeoutchanged', time)

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

            var index                   = this.timeoutsCount.counter++

            // in NodeJS `setTimeout` returns an object and not a simple ID, so we try hard to store that object under unique index
            // also using `setTimeout` from the scope of test - as timeouts in different scopes in browsers are mis-synchronized
            // can't just use `this.originalSetTimeout` because of scoping issues
            var timeoutId               = originalSetTimeout(function () {

                if (me.hasAsyncFrame(index)) {
                    if (!errback || !errback.call(me, me)) me.fail(R.get('noMatchingEndAsync', { time : time }))

                    me.endAsync(index)
                }
            }, time)

            this.timeoutIds[ index ]    = timeoutId

            return index
        },


        timeoutIdToIndex : function (id) {
            var index

            if (typeof id == 'object') {
                index       = id.__index
            } else {
                index       = this.idsToIndex[ id ]
            }

            return index
        },


        hasAsyncFrame : function (index) {
            return this.timeoutIds.hasOwnProperty(index)
        },


        hasAsyncFrameByTimeoutId : function (id) {
            return this.timeoutIds.hasOwnProperty(this.timeoutIdToIndex(id))
        },


        /**
         * This method finalize the "asynchronous frame" started with {@link #beginAsync}.
         *
         * @param {Object} frame The frame to finalize (returned by {@link #beginAsync} method
         */
        endAsync : function (index) {
            var originalSetTimeout      = this.originalSetTimeout
            var originalClearTimeout    = this.originalClearTimeout || this.global.clearTimeout
            var counter                 = 0
            var R                       = Siesta.Resource('Siesta.Test');

            if (index == null) Joose.O.each(this.timeoutIds, function (timeoutId, indx) {
                index = indx
                if (counter++) throw new Error(R.get('endAsyncMisuse'))
            })

            var timeoutId               = this.timeoutIds[ index ]

            // need to call in this way for IE < 9
            originalClearTimeout(timeoutId)
            delete this.timeoutIds[ index ]

            var me = this

            if (this.processed && !this.isFinished())
                // to allow potential call to `done` after `endAsync`
                originalSetTimeout(function () {
                    me.finalize()
                }, 1)
        },


        clearTimeouts : function () {
            var originalClearTimeout    = this.originalClearTimeout

            Joose.O.each(this.timeoutIds, function (value, id) {
                originalClearTimeout(value)
            })

            this.timeoutIds = {}
        },


        processSubTestConfig : function (config) {
            var cfg = Joose.O.extend({
                parent                  : this,

                isTodo                  : this.isTodo,
                transparentEx           : this.transparentEx,

                waitForTimeout          : this.waitForTimeout,
                waitForPollInterval     : this.waitForPollInterval,
                defaultTimeout          : this.defaultTimeout,
                timeout                 : this.subTestTimeout,
                subTestTimeout          : this.subTestTimeout,

                global                  : this.global,
                url                     : this.url,
                scopeProvider           : this.scopeProvider,
                project                 : this.project,
                generation              : this.generation,
                launchId                : this.launchId,

                overrideSetTimeout      : this.overrideSetTimeout,
                originalSetTimeout      : this.originalSetTimeout,
                originalClearTimeout    : this.originalClearTimeout,

                // share the same counter for the whole subtests tree
                timeoutsCount           : this.timeoutsCount,

                autoCheckGlobals        : false,
                needToCleanup           : false,

                breakTestOnFail         : this.breakTestOnFail,
                breakSubTestOnFail      : this.breakSubTestOnFail
            }, config)

            return cfg
        },


        /**
         * Returns a new instance of the test class, configured as being a "sub test" of the current test.
         *
         * The number of nesting levels is not limited - ie sub-tests may have own sub-tests.
         *
         * Note, that this method does not starts the sub test, but only instatiate it. To start the sub test,
         * use the {@link #launchSubTest} method or the {@link #subTest} helper method.
         *
         * @param {String} name The name of the test. Will be used in the UI, as the parent node name in the assertions tree
         * @param {Function} code A function with test code. Will receive a test instance as the 1st argument.
         * @param {Number} [timeout] A maximum duration (in ms) for this sub test. If test will not complete within this time,
         * it will be considered failed. If not provided, the {@link Siesta.Project#subTestTimeout} value is used.
         *
         * @return {Siesta.Test} A sub test instance
         */
        getSubTest : function (arg1, arg2, arg3) {
            var config
            var R = Siesta.Resource('Siesta.Test');

            if (arguments.length == 2 || arguments.length == 3)
                config = {
                    name        : arg1,
                    run         : arg2,
                    timeout     : arg3
                }
            else if (arguments.length == 1 && this.typeOf(arg1) == 'Function')
                config  = {
                    name        : 'Sub test',
                    run         : arg1
                }

            config              = config || arg1 || {}

            // pass-through only valid timeout values
            if (config.timeout == null) delete config.timeout

            var name            = config.name

            if (!config.run) {
                this.failWithException(R.get('codeBodyMissingForSubTest', { name : name }))
                throw new Error(R.get('codeBodyMissingForSubTest', { name : name }))
            }
            if (!config.run.length) {
                this.failWithException(R.get('codeBodyMissingTestArg', { name : name }))
                throw new Error(R.get('codeBodyMissingTestArg', { name : name }))
            }

            // in the following code, we cache a specialized version of the current test class,
            // that version has "Siesta.Test.Sub" role consumed, as a trait (on the instance level)
            // this is done for efficiency (to not create a separate class per every sub test)
            // and also because Chrome was throwing this exception randomly:
            //     Required attribute [parent] is missed during initialization of undefined
            // caching seems to have that fixed
            var constructor     = config.meta || this.meta.c
            var cls             = constructor
            var cfg             = this.processSubTestConfig(config)

            if (constructor.__WITHSUB__)
                cls             = constructor.__WITHSUB__
            else
                // only need trait for the top level test
                if (!this.parent) cfg.trait = Siesta.Test.Sub

            var subTest         = new cls(cfg)

            if (!this.parent && !constructor.__WITHSUB__) constructor.__WITHSUB__ = subTest.meta.c

            return subTest
        },


        /**
         * This method launch the provided sub test instance.
         *
         * @param {Siesta.Test} subTest A test instance to launch
         * @param {Function} callback A function to call, after the test is completed. This function is called regardless from the test execution result.
         */
        launchSubTest : function (subTest, callback) {
            if (this.parent && this.parent.finalizationStarted) return

            var me          = this
            var R           = Siesta.Resource('Siesta.Test');
            var timeout     = subTest.timeout || this.subTestTimeout

            var async       = this.beginAsync(timeout, function () {
                me.fail(R.get('failedToFinishWithin', { name : subTest.name ? '[' + subTest.name + ']' : '', timeout : timeout }))

                me.restoreTimeoutOverrides()

                testEndListener.remove()

                subTest.finalize(true)

                callback && callback(subTest)

                return true
            })

            var testEndListener = subTest.on('testfinalize', function () {
                me.endAsync(async)

                me.restoreTimeoutOverrides()

                callback && callback(subTest)
            })

            this.addResult(subTest.getResults())

            subTest.start()
        },


        /**
         * With this method you can mark a group of assertions as "todo", assuming they most likely will fail,
         * but it's still worth to try to run them.
         * The supplied `code` function will be run, it will receive a new test instance as the 1st argument,
         * which should be used for assertion checks (and not the primary test instance, received from `StartTest`).
         *
         * Assertions, failed inside of the `code` block will be still treated by project as "green".
         * Assertions, passed inside of the `code` block will be treated by project as bonus ones and highlighted.
         *
         * See also {@link Siesta.Test.ExtJS#knownBugIn} and {@link Siesta.Test.ExtJS#snooze} methods. Note, that this method will start a new {@link #subTest sub test}.
         *
         * For example:

            t.todo('Scheduled for 4.1.x release', function (todo) {

                var treePanel    = new Ext.tree.Panel()

                todo.is(treePanel.getView().store, treePanel.store, 'NodeStore and TreeStore have been merged and there is only 1 store now');
            })

         * @param {String} why The reason/description for the todo
         * @param {Function} code A function, wrapping the "todo" assertions. This function will receive a special test class instance
         * which should be used for assertion checks
         */
        todo : function (why, code, callback) {
            if (this.typeOf(why) == 'Function') why = [ code, code = why ][ 0 ]

            var todo        = this.getSubTest({
                name            : why,

                run             : code,

                isTodo          : true,
                transparentEx   : false
            })

            this.launchSubTest(todo, callback)
        },


        /**
         * This method allows you to "snooze" the failing test (make it a {@link Siesta.Test#todo todo test} until certain date.
         * After that date, test will become "normal" again. Use with care :)
         *
            t.snooze('2014-10-10', function (todo) {

                var treePanel    = new Ext.tree.Panel()

                todo.is(treePanel.getView().store, treePanel.store, 'NodeStore and TreeStore have been merged and there is only 1 store now');
            })
         *
         * @param {String/Date} snoozeUntilDate The date until which we don't want to hear about this test. Can be provided as `Date` instance or a string, recognized by `Date` constructor
         * @param {Function} fn The function body of the test, will receive a new test instance as 1st argument
         * @param {String} reason The reason or explanation why this test is "snoozed"
         */
        snooze : function(snoozeUntilDate, fn, reason) {
            var R           = Siesta.Resource('Siesta.Test');
            var snoozeDate  = new Date(snoozeUntilDate)

            if (new Date() > snoozeDate) {
                this.it('Unsnoozed', fn, null, false, false);
            } else {
                this.it(R.get('Snoozed until') + ' ' + snoozeDate + ': ' + (reason || ''), fn, null, false, true);
            }
        },



        /**
         * This method starts a new sub test. Sub tests have separate order of assertions. In the browser UI,
         * sub tests are presented with the "parent" node of the assertions tree. Sub tests are useful if you want to test
         * several asynchronous processes in parallel, and would like to see assertions from every process separated.
         *
         * Sub tests may have their own sub tests, the number of nesting levels is not limited.
         *
         * Sub test can contain asynchronous methods as any other tests. Sub tests are considered completed
         * only when all of its asynchronous methods have completed *and* all of its sub-tests are completed too.
         *
         * For example:
         *

    t.subTest('Load 1st store', function (t) {
        var async   = t.beginAsync()

        store1.load({
            callback : function () {
                t.endAsync(async);
                t.isGreater(store1.getCount(), 0, "Store1 has been loaded")
            }
        })
    })

    t.subTest('Load 2nd store', function (t) {
        var async   = t.beginAsync()

        store2.load({
            callback : function () {
                t.endAsync(async);
                t.isGreater(store2.getCount(), 0, "Store2 has been loaded")
            }
        })
    })

         * Note, that sub test starts right away, w/o waiting for any previous sub tests to complete. If you'd like to run several sub-tests
         * sequentially, use {@link #chain} method in combination with {@link #getSubTest} method.
         *
         * @param {String} desc The name of the sub test. Will be shown as the name of the parent node in assertion tree.
         * @param {Function} code The test function to execute. It will receive a test instance as 1st argument. This test instance *must* be
         * used for assertions inside of the test function
         * @param {Function} callback The callback to execute after the sub test completes (either successfully or not)
         * @param {Number} [timeout] A maximum duration (in ms) for this sub test. If test will not complete within this time,
         * it will be considered failed. If not provided, the {@link Siesta.Project#subTestTimeout} value is used.
         */
        subTest : function (desc, code, callback, timeout) {
            var subTest     = this.getSubTest({
                name            : desc || Siesta.Resource('Siesta.Test', 'Subtest'),
                timeout         : timeout,
                run             : code
            })

            this.launchSubTest(subTest, callback)

            return subTest
        },


        stringifyException : function (e, stackTrace) {
            var stringified             = e + ''
            var annotation              = typeof stackTrace === 'string' ? stackTrace : (stackTrace || this.getStackTrace(e) || []).join('\n')

            // prepend the exception message to the stack trace if its not already there
            if (annotation.indexOf(stringified) == -1) annotation = stringified + '\n' + annotation

            return annotation
        },


        failWithException : function (e, description, useDoFinalize) {
            var R                       = Siesta.Resource('Siesta.Test');

            this.failed                 = true

            this.failedException        = e + ''
            this.failedExceptionType    = this.typeOf(e)

            var stackTrace              = this.getStackTrace(e)

            this.addResult(new Siesta.Result.Assertion({
                isException     : true,
                exceptionType   : this.failedExceptionType,
                passed          : false,
                description     : description ? description : ((this.parent ? R.get('Subtest') + " `" + this.name + "`" : R.get('Test') + ' ') + ' ' + R.get('threwException')),
                annotation      : this.stringifyException(e, stackTrace)
            }))

            /**
             * This event is fired when an individual test case has thrown an exception.
             *
             * This event bubbles up to the {@link Siesta.Project project}, so you can observe it on the project as well.
             *
             * @event testfailedwithexception
             * @member Siesta.Test
             * @param {JooseX.Observable.Event} event The event instance
             * @param {Siesta.Test} test The test instance that just threw an exception
             * @param {Object} exception The exception thrown
             */
            this.fireEvent('testfailedwithexception', this, e, stackTrace);

            this.onFailedAssertion(true)

            var testToFinalize  = this.breakTestOnFail ? this.getRootTest() : this

            if (useDoFinalize)
                this.doFinalize()
            else
                testToFinalize.finalize(true)
        },


        restoreTimeoutOverrides : function () {
            if (this.overrideSetTimeout) {
                this.global.setTimeout      = this.overrideForSetTimeout
                this.global.clearTimeout    = this.overrideForClearTimeout
            }
        },


        // start method can potentially immediately fail the test because of `preloadErrors`
        // it should not access the test's scope because of possible cross-origin failure
        // but it still fires the "teststart" event to notify about assertions
        start : function (preloadErrors) {
            var me          = this;
            var R           = Siesta.Resource('Siesta.Test');

            if (this.startDate) throw R.get('testAlreadyStarted');

            this.startDate  = new Date() - 0

            /**
             * This event is fired when an individual test case starts. When *started*, the test will be waiting for
             * the {@link #isReady} condition to be fullfilled and the {@link #setup} method to complete.
             * After that the test will be *launched* (and execute the `StartTest` function).
             *
             * This event bubbles up to the {@link Siesta.Project project}, you can observe it on the project as well.
             *
             * @event teststart
             * @member Siesta.Test
             * @param {JooseX.Observable.Event} event The event instance
             * @param {Siesta.Test} test The test instance that just has started
             */
            this.fireEvent('teststart', this);

            if (preloadErrors && preloadErrors.length) {
                // this branch bypasses the "doStart" and "finalize" methods
                // its kind of "shortcut execution path"
                Joose.A.each(preloadErrors, function (error) {
                    if (!error.isException)
                        me.fail(error.message)
                    else {
                        me.failWithException(error.message, '', true)
                        return false
                    }
                })

                me.doFinalize()

                return true
            } else
                this.doStart()
        },


        doStart : function () {
            var me          = this
            var R           = Siesta.Resource('Siesta.Test');

            me.onTestStart()

            // Sub-tests should not perform the `setup` or wait for `isReady` readyness
            if (me.parent || me.reusingSandbox) {
                me.launch()
                return
            }

            var errorMessage;

            // Note, that `setTimeout, setInterval` and similar methods here are from the project context

            var cont            = function (isReadyError) {
                var hasTimedOut     = false

                var setupTimeout    = setTimeout(function () {
                    hasTimedOut     = true
                    me.launch(R.get('setupTookTooLong'))
                }, me.isReadyTimeout)

                me.setup(
                    function () {
                        if (!hasTimedOut) {
                            clearTimeout(setupTimeout)
                            me.launch(isReadyError)
                        }
                    },
                    function (setupError) {
                        if (!hasTimedOut) {
                            clearTimeout(setupTimeout)
                            me.launch(isReadyError || setupError)
                        }
                    }
                );
            }

            var readyRes        = me.isReady();

            if (readyRes.ready) {
                // We're ready to go
                cont();
            } else {
                // Need to wait for isReady to give green light
                var timeout         = setTimeout(function () {
                    clearInterval(interval)
                    cont(errorMessage)

                }, me.isReadyTimeout)

                var interval = setInterval(function(){
                    readyRes = me.isReady();

                    if (readyRes.ready) {
                        clearInterval(interval)
                        clearTimeout(timeout)
                        cont();
                    } else {
                        errorMessage = readyRes.reason || errorMessage;
                    }
                }, 100);
            }
        },


        /**
         * This method can perform any setup code your tests need. It is called before the begining of every test and receives
         * a callback and errback, either of those should be called once the setup has completed (or failed).
         * See also {@link #tearDown}.
         *
         * ** IMPORTANT ** Make sure you've called the "super" `setup` method, to perform default setup actions. See below for example.
         *
         * Typical usage for this method can be for example to log in into the application, before interacting with it:
         *

    Class('My.Test.Class', {

        isa         : Siesta.Test.Browser,

        override : {

            setup : function (callback, errback) {
                this.SUPER(function () {

                    Ext.Ajax.request({
                        url     : 'do_login.php',

                        params  : { ... },

                        success : function () {
                            callback()
                        },
                        failure : function () {
                            errback('Login failed')
                        }
                    })

                }, errback)
            }
        },

        ....
    })

         *
         * This method will be called *after* the {@link #isReady} method has reported that the test is ready to start.
         *
         * If the setup has failed for some reason, then an errback should be called and a failing assertion will be added to the test
         * (though the test will be launched anyway). A text of the failed assertion can be given as the 1st argument for the errback.
         *
         * Note, that the setup is supposed to be completed within the {@link #isReadyTimeout} timeout, otherwise it will be
         * considered failed and the test will be launched with a failed assertion.
         *
         * If you need to perform a setup at an earlier point, check the {@link #earlySetup} method.
         *
         * @param {Function} callback A function to call when the setup has completed successfully
         * @param {Function} errback A function to call when the setup has completed with an error
         */
        setup : function (callback, errback) {
            callback.call(this)
        },


        /**
         * This method can perform any asynchronous finalization code your tests need. It is called after the test has
         * been finished (or finalized externally by any reason, for example if user re-starts the test).
         * This method receives a callback and errback, either of those should be called once the tear down has completed
         * (or has failed). Typical usage for this method can be for example to clear the database or release some other resource.
         *
         * **Note** though, that if test suite has experienced a hard failure, this method may not be called.
         *

    Class('My.Test.Class', {

        isa         : Siesta.Test.Browser,

        override : {

            tearDown : function (callback, errback) {
                Ext.Ajax.request({
                    url     : 'clear_the_db.php',

                    params  : { ... },

                    success : function () {
                        callback()
                    },
                    failure : function () {
                        errback("Error message")
                    }
                })
            }
        },

        ....
    })

         *
         * If the tearDown has failed for some reason, then an errback should be called and a failing assertion will be added to the test
         * (though the test will be launched anyway). A text of the failed assertion can be given as the 1st argument for the errback.
         *
         * Note, that the tear down process is supposed to be completed within the {@link #isReadyTimeout} timeout, after this
         * timeout a failing assertion will be added to the test and test suite will just continue execution.
         *
         * @param {Function} callback A function to call when the tear down process has completed successfully
         * @param {Function} errback A function to call when the tear down process has failed.
         * @param {String} [errback.errorMessage] An error message which will be added as a failing assertion to the test.
         */
        tearDown : function (callback, errback) {
            callback.call(this)
        },


        /**
         * This method can perform any setup code your tests need. It is the earliest point for doing setup, it is called
         * even before the iframe of the test is created and started loading. Note, that this means there's no DOM or `window`
         * element the test can access at this point. Normally, you should use the {@link #setup} method
         * for tests initialization purposes.
         *
         * ** IMPORTANT ** Make sure you've called the "SUPER" `earlySetup` method, to perform default setup actions. See below for example.
         *
         * Typical usage for this method can be, for example, to clear the database, before starting to
         * load the {@link Siesta.Project.Browser#pageUrl pageUrl} link.
         *
         * This method receives a callback and errback, either of these should be called once the setup has completed (or failed).
         *

    Class('My.Test.Class', {

        isa         : Siesta.Test.Browser,

        override : {

            earlySetup : function (callback, errback) {
                this.SUPER(
                    function () {
                        Ext.Ajax.request({
                            url     : 'clear_test_db.php',

                            params  : { ... },

                            success : function () {
                                callback()
                            },
                            failure : function () {
                                errback('Reseting DB has failed')
                            }
                        })
                    },
                    errback
                )
            }
        },

        ....
    })

         *
         * If the setup has failed for some reason, then an errback should be called and a failing assertion will be added to the test
         * (though the test will be launched anyway). A text of the failed assertion can be given as the 1st argument for the errback.
         *
         * Note, that the setup is supposed to be completed within the {@link #isReadyTimeout} timeout, otherwise it will be
         * considered failed and the test will be launched with a failed assertion.
         *
         * @param {Function} callback A function to call when the setup has completed successfully
         * @param {Function} errback A function to call when the setup has completed with an error
         */
        earlySetup : function (callback, errback) {
            callback.call(this)
        },


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

            this.forEachUnexpectedGlobal(function (name) {
                try {
                    // can throw exception in IE8
                    delete global[ name ]
                } catch (e) {
                }
            })
        },


        // this method assumes "overrideSetTimeout" option is enabled
        clearAsyncFrameGlobally : function (id) {
            var topTest     = this

            while (topTest.parent) topTest = topTest.parent

            topTest.eachSubTest(function (subTest) {
                if (subTest.hasAsyncFrameByTimeoutId(id)) {
                    subTest.overrideForClearTimeout(id)
                    return false
                }
            })
        },


        launch : function (errorMessage) {
            if (errorMessage) {
                var R = Siesta.Resource('Siesta.Test');

                this.fail(R.get('errorBeforeTestStarted'), {
                    annotation      : errorMessage
                })
            }

            var me                      = this
            var global                  = this.global

            var scopeProvider           = this.scopeProvider

            var originalSetTimeout      = this.originalSetTimeout
            var originalClearTimeout    = this.originalClearTimeout

            if (this.overrideSetTimeout) {
                // see http://www.adequatelygood.com/2011/4/Replacing-setTimeout-Globally
                if (!this.reusingSandbox && !this.parent) scopeProvider.runCode('var setTimeout, clearTimeout;')

                global.setTimeout = this.overrideForSetTimeout = function (func, delay) {

                    var index = me.timeoutsCount.counter++

                    // in NodeJS `setTimeout` returns an object and not a simple ID, so we try hard to store that object under unique index
                    // also using `setTimeout` from the scope of test - as timeouts in different scopes in browsers are mis-synchronized
                    var timeoutId = originalSetTimeout(function () {
                        originalClearTimeout(timeoutId)
                        delete me.timeoutIds[ index ]

                        // if the test func has been executed, but the test was not finalized yet - then we should try to finalize it
                        if (me.processed && !me.isFinished())
                            // we are doing that after slight delay, potentially allowing to setup some other async frames in the "func" below
                            originalSetTimeout(function () {
                                me.finalize()
                            }, 1)

                        func()

                    }, delay)

                    // in NodeJS saves the index of the timeout descriptor to the descriptor
                    if (typeof timeoutId == 'object')
                        timeoutId.__index = index
                    else
                        // in browser (where `timeoutId` is a number) - to the `idsToIndex` hash
                        me.idsToIndex[ timeoutId ] = index

                    return me.timeoutIds[ index ] = timeoutId
                }

                global.clearTimeout = this.overrideForClearTimeout = function (id) {
                    if (id == null) return

                    // if there's no timeout id with this index, that probably means
                    // that this "clearTimeout" call corresponds to the "setTimeout" from some other
                    // sub test - parent most probably (or sibling sub test)
                    // strictly that may not be true, because user can launch several sub tests
                    // simultaneously, but, "overrideSetTimeout" for that case can not be supported reliably
                    // anyway, as we need to know from what test the "setTimeout" call comes (to keep it
                    // active) and we can't override it twice
                    if (!me.hasAsyncFrameByTimeoutId(id)) {
                        me.clearAsyncFrameGlobally(id)

                        return
                    }

                    originalClearTimeout(id)

                    var index       = me.timeoutIdToIndex(id)

                    if (index != null) delete me.timeoutIds[ index ]

                    // if the test func has been executed, but the test was not finalized yet - then we should try to finalize it
                    if (me.processed && !me.isFinished())
                        // we are doing that after slight delay, potentially allowing to setup some other async frames after the "clearTimeout" will complete
                        originalSetTimeout(function () {
                            me.finalize()
                        }, 1)
                }
            }
            // eof this.overrideSetTimeout

            // we only don't need to cleanup up when doing a self-testing or for sub-tests
            if (this.needToCleanup) {
                scopeProvider.beforeCleanupCallback = function () {
                    // if scope cleanup happens most probably user has restarted the test and is not interested in the results
                    // of previous launch
                    // finalizing the previous test in such case
                    if (!me.isFinished()) me.finalize(true)

                    if (me.overrideSetTimeout) {
                        global.setTimeout           = originalSetTimeout
                        global.clearTimeout         = originalClearTimeout
                    }

                    // cleanup the closures just in case (probably useful for IE)
                    originalSetTimeout          = originalClearTimeout  = null
                    global                      = null

                    // this iterator will also process "this" test instance too
                    me.eachSubTest(function (subTest) {
                        subTest.cleanup()
                    })
                }
            }

            if (this.reusingSandbox && this.sandboxCleanup && !this.parent) {
                this.cleanupContextBeforeStart()
            }

            var result
            var run     = this.run
            var e

            if (this.transparentEx) {
                if (me.project.debuggerOnStart && !me.parent) eval("debugger")

                result  = run(me)
            } else
                e = this.getExceptionCatcher()(function () {

                    if (me.project.debuggerOnStart && !me.parent) eval("debugger")

                    result = run(me)
                })

            if (result) this.handleReturnedPromise(
                result,
                null,
                this.formatString(
                    'The promise returned from the sub-test [' + me.name + '] did not resolve within {time}ms',
                    { time : me.defaultTimeout }
                )
            )

            this.afterLaunch(e)
        },


        handleReturnedPromise : function (promise, callback, message) {
            // this seems to be the only valid check for promise
            // https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise
            if (!promise || typeof promise.then !== 'function') return

            var me      = this

            var async   = this.beginAsync(this.defaultTimeout, function () {
                me.fail(message)
                return true
            })

            promise.then(function (value) {
                me.endAsync(async)

                callback && callback(value)
            }, function (e) {
                me.endAsync(async)

                if (e) {
                    me.failWithException(e, "Unhandled promise rejection during the test: [" + me.name + "]")

                    if (me.transparentEx && typeof console !== 'undefined') console.error(e)
                }
            })
        },


        // called before the iframe of the test is removed from DOM
        cleanup : function () {
            this.overrideForSetTimeout  = this.overrideForClearTimeout  = null
            this.originalSetTimeout     = this.originalClearTimeout     = null
            this.global                 = this._global = this.run       = null
            this.exceptionCatcher       = this.testErrorClass           = null
            this.startTestAnchor                                        = null

            this.scopeProvider          = null

            this.purgeListeners()
        },


        // a method executed after the "run" function has been ran - used in BDD role for example
        afterLaunch : function (e) {
            if (e)
                this.failWithException(e)
            else
                this.finalize()
        },


        finalize : function (force) {
            var me          = this
            var R           = Siesta.Resource('Siesta.Test');

            if (me.finalizationStarted || me.isFinished()) return

            me.processed    = true

            if (force) {
                // need to have this flag set before `finalize` is called
                // for child tests
                me.finalizationStarted  = true

                me.clearTimeouts()

                me.eachChildTest(function (childTest) { childTest.finalize(true) })
            }

            if (!Joose.O.isEmpty(me.timeoutIds)) {
                if (
                    !me.__timeoutWarning && me.overrideSetTimeout && me.lastActivityDate &&
                    new Date() - me.lastActivityDate > me.defaultTimeout * 2
                ) {
                    me.diag(R.get('testStillRunning'));
                    me.warn(R.get('testNotFinalized', { url : me.url }));
                    me.__timeoutWarning = true;
                }

                return
            }

            try {
                // test finalization should be proteced from exceptions, otherwise the "me.callback()" wont' be called
                // and "testfinalize" event won't be fired, which will lead to "inactivity timeout" error in automation launchers
                if (!me.isDone && me.doDone(force) === false) return
            } catch (e) {
                me.fail("Exception during test finalization" + e.stack)
                typeof console !== 'undefined' && console.log("Exception during test finalization", e.stack)
            }

            me.finalizationStarted  = true

            me.fireEvent('teststop', me);

            var finalizationCodeStarted     = false

            // will be called only once
            var finalizationCode    = function (tearDownError) {
                if (finalizationCodeStarted) return

                finalizationCodeStarted     = true

                if (tearDownError) me.fail(tearDownError)

                me.doFinalize()
            }

            // sub-tests don't do the "tearDown" process
            if (me.parent || me.reusingSandbox) {
                finalizationCode()

                return
            }

            var originalSetTimeout      = me.originalSetTimeout
            var originalClearTimeout    = me.originalClearTimeout

            var timeout         = originalSetTimeout(function () {
                finalizationCode(R.get('testTearDownTimeout'))
            }, me.isReadyTimeout)

            try {
                me.tearDown(function () {
                    originalClearTimeout(timeout)

                    finalizationCode()
                }, function (error) {
                    originalClearTimeout(timeout)

                    finalizationCode(error)
                })
            } catch (e) {
                finalizationCode(e + '')
            }
        },


        doFinalize : function () {
            var me              = this

            // will be called only once
            if (me.endDate) return

            me.endDate          = new Date() - 0

            if (!me.parent) me.addResult(new Siesta.Result.Summary({
                isFailed            : me.isFailed(),
                description         : me.getSummaryMessage()
            }))

            try {
                me.onTestFinalize()
            } catch (e) {
                typeof console !== 'undefined' && console.log(e)
            }

            /**
             * This event is fired when an individual test case ends (either because it has completed correctly or thrown an exception).
             *
             * This event bubbles up to the {@link Siesta.Project project}, so you can observe it on the project as well.
             *
             * @event testfinalize
             * @member Siesta.Test
             * @param {JooseX.Observable.Event} event The event instance
             * @param {Siesta.Test} test The test instance that just has completed
             */
            me.fireEvent('testfinalize', me);

            // a test end event that bubbles
            me.fireEvent('testendbubbling', me);

            me.callback && me.callback()

            // help garbage collector to cleanup all the context of this callback (huge impact)
            me.callback         = null
        },


        onBeforeTestFinalize : function () {
        },


        onTestFinalize : function () {
        },


        onTestStart : function () {
        },


        getSummaryMessage : function (lineBreaks) {
            var res             = []

            var passCount       = this.getPassCount()
            var failCount       = this.getFailCount()
            var assertPlanned   = this.assertPlanned
            var total           = failCount + passCount

            res.push('Passed: ' + passCount)
            res.push('Failed: ' + failCount)

            if (!this.failed) {
                // there was a t.plan() call
                if (assertPlanned != null) {
                    if (total < assertPlanned)
                        res.push('Looks like you planned ' + assertPlanned + ' tests, but ran only ' + total)

                    if (total > assertPlanned)
                        res.push('Looks like you planned ' + assertPlanned + ' tests, but ran ' +  (total - assertPlanned) + ' extra tests, ' + total + ' total.')

                    if (total == assertPlanned && !failCount) res.push('All tests passed')
                } else {
                    var R = Siesta.Resource('Siesta.Test');

                    if (!this.isDoneCorrectly()) res.push(R.get('missingDoneCall'))

                    if (this.isDoneCorrectly() && !failCount) res.push(R.get('allTestsPassed'))
                }
            }

            return lineBreaks ? res.join(lineBreaks) : res
        },


        /**
         * This method indicates that the test has reached the expected point of its completion and no more assertions are planned.
         * Adding assertions after the call to `done` will be considered as a failure.
         *
         * This method **does not** stop the execution of the test. For that, see the {@link #exit} method.
         *
         * See also {@link Siesta.Project#needDone}
         *
         *
         * @param {Number} delay Optional. When provided, the test will not complete right away, but will wait for `delay` milliseconds for additional assertions.
         */
        done : function (delay) {
            var me                      = this

            if (delay) {
                var async               = this.beginAsync()
                var originalSetTimeout  = this.originalSetTimeout

                originalSetTimeout(function () {
                    me.endAsync(async)
                    me.done()
                }, delay)

            } else {
                this.doDone(false)

                if (this.processed) this.finalize()
            }
        },


        doDone : function (force) {
            var me          = this

            // this is the early "testfinalize" hook, we need "early" and "regular" hooks, since we want the globals check to be the last assertion
            // this event is basically cancellable "testfinalize"
            me.fireEvent('beforetestfinalizeearly', me)

            // Firing the `beforetestfinalizeearly` events may trigger additional test actions
            if (!Joose.O.isEmpty(me.timeoutIds)) {
                if (force)
                    me.clearTimeouts()
                else
                    return false
            }

            // assertion can stil be added in this method and the following event listeners
            // but not after!
            me.onBeforeTestFinalize()

            /**
             * This event is fired before each individual test case ends (no any corresponding Harness actions will have been run yet).
             *
             * This event bubbles up to the {@link Siesta.Project project}, so you can observe it on the project as well.
             *
             * @event beforetestfinalize
             * @member Siesta.Test
             * @param {JooseX.Observable.Event} event The event instance
             * @param {Siesta.Test} test The test instance that is about to finalize
             */
            me.fireEvent('beforetestfinalize', me);

            this.isDone     = true
        },

        // `isDoneCorrectly` means that either test does not need the call to `done`
        // or the call to `done` has been already made
        isDoneCorrectly : function () {
            return !this.needDone || this.isDone
        },


        getAssertionCount : function (excludeTodo) {
            var count   = 0

            this.eachAssertion(function (assertion) {
                if (!excludeTodo || !assertion.isTodo) count++
            })

            return count
        },


        // cached method except the "includeTodo" case
        getPassCount : function (includeTodo) {
            if (this.$passCount != null && !includeTodo) return this.$passCount

            var passCount = 0

            this.eachAssertion(function (assertion) {
                if (assertion.passed && (includeTodo || !assertion.isTodo)) passCount++
            })

            return includeTodo ? passCount : this.$passCount = passCount
        },

        getTodoPassCount : function () {
            var todoCount = 0;

            this.eachAssertion(function (assertion) {
                if (assertion.isTodo && assertion.passed) todoCount++;
            });

            return todoCount;
        },

        getTodoFailCount : function () {
            var todoCount = 0;

            this.eachAssertion(function (assertion) {
                if (assertion.isTodo && !assertion.passed) todoCount++;
            });

            return todoCount;
        },


        // cached method except the "includeTodo" case
        getFailCount : function (includeTodo) {
            if (this.$failCount != null && !includeTodo) return this.$failCount

            var failCount = 0

            this.eachAssertion(function (assertion) {
                if (!assertion.passed && (includeTodo || !assertion.isTodo)) failCount++
            })

            return includeTodo ? failCount : this.$failCount = failCount
        },


        getFailedAssertions : function () {
            var failed      = [];

            this.eachAssertion(function (assertion) {
                if (!assertion.isPassed()) failed.push(assertion)
            })

            return failed
        },


        isPassed : function () {
            var passCount       = this.getPassCount()
            var failCount       = this.getFailCount()
            var assertPlanned   = this.assertPlanned

            return this.isFinished() && (!this.failed || this.isTodo) && !failCount && (
                assertPlanned != null && passCount == assertPlanned
                    ||
                assertPlanned == null && this.isDoneCorrectly()
            )
        },


        isFailed : function () {
            var passCount       = this.getPassCount()
            var failCount       = this.getFailCount()
            var assertPlanned   = this.assertPlanned

            return this.failed || failCount || (

                this.isFinished() && (
                    assertPlanned != null && passCount != assertPlanned
                        ||
                    assertPlanned == null && !this.isDoneCorrectly()
                )
            )
        },


        isFailedWithException : function () {
            return this.failed
        },


        isStarted : function () {
            return this.startDate != null
        },


        isFinished : function () {
            return this.endDate != null
        },


        getDuration : function () {
            return this.endDate - this.startDate
        },


        getBubbleTarget : function () {
            return this.parent || this.project;
        },


        warn : function (message) {
            this.addResult(new Siesta.Result.Diagnostic({
                description : message,
                isWarning   : true
            }))
        },


        // TODO duplication with identical project method, move to a role
        flattenArray : function (array, stripEmpty) {
            var me          = this
            var result      = []

            Joose.A.each(array, function (el) {
                if (me.typeOf(el) == 'Array')
                    result.push.apply(result, me.flattenArray(el, stripEmpty))
                else
                    if (!stripEmpty || el)
                        result.push(el)
            })

            return result
        },


        trimString : function (string) {
            // "polyfill" regexp from MDN
            // Make sure we trim BOM and NBSP
            return String(string).replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '')
        },


        buildActionableMethods : function () {
            var methods     = {}

            this.meta.getMethods().each(function (method, name) {
                methods[ name.toLowerCase() ] = name
            })

            return methods
        },


        getJUnitClass : function () {
            return this.jUnitClass || this.meta.name || 'Siesta.Test'
        },


        // to give test scripts access to locales
        resource : function () {
            return Siesta.Resource.apply(Siesta.Resource, arguments)
        },


        getRootTest : function () {
            var root        = this

            while (root.parent) root = root.parent

            return root
        },


        onException : function () {
        }
    }
    // eof methods

})
//eof Siesta.Test