define([ 'dojo/aspect', 'dojo/has', 'dojo/lang', 'dojo/Promise', //'dojo/Deferred', 'dojo/has!host-browser?dojo/request', 'dojo/has!host-browser?dojo/io-query', 'dojo/has!host-node?dojo/node!path', '../../main', '../parseArgs', '../util', 'require', 'module' ], function (aspect, has, lang, Promise,request, ioQuery, pathUtil, main, parseArgs, util, require,module) { /** * For testing sessions running through the Intern proxy, tells the remote test system that an error occured when * attempting to set up this environment. * * @function * @param {Error} error */ var sendErrorToConduit = (function () { var sequence = 0; return function (error) { var sessionIdFromUrl = /[?&]sessionId=([^&]+)/.exec(location.search); if (!sessionIdFromUrl) { return; } var sessionId = decodeURIComponent(sessionIdFromUrl[1]); // Proxy expects data to be an array of serialized objects var data = [ JSON.stringify({ sequence: sequence, sessionId: sessionId, payload: [ 'fatalError', // Non-standard `sessionId` property is used by ClientSuite in the test runner to associate // a fatal error with a particular environment { name: error.name, message: error.message, stack: error.stack, sessionId: sessionId } ] }) ]; request(require.toUrl('intern/'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data) }); // The sequence must not be incremented until after the data is successfully serialised, since an error // during serialisation might occur, which would mean the request is never sent, which would mean the // dispatcher on the server-side will stall because the sequence numbering will be wrong ++sequence; }; })(); /** * The PreExecutor executor handles loading the user’s configuration and setting up the environment with the proper * AMD loader. * * @constructor * @param {Object} kwArgs */ function PreExecutor(kwArgs) { this.defaultLoaderOptions = kwArgs.defaultLoaderOptions; var executorId = kwArgs.executorId; if (executorId.indexOf('/') === -1) { executorId = executorId.charAt(0).toUpperCase() + executorId.slice(1); executorId = require.toAbsMid('./' + executorId); } this.executorId = executorId; this.config = kwArgs.config; } PreExecutor.prototype = { constructor: PreExecutor, /** * Default loader configuration that needs to be passed to the new loader. * * @type {Object} */ defaultLoaderOptions: null, _earlyErrorHandle: null, _earlyEvents: null, /** * The module ID of the executor to load. * * @type {string} */ executorId: null, /** * Gets arguments from the command-line/query-string. * * @returns {Object} */ getArguments: function () { var kwArgs; if (has('host-browser')) { kwArgs = parseArgs.fromQueryString(location.search); } else if (has('host-node')) { kwArgs = parseArgs.fromCommandLine(process.argv.slice(2)); } [ 'environments', 'functionalSuites', 'reporters', 'suites' ].forEach(function (name) { var value = kwArgs[name]; if (value != null && !Array.isArray(value)) { kwArgs[name] = value === '' ? [] : [ value ]; } }); if (kwArgs.excludeInstrumentation === 'true') { kwArgs.excludeInstrumentation = true; } else if (typeof kwArgs.excludeInstrumentation === 'string') { kwArgs.excludeInstrumentation = new RegExp(kwArgs.excludeInstrumentation); } this.getArguments = function () { return kwArgs; }; return kwArgs; }, /** * Gets the user’s configuration. * * @returns {Promise.} */ getConfig: function (args) { var self = this; var config = this.config; if (!config) { throw new Error(module.id + ' :Missing missing property "config"'); } /* jshint maxcomplexity:20 */ config = lang.deepMixin(config, args); if (typeof config.loader === 'object') { self._earlyEvents.push([ 'deprecated', 'The "loader" configuration option', '"loaderOptions"' ]); config.loaderOptions = config.loader; } if (typeof config.useLoader === 'object') { self._earlyEvents.push([ 'deprecated', 'The "useLoader" configuration option', '"loaders"' ]); config.loaders = config.useLoader; } config.loaderOptions = config.loaderOptions || {}; var isAbsoluteBaseUrl; if (has('host-node')) { if (config.basePath == null) { config.basePath = process.cwd(); } config.basePath = util.normalizePath(config.basePath); if (config.basePath.charAt(config.basePath.length - 1) !== '/') { config.basePath += '/'; } // The crappy fallback function is for Node 0.10; remove it when Node 0.10 is officially dropped isAbsoluteBaseUrl = pathUtil.isAbsolute || function (path) { if (pathUtil.sep === '/') { return path.charAt(0) === '/'; } else { return /^\w+:/.test(path); } }; } else if (has('host-browser')) { (function () { var defaultBasePath = config.initialBaseUrl || // replacing `/node_modules/intern/client.html` with `/`, allowing for directory name // derivatives util.normalizePath(location.pathname.replace(/(?:\/+[^\/]*){3}\/?$/, '/')); if (config.basePath == null) { config.basePath = defaultBasePath; } else if (config.basePath.charAt(0) === '.') { config.basePath = util.normalizePath(defaultBasePath + config.basePath); } if (config.basePath.charAt(config.basePath.length - 1) !== '/') { config.basePath += '/'; } })(); isAbsoluteBaseUrl = function (url) { // Detect both schema-ed and schema-less URLs return /^(?:\w+:|\/\/\w)/.test(url); }; if (args.loaders && args.loaders['host-browser']) { util.assertSafeModuleId(args.loaders['host-browser']); } } // If the baseUrl is unset, then it will be the default from client.html or the cwd, which would be // inconsistent if (!config.loaderOptions.baseUrl) { config.loaderOptions.baseUrl = config.basePath; } // non-absolute loader baseUrl needs to be fixed up to be relative to the defined basePath, not to // client.html or process.cwd() else if (!isAbsoluteBaseUrl(config.loaderOptions.baseUrl)) { config.loaderOptions.baseUrl = util.normalizePath(config.basePath + config.loaderOptions.baseUrl); } if (config.grep == null) { config.grep = new RegExp(''); } else { var grep = /^\/(.*)\/([gim]*)$/.exec(config.grep); if (grep) { config.grep = new RegExp(grep[1], grep[2]); } else { config.grep = new RegExp(config.grep, 'i'); } } config.instrumenterOptions = config.instrumenterOptions || {}; if (config.coverageVariable) { config.instrumenterOptions.coverageVariable = config.coverageVariable; } if (config.proxyPort == null) { config.proxyPort = 9000; } else if (typeof config.proxyPort === 'string') { if (isNaN(config.proxyPort)) { throw new Error('proxyPort must be a number'); } config.proxyPort = Number(config.proxyPort); } // If the user doesn't specify a proxy URL, construct one using the proxy port. if (config.proxyUrl == null) { config.proxyUrl = 'http://localhost:' + config.proxyPort + '/'; } var promise = new Promise(function(resolve){ resolve(config); }); this.getConfig = function () { return promise; }; return promise; }, /** * Handles errors that occur during the pre-execution sequence. * * @param {Error} error */ _handleError: function (error) { if (has('host-browser')) { if (location.pathname.replace(/\/+[^\/]*$/, '/').slice(-10) === '/__intern/') { sendErrorToConduit(error); } var htmlError = util.getErrorMessage(error).replace(/&/g, '&').replace(/' + htmlError + ''; document.body.appendChild(errorNode); } else /* istanbul ignore else */ if (typeof console !== 'undefined') { console.error(util.getErrorMessage(error)); // TODO: The loader needs to be fixed to allow errbacks to `require` calls so we don’t just exit on // early error but can instead propagate loader errors through the `PreExecutor#run` promise chain if (has('host-node')) { process.exit(1); } } }, /** * Loads the constructor for the real executor for this test run via the final loader environment. * * @param {string} executorId The module ID of the executor. * @param {Function} require An AMD loader `require` function. * @returns {Promise.} Executor constructor. */ _loadExecutorWithLoader: function (executorId, require) { return new Promise(function (resolve, reject) { require([ executorId ], resolve, reject); }); }, /** * Registers a global error handler. * * @param {(error:Error) => void} handler * @returns {{ remove: () => void }} */ registerErrorHandler: function (handler) { if (this._earlyErrorHandle) { this._earlyErrorHandle.remove(); this._earlyErrorHandle = null; } if (has('host-browser')) { /* jshint browser:true */ return aspect.before(window, 'onerror', function (message, url, lineNumber, columnNumber, error) { error = error || new Error(message + ' at ' + url + ':' + lineNumber + (columnNumber !== undefined ? ':' + columnNumber : '')); handler(error); }); } else if (has('host-node')) { /* jshint node:true */ process.on('uncaughtException', function (error) { handler(error); }); return { remove: function () { this.remove = function () {}; process.removeListener('uncaughtException', handler); } }; } }, /** * Runs the test executor. */ run: function () { var self = this; var args = this.getArguments(); var config; var earlyErrorHandler = lang.bind(this, '_handleError'); var executor; this._earlyErrorHandle = this.registerErrorHandler(earlyErrorHandler); this._earlyEvents = []; // TODO: Eliminate main.args, main.config, and main.mode in a future release var executionMode = (function (id) { if (id === require.toAbsMid('./Client')) { return 'client'; } else if (id === require.toAbsMid('./Runner')) { return 'runner'; } else { return 'custom'; } })(this.executorId); // These values must be populated on the main module prior to loading the configuration module because // the configuration module may depend on them in order to perform configuration main.args = args; main.mode = executionMode; main.config = config; function getConfig() { return self.getConfig(args).then(function (_config) { config = _config; }); } function loadExecutorWithLoader(loader) { return self._loadExecutorWithLoader(self.executorId, loader); } function populateMainModule(loader) { return util.getModule('intern/main').then(function (main) { // The main module needs to be repopulated here because a loader swap may have occurred, // in which case this main module is not the same as the main module loaded as a dependency of // PreExecutor main.args = args; main.mode = executionMode; main.config = config; return loader; }); } /** * Expand any globs in the suites and functionalSuites lists */ function resolveSuites(loader) { var promise; if (has('host-node')) { promise = new Promise(function (resolve) { config.suites = util.resolveModuleIds(config.suites); config.functionalSuites = util.resolveModuleIds(config.functionalSuites); resolve(); }); } // Only try to g else if (has('host-browser') && config.suites.some(util.isGlobModuleId)) { var query = ioQuery.objectToQuery({ suites: JSON.stringify(config.suites) }); var url = require.toUrl('intern/__resolveSuites__') + '?' + query; promise = request(url, { method: 'GET' }).then(function (response) { if (response.statusCode === 200 && response.data) { config.suites = JSON.parse(response.data); } else { throw Error('Error resolving suites -- Intern proxy is not available or ' + 'did not return data'); } }); } else { promise = Promise.resolve(); } return promise.then(function () { // pass-through the loader argument return loader; }); } function runExecutor(Executor) { executor = new Executor(config, self); self._earlyEvents.forEach(function (event) { executor.reporterManager.emit.apply(executor.reporterManager, event); }); return executor.run(); } function swapLoader() { return self.swapLoader(config.basePath, config.loaders, config.loaderOptions); } var promise = Promise.resolve() .then(getConfig) .then(swapLoader) .then(resolveSuites) .then(populateMainModule) .then(loadExecutorWithLoader) .then(runExecutor) ['catch'](function (error) { // a fatal error hasn't been reported -- ensure the user is notified if (!error.reported) { earlyErrorHandler(error); } throw error; }); this.run = function () { return promise; }; return promise; }, /** * Swaps the current AMD loader with a different AMD loader. * * @param {{ host-node?: string, host-browser?: string }} loaders Paths to loaders for different environments, * relative to the user configuration module ID. * @param {Object} loaderOptions AMD loader configuration object. * @returns {Promise.} A promise that resolves to an AMD `require` function. */ swapLoader: function (basePath, loaders, loaderOptions) { loaders = loaders || {}; var self = this; var global = (function () { return this; })(); return new Promise(function (resolve, reject) { if (has('host-node') && loaders['host-node']) { var require = global.require.nodeRequire; // Someone is attempting to use the loader module that has already been loaded. If we were to try // loading again without deleting it from `require.cache`, Node.js would not re-execute the loader // code (the module is cached), so the global `define` that is being undefined below will never be // redefined. There is no reason to do anything more in this case; just use the already loaded // loader as-is if (require.cache[require.resolve(loaders['host-node'])]) { resolve(global.require); return; } global.require = global.define = undefined; var id = loaders['host-node']; var moduleUtil = require('module'); if (moduleUtil._findPath && moduleUtil._nodeModulePaths) { var localModulePath = moduleUtil._findPath(id, moduleUtil._nodeModulePaths(basePath)); if (localModulePath !== false) { id = localModulePath; } } var amdRequire = require(id); // The Dojo 1 loader does not export itself, it only exposes itself globally; in this case // `amdRequire` is an empty object, not a function. Other loaders return themselves and do not // expose globally. This hopefully covers all known loader cases amdRequire = typeof amdRequire === 'function' ? amdRequire : global.require; // Expose the require globally so dojo/node can hopefully find the original Node.js require; // this is needed for at least RequireJS 2.1, which does not expose the global require // to child modules if (!global.require) { global.require = amdRequire; } resolve(amdRequire); } else if (has('host-browser') && loaders['host-browser']) { global.require = global.define = undefined; var script = document.createElement('script'); script.onload = function () { this.onload = this.onerror = null; resolve(global.curl || global.requirejs || global.require); }; script.onerror = function () { this.parentNode.removeChild(this); this.onload = this.onerror = null; reject(new Error('Failed to load AMD loader from ' + script.src)); }; var loaderUrl = loaders['host-browser']; if (!util.isAbsoluteUrl(loaderUrl)) { loaderUrl = basePath + loaderUrl; } script.src = loaderUrl; document.head.appendChild(script); } else { resolve(global.require); } }).then(function (loader) { var setConfig = loader.config ? loader.config.bind(loader) : loader; setConfig(self.defaultLoaderOptions); if (loaderOptions) { if ( loaderOptions.map && loaderOptions.map['*'] && self.defaultLoaderOptions && self.defaultLoaderOptions.map && self.defaultLoaderOptions.map['*'] ) { var userStarMap = loaderOptions.map['*']; var defaultStarMap = self.defaultLoaderOptions.map['*']; for (var key in defaultStarMap) { if (!(key in userStarMap)) { userStarMap[key] = defaultStarMap[key]; } } } setConfig(loaderOptions); } return loader; }); } }; return PreExecutor; });