    /*

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

    */
    // workaround for: http://www.sencha.com/forum/showthread.php?299660-5.1.0.107-Exception-thrown-when-opening-any-example-in-Chrome-touch-simulation-mode&p=1094459#post1094459

    // this condition seems to point that Chrome is opened in device simulation mode, w/o touch events enabled from command line
    // seems "TouchEvents" + "!Touch" confuses Ext and exception is thrown
    if (Ext.supports && Ext.supports.TouchEvents && !Ext.supports.Touch) {
        // w/o these, UI throws exceptions
        Ext.supports.Touch          = false
        Ext.supports.touchScroll    = false
    }
    // eof workaround

    Ext.define('Siesta.Project.Browser.UI.Viewport', {

        extend : 'Ext.container.Viewport',

        mixins     : [
            'Siesta.Project.Browser.UI.CanFillAssertionsStore'
        ],
        controller : 'viewport',

        requires        : [
            'Siesta.Project.Browser.UI.ViewportController',
            'Ext.state.LocalStorageProvider',
            'Ext.state.CookieProvider',

            'ExtX.Reference.Slot',

            'Siesta.Project.Browser.UI.TestGrid',
            'Siesta.Project.Browser.UI.ResultPanel',

            'Siesta.Project.Browser.Model.AssertionTreeStore',
            'Siesta.Project.Browser.Model.FilterableTreeStore',
            'Siesta.Project.Browser.Model.TestFile',
            'Siesta.Project.Browser.Model.Assertion',

            'Siesta.Project.Browser.UI.VersionUpdateButton',

            'Siesta.Project.Browser.UI.Override',
            // requiring "Ext.Img" for it to be in the "siesta-all.js" bundle
            // otherwise it is included into "siesta-coverage-all.js" bundle and when using "siesta-no-ext-all.js" setup
            // Ext.Img is double included
            'Ext.Img'
        ],

        title               : null,

        project             : null,

        // need to set stateful properties before `initComponent`
        stateful            : false,
        layout              : 'border',

        // stateful
        selection           : null,
        filter              : null,
        filterGroups        : false,
        // eof stateful

        testsStore          : null,

        contextMenu         : null,
        enableVersionCheck  : true,

        collapsedNodes      : null,

        showSizeControls    : false,

        expectedExtJSVersion : '6.0.1.250',

        viewportSizes : [
            [640, 480],
            [800, 600],
            [1024, 768],
            [1920, 1080],
            [2048, 1536]
        ],

        idGenSequence       : 1,

        isReadOnlyReport    : false,
        dataUrl             : 'report-data.json',

        autoLaunch          : false,

        initComponent : function () {
            var project

            if (this.isReadOnlyReport) {
                project         = new Siesta.Project.Browser()
                project.configure({
                    stateful        : false
                })
                this.project    = project
            } else
                project         = this.project;

            Ext.getBody().addCls('siesta')

            Ext.getBody().on('keydown', this.onBodyKeyDown, this)
            Ext.setGlyphFontFamily('FontAwesome');

            Ext.state.Manager.setProvider(Ext.supports.LocalStorage ? new Ext.state.LocalStorageProvider() : new Ext.state.CookieProvider())

            this.selection      = {}

            // config given during viewport instantiation
            var filter          = this.filter

            if (project.stateful) this.applyState(this.loadState())

            // config given during viewport instantiation - comes from query string ?filter= overrides everything
            if (filter) {
                this.filter         = filter
                this.filterGroups   = false
            }

            var data = this.buildTreeData({
                id    : 'root',
                group : 'test suite' + this.title,
                items : project.descriptors
            }).children;

            var testsStore = this.testsStore = new Siesta.Project.Browser.Model.FilterableTreeStore({
                model : 'Siesta.Project.Browser.Model.TestFile',

                sortOnLoad : false,

                root : {
                    expanded : true,

                    children : data
                },

                proxy : 'memory',

                listeners : {
                    nodecollapse : this.saveState,
                    nodeexpand   : this.saveState,

                    scope : this
                }
            })

            Ext.apply(this, {
                slots           : true,

                items : [
                    {
                        region              : 'west',
                        xtype               : 'testgrid',
                        store               : testsStore,

                        stateful            : project.stateful,

                        slot                : 'filesTree',
                        id                  : project.id.replace(/\W/g, '_') + '-testTree',

                        showSizeControls    : this.showSizeControls,
                        viewportSizes       : this.viewportSizes,
                        stateConfig         : this.getState(),
                        project             : this.project,
                        isReadOnlyReport    : this.isReadOnlyReport,

                        animate             : !Ext.isIE,
                        split               : {
                            size    : 7
                        },

                        filter              : this.filter,
                        filterGroups        : this.filterGroups,

                        listeners           : {
                            viewready   : this.onFilterApplied,
                            scope       : this
                        }
                    },
                    {
                        xtype          : 'resultpanel',
                        region         : 'center',
                        slot           : 'resultPanel',
                        cls            : 'resultPanel-panel',

                        viewDOM                 : this.getOption('viewDOM'),
                        scaleToFit              : this.getOption('scaleToFit'),
                        scaleToFitMode          : this.getOption('scaleToFitMode'),

                        id                      : project.id.replace(/\W/g, '_') + '-resultpanel',
                        project                 : project,
                        recorderConfig          : project.recorderConfig,

                        isReadOnlyReport        : this.isReadOnlyReport,

                        maintainViewportSize    : project.maintainViewportSize,
                        domContainerRegion      : project.domContainerRegion,

                        listeners       : {
                            newjsonreport   : this.onNewJsonReport,
                            scope           : this
                        }
                    }
                ]
                // eof main content area
            })

            this.callParent()

            this.slots.filesTree.store.on({
                'filter-set'   : this.saveState,
                'filter-clear' : this.saveState,

                scope : this
            })

            this.on('statechange', this.saveState, this)

            project.on('testendbubbling', this.onEveryTestEnd, this)
            project.on('testsuitelaunch', this.onTestSuiteLaunch, this)
            project.on('assertiondiscard', this.onAssertionDiscarded, this)

            if (window.location.href.match('^file:///') && !(this.isReadOnlyReport && Ext.browser.is('Firefox'))) {
                var R = Siesta.Resource('Siesta.Project.Browser.UI.Viewport');

                Ext.Msg.alert(R.get('httpWarningTitle'), R.get('httpWarningDesc'))
            } else
                if (this.isReadOnlyReport) this.fetchJsonReport()
        },

        onNewJsonReport : function (field, report) {
            this.loadJsonReport(report)
        },

        buildTreeData : function (descriptor) {
            var data            = {
                id         : descriptor.id,

                title      : descriptor.group || descriptor.title || descriptor.name,
                descriptor : descriptor,

                // HACK, bypass Ext JS cloning
                nodeType   : 1,
                cloneNode  : function () { return this; }
                // EOF HACK
            }

            data.tooltip        = descriptor.desc || data.title

            var me              = this
            var prevId          = data.id
            var collapsedNodes  = this.collapsedNodes || {}

            if (descriptor.group) {

                var children = []

                Ext.each(descriptor.items, function (desc) {
                    children.push(me.buildTreeData(desc))
                })

                Ext.apply(data, {
                    expanded : (collapsedNodes[prevId] != null || descriptor.expanded === false) ? false : true,
                    // || false is required for TreeView - it checks that "checked" field contains Boolean
                    checked  : me.selection.hasOwnProperty(prevId) || false,

                    folderStatus : 'yellow',

                    children : children,
                    leaf     : false
                })

            } else {
                Ext.apply(data, {
                    url         : descriptor.url,

                    leaf        : true,
                    // || false is required for TreeView - it checks that "checked" field contains Boolean
                    checked     : me.selection.hasOwnProperty(prevId) || false,

                    passCount   : 0,
                    failCount   : 0,

                    time        : 0,

                    assertionsStore : new Siesta.Project.Browser.Model.AssertionTreeStore({
                        //autoDestroy : true,
                        model : 'Siesta.Project.Browser.Model.Assertion',

                        proxy : 'memory',

                        root : {
                            id       : '__ROOT__',
                            expanded : true
                        }
                    })
                })
            }

            return data
        },

        onBodyKeyDown : function (e) {
            if (e.ctrlKey) {
                var project     = this.project

                if (e.getKey() == Ext.event.Event[ project.rerunHotKey ]) {
                    this.runTest();
                    e.preventDefault();
                }

                if (e.getKey() == Ext.event.Event[ project.observerModeHotKey ]) {
                    var mode        = !project.observerMode

                    var menuItem    = this.down('#observerModeMenuItem')

                    menuItem.setChecked(mode)

                    menuItem.fireEvent('click', menuItem)

                    e.preventDefault();
                }
            }
        },

        buildTreeDataFromJSONSReportNode : function (reportNode) {
            var data = {
                id          : this.idGenSequence++,
                title       : reportNode.group || reportNode.name || reportNode.url.replace(/(?:.*\/)?([^/]+)$/, '$1'),
                descriptor  : reportNode,

                // HACK, bypass Ext JS cloning
                nodeType    : 1,
                cloneNode   : function () {
                    return this;
                }
            }

            var me              = this

            if (reportNode.group) {
                var children = []

                Ext.each(reportNode.items, function (reportNode) {
                    children.push(me.buildTreeDataFromJSONSReportNode(reportNode))
                })

                Ext.apply(data, {
                    expanded        : true,
                    // disable checkbox selection
                    checked         : null,
                    leaf            : false,

                    folderStatus    : 'yellow',

                    children        : children
                })

            } else {
                var parentTest      = new Siesta.Test.Browser({
                    project     : this.project,

                    url         : reportNode.url,
                    name        : reportNode.name,

                    startDate   : new Date(reportNode.startDate)
                })

                Ext.apply(data, {
                    url             : reportNode.url,

                    leaf            : true,
                    // disable checkbox selection
                    checked         : null,

                    time            : reportNode.endDate - reportNode.startDate,

                    test            : parentTest,

                    assertionsStore : new Siesta.Project.Browser.Model.AssertionTreeStore({
                        model : 'Siesta.Project.Browser.Model.Assertion',

                        proxy : 'memory',

                        root : {
                            id          : '__ROOT__',
                            expanded    : true,

                            children    : this.buildAssertionDataFromJSONSReportNode(reportNode.assertions, parentTest)
                        }
                    })
                })

                parentTest.endDate  = new Date(reportNode.endDate)

                Ext.apply(data, {
                    passCount       : parentTest.getPassCount(),
                    failCount       : parentTest.getFailCount(),
                    todoPassCount   : parentTest.getTodoPassCount(),
                    todoFailCount   : parentTest.getTodoFailCount()
                })
            }

            return data
        },

        buildAssertionDataFromJSONSReportNode : function (assertions, parentTest) {
            var me      = this

            var res     = Joose.A.map(assertions, function (assertionNode) {
                var isSubTest       = assertionNode.type == 'Siesta.Result.SubTest'
                var result

                var data            = {
                    id                  : me.idGenSequence++,

                    leaf                : !isSubTest,
                    expanded            : isSubTest && (assertionNode.bddSpecType != 'it' || !assertionNode.passed)
                }

                if (isSubTest) {
                    var subTest     = new Siesta.Test.Browser({
                        trait       : Siesta.Test.Sub,
                        name        : assertionNode.name,

                        isTodo      : assertionNode.isTodo,

                        startDate   : new Date(assertionNode.startDate),

                        url         : parentTest.url,
                        parent      : parentTest,
                        specType    : assertionNode.bddSpecType
                    })

                    data.children   = me.buildAssertionDataFromJSONSReportNode(assertionNode.assertions, subTest)

                    subTest.endDate = new Date(assertionNode.endDate)

                    result          = subTest.getResults()
                } else {
                    var resultCls   = Joose.S.strToClass(assertionNode.type)

                    result          = new resultCls(assertionNode)
                }

                parentTest.addResult(result)

                data.result         = result

                return data
            })

            if (!parentTest.parent) {
                var summary     = new Siesta.Result.Summary({
                    isFailed            : parentTest.isFailed(),
                    description         : parentTest.getSummaryMessage()
                })

                parentTest.addResult(summary)

                res.push({
                    id                  : this.idGenSequence++,

                    result              : summary,

                    loaded              : true,
                    leaf                : true,
                    expanded            : false
                })
            }

            return res
        },

        loadJsonReport : function (report) {
            this.title          = report.testSuiteName || ''

            this.testsStore.setRootNode(this.buildTreeDataFromJSONSReportNode({
                group       : 'TOP',
                items       : report.testCases
            }))

            this.testsStore.getRootNode().cascadeBy(function (node) {
                if (node.parentNode && !node.isLeaf()) node.updateFolderStatus()
            })

            this.updateStatusIndicator();
        },

        fetchJsonReport : function () {
            var me      = this

            var loadingMask = new Ext.LoadMask({
                target : this,
                msg    : Siesta.Resource('Siesta.Project.Browser.UI.CoverageReport', 'loadingText')
            });

            loadingMask.show()

    //        use this code for testing
    //        setTimeout(function () {
    //            loadingMask.hide()
    //
    //            me.loadHtmlReport(me.htmlReport)
    //        }, 2000)

            Ext.Ajax.request({
                url         : this.dataUrl,

                success : function (response) {
                    loadingMask.hide()
                    me.loadJsonReport(Ext.JSON.decode(response.responseText))
                },

                failure : function () {
                    loadingMask.hide()
                    Ext.Msg.show({
                        title   : Siesta.Resource('Siesta.Project.Browser.UI.CoverageReport', 'loadingErrorText'),
                        msg     : Siesta.Resource('Siesta.Project.Browser.UI.CoverageReport', 'loadingErrorMessageText') + me.dataUrl,
                        buttons : Ext.MessageBox.OK,
                        icon    : Ext.MessageBox.ERROR
                    })
                }
            })
        },

        onFilterApplied : function () {
            if (this.autoLaunch) {
                this.runAll()
            } else
                if (this.getOption('autoRun')) {
                    var checked = this.getChecked()

                    // either launch the suite for checked tests or for all
                    this.project.launch(checked.length && checked || this.project.descriptors)
                }
        },

        setNodeChecked : function (testFile, checked, doNotCascade, skipSave) {
            var me = this
            var id = testFile.getId()

            if (checked)
                this.selection[id] = 1
            else
                delete this.selection[id]

            testFile.set('checked', checked)

            // when unchecking the node - uncheck the parent node (folder) as well
            if (!checked && testFile.parentNode) me.setNodeChecked(testFile.parentNode, false, true, true)

            // only cascade for folders and when `doNotCascade` is false
            if (!testFile.isLeaf() && !doNotCascade) Ext.each(testFile.childNodes, function (childNode) {
                me.setNodeChecked(childNode, checked, false, true)
            })

            if (!skipSave) this.saveState()
        },

        forEachTestFile : function (func, scope) {
            scope           = scope || this
            var testsStore  = this.testsStore
            var root        = testsStore.getRootNode()

            root.cascadeBy(function (node) {
                var isFilteredIn        = testsStore.isNodeFilteredIn(node)

                if (node != root && isFilteredIn) func.call(scope, node)

                // do not cascade for child nodes if parent is not filtered in - faster tree traversing
                return isFilteredIn
            })
        },

        getChecked : function () {
            var descriptors = []

            this.forEachTestFile(function (testFileRecord) {
                if (testFileRecord.get('checked') && testFileRecord.isLeaf()) descriptors.push(testFileRecord.get('descriptor'))
            })

            return descriptors
        },

        runChecked : function () {
            var checked = this.getChecked();

            if (checked.length > 0) {
                this.project.launch(this.getChecked())
            }
        },

        runFailed : function () {
            var descriptors = []

            this.forEachTestFile(function (testFileRecord) {
                var test = testFileRecord.get('test')

                if (test && test.isFailed()) descriptors.push(testFileRecord.get('descriptor'))
            })

            if (descriptors.length > 0) {
                this.project.launch(descriptors)
            }
        },

        runAll : function () {
            var allDesc = []

            this.forEachTestFile(function (testFile) {
                if (testFile.isLeaf()) allDesc.push(testFile.get('descriptor'))
            })

            if (allDesc.length > 0) {
                this.project.launch(allDesc)
            }
        },

        // this method is never called currently, because there's no corresponding button in the test grid bottom bar
        stopSuite : function (button) {
            button.disable()

            this.project.stopCurrentLaunch();

            setTimeout(function () {
                button.enable()
            }, 1000)
        },

        onTestSuiteStop : function (sourceTest) {
            this.testsStore.forEach(function (testFileRecord) {
                if (testFileRecord.get('isStarting')) {
                    testFileRecord.set('isStarting', false);
                }
            });

            if (sourceTest) {
                var testRecord = this.testsStore.getNodeById(sourceTest.url)

                this.slots.filesTree.getSelectionModel().select(testRecord);
            }
        },

        // looks less nice than setting it only after preload for some reason
        onBeforeScopePreload : function (scopeProvider, url) {
            var testRecord = this.testsStore.getNodeById(url)
        },

        isTestRunningVisible : function (test) {
            // return false for test's running in popups (not iframes), since we can't show any visual accompaniment for them
            if (!(test.scopeProvider instanceof Scope.Provider.IFrame)) return false;

            // if there is a "forced to be on top" test then we only need to compare the tests instances
            if (this.project.testOfForcedIFrame) {
                return this.project.testOfForcedIFrame.isFromTheSameGeneration(test)
            }

            // otherwise the only possibly visible test is the one of the current assertion grid
            var resultPanel = this.slots.resultPanel;

            // if resultPanel has no testRecord it hasn't yet been assigned a test record
            if (!resultPanel.test || !resultPanel.test.isFromTheSameGeneration(test)) {
                return false;
            }

            // now we know that visible assertion grid is from our test and there is no "forced on top" test
            // we only need to check visibility (collapsed / expanded of the right panel
            return resultPanel.isFrameVisible()
        },

        resetDescriptors  : function (descriptors) {
            var testsStore = this.testsStore;

            Joose.A.each(this.project.flattenDescriptors(descriptors), function (descriptor) {
                var testRecord = testsStore.getNodeById(descriptor.id);

                testRecord.get('assertionsStore').removeAll(true)
                testRecord.reject();
                // || false is required for TreeView - it checks that "checked" field contains Boolean
                testRecord.set('checked', this.selection.hasOwnProperty(descriptor.id) || false)
            }, this);
        },

        // method is called when test suite (any several tests) starts - before caching the script contents
        // at this point we don't know yet about missing test files
        onTestSuiteStart  : function (descriptors) {
            Ext.getBody().addCls('testsuite-running');

            var filesTree       = this.slots.filesTree
            var selModel        = filesTree.getSelectionModel()
            var prevSelection   = selModel.getLastSelected()
            var testsStore      = this.testsStore

            this.resetDescriptors(descriptors);

            // restore the selection after data reload
            if (prevSelection) selModel.select(testsStore.getNodeById(prevSelection.getId()))
        },

        // method is called when test suite (any several tests) launches - after the caching the script contents
        // has completed and 1st test is about to start
        // at this point we know about missing files and `desc.isMissing` property is set
        onTestSuiteLaunch : function (event, descriptors) {
            var testsStore = this.testsStore

            var updated = {}

            Joose.A.each(this.project.flattenDescriptors(descriptors), function (descriptor) {
                var testRecord = testsStore.getNodeById(descriptor.id)

                testRecord.set({
                    isMissing  : descriptor.isMissing,
                    isStarting : true
                })

                var groupNode = testRecord.parentNode

                if (groupNode && !updated[groupNode.getId()]) {
                    // trying hard to prevent extra updates
                    for (var node = groupNode; node; node = node.parentNode) updated[node.getId()] = true

                    groupNode.updateFolderStatus()
                }
            })
        },

        onTestSuiteEnd : function (descriptors) {
            Ext.getBody().removeCls('testsuite-running');

            this.updateStatusIndicator();

            // Without this the keyboard hotkey won't work (since one of the frames will steal focus likely)
            if (Ext.isChrome) {
                window.focus();
            } else {
                document.body.tabIndex = -1;
                document.body.focus();
            }
        },

        onTestStart        : function (test) {
            var testRecord = this.testsStore.getNodeById(test.url)

            if (!this.isTestUpdateActual(test, testRecord)) return

            testRecord.beginEdit()

            // will trigger an update in grid
            testRecord.set({
                test      : test,
                isRunning : true
            })

            testRecord.endEdit()

            var currentSelection = this.slots.filesTree.getSelectionModel().getLastSelected()

            // activate the assertions grid for currently selected row, or, if the main area is empty
            if (currentSelection && currentSelection.getId() == test.url) {
                var resultPanel = this.slots.resultPanel

                resultPanel.showTest(test, testRecord.get('assertionsStore'))
                resultPanel.setInitializing(false);

                T           = test
            }
        },

        // this method checks that test update, coming from given `test` is actual
        // update may be not actual, if user has re-launched the test, so new test already presents
        isTestUpdateActual : function (test, testRecord) {
            return test.launchId == this.project.currentLaunchId
        },

        onTestUpdate   : function (test, result, parentResult) {
            var testRecord = this.testsStore.getNodeById(test.url)

            // need to check that test record contains the same test instance as the test in arguments (or its sub-test)
            // test instance may change if user has restarted a test for example
            if (this.isTestUpdateActual(test, testRecord)) {
                this.processNewResult(testRecord.get('assertionsStore'), test, result, parentResult)
            }
        },

        onAssertionDiscarded: function (event, test, result) {
            var testRecord = this.testsStore.getNodeById(test.url)

            // need to check that test record contains the same test instance as the test in arguments (or its sub-test)
            // test instance may change if user has restarted a test for example
            if (this.isTestUpdateActual(test, testRecord)) {
                var assertionStore = testRecord.get('assertionsStore')

                assertionStore.getNodeById(result.id).remove()
            }
        },

        // only triggered for "root" tests
        onTestEnd      : function (test) {
            var testRecord = this.testsStore.getNodeById(test.url)

            // need to check that test record contains the same test instance as the test in arguments (or its sub-test)
            // test instance may change if user has restarted a test for example
            if (this.isTestUpdateActual(test, testRecord)) {
                testRecord.beginEdit()

                testRecord.set({
                    'passCount'     : test.getPassCount(),
                    'failCount'     : test.getFailCount(),
                    'todoPassCount' : test.getTodoPassCount(),
                    'todoFailCount' : test.getTodoFailCount(),
                    'time'          : test.getDuration() + 'ms'
                });

                testRecord.endEdit()

                testRecord.parentNode && testRecord.parentNode.updateFolderStatus()
            }

            this.updateStatusIndicator()
        },

        // is bubbling and thus triggered for all tests (including sub-tests)
        onEveryTestEnd : function (event, test) {
            var testRecord = this.testsStore.getNodeById(test.url)

            // need to check that test record contains the same test instance as the test in arguments (or its sub-test)
            // test instance may change if user has restarted a test for example
            if (this.isTestUpdateActual(test, testRecord)) {
                this.processEveryTestEnd(testRecord.get('assertionsStore'), test)
            }
        },

        onTestFail : function (test, exception, stack) {
            var testRecord = this.testsStore.getNodeById(test.url)

            // need to check that test record contains the same test instance as the test in arguments
            // test instance may change if user has restarted a test for example
            if (this.isTestUpdateActual(test, testRecord) && !test.isTodo) {
                testRecord.set('isFailed', true)

                testRecord.parentNode && testRecord.parentNode.updateFolderStatus()
            }
        },

        getOption : function (name, onlyOwn) {
            switch (name) {
                case 'selection'    :
                    return this.selection

                default             :
                    var project     = this.project

                    if (onlyOwn) return project.hasOwnProperty(name) ? project[ name ] : undefined

                    return project[ name ]
            }
        },

        setOption : function (name, value) {
            var project     = this.project

            switch (name) {
                case 'selection' :
                    return this.selection       = value || {}

                case 'collapsedNodes' :
                    return this.collapsedNodes  = value

                case 'filter' :
                    return this.filter          = value

                case 'filterGroups' :
                    return this.filterGroups    = value

                case 'observerMode' :
                    project.setObserverMode(value)
                    break

                // UI only option, does not correspond to viewport/project config
                case 'mouseSimSlow' :
                    if (value) {
                        project[ 'speedRun' ]       = false
                        project[ 'superSpeedRun' ]  = false
                    }
                    break

                // UI only option, does not correspond to viewport/project config
                case 'mouseSimFast' :
                    if (value) {
                        project[ 'speedRun' ]       = true
                        project[ 'superSpeedRun' ]  = false
                    }
                    break

                // UI only option, does not correspond to viewport/project config
                case 'mouseSimFastest' :
                    if (value) {
                        project[ 'speedRun' ]       = true
                        project[ 'superSpeedRun' ]  = true
                    }
                    break

                default :
                    return project[ name ]      = value
            }
        },

        getState : function (onlyOwnProperties) {
            // getState is also called early, when there's no child components
            // the absense of "this.slots" indicates this
            var domContainer    = this.slots && this.down('domcontainer')

            var state   = {
                // UI configs
                scaleToFit      : domContainer ? domContainer.scaleToFit : this.scaleToFit,
                scaleToFitMode  : domContainer ? domContainer.scaleToFitMode : this.scaleToFitMode,

                selection       : this.getCheckedNodes(),
                collapsedNodes  : this.getCollapsedFolders(),

                filter          : this.slots ? this.slots.filesTree.getFilterValue() : this.filter,
                filterGroups    : this.slots ? this.slots.filesTree.getFilterGroups() : this.filterGroups
            }

            var me      = this

            // project configs
            Joose.A.each([
                'autoRun', 'speedRun', 'superSpeedRun', 'viewDOM', 'transparentEx', 'breakOnFail',
                'debuggerOnFail', 'observerMode'
            ], function (name) {
                var value   = me.getOption(name, onlyOwnProperties)

                if (onlyOwnProperties) {
                    if (value !== undefined) state[ name ] = value
                } else
                    state[ name ] = value
            })

            return state
        },

        getCheckedNodes : function () {
            var checked = {}

            this.testsStore.forEach(function (treeNode) {
                if (treeNode.get('checked')) checked[treeNode.getId()] = 1
            })

            return checked
        },

        getCollapsedFolders : function () {
            var collapsed = {}

            this.testsStore.forEach(function (treeNode) {
                if (!treeNode.isLeaf() && !treeNode.isExpanded()) collapsed[treeNode.getId()] = 1
            })

            return collapsed
        },

        applyState : function (state) {
            var me = this

            if (state) Joose.O.each(state, function (value, name) {
                me.setOption(name, value)
            })
        },

        getStateId : function () {
            return 'test-run-' + this.title
        },

        loadState : function () {
            var stateId     = this.getStateId()
            var state       = Ext.state.Manager.get(stateId)

            if (!state) return

            if (!state.collapsedNodes)  state.collapsedNodes = Ext.state.Manager.get(stateId + '-collapsed')
            if (!state.selection)       state.selection = Ext.state.Manager.get(stateId + '-selection')

            return state
        },

        saveState : function () {
            var stateId     = this.getStateId()
            var state       = this.getState(true)

            Ext.state.Manager.set(stateId + '-collapsed', state.collapsedNodes)
            Ext.state.Manager.set(stateId + '-selection', state.selection)

            delete state.collapsedNodes
            delete state.selection

            Ext.state.Manager.set(stateId, state)
        },

        uncheckAllExcept : function (testFile) {
            var me = this

            this.testsStore.forEach(function (node) {
                if (node != testFile) me.setNodeChecked(node, false, true)
            })
        },

        launchTest : function (testFile) {
            if (this.isReadOnlyReport) return

            var resultPanel = this.slots.resultPanel

            var isLeaf      = testFile instanceof Ext.data.Model ? testFile.data.leaf : true;
            var descriptor  = testFile instanceof Siesta.Test ? this.project.getScriptDescriptor(testFile) : testFile.get('descriptor');

            // clear the content of the result panel when launching a single test
            if (isLeaf) {
                Ext.History.add(descriptor.url);
                // assertions of the tests being launched will be cleared in the `onTestSuiteStart` method
                resultPanel.setInitializing(true);
            }

            this.project.launch([ descriptor ])
        },

        updateStatusIndicator : function () {
            // can remain neutral if all files are missing for example
    //        var isNeutral       = true
    //        var allGreen        = true
    //        var hasFailures     = false

            var totalPassed = 0
            var totalFailed = 0

            this.testsStore.forEach(function (testFileRecord) {
                var test = testFileRecord.get('test')

                // if there's at least one test - state is not neutral
                if (test && test.isFinished()) {
    //                isNeutral       = false

    //                allGreen        = allGreen      && test.isPassed()
    //                hasFailures     = hasFailures   || test.isFailed()

                    totalPassed += test.getPassCount()
                    totalFailed += test.getFailCount()
                }
            })

            this.slots.filesTree.updateStatus(totalPassed, totalFailed);
        },

        /**
         * Re-runs the latest executed test
         */
        runTest : function () {
            if (this.isReadOnlyReport) return

            var toRun = this.slots.filesTree.getSelectionModel().getSelection()[0] ||
                        this.slots.resultPanel.test;

            if (toRun) {
                this.launchTest(toRun);
            }
        },

        afterRender : function () {
            this.callParent(arguments);

            if (Ext.versions.extjs.isLessThan(this.expectedExtJSVersion) && window.console) {
                console.log('Wrong Ext JS version detected.', 'Siesta expects that you use Ext JS version >= ' + this.expectedExtJSVersion + '. You may experience errors when using another version');
            }

            if (!this.project.isAutomated) setTimeout(Ext.Function.bind(this.deferredSetup, this), 1000);
        },

        deferredSetup : function() {
            Ext.QuickTips && Ext.QuickTips.init();

            if (this.enableVersionCheck && Siesta.Project.Browser.UI.VersionUpdateButton) {
                new Siesta.Project.Browser.UI.VersionUpdateButton();
            }
        },

        onManualCloseOfForcedIframe : function (test) {
            var domContainer        = this.down('domcontainer')

            if (domContainer && domContainer.test == test) domContainer.alignIFrame(true)
        }
    })
    //eof Siesta.Project.Browser.UI.Viewport