/*

Siesta 5.1.0
Copyright(c) 2009-2018 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
    },


    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) {
                var shouldIgnore    = /__SIESTA_TEST_EXIT_EXCEPTION__/
                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