Source: SeLite/bootstrap/src/chrome/content/extensions/bootstrap-core.js

/*  Copyright 2012, 2013, 2014, 2015, 2016 Peter Kehl
    This file is part of SeLite Bootstrap.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
/** @param {object} global Global object, as per https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects. Its value is value of operator 'this'. I need it, so that I can call loadSubScript() with charset set to 'UTF-8'.
 * */
(function(global) { // Anonymous function separates local variables from Selenium Core scope
    var loadedTimes= SeLiteExtensionSequencer.coreExtensionsLoadedTimes['SeLiteBootstrap'] || 0;
    if( loadedTimes===1 ) { // Ignore the first load, because Se IDE somehow discards that Selenium.prototype. So set up the overrides on 2nd load.
        
        /** @var Object serving as an associative array [string file path] => int lastModifiedTime
         **/
        Selenium.bootstrapScriptLoadTimestamps= {};
        var FileUtils= Components.utils.import("resource://gre/modules/FileUtils.jsm", {} ).FileUtils;
        var Services= Components.utils.import("resource://gre/modules/Services.jsm", {} ).Services;
        /** There are two sets of events when we want to call reloadScripts(), which are handled separately:
            - executing a single test command / run a testcase / run each testcase in a testsuite. Handled by tail-intercept of Selenium.prototype.reset() below.
            - run a testcase/testsuite, pause it (or not), modify a file loaded via SeBootstrap (and make the test continue if you paused it earlier), SeBootstrap will not re-trigger Selenium.prototype.reset() (until next run of a single command/testcase/testsuite). That's handled by TestCaseDebugContext.prototype.nextCommand(). This function is defined in sister extension: testcase-debug-context. Then it's intercepted in SelBlocks Global.
        */
        // Tail intercept of Selenium.reset().
          var origReset = Selenium.prototype.reset;

          Selenium.prototype.reset= function reset() {
          // @TODO Use interceptBefore() from SelBlocks - if SelBlocksGlobal stays as a part of SeLite
                Selenium.bootstrapReloadScripts();
                origReset.call(this);
          };

        var /*@TODO const*/ subScriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                .getService(Components.interfaces.mozIJSSubScriptLoader);

        var bootstrappedListChanged= false;
        var bootstrappedCoreExtensions= SeLiteSettings.loadFromJavascript( 'extensions.selite-settings.common' ).getField( 'bootstrappedCoreExtensions' );
        var bootstrappedCoreExtensionsRecord; // This will be a result of bootstrappedCoreExtensions.getDownToFolder(..). It will be cached, and re-loaded when bootstrappedListChanged is true.
        /*** This (re)loads and processes any updated custom .js file(s) - either if they were not loaded yet,
         *   or if they were modified since then. It also reloads them if their timestamp changed, but the contents didn't
         *   - no harm in that.
         */
        Selenium.bootstrapReloadScripts= function bootstrapReloadScripts() {
            editor.seleniumAPI.Selenium= Selenium;
            editor.seleniumAPI.LOG= LOG;

            if( bootstrappedListChanged || bootstrappedCoreExtensionsRecord===undefined ) {
                bootstrappedCoreExtensionsRecord= bootstrappedCoreExtensions.getDownToFolder( /*folderPath*/undefined, /*dontCache*/true );
                bootstrappedListChanged= false;
            }
            
            var files= {}; // { string filePath: nsIFile object } for all bootstrapped files
            var anyFileNewOrModified= false;
            for( var filePath in bootstrappedCoreExtensionsRecord.entry ) {
                try {
                    files[filePath]= new FileUtils.File(filePath); // Object of class nsIFile
                }
                catch( exception ) {
                    LOG.warn( "SeBootstrap tried to (re)load a non-existing file " +filePath );
                    continue;
                }
                anyFileNewOrModified|= !(filePath in Selenium.bootstrapScriptLoadTimestamps) || Selenium.bootstrapScriptLoadTimestamps[filePath]!==files[filePath].lastModifiedTime;
            }
            if( anyFileNewOrModified ) {
                for( var filePath in files ) { // Reload all files, not just the modified one(s). That allows dependant files to maintain their book keeping.
                    // Let's set the timestamp before loading & executing the file. This ensures that if something goes wrong in that file, it won't be re-run
                    // until it's updated (or until you reload Selenium IDE).
                    Selenium.bootstrapScriptLoadTimestamps[filePath]= files[filePath].lastModifiedTime;

                    var fileUrl= Services.io.newFileURI( files[filePath] );
                    try {
                        // When I passed editor.seleniumAPI, then bootstrapped extension must have defined global variables (without _var_ keyword) and therefore it couldn't use Javascript strict mode.
                        subScriptLoader.loadSubScriptWithOptions( fileUrl.spec, {
                            target: global,
                            charset: 'UTF-8',
                            ignoreCache: true
                        } );
                        // This could also be done via Components.utils.import( tmpFileUrl.spec, scope ) and Components.utils.unload(url). However, the .js file would have to define var EXPORTED_SYMBOLS= ['symbol1', 'symbol2', ...];
                    }
                    catch(error ) {
                        var msg= "SeBootstrap tried to evaluate " +filePath+ " and it failed with:\n"
                            +SeLiteMisc.addStackToMessage( error, true )+ '.';
                        SeLiteMisc.log().error( msg );
                    }
                }
            }
        };
        
        SeLiteSettings.setBootstrappedListAsChanged= function setBootstrappedListAsChanged() {
            bootstrappedListChanged= true;
        };
        
        SeLiteSettings.addTestSuiteFolderChangeHandler( SeLiteSettings.setBootstrappedListAsChanged );
    }
    if( loadedTimes>=2 ) {
        throw new Error('SeLiteBootstrap already loaded ' +loadedTimes );
    }
    SeLiteExtensionSequencer.coreExtensionsLoadedTimes['SeLiteBootstrap']= loadedTimes+1;
})(this);