    /*

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

    */
    /**

    @class Siesta.Project

    `Siesta.Project` is an abstract base project class in Siesta hierarchy. This class provides no UI,
    you should use one of it subclasses, for example {@link Siesta.Project.Browser} or {@link Siesta.Project.Browser.ExtJS}

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

    Synopsys
    ========

        var project = new Siesta.Project.Browser.ExtJS();

        project.configure({
            title     : 'Awesome Test Suite',

            transparentEx       : true,

            autoCheckGlobals    : true,
            expectedGlobals     : [
                'Ext',
                'Sch'
            ],

            preload : [
                "http://cdn.sencha.io/ext-4.0.2a/ext-all-debug.js",
                "../awesome-project-all.js",
                {
                    text    : "console.log('preload completed')"
                }
            ]
        })

        project.plan(
            // simple string - url relative to project file
            'sanity.t.js',

            // test file descriptor with own configuration options
            {
                url     : 'basic.t.js',

                // replace `preload` option of project
                preload : [
                    "http://cdn.sencha.io/ext-4.0.6/ext-all-debug.js",
                    "../awesome-project-all.js"
                ]
            },

            // groups ("folders") of test files (possibly with own options)
            {
                group       : 'Sanity',

                autoCheckGlobals    : false,

                items       : [
                    'data/crud.t.js',
                    ...
                ]
            },
            ...
        )

    */

    Class('Siesta.Project', {

        does        : [
            JooseX.Observable,
            Siesta.Util.Role.CanGetType,
            Siesta.Util.Role.CanDetectES6
        ],

        has : {
            /**
             * @cfg {String} title The title of the test suite. Can contain HTML. When provided in the test file descriptor - will change the name of test in the project UI.
             */
            title               : null,

            /**
             * @cfg {String} desc The description of the test. Can contain HTML. When provided, will be shown as the tooltip in the tests grid.
             */
            desc                : null,

            /**
             * @cfg {Class} testClass The test class which will be used for creating test instances, defaults to {@link Siesta.Test}.
             * You can subclass {@link Siesta.Test} and provide a new class.
             *
             * This option can be also specified in the test file descriptor.
             */
            testClass           : Siesta.Test,
            contentManagerClass : Siesta.Content.Manager,

            // fields of test descriptor:
            // - id - either `url` or wbs + group - computed
            // - url
            // - isMissing - true if test file is missing
            // - testCode - a test code source (can be provided by user)
            // - testConfig - config object provided to the StartTest
            // - index - (in the group) computed
            // - scopeProvider
            // - scopeProviderConfig
            // - preload
            // - alsoPreload
            // - parent - parent descriptor (or project for top-most ones) - computed
            // - preset - computed by project - instance of Siesta.Content.Preset
            // - forceDOMVisible - true to show the <iframe> on top of all others when running this test
            //                     (required for IE when using "document.getElementFromPoint()")
            // OR - object
            // - group - group name
            // - items - array of test descriptors
            // - expanded - initial state of the group (true by default)
            descriptors         : Joose.I.Array,
            descriptorsById     : Joose.I.Object,

            launchCounter       : 0,

            launches            : Joose.I.Object,

            scopesByURL         : Joose.I.Object,
            testsByURL          : Joose.I.Object,

            /**
             * @cfg {Boolean} transparentEx When set to `true` project will not try to catch any exception, thrown from the test code.
             * This is very useful for debugging - you can for example use the "break on error" option in Firebug.
             * But, using this option may naturally lead to unhandled exceptions, which may leave the project in incosistent state -
             * refresh the browser page in such case.
             *
             * Defaults to `false` - project will do its best to detect any exception thrown from the test code.
             *
             * This option can be also specified in the test file descriptor.
             */
            transparentEx       : false,

            scopeProviderConfig     : null,
            scopeProvider           : null,

            /**
             * @cfg {String} runCore Either `parallel` or `sequential`. Indicates how the individual tests should be run - several at once or one-by-one.
             * Default value is "parallel". You do not need to change this option usually.
             */
            runCore                 : 'parallel',

            /**
             * @cfg {Number} maxThreads The maximum number of tests running at the same time. Only applicable for `parallel` run-core.
             */
            maxThreads              : 4,

            /**
             * @cfg {Boolean} autoCheckGlobals When set to `true`, project will automatically issue an {@link Siesta.Test#verifyGlobals} assertion at the end of each test,
             * so you won't have to manually specify it each time. The assertion will be triggered only if test completed successfully. Default value is `false`.
             * See also {@link #expectedGlobals} configuration option and {@link Siesta.Test#expectGlobals} method.
             *
             * This option will be always disabled in Opera, since every DOM element with `id` is being added as a global symbol in it.
             *
             * This option can be also specified in the test file descriptor.
             */
            autoCheckGlobals        : false,

            disableGlobalsCheck     : false,

            /**
             * @cfg {Array} expectedGlobals An array of strings or regular expressions which are likely to present in the scope of each test. There is no need to provide the name
             * of built-in globals - project will automatically scan them from the empty context. Only provide the names of global properties which will be created
             * by your preload code.
             *
             * For example
             *
        project.configure({
            title               : 'Ext Scheduler Test Suite',

            autoCheckGlobals    : true,
            expectedGlobals     : [
                'Ext',
                'MyProject',
                /jQuery\d+/, // Can use RegExp too!
            ],
            ...
        })

             * This option can be also specified in the test file descriptor.
             */
            expectedGlobals         : Joose.I.Array,
            // will be populated by `populateCleanScopeGlobals`
            cleanScopeGlobals       : Joose.I.Array,

            /**
             * @cfg {Array} preload
             *
             * The array which contains the *preload descriptors* describing which files/code should be preloaded into the scope of each test.
             *
             * Preload descriptor can be:
             *
             * - a string, containing an url to load (cross-domain urls are ok, if url ends with ".css" it will be loaded as CSS)
             * - an object `{ type : 'css/js', url : '...' }` allowing to specify the CSS files with different extension
             * - an object `{ type : 'css/js', content : '...' }` allowing to specify the inline content for script / style. The content should only be the tag content - not the tag itself, it'll be created by Siesta.
             * - an object `{ text : '...' }` which is a shortcut for `{ type : 'js', content : '...' }`
             *
             * `preload` array can contain other nested arrays which will be flattened recursively. Any "empty" values
             * (like `null`, empty string, false etc) will be ignored.
             *
             * For example:
             *
        project.configure({
            title           : 'Ext Scheduler Test Suite',

            preload         : [
                'http://cdn.sencha.io/ext-4.0.2a/resources/css/ext-all.css',
                'http://cdn.sencha.io/ext-4.0.2a/ext-all-debug.js',
                {
                    text    : 'MySpecialGlobalFunc = function () { if (typeof console != "undefined") ... }'
                },
                // simple conditional preload
                someCondition ?
                    [
                        'http://mydomain.com/file.css',
                        'http://mydomain.com/file.js'
                    ]
                :
                    null
            ],
            ...
        })

             * This option can be also specified in the test file descriptor. **Note**, that if test descriptor has non-empty
             * {@link Siesta.Project.Browser#pageUrl pageUrl} option, then *it will not inherit* the `preload` option
             * from parent descriptors or project, **unless** it has the `preload` config set to string `inherit`.
             * If both `pageUrl` and `preload` are set on the project level, `preload` value still will be inherited. For example:
             *
        project.configure({
            pageUrl         : 'general-page.html',
            preload         : [ 'my-file.js' ],
            ...
        })

        project.plan(
            // this test will inherit both `pageUrl` and `preload`
            'test1.js',
            {
                // no preloads inherited
                pageUrl     : 'host-page.html',
                url         : 'test2.js'
            },
            {
                // inherit `preload` value from the upper level - [ 'my-file.js' ]
                pageUrl     : 'host-page.html',
                preload     : 'inherit',
                url         : 'test3.js'
            },
            {
                group       : 'Some group',
                pageUrl     : 'host-page2.html',
                preload     : 'inherit',

                items           : [
                    {
                        // inherit `pageUrl` value from the group
                        // inherit `preload` value from the upper level - [ 'my-file.js' ]
                        url     : 'test3.js'
                    }
                ]
            }
        )

             * When loading ES6 modules, one need to indicate this using the `isEcmaModule` property of the preload descriptor.
             * In this case, the module `<script>` tag will be created with the `type` attribute set to `module`, instead of `text/javascript`.
             *

        project.configure({
            preload         : [
                {
                    type            : 'js',
                    url             : 'some_file.js',
                    isEcmaModule    : true
                },
                {
                    type            : 'js',
                    content         : 'import {something} from "another/module.js"',
                    isEcmaModule    : true
                }

            ],
            ...
        })

             *
             *
             */
            preload                 : Joose.I.Array,

            /**
             * @cfg {Array} alsoPreload The array with preload descriptors describing which files/code should be preloaded **additionally**.
             *
             * This option can be also specified in the test file descriptor.
             */

            /**
             * @cfg {Object} listeners The object which keys corresponds to event names and values - to event handlers. If provided, the special key "scope" will be treated as the
             * scope for all event handlers, otherwise the project itself will be used as scope.
             *
             * Note, that the events from individual {@link Siesta.Test test cases} instances will bubble up to the project - you can listen to all of them in one place:
             *

        project.configure({
            title     : 'Awesome Test Suite',

            preload : [
                'http://cdn.sencha.io/ext-4.1.0-gpl/resources/css/ext-all.css',
                'http://cdn.sencha.io/ext-4.1.0-gpl/ext-all-debug.js',

                'preload.js'
            ],

            listeners : {
                testsuitestart      : function (event, project) {
                    log('Test suite is starting: ' + project.title)
                },
                testsuiteend        : function (event, project) {
                    log('Test suite is finishing: ' + project.title)
                },
                teststart           : function (event, test) {
                    log('Test case is starting: ' + test.url)
                },
                testupdate          : function (event, test, result) {
                    log('Test case [' + test.url + '] has been updated: ' + result.description + (result.annotation ? ', ' + result.annotation : ''))
                },
                testfailedwithexception : function (event, test) {
                    log('Test case [' + test.url + '] has failed with exception: ' + test.failedException)
                },
                testfinalize        : function (event, test) {
                    log('Test case [' + test.url + '] has completed')
                }
            }
        })

             */

            /**
             * @cfg {Boolean} cachePreload When set to `true`, project will cache the content of the preload files and provide it for each test, instead of loading it
             * from network each time. This option may give a slight speedup in tests execution (especially when running the suite from the remote server), but see the
             * caveats below. Default value is `false`.
             *
             * Caveats: this option doesn't work very well for CSS (due to broken relative urls for images). Also its not "debugging-friendly" - as you will not be able
             * to setup breakpoints for cached code.
             */
            cachePreload            : false,

            mainPreset              : null,
            emptyPreset             : null,

            /**
             * @cfg {Number} keepNLastResults
             *
             * Indicates the number of the test results which still should be kept, for user examination.
             * Results are cleared when their total number exceed this value, based on FIFO order.
             */
            keepNLastResults        : 2,

            lastResultsURLs         : Joose.I.Array,
            lastResultsByURL        : Joose.I.Object,

            /**
             * @cfg {Boolean} breakOnFail When set to `true`, the project will not start launching any further tests after
             * detecting a failed assertion. When running in automation mode, test suite will be finalized immediately,
             * ignoring the --rerun-failed option.
             *
             * Default value is `false`.
             */
            breakOnFail             : false,

            /**
             * @cfg {Boolean} overrideSetTimeout When set to `true`, the tests will override the native "setTimeout" from the context of each test
             * for asynchronous code tracking. If setting it to `false`, you will need to use `beginAsync/endAsync` calls to indicate that test is still running.
             *
             * Note, that this option may not work reliably, when used for several sub tests launched simultaneously (for example
             * for several sibling {@link Siesta.Test#todo} sections.
             *
             * This option can be also specified in the test file descriptor. Defaults to `false`.
             */
            overrideSetTimeout      : false,

            /**
             * @cfg {Boolean} needDone When set to `true`, the tests will must indicate that that they have reached the correct
             * exit point with `t.done()` call, after which, adding any assertions is not allowed.
             * Using this option will ensure that test did not exit prematurely with some exception silently caught.
             *
             * This option can be also specified in the test file descriptor.
             */
            needDone                : false,

            // the default timeout for tests will be increased when launching more than this number of files
            increaseTimeoutThreshold    : 8,

            // the start and end dates for the most recent `launch` method
            startDate               : null,
            endDate                 : null,

            /**
             * @cfg {Number} waitForTimeout Default timeout for `waitFor` (in milliseconds). Default value is 10000.
             *
             * This option can be also specified in the test file descriptor.
             */
            waitForTimeout          : 10000,

            /**
             * @cfg {Number} defaultTimeout Default timeout for `beginAsync` operation (in milliseconds). Default value is 15000.
             *
             * This option can be also specified in the test file descriptor.
             */
            defaultTimeout          : 15000,

            /**
             * @cfg {Number} subTestTimeout Default timeout for sub tests. Default value is twice bigger than {@link #defaultTimeout}.
             *
             * This option can be also specified in the test file descriptor.
             */
            subTestTimeout          : null,

            /**
             * @cfg {Number} isReadyTimeout Default timeout for test start (in milliseconds). Default value is 15000. See {@link Siesta.Test#isReady} for details.
             *
             * This option can be also specified in the test file descriptor.
             */
            isReadyTimeout          : 10000,

            /**
             * @cfg {Number} pauseBetweenTests Default timeout between tests (in milliseconds). Increase this settings for big test suites, to give browser time for memory cleanup.
             */
            pauseBetweenTests       : 10,

            /**
             * @cfg {Boolean} failOnExclusiveSpecsWhenAutomated When this option is enabled and Siesta is running in automation mode
             * (using WebDriver or Puppeteer launcher) any exclusive BDD specs found (like {@link Siesta.Test#iit t.iit} or {@link Siesta.Test#ddescribe t.ddescribe}
             * will cause a failing assertion. The idea behind this setting is that such "exclusive" specs should only be used during debugging
             * and are often mistakenly committed in the codebase, leaving other specs not executed.
             *
             * This option can be also specified in the test file descriptor.
             */
            failOnExclusiveSpecsWhenAutomated   : false,

            /**
             * @cfg {Date/String} snooze
             *
             * Either a `Date` instance or a string, recognized by the [Date constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse).
             *
             * If test is running prior the specified date, the whole test will be made a "todo". See the {@link Siesta.Test#snooze} method.
             *
             * Example:
             *

        project.plan(
            {
                group       : 'Some group',

                snooze      : '2016-10-11',

                items           : [
                    ...
                ]
            }
        )

             *
             * This option can be also specified in the test file descriptor.
             */
            snooze                      : null,

            /**
             * @cfg {String} referenceUrl
             *
             * The url, containing additional information about the test. This option is inherited from the group configs,
             * as other options. In the Siesta user interface, `CTRL+click` on the
             * test row will open a new browser window, pointing to this url. Can be used to link the test with some external
             * resource like ticket, screenshot, etc.

        project.plan(
             {
                 url                : 'my_test.t.js',
                 referenceUrl       : 'http://jira.com/jira_issue'
             }
        )
             * This option can be also specified in the test file descriptor.
             */
            referenceUrl                : null,

            /**
             * @cfg {Boolean} suppressPassedWaitForAssertion
             *
             * When enabled, the passed "waitFor" assertions won't be included in the tests.
             *
             * This option can be also specified in the test file descriptor.
             */
            suppressPassedWaitForAssertion  : false,

            /**
             * @cfg {Boolean} isEcmaModule
             *
             * This option can be specified in the test file descriptor and/or as the global project config. In the latter case it will affect all tests.
             *
             * When enabled, the test script file (the one containing the `StartTest()` function) will be loaded using
             * `<script type="module">` instead of `<script type="text/javascript">`
             *
             * See also a note in the {@link Siesta.Project#preload preload} config.
             */
            isEcmaModule                : null,

            setupDone                   : false,

            sourceLineForAllAssertions  : false,

            currentLaunchId             : null,

            isAutomated                 : false,
            autoLaunchTests             : true,

            configSynonyms              : function () { return this.processConfigSynonyms(this.buildConfigSynonyms()) },

            uniqueCounter                   : 0,
            valueToHashIndicies             : Joose.I.Object,

            // lazy attribute, should be accessed with "getSandboxHashStructure" method
            sandboxHashStructure            : {
                lazy    : 'this.buildSandboxHashStructure'
            },

            /**
             * @cfg {Boolean} sandbox
             *
             * This option controls whether the individual tests should be run in isolation from each other. By default it is enabled,
             * and every test file will be run inside of the newly created iframe (or in the separate Node.js process), so that it can not interfere with
             * any other test. Such setup gives you predictable starting state for every test, removes the need for any kind of
             * cleanup at the end of the test and is more robust in general.
             *
             * However, the setup of the new sandbox creates some overhead. If you are sure that your tests
             * do not modify any global state (like global variable that can affect the other test) you may want to run
             * all of them in the same context, saving the setup time. In this case, you may want to disable this option.
             *
             * Siesta collects all tests with this option disabled and split them into chunks. Every chunk will have exactly
             * the same values for the configs that influence the initial setup of the page: {@link #preload}, {@link #alsoPreload},
             * {@link #pageUrl}, {@link Siesta.Test.ExtJS#requires} and some others. The tests inside of every
             * chunk will be run sequentially, in the same sandbox.
             *
             * **Important**: The 1st test in every chunk will be run normally. Starting from the 2nd one, tests
             * will skip the {@link Siesta.Test#isReady} check and {@link Siesta.Test#setup} methods. This is because all the
             * setup is supposed to be already done by the 1st test. This behavior may change (or made configurable) in the future.
             *
             * This option can be specified in the test file descriptor.
             *
             * See also {@link #sandboxBoundaryByGroup}, {@link #sandboxCleanup}
             */
            sandbox                         : true,

            /**
             * @cfg {Boolean} sandboxBoundaryByGroup
             *
             * Only applicable for tests with the {@link #sandbox} option *disabled*.
             *
             * when this option is enabled, the tests to be run in the same context will be guaranteed to reside in the same group.
             * If a new test group starts (even with the same "preload" config) - a fresh context for that group will be created
             * by Siesta.
             *
             * For example, in the following setup, both "Group 1" and "Group 2" have sandboxing disabled and the
             * same "preload" config. If `sandboxBoundaryByGroup` will be disabled all 4 individual tests will be run
             * in the same context. If `sandboxBoundaryByGroup` will be enabled, separate fresh context will be created
             * for the tests from each group.
             *

        project.configure({
            preload     : [ ... ]
        });

        project.plan(
            {
                group       : 'Group 1',
                sandbox     : false,
                items       : [
                    '010-basics/010_sanity.t.js',
                    '010-basics/020_jshint.t.js'
                ]
            },
            {
                group       : 'Group 2',
                sandbox     : false,
                items       : [
                    '020-basics/010_sanity.t.js',
                    '020-basics/030_bdd.t.js'
                ]
            },
            ...
        )

             *
             */
            sandboxBoundaryByGroup          : true,

            /**
             * @cfg {Boolean} sandboxCleanup
             *
             * Only applicable for tests with the {@link #sandbox} option *disabled*. When enabled, test that runs
             * in shared sandbox (the sandbox in which another test just has been run) will perform a cleanup.
             *
             * By default it will remove any "unexpected" globals (see {@link #expectedGlobals}) and clear the DOM.
             *
             * If you will disable this option, every new test in the "groups" will start from the state previous test
             * has finished the execution. This will allow you split one big test scenario into several files
             *
             * This option can be specified in the test file descriptor.
             */
            sandboxCleanup                  : true,

            /**
             * @cfg {Boolean} debuggerOnFail When set to `true`, the project will issue a `debugger` statement after detecting a failed assertion, allowing you
             * to inspect the internal state of the test in the browser's debugger. Default value is `false`.
             */
            debuggerOnFail                  : false,

            /**
             * @cfg {Boolean} debuggerOnStart When set to `true`, the project will issue a `debugger` statement before launching any test. Default value is `false`.
             */
            debuggerOnStart                 : false
        },

        methods : {

            initialize : function () {
                var me      = this

                me.on('testupdate', function (event, test, result, parentResult) {
                    me.onTestUpdate(test, result, parentResult);
                })

                me.on('testfailedwithexception', function (event, test, exception, stack) {
                    me.onTestFail(test, exception, stack);
                })

                me.on('teststart', function (event, test) {
                    me.onTestStart(test);
                })

                me.on('testfinalize', function (event, test) {
                    me.onTestEnd(test);
                })
            },

            buildConfigSynonyms : function () {
                return {}
            },

            // creates a reference from every synonym to a full list of synonyms, including the main name itself
            // { 'main' : [ 'main', 'syn1', 'syn2' ], 'syn1' : [ 'main', 'syn1', 'syn2' ], 'syn2' : [ 'main', 'syn1', 'syn2' ] }
            processConfigSynonyms : function (synonyms) {
                var result      = {}

                Joose.O.each(synonyms, function (synonymsList, mainName) {
                    if (synonymsList instanceof Array)
                        synonymsList.unshift(mainName)
                    else
                        synonymsList = [ mainName, synonymsList ]

                    Joose.A.each(synonymsList, function (synonym) {
                        result[ synonym ] = synonymsList
                    })
                })

                return result
            },

            onTestUpdate : function (test, result, parentResult) {
            },

            onTestFail : function (test, exception, stack) {
            },

            onTestStart : function (test) {
            },

            onTestEnd : function (test) {
            },

            onTestSuiteStart : function (descriptors, contentManager, launchState) {
                this.startDate  = new Date()

                /**
                 * This event is fired when the test suite starts. Note, that when running the test suite in the browser, this event can be fired several times
                 * (for each group of tests you've launched).
                 *
                 * You can subscribe to it, using regular ExtJS syntax:
                 *
                 *      project.on('testsuitestart', function (event, project) {}, scope, { single : true })
                 *
                 * See also the "/examples/events"
                 *
                 * @event testsuitestart
                 * @member Siesta.Project
                 * @param {JooseX.Observable.Event} event The event instance
                 * @param {Siesta.Project} project The project that just has started
                 */
                this.fireEvent('testsuitestart', this, launchState)
            },

            onTestSuiteEnd : function (descriptors, contentManager, launchState) {
                this.endDate    = new Date()

                /**
                 * This event is fired when the test suite ends. Note, that when running the test suite in the browser, this event can be fired several times
                 * (for each group of tests you've launched).
                 *
                 * @event testsuiteend
                 * @member Siesta.Project
                 * @param {JooseX.Observable.Event} event The event instance
                 * @param {Siesta.Project} project The project that just has ended
                 */
                this.fireEvent('testsuiteend', this, launchState)
            },

            onBeforeScopePreload : function (scopeProvider, url) {
                this.fireEvent('beforescopepreload', scopeProvider, url)
            },

            onAfterScopePreload : function (scopeProvider, url) {
                this.fireEvent('afterscopepreload', scopeProvider, url)
            },

            onCachingError : function (descriptors, contentManager) {
            },

            /**
             * This method configures the project instance. It just copies the passed configuration option into project instance.
             *
             * @param {Object} config - configuration options (values of attributes for this class)
             */
            configure : function (config) {
                Joose.O.copy(config, this)

                var me      = this

                if (config.listeners) Joose.O.each(config.listeners, function (value, name) {
                    if (name == 'scope') return

                    me.on(name, value, config.scope || me)
                })
            },

            // backward compat
            processPreloadArray : function (preload) {
                var me      = this

                preload     = this.flattenArray(preload, true)

                Joose.A.each(preload, function (obj, index) {
                    // do not process { text : "" } preload descriptors
                    if (Object(obj) === obj) {
                        if (obj.url) obj.url    = me.normalizeURL(obj.url)
                    } else
                        preload[ index ]        = me.normalizeURL(obj)
                })

                return preload
            },

            populateCleanScopeGlobals : function (scopeProvider, callback) {
                var scopeProviderClass  = eval(scopeProvider)
                var cleanScope          = new scopeProviderClass()

                var cleanScopeGlobals   = this.cleanScopeGlobals

                // we can also use "create" and not "setup" here
                // create will only create the iframe (in browsers) and will not try to update its content
                // the latter crashes IE8
                cleanScope.setup(function () {

                    for (var name in cleanScope.scope) cleanScopeGlobals.push(name)

                    callback()

                    // this setTimeout seems to stop the spinning loading indicator in FF
                    // accorting to https://github.com/3rd-Eden/Socket.IO/commit/bad600fb1fb70238f42767c56f01256470fa3c15
                    // it only works *after* onload (this callback will be called *in* onload)

                    setTimeout(function () {
                        // will remove the iframe (in case of browser project) from DOM and stop loading indicator
                        cleanScope.cleanup()
                    }, 0)
                })
            },

            startSingle : function (desc, callback) {
                var me              = this

                this.__counter__    = this.__counter__ || 0

                var startSingle     = function () {
                    me.launch([ me.normalizeDescriptor(desc, me, me.__counter__++) ], callback)
                }

                me.setupDone ? startSingle() : this.setup(startSingle)
            },

            setup : function (callback) {
                var me              = this

                this.mainPreset     = new Siesta.Content.Preset({
                    preload     : this.processPreloadArray(this.preload)
                })

                this.emptyPreset    = new Siesta.Content.Preset()

                // A system level descriptor used by the recorder
                me.descriptors.push({
                    isSystemDescriptor  : true,
                    url                 : '/'
                });

                me.normalizeDescriptors(me.descriptors)

                this.populateCleanScopeGlobals(this.scopeProvider, callback)
            },

            /**
             * This method adds *test file descriptors* (test files), to the project. It can be called several times.
             *
             * A test file descritor is one of the following:
             *
             * - a string, containing a test file url. The url should be unique among all tests. If you need to re-use the same test
             * file, you can add an arbitrary query string to it: `my_test.t.js?copy=1`
             * - an object containing the `url` property `{ url : '...', option1 : 'value1', option2 : 'value2' }`. The `url` property should point to the test file.
             * Other properties can contain values of some configuration options of the project (marked accordingly). In this case, they will **override** the corresponding values,
             * provided to project or parent descriptor.
             * - an object `{ group : 'groupName', items : [], expanded : true, option1 : 'value1' }` specifying the folder of test files. The `expanded` property
             * sets the initial state of the folder - "collapsed/expanded". The `items` property can contain an array of test file descriptors.
             * Other properties will override the applicable project options **for all child descriptors**.
             *
             * If test descriptor is `null` or other "falsy" value it is ignored.
             *
             * Groups (folder) may contain nested groups. Number of nesting levels is not limited.
             *
             * For example, one may easily have a special group of test files, having its own `preload` configuration (for example for testing on-demand loading). In the same
             * time some test in that group may have its own preload, overriding others.

        project.configure({
            title           : 'Ext Scheduler Test Suite',
            preload         : [
                'http://cdn.sencha.io/ext-4.0.2a/resources/css/ext-all.css',
                'http://cdn.sencha.io/ext-4.0.2a/ext-all-debug.js',
                '../awesome-app-all-debug.js'
            ],
            ...
        })

        project.plan(
            // regular file
            'data/crud.t.js',
            // a group with own "preload" config for its items
            {
                group       : 'On-demand loading',

                preload     : [
                    'http://cdn.sencha.io/ext-4.0.2a/resources/css/ext-all.css',
                    'http://cdn.sencha.io/ext-4.0.2a/ext-all-debug.js',
                ],
                items       : [
                    'ondemand/sanity.t.js',
                    'ondemand/special-test.t.js',
                    // a test descriptor with its own "preload" config (have the most priority)
                    {
                        url         : 'ondemand/4-0-6-compat.t.js',
                        preload     : [
                            'http://cdn.sencha.io/ext-4.0.6/resources/css/ext-all.css',
                            'http://cdn.sencha.io/ext-4.0.6/ext-all-debug.js',
                        ]
                    },
                    // sub-group
                    {
                        group       : 'Sub-group',
                        items       : [
                            ...
                        ]
                    }
                ]
            },
            ...
        )

             * Additionally, you can provide a test descriptor in the test file itself, adding it as the 1st or 2nd argument for `StartTest` call:
             *
        StartTest({
            autoCheckGlobals    : false,
            alsoPreload         : [ 'some_additional_preload.js' ]
        }, function (t) {
            ...
        })
             *
             * Values from this object takes the highest priority and will override any other configuration.
             *
             * Test descriptor may contain special property - `config` which will be applied to the test instance created. Be careful not to overwrite
             * standard properties and methods!
             *

        project.plan(
            {
                url         : 'ondemand/4-0-6-compat.t.js',
                config      : {
                    myProperty1     : 'value1',
                    myProperty2     : 'value2'
                }
            },
            ...
        )

        StartTest(function (t) {
            if (t.myProperty1 == 'value1') {
                // do this
            }
            ...
        })

             *
             * @param {Array/Mixed} descriptor1 or an array of descriptors
             * @param {Mixed} descriptor2
             * @param {Mixed} descriptorN
             */
            plan : function () {
                var descriptors     = this.flattenArray(arguments)

                // A system level descriptor used by the recorder
                this.descriptors.push.apply(this.descriptors, descriptors)
            },

            /**
             * This method will launch a test suite.
             *
             * For backward compatibility, it also calls {@link #plan} with its arguments.
             */
            start : function () {
                var me          = Siesta.my.activeHarness = this

                me.plan(this.flattenArray(arguments))

                this.setup(function () {
                    me.setupDone        = true

                    me.fireEvent('setupdone')

                    if (me.autoLaunchTests) me.launch(me.descriptors)
                })
            },

            /**
             * This method will read the content of the provided `url` then will try to parse it as JSON
             * and pass to the regular {@link #start} method. The file on the `url` should contain
             * a valid JSON array object with test descriptors.
             *
             * You can use this method in conjunction with the `bin/discover` utility, which can
             * auto-discover the test files and generate a starter file for you. In such setup, it is convenient
             * to specify the test configs in the test file itself (see {@link #start} method for details).
             * However, in such setup, you can not use conditional processing of the descriptors set, so
             * you decide what fits best to your needs.
             *
             * @param {String} url
             */
            startFromUrl : function (url) {
                var contentManager  = new this.contentManagerClass({
                    project         : this,
                    presets         : [  new Siesta.Content.Preset({ preload : [ url ] }) ]
                })

                var me      = this

                contentManager.cache(function () {
                    var content     = contentManager.getContentOf(url)

                    try {
                        var descriptors     = JSON.parse(content)
                    } catch (e) {
                        alert("The content of: " + url + " is not a valid JSON")
                        return
                    }

                    if (me.typeOf(descriptors) == 'Array')
                        me.start(descriptors)
                    else {
                        alert("The content of: " + url + " is not an array")
                    }

                }, function () {
                    alert("Can not load the content of: " + url)
                })
            },

            // good to have this as a separate method for testing
            normalizeDescriptors : function (descArray) {
                var me              = this

                var descriptors     = []

                Joose.A.each(descArray, function (desc, index) {
                    if (desc) descriptors.push(me.normalizeDescriptor(desc, me, index))
                })

                me.descriptors      = descriptors
            },

            launch : function (descriptors, callback, errback) {
                var launchId                = this.currentLaunchId  = ++this.launchCounter
                var me                      = this

                //console.time('launch')
                //console.time('launch-till-preload')
                //console.time('launch-after-preload')

                // no folders, only leafs
                var flattenDescriptors      = this.flattenDescriptors(descriptors)
                // the preset for the test scripts files
                var testScriptsPreset       = new Siesta.Content.Preset()
                var presets                 = [ testScriptsPreset, this.mainPreset ]

                var notLaunchedByAutomationId   = {}

                Joose.A.each(flattenDescriptors, function (desc) {
                    if (desc.preset != me.mainPreset && desc.preset != me.emptyPreset) presets.push(desc.preset)

                    if (!desc.testCode) testScriptsPreset.addResource(desc.url)

                    me.deleteTestByURL(desc.url)

                    // only used in automation, where the `desc.automationElementId` is populated
                    notLaunchedByAutomationId[ desc.automationElementId ] = 1
                })

                // cache either everything (this.cachePreload) or only the test files (to be able to show missing files / show content)
                var contentManager  = new this.contentManagerClass({
                    project         : this,
                    presets         : [ testScriptsPreset ].concat(this.cachePreload ? presets : [])
                })

                var launchState     = this.launches[ launchId ] = {
                    launchId            : launchId,
                    increaseTimeout     : this.runCore == 'parallel' && flattenDescriptors.length > this.increaseTimeoutThreshold,
                    descriptors         : flattenDescriptors,
                    contentManager      : contentManager,
                    needToStop          : false,
                    notLaunchedByAutomationId   : notLaunchedByAutomationId
                }

                //console.time('caching')

                me.onTestSuiteStart(descriptors, contentManager, launchState)

                contentManager.cache(function () {

                    //console.timeEnd('caching')

                    Joose.A.each(flattenDescriptors, function (desc) {
                        var url             = desc.url

                        if (contentManager.hasContentOf(url)) {
                            // the test descriptor defined in the test file itself, takes the highest precendence
                            var testConfig  = desc.testConfig = Siesta.getConfigForTestScript(contentManager.getContentOf(url))

                            // if testConfig contains the "preload" or "alsoPreload" key - then we need to update the preset of the descriptor
                            if (testConfig && (testConfig.preload || testConfig.alsoPreload)) desc.preset = me.getDescriptorPreset(desc)
                        } else
                            // if test code is provided, then test is considered not missing
                            // allow subclasses to define there own logic when found missing test file
                            if (!desc.testCode) me.markMissingFile(desc)

                        me.normalizeScopeProvider(desc)
                    })

                    me.fireEvent('testsuitelaunch', descriptors, contentManager, launchState)

                    me.runCoreGeneral(flattenDescriptors, contentManager, launchState, launchState.callback = function () {
                        me.onTestSuiteEnd(descriptors, contentManager, launchState)

                        callback && callback(descriptors)

                        launchState.needToStop  = true

                        delete me.launches[ launchId ]
                    })

                }, function () {}, true)
            },

            markMissingFile : function (desc) {
                desc.isMissing = true
            },

            flattenDescriptors : function (descriptors, includeFolders) {
                var flatten     = []
                var me          = this

                Joose.A.each(descriptors, function (descriptor) {
                    if (descriptor.group) {
                        if (includeFolders) flatten.push(descriptor)

                        flatten.push.apply(flatten, me.flattenDescriptors(descriptor.items, includeFolders))
                    } else
                        if (!descriptor.isSystemDescriptor) flatten.push(descriptor)
                })

                return flatten
            },

            forEachDescriptor : function (descriptors, includeFolders, func, scope) {
                var me          = this

                return Joose.A.each(descriptors, function (descriptor) {
                    if (descriptor.group) {
                        if (includeFolders)
                            if (func.call(scope || me, descriptor) === false) return false

                        return me.forEachDescriptor(descriptor.items, includeFolders, func, scope)
                    } else
                        if (!descriptor.isSystemDescriptor)
                            if (func.call(scope || me, descriptor) === false) return false
                })
            },

            lookUpValueInDescriptorTree : function (descriptor, configName, doNotLookAtRoot) {
                if (this.descriptorHasOwnValueFor(descriptor, configName)) return this.getConfigValueFromDescriptor(descriptor, configName)

                var parent  = descriptor.parent

                if (parent) {
                    if (parent == this)
                        if (doNotLookAtRoot)
                            return undefined
                        else
                            // using project instance itself as a descriptor - a bit hackish because of the "testConfig" property,
                            // which is being checked first
                            return this.getConfigValueFromDescriptor(this, configName)

                    return this.lookUpValueInDescriptorTree(parent, configName, doNotLookAtRoot)
                }

                return undefined
            },

            descriptorHasOwnValueFor : function (descriptor, configName) {
                // the test descriptor, defined in the test file (as the 1st arg to StartTest)
                // takes the highest priority
                var testConfig          = descriptor.testConfig

                // "fast" branch
                if (testConfig && testConfig.hasOwnProperty(configName) || descriptor.hasOwnProperty(configName)) return true

                var synonymList         = this.configSynonyms[ configName ]

                var result              = false

                // now checking synonims
                if (synonymList) Joose.A.each(synonymList, function (synonym) {
                    // already checked above
                    if (synonym != configName) {
                        if (testConfig && testConfig.hasOwnProperty(synonym) || descriptor.hasOwnProperty(synonym)) {
                            result      = true
                            return false
                        }
                    }
                })

                return result
            },

            getConfigValueFromDescriptor : function (descriptor, configName, allowInherited) {
                if (descriptor == this) {
                    var synonymList         = this.configSynonyms[ configName ] || [ configName ]

                    var result, foundOwnValue

                    Joose.A.each(synonymList, function (synonym) {
                        if (descriptor.hasOwnProperty(synonym)) {
                            result          = descriptor[ synonym ]
                            foundOwnValue   = true
                            return false
                        }
                    })

                    return foundOwnValue ? result : this[ configName ]
                } else {
                    var testConfig          = descriptor.testConfig

                    // "fast" branch
                    if (testConfig && testConfig.hasOwnProperty(configName)) return testConfig[ configName ]
                    if (descriptor.hasOwnProperty(configName)) return descriptor[ configName ]

                    var synonymList         = this.configSynonyms[ configName ]

                    var result

                    // now checking synonims
                    if (synonymList) Joose.A.each(synonymList, function (synonym) {
                        // already checked above
                        if (synonym != configName) {
                            if (testConfig && testConfig.hasOwnProperty(synonym) || descriptor.hasOwnProperty(synonym)) {
                                result      = testConfig && testConfig.hasOwnProperty(synonym) ? testConfig[ synonym ] : descriptor[ synonym ]
                                return false
                            }
                        }
                    })

                    return result
                }
            },

            getDescriptorConfig : function (descriptor, configName, doNotLookAtRoot) {
                return this.lookUpValueInDescriptorTree(descriptor, configName, doNotLookAtRoot)
            },

            getDescriptorPreset : function (desc) {
                var preload                 = this.getDescriptorConfig(desc, 'preload', true)
                var alsoPreload             = this.getDescriptorConfig(desc, 'alsoPreload', true)

                if (preload || alsoPreload) {
                    var totalPreload        = (preload || this.preload || []).concat(alsoPreload || [])

                    // filter out empty array as preloads - return `emptyPreset` for them
                    return totalPreload.length ? new Siesta.Content.Preset({ preload : this.processPreloadArray(totalPreload) }) : this.emptyPreset
                }

                return this.mainPreset
            },

            normalizeScopeProvider : function (desc) {
                var scopeProvider = this.getDescriptorConfig(desc, 'scopeProvider')

                if (scopeProvider) {
                    var match

                    if (match = /^=(.+)/.exec(scopeProvider))
                        scopeProvider = match[ 1 ]
                    else
                        scopeProvider = scopeProvider.replace(/^(Scope\.Provider\.)?/, 'Scope.Provider.')
                }

                desc.scopeProvider          = scopeProvider
                desc.scopeProviderConfig    = this.getDescriptorConfig(desc, 'scopeProviderConfig')
            },

            normalizeDescriptor : function (desc, parent, index, level) {
                if (desc.normalized) return desc

                if (typeof desc == 'string') desc = { url : desc }

                level       = level || 0

                var me      = this

                desc.parent = parent

                // folder
                if (desc.group) {
                    desc.id     = parent == this ? 'testFolder-' + level + '-' + index : parent.id + '/' + level + '-' + index

                    var items   = []

                    Joose.A.each(desc.items || [], function (subDesc, index) {
                        if (subDesc) items.push(me.normalizeDescriptor(subDesc, desc, index, level + 1))
                    })

                    desc.items  = items

                } else {
                    // leaf case
                    desc.id                     = desc.url
                    desc.preset                 = this.getDescriptorPreset(desc)

                    desc.name                   = desc.name || desc.url.replace(/(?:.*\/)?([^/]+)$/, '$1')

                    // the only thing left to normalize in the descriptor is now "scopeProvider"
                    // we postpone this normalization to the moment after loading of the test files,
                    // since they can also contain "scopeProvider"-related configs
                    // see "normalizeScopeProvider"
                }

                this.descriptorsById[ desc.id ] = desc

                desc.normalized     = true

                return desc
            },

            runCoreGeneral : function (descriptors, contentManager, launchState, callback) {
                var runCoreMethod   = 'runCore' + Joose.S.uppercaseFirst(this.runCore)

                if (typeof this[ runCoreMethod ] != 'function') throw new Error("Invalid `runCore` specified: [" + this.runCore + "]")

                this[ runCoreMethod ](descriptors, contentManager, launchState, callback)
            },

            runCoreParallel : function (descriptors, contentManager, launchState, callback) {
                var me              = this
                var processedNum    = 0
                var count           = descriptors.length

                if (!count) callback()

                var hasExited               = false
                var hasLaunchedAllThreads   = false

                var doProcessURL  = function (desc) {
                    me.processURL(desc, desc.index, contentManager, launchState, function () {
                        processedNum++

                        // set the internal closure `exitLoop` to stop launching new branches
                        // on the 1st encountering of `me.needToStop` flag
                        if (launchState.needToStop) {

                            if (!hasExited) {
                                hasExited = true
                                callback()
                            }

                            return
                        }

                        if (processedNum == count)
                            callback()
                        else
                            launchThread(descriptors)
                    })
                }

                var launchThread  = function (descriptors) {
                    var desc = descriptors.shift()

                    if (!desc) return

                    if (hasLaunchedAllThreads)
                        setTimeout(function () {
                            doProcessURL(desc)
                        }, me.pauseBetweenTests)
                    else
                        doProcessURL(desc)
                }

                for (var i = 1; i <= this.maxThreads; i++) launchThread(descriptors)

                hasLaunchedAllThreads = true
            },

            runCoreSequential : function (descriptors, contentManager, launchState, callback) {
                if (descriptors.length && !launchState.needToStop) {
                    var desc        = descriptors.shift()
                    var me          = this

                    this.processURL(desc, desc.index, contentManager, launchState, function () {

                        if (descriptors.length && !launchState.needToStop)
                            setTimeout(function () {
                                me.runCoreSequential(descriptors, contentManager, launchState, callback)
                            }, me.pauseBetweenTests)
                        else
                            callback()
                    })

                } else
                    callback()
            },

            stopCurrentLaunch : function (sourceTest) {
                var launchState     = this.launches[ sourceTest ? sourceTest.launchId : this.currentLaunchId ]

                if (launchState && !launchState.needToStop) {
                    // this will indicate to the `onTestUpdate` and other methods that updates are coming from the
                    // stale launch and should not be reported (updates could be generated in the `test.finalize()` below)
                    launchState.needToStop  = true

                    var me                  = this;

                    Joose.A.each(launchState.descriptors, function (desc) {
                        var test    = me.testsByURL[ desc.url ]

                        if (test) {
                            // exceptions can arise if test page has switched to different context for example (click on the link)
                            // and siesta is trying to clear the timeouts with "clearTimeout"
                            try {
                                test.finalize(true)
                            } catch (e) {
                            }
                        }
                    })

                    // indicate that something has been changed indeed by returning `true`
                    return true
                }
            },

            getSeedingCode : function (desc, launchId) {
                var code    = function (descId, launchId) {
                    StartTest = startTest = function () {
                        arguments.callee.args   = arguments
                    }
                    describe                = function () {
                        if (describe.called) throw new Error("`describe()` used as global function instead of test method `t.describe()`")
                        describe.called = true

                        StartTest.apply(this, arguments)
                    }

                    StartTest.launchId          = launchId
                    StartTest.id                = descId

                    // for older IE - the try/catch should be from the same context as the exception
                    StartTest.exceptionCatcher  = function (func) { var ex; try { func() } catch (e) { ex = e; } return ex == '__SIESTA_TEST_EXIT_EXCEPTION__' ? undefined : ex; };

                    // for Error instances we try to pick up the values from "message" or "description" property
                    // so need to have a correct constructor from the context of test
                    StartTest.testErrorClass    = Error;
                }

                return ';(' + code.toString() + ')(' + JSON.stringify(desc.id) + ', ' + launchId + ')'
            },

            getScopeProviderConfigFor : function (desc, launchId) {
                var config          = Joose.O.copy(desc.scopeProviderConfig || {})

                config.seedingCode  = this.getSeedingCode(desc, launchId)
                config.launchId     = launchId

                return config
            },

            keepTestResult : function (url) {
                // already keeping
                if (this.lastResultsByURL[ url ]) {
                    var indexOf     = -1

                    Joose.A.each(this.lastResultsURLs, function (resultUrl, i) {
                        if (resultUrl == url) { indexOf = i; return false }
                    })

                    this.lastResultsURLs.splice(indexOf, 1)
                    this.lastResultsURLs.push(url)

                    return
                }

                this.lastResultsURLs.push(url)
                this.lastResultsByURL[ url ] = true

                if (this.lastResultsURLs.length > this.keepNLastResults) this.releaseTestResult()
            },

            releaseTestResult : function () {
                if (this.lastResultsURLs.length <= this.keepNLastResults) return

                var url     = this.lastResultsURLs.shift()

                delete this.lastResultsByURL[ url ]

                var test    = this.getTestByURL(url)

                if (test && test.isFinished()) this.cleanupScopeForURL(url)
            },

            isKeepingResultForURL : function (url) {
                return this.lastResultsByURL[ url ]
            },

            setupScope : function (desc, launchId) {
                var url                 = desc.url

                var alreadyExisting     = this.scopesByURL[ url ]
                // if test suite has been restarted at the "testsuitestart" point
                // then previous launch will concur the latest launch for the "this.scopesByURL" state
                // so we prevent the older launch to overwrite the newer
                var isOutdatedRequest    = alreadyExisting && alreadyExisting.launchId > launchId

                var scopeProviderClass  = eval(desc.scopeProvider)

                var newProvider         = new scopeProviderClass(this.getScopeProviderConfigFor(desc, launchId))

                if (isOutdatedRequest) {
                    return newProvider
                } else {
                    this.cleanupScopeForURL(url)

                    this.keepTestResult(url)

                    return this.scopesByURL[ url ] = newProvider
                }
            },

            cleanupScopeForURL : function (url) {
                var scopeProvider = this.scopesByURL[ url ]

                if (scopeProvider) {
                    delete this.scopesByURL[ url ]

                    scopeProvider.cleanup()
                }
            },

            // should prepare the "seedingScript" - include it to the `scopeProvider`
            prepareScopeSeeding : function (scopeProvider, desc, contentManager) {
                var isEcmaModule    = this.getDescriptorConfig(desc, 'isEcmaModule')

                if (desc.testCode || this.cachePreload && contentManager.hasContentOf(desc.url))
                    scopeProvider.addPreload({
                        type            : 'js',
                        content         : desc.testCode || (contentManager.getContentOf(desc.url) + '\n//# sourceURL=' + desc.url),
                        isEcmaModule    : isEcmaModule
                    })
                else {
                    scopeProvider.seedingScript             = this.resolveURL(desc.url, scopeProvider, desc)
                    scopeProvider.seedingScriptIsEcmaModule = isEcmaModule
                }
            },

            // should normalize non-standard urls (like specifying Class.Name in preload)
            // such behavior is not documented and generally deprecated
            normalizeURL : function (url) {
                return url
            },

            resolveURL : function (url, scopeProvider, desc) {
                return url
            },

            canUseCachedContent : function (resource, desc) {
                return this.cachePreload && resource instanceof Siesta.Content.Resource.JavaScript
            },

            addCachedResourceToPreloads : function (scopeProvider, contentManager, resource, desc) {
                scopeProvider.addPreload({
                    type        : 'js',
                    content     : contentManager.getContentOf(resource)
                })
            },

            getOnErrorHandler : function (testHolder, preloadErrors) {
                var R = Siesta.Resource('Siesta.Project');

                // dirty, all-in-one error handler, ideally should be refactored
                return function (msg, url, lineNumber, col, error, promiseRejectionEvent) {
                    // according to https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
                    // the `ResizeObserver loop limit exceeded` is some dummy error that can be safely ignored
                    var shouldIgnore    = /__SIESTA_TEST_EXIT_EXCEPTION__|ResizeObserver loop limit exceeded/
                    var test            = testHolder.test

                    if (promiseRejectionEvent) {
                        msg             = promiseRejectionEvent.reason || ''
                        url             = lineNumber = col = ''
                    }

                    // Either an HTMLElement load failure - "window.addEventListener('error', handler, true)"
                    // OR
                    // Error in a script on another domain (message Script error)
                    if (arguments.length == 1) {
                        var event       = msg

                        error           = event.error

                        if (event.target && event.target.tagName && !error) {
                            msg         = R.get('resourceFailedToLoad', { nodeName : event.target ? event.target.nodeName.toUpperCase() : ''});
                            url         = event.srcElement ? event.srcElement.href || event.srcElement.src : ''
                            lineNumber  = ''

                            test.fail(msg + ' ' + (event.target ? event.target.outerHTML : url));

                            return;
                        } else {
                            msg         = event.message;
                            url         = '';
                            lineNumber  = 0;
                        }
                    }

                    if (shouldIgnore.test(msg)) return

                    // this handler can still be called if test uses "failOnResourceLoadError", which install this handler to
                    // 'error' event of the `window`, instead of window.onerror
                    // so we consult with the test if we should ignore this exception, as it will be handled somewhere else
                    if (test && test.onException(msg, url, lineNumber, col, error) === true) return

                    if (test && test.isStarted()) {
                        test.nbrExceptions++;

                        // this is the final "message receiver" in test.. probably test should install on error handlers
                        // by itself somehow..
                        test.failWithException(
                            error || (msg + ' ' + url + ' ' + lineNumber),
                            promiseRejectionEvent ? "Unhandled promise rejection in test " + (test.name || test.url) : null
                        )
                    } else {
                        preloadErrors && preloadErrors.push({
                            isException     : true,
                            message         : error && error.stack ? error.stack + '' : msg + ' ' + url + ' ' + lineNumber
                        })
                    }
                }
            },

            processURL : function (desc, index, contentManager, launchState, callback, noCleanup, sharedSandboxState) {
                var me      = this
                var url     = desc.url

                if (desc.isMissing) {
                    callback()

                    return
                }

                // delete the test from "not launched" as soon as the processing has started
                delete launchState.notLaunchedByAutomationId[ desc.automationElementId ]

                // a magical shared object, which will contain the `test` property with test instance, once the test will be created
                var testHolder      = {}
                // an array of errors occured during preload phase
                var preloadErrors   = []

                var onErrorHandler  = this.getOnErrorHandler(testHolder, preloadErrors)
                var scopeProvider   = this.setupScope(desc, launchState.launchId)
                var transparentEx   = this.getDescriptorConfig(desc, 'transparentEx')

                // trying to setup the `onerror` handler as early as possible - to detect each and every exception from the test
                var onErrorHandlerBorrowing = scopeProvider.addOnErrorHandler(onErrorHandler, !transparentEx)

    //            scopeProvider.addPreload({
    //                type        : 'js',
    //                content     : 'console.time("scope-onload")'
    //            })

                desc.preset.eachResource(function (resource) {
                    var hasContent      = contentManager.hasContentOf(resource)

                    if (hasContent && me.canUseCachedContent(resource, desc)) {
                        me.addCachedResourceToPreloads(scopeProvider, contentManager, resource, desc)
                    } else {
                        var resourceDesc    = resource.asDescriptor()

                        if (resourceDesc.url) resourceDesc.url = me.resolveURL(resourceDesc.url, scopeProvider, desc)

                        scopeProvider.addPreload(resourceDesc)
                    }
                })

                me.prepareScopeSeeding(scopeProvider, desc, contentManager)

                var testClass       = me.getDescriptorConfig(desc, 'testClass')
                if (me.typeOf(testClass) == 'String') testClass = Joose.S.strToClass(testClass)

                var testConfig      = me.getNewTestConfiguration(desc, scopeProvider, contentManager, launchState, sharedSandboxState)

                // create the test instance early, so that one can perform some setup (as the test class method call)
                // even before the "pageUrl" starts loading
                var test            = testHolder.test = new testClass(testConfig)

                this.onBeforeScopePreload(scopeProvider, url, test)

                test.earlySetup(function () {
                    cont()
                }, function (errorMessage) {
                    preloadErrors.push({ isException : false, message : errorMessage })

                    cont()
                })

                function cont() {
                    //console.timeEnd('launch-till-preload')

                    //console.time('preload')

        //            scopeProvider.addPreload({
        //                type        : 'js',
        //                content     : 'console.timeEnd("scope-onload")'
        //            })
                    var R       = Siesta.Resource('Siesta.Project')

                    scopeProvider.setup(function (scopeProvider, failedPreloads, crossOriginFailed) {
                        me.onAfterScopePreload(scopeProvider, url, test, failedPreloads)

                        failedPreloads && Joose.O.each(failedPreloads, function (value, url) {
                            preloadErrors.unshift({
                                isException : false,
                                message     : R.get('preloadHasFailed', { url : url })
                            })
                        })

                        if (crossOriginFailed) {
                            preloadErrors.push({
                                isException : true,
                                message     : R.get('crossOriginFailed', { url : scopeProvider.sourceURL })
                            })
                        }

                        // scope provider has been cleaned up while setting up? (may be user has restarted the test)
                        // then do nothing
                        if (!scopeProvider.scope) { callback(); return }

                        me.launchTest({
                            testHolder          : testHolder,
                            desc                : desc,
                            scopeProvider       : scopeProvider,
                            contentManager      : contentManager,
                            launchState         : launchState,
                            preloadErrors       : preloadErrors,
                            onErrorHandler      : onErrorHandler,
                            onErrorHandlerBorrowing : onErrorHandlerBorrowing,

                            // need to provide the "startTestAnchor" explicitly (and not just get from "scope" inside of the "launchTest"
                            // method, because for "enablePageRedirect" method, startAnchor is calculated differently
                            startTestAnchor     : crossOriginFailed ? null : scopeProvider.scope.StartTest,
                            noCleanup           : noCleanup,
                            reusingSandbox      : false
                        }, callback)
                    })
                }
            },

            launchTest : function (options, callback) {
                var scopeProvider   = options.scopeProvider
                var desc            = options.desc
    //            desc, scopeProvider, contentManager, options, preloadErrors, onErrorHandler, callback

                //console.timeEnd('preload')
                //console.timeEnd('launch-after-preload')
                var me              = this
                var url             = desc.url

                var testHolder      = options.testHolder
                var noCleanup       = options.noCleanup
                var cleanupUrl      = options.cleanupUrl
                var test            = testHolder.test

                // "main" test callback, called once test is completed
                test.callback       = function () {
                    if (!noCleanup && !me.isKeepingResultForURL(url)) {
                        // `cleanupUrl` will be different for shared sandbox tests
                        me.cleanupScopeForURL(cleanupUrl || url)
                    }

                    callback && callback(testHolder)

                    testHolder  = null
                }

                if (scopeProvider.crossOriginFailed) {
                    // test will immediately fail
                    if (!test.isFinished()) test.start(options.preloadErrors)

                    options         = null
                    test            = null

                    return
                }

                var startTestAnchor = options.startTestAnchor
                var args            = startTestAnchor && startTestAnchor.args
                var global          = scopeProvider.scope

                // additional setup of the test instance, setting up the properties, which are known only after scope
                // is loaded
                Joose.O.extend(test, {
                    startTestAnchor     : startTestAnchor,
                    exceptionCatcher    : startTestAnchor.exceptionCatcher,
                    testErrorClass      : startTestAnchor.testErrorClass,

                    global              : global,

                    // the "options" part is used by the "enablePageRedirect" branch, where
                    // the test script is executed in different context from the "global" context
                    originalSetTimeout  : options.originalSetTimeout || global.setTimeout,
                    originalClearTimeout: options.originalClearTimeout || global.clearTimeout,

                    // pick either 1st or 2nd argument depending which one is a function
                    run                 : args && (typeof args[ 0 ] == 'function' ? args[ 0 ] : args[ 1 ]),

                    reusingSandbox      : options.reusingSandbox
                })

                // after the scope setup, the `onerror` handler might be cleared - installing it again
                if (options.onErrorHandler) {
                    if (options.onErrorHandlerBorrowing) options.onErrorHandlerBorrowing()

                    options.onErrorHandlerBorrowing = scopeProvider.addOnErrorHandler(options.onErrorHandler, !this.getDescriptorConfig(desc, 'transparentEx'))
                }

                this.saveTestWithURL(url, test)

                var doLaunch        = function() {
                    // scope provider has been cleaned up while setting up? (may be user has restarted the test)
                    // then do nothing
                    if (!scopeProvider.scope) { callback(); return }

                    //console.timeEnd('launch')

                    me.fireEvent('beforeteststart', test)

                    // in the edge case, test can be already finished before its even started :)
                    // this happens if user re-launch the test during these 10ms - test will be
                    // finalized forcefully in the "deleteTestByUrl" method
                    if (!test.isFinished()) test.start(options.preloadErrors)

                    options         = null
                    test            = null
                }

                if (options.reusingSandbox)
                    doLaunch()
                else {
                    if (scopeProvider instanceof Scope.Provider.IFrame)
                        // start the test after slight delay - to run it already *after* onload (in browsers)
                        global.setTimeout(doLaunch, 10)
                    else
                        // for Window provider, `global.setTimeout` seems to not execute passed function _sometimes_
                        // also increase the "onload" delay
                        setTimeout(doLaunch, 50)
                }
            },

            getNewTestConfiguration : function (desc, scopeProvider, contentManager, launchState, sharedSandboxState) {
                var groups          = []
                var currentDesc     = desc.parent

                while (currentDesc) {
                    // do not push name of the top-level "hidden" group which has no parent
                    currentDesc.parent && groups.unshift(String(currentDesc.group))

                    currentDesc     = currentDesc.parent
                }

                var config          = {
                    url                 : desc.url,
                    name                : desc.name,

                    referenceUrl        : this.getDescriptorConfig(desc, 'referenceUrl'),

                    launchId            : launchState.launchId,

                    automationElementId : desc.automationElementId,
                    groups              : groups,
                    jUnitClass          : this.getDescriptorConfig(desc, 'jUnitClass'),

                    project             : this,

                    expectedGlobals     : this.cleanScopeGlobals.concat(this.getDescriptorConfig(desc, 'expectedGlobals')),
                    autoCheckGlobals    : this.getDescriptorConfig(desc, 'autoCheckGlobals'),
                    disableGlobalsCheck : this.disableGlobalsCheck,

                    scopeProvider       : scopeProvider,

                    contentManager      : contentManager,

                    transparentEx       : this.getDescriptorConfig(desc, 'transparentEx'),
                    needDone            : this.getDescriptorConfig(desc, 'needDone'),

                    overrideSetTimeout          : this.getDescriptorConfig(desc, 'overrideSetTimeout'),

                    defaultTimeout              : this.getDescriptorConfig(desc, 'defaultTimeout') * (launchState.increaseTimeout ? 2 : 1),
                    subTestTimeout              : this.getDescriptorConfig(desc, 'subTestTimeout') * (launchState.increaseTimeout ? 2 : 1),
                    waitForTimeout              : this.getDescriptorConfig(desc, 'waitForTimeout') * (launchState.increaseTimeout ? 3 : 1),
                    isReadyTimeout              : this.getDescriptorConfig(desc, 'isReadyTimeout'),

                    sourceLineForAllAssertions  : this.sourceLineForAllAssertions,

                    sandboxCleanup              : this.getDescriptorConfig(desc, 'sandboxCleanup'),
                    sharedSandboxState          : sharedSandboxState,

                    config                      : this.getDescriptorConfig(desc, 'config'),

                    failOnExclusiveSpecsWhenAutomated   : this.getDescriptorConfig(desc, 'failOnExclusiveSpecsWhenAutomated'),

                    // enableCodeCoverage          : this.getDescriptorConfig(desc, 'enableCodeCoverage'),
                    snoozeUntil                 : this.getDescriptorConfig(desc, 'snooze'),
                    suppressPassedWaitForAssertion  : this.getDescriptorConfig(desc, 'suppressPassedWaitForAssertion')
                }

                return config
            },

            getScriptDescriptor : function (id) {
                return this.descriptorsById[ id ]
            },

            getDescById : function (id) {
                return this.descriptorsById[ id ]
            },

            getTestByURL : function (url) {
                return this.testsByURL[ url ]
            },

            saveTestWithURL : function (url, test) {
                this.testsByURL[ url ] = test
            },

            deleteTestByURL : function (url) {
                var test    = this.testsByURL[ url ]

                if (test) {
                    // exceptions can arise if test page has switched to different context for example (click on the link)
                    // and siesta is trying to clear the timeouts with "clearTimeout"
                    try {
                        test.finalize(true)
                    } catch (e) {
                    }
                    this.cleanupScopeForURL(url)
                }

                delete this.testsByURL[ url ]
            },

            allPassed : function () {
                var allPassed       = true
                var me              = this

                Joose.A.each(this.flattenDescriptors(this.descriptors), function (descriptor) {
                    // if at least one test is missing then something is wrong
                    if (descriptor.isMissing) { allPassed = false; return false }

                    var test        = me.getTestByURL(descriptor.url)

                    // ignore missing tests (could be skipped by test filtering)
                    if (!test) return

                    allPassed       = allPassed && test.isPassed()

                    // stop iteration if found failed test
                    if (!allPassed) return false
                })

                return allPassed
            },

            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
            },

            buildSandboxHashStructure : function () {
                return [
                    'preload',
                    'alsoPreload',
                    'hostPageUrl',
                    'pageUrl',
                    'useStrictMode',
                    'overrideSetTimeout'
                ]
            },

            assignUniqueTag     : function (value, configName) {
                // has to be an Object-like value (object, array, function, etc)
                if (value == null) return ''

                if (value === Object(value)) {
                    if (value.__UNIQUE__) return value.__UNIQUE__

                    return value.__UNIQUE__ = (this.uniqueCounter++).toString(36)
                } else {
                    value                   = value + ''

                    var configIndex         = this.valueToHashIndicies[ configName ]

                    if (!configIndex) configIndex = this.valueToHashIndicies[ configName ] = {}

                    if (configIndex[ value ]) return configIndex[ value ]

                    return configIndex[ value ] = (this.uniqueCounter++).toString(36)
                }
            },

            calculateSharedContextGroupHash : function (desc) {
                var me              = this
                var structure       = this.getSandboxHashStructure()

                var hash            = ''

                Joose.A.each(structure, function (configName) {
                    hash            += me.assignUniqueTag(me.getDescriptorConfig(desc, configName), configName)
                })

                if (this.sandboxBoundaryByGroup) hash += this.assignUniqueTag(desc.parent)

                return hash
            },

            sortDescriptors : function (descriptors, forcedRunCore, idsOnly) {
                var me                  = this
                var canRunParallel      = []
                var mustRunSequential   = []

                // array of { groupHash : ..., items : [] } objects
                var sharedContextGroups = []
                var groupsByHash        = {}

                Joose.A.each(descriptors, function (desc) {
                    if (!me.getDescriptorConfig(desc, 'sandbox')) {
                        var hash        = me.calculateSharedContextGroupHash(desc)
                        var group       = groupsByHash[ hash ]

                        if (!group) {
                            group       = groupsByHash[ hash ] = { groupHash : hash, items : [] }
                            sharedContextGroups.push(group)
                        }

                        group.items.push(idsOnly ? desc.id : desc)
                    } else {
                        var runCore         = forcedRunCore || me.getDescriptorConfig(desc, 'runCore')

                        if (runCore == 'sequential' || me.testMustRunSequential(desc))
                            mustRunSequential.push(idsOnly ? desc.id : desc)
                        else
                            canRunParallel.push(idsOnly ? desc.id : desc)
                    }
                })

                return {
                    sharedContextGroups     : sharedContextGroups,
                    mustRunSequential       : mustRunSequential,
                    canRunParallel          : canRunParallel
                }
            },

            hasDescriptorWithNativeEventsSimulation : function (descriptors) {
                return false
            }
        },
        // eof methods

        my : {

            has     : {
                HOST            : null,
                instance        : null
            },

            methods : {

                // backward compat for static project instance
                staticDeprecationWarning : function (methodName) {
                    var message     = Siesta.Resource('Siesta.Project', 'staticDeprecationWarning', { methodName : methodName, projectClass : this.HOST + '' })

                    if (typeof console != 'undefined') console.warn(message)
                },

                configure : function (config) {
                    this.staticDeprecationWarning('configure')

                    var instance        = this.instance = new this.HOST()

                    return instance.configure(config)
                },

                start : function () {
                    this.staticDeprecationWarning('start')

                    return this.instance.start.apply(this.instance, arguments)
                },

                on : function () {
                    this.staticDeprecationWarning('on')

                    return this.instance.on.apply(this.instance, arguments)
                }
                // eof backward compat
            }
        }
    })
    //eof Siesta.Project

    // backward compat
    Siesta.Harness = Siesta.Project