diff --git a/docs/referenceConf.js b/docs/referenceConf.js index c00b126b3..ae775b19c 100644 --- a/docs/referenceConf.js +++ b/docs/referenceConf.js @@ -146,6 +146,16 @@ exports.config = { // How long to wait for a page to load. getPageTimeout: 10000, + // A callback function called once configs are read but before any environment + // setup. This will only run once, and before onPrepare. + // You can specify a file containing code to run by setting beforeLaunch to + // the filename string. + beforeLaunch: function() { + // At this point, global variable 'protractor' object will NOT be set up, + // and globals from the test framework will NOT be available. The main + // purpose of this function should be to bring up test dependencies. + }, + // A callback function called once protractor is ready and available, and // before the specs are executed. // If multiple capabilities are being run, this will run once per @@ -168,9 +178,15 @@ exports.config = { // A callback function called once the tests have finished running and // the WebDriver instance has been shut down. It is passed the exit code - // (0 if the tests passed or 1 if not). This is called once per capability. + // (0 if the tests passed). This is called once per capability. onCleanUp: function(exitCode) {}, + // A callback function called once all tests have finished running and + // the WebDriver instance has been shut down. It is passed the exit code + // (0 if the tests passed). This is called only once before the program + // exits (after onCleanUp). + afterLaunch: function() {}, + // The params object will be passed directly to the Protractor instance, // and can be accessed from your test as browser.params. It is an arbitrary // object and can contain anything you may need in your test. diff --git a/lib/frameworks/README.md b/lib/frameworks/README.md index f61d8a289..3fb0bdda1 100644 --- a/lib/frameworks/README.md +++ b/lib/frameworks/README.md @@ -19,7 +19,7 @@ Requirements - `runner.emit` must be called with `testPass` and `testFail` messages. - - `runner.runTestPreparers` must be called before any tests are run. + - `runner.runTestPreparer` must be called before any tests are run. - `runner.getConfig().onComplete` must be called when tests are finished. diff --git a/lib/frameworks/cucumber.js b/lib/frameworks/cucumber.js index 4c0e33b7a..b5e572da8 100644 --- a/lib/frameworks/cucumber.js +++ b/lib/frameworks/cucumber.js @@ -58,7 +58,7 @@ exports.run = function(runner, specs) { } global.cucumber = Cucumber.Cli(execOptions); - return runner.runTestPreparers().then(function () { + return runner.runTestPreparer().then(function () { return q.promise(function (resolve, reject) { global.cucumber.run(function (succeeded) { try { diff --git a/lib/frameworks/jasmine.js b/lib/frameworks/jasmine.js index a1c593f4a..4cae42b82 100644 --- a/lib/frameworks/jasmine.js +++ b/lib/frameworks/jasmine.js @@ -37,7 +37,7 @@ exports.run = function(runner, specs) { // get to complete first. jasmine.getEnv().addReporter(new RunnerReporter(runner)); - return runner.runTestPreparers().then(function() { + return runner.runTestPreparer().then(function() { return q.promise(function (resolve, reject) { var jasmineNodeOpts = runner.getConfig().jasmineNodeOpts; var originalOnComplete = runner.getConfig().onComplete; diff --git a/lib/frameworks/mocha.js b/lib/frameworks/mocha.js index 98f859da0..b7c5c443c 100644 --- a/lib/frameworks/mocha.js +++ b/lib/frameworks/mocha.js @@ -50,7 +50,7 @@ exports.run = function(runner, specs) { mocha.loadFiles(); - runner.runTestPreparers().then(function() { + runner.runTestPreparer().then(function() { specs.forEach(function(file) { mocha.addFile(file); diff --git a/lib/launcher.js b/lib/launcher.js index 607b53e74..50d5240c7 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -6,7 +6,8 @@ var child = require('child_process'), ConfigParser = require('./configParser'), - TaskScheduler = require('./taskScheduler'); + TaskScheduler = require('./taskScheduler'), + helper = require('./util'); var launcherPrefix = '[launcher]'; var RUNNERS_FAILED_EXIT_CODE = 100; @@ -131,60 +132,76 @@ var init = function(configFile, additionalConfig) { this.reporter.reportHeader_(); }; - // Don't start new process if there is only 1 task. - var totalTasks = scheduler.numTasksRemaining(); - if (totalTasks === 1) { - var Runner = require('./runner'); - var task = scheduler.nextTask(); - config.capabilities = task.capability; - config.specs = task.specs; + var cleanUpAndExit = function(exitCode) { + helper.runFilenameOrFn_(config.configDir, config.afterLaunch, [exitCode]). + then(function(returned) { + if (typeof returned === 'number') { + process.exit(returned); + } else { + process.exit(exitCode); + } + }, function(err) { + log_('Error:', err); + process.exit(1); + }); + }; - var runner = new Runner(config); - runner.run().then(function(exitCode) { - process.exit(exitCode); - }).catch(function(err) { - log_('Error:', err.stack || err.message || err); - process.exit(1); - }); - } else { - if (config.debug) { - throw new Error('Cannot run in debug mode with ' + - 'multiCapabilities, count > 1, or sharding'); - } - for (var i = 0; i < scheduler.maxConcurrentTasks(); ++i) { - var createNextRunnerFork = function() { - var task = scheduler.nextTask(); - if (task) { - var done = function() { - task.done(); - createNextRunnerFork(); - }; - var runnerFork = new RunnerFork(task); - runnerFork.addEventHandlers(done); - runnerFork.run(); - } - }; - createNextRunnerFork(); - } - log_('Running ' + scheduler.countActiveTasks() + ' instances of WebDriver'); + helper.runFilenameOrFn_(config.configDir, config.beforeLaunch).then(function() { + // Don't start new process if there is only 1 task. + var totalTasks = scheduler.numTasksRemaining(); + if (totalTasks === 1) { + var Runner = require('./runner'); + var task = scheduler.nextTask(); + config.capabilities = task.capability; + config.specs = task.specs; - process.on('exit', function(code) { - if (code) { - log_('Process exited with error code ' + code); - process.exit(code); - } else if (runnerErrorCount > 0) { - reporter.reportSummary(); - process.exit(RUNNERS_FAILED_EXIT_CODE); - } else { - if (scheduler.numTasksRemaining() > 0) { - throw new Error('BUG: launcher exited with ' + - scheduler.numTasksRemaining() + ' tasks remaining'); - } - reporter.reportSummary(); - process.exit(0); + var runner = new Runner(config); + runner.run().then(function(exitCode) { + cleanUpAndExit(exitCode); + }).catch(function(err) { + log_('Error:', err.stack || err.message || err); + cleanUpAndExit(1); + }); + } else { + if (config.debug) { + throw new Error('Cannot run in debug mode with ' + + 'multiCapabilities, count > 1, or sharding'); } - }); - } + for (var i = 0; i < scheduler.maxConcurrentTasks(); ++i) { + var createNextRunnerFork = function() { + var task = scheduler.nextTask(); + if (task) { + var done = function() { + task.done(); + createNextRunnerFork(); + }; + var runnerFork = new RunnerFork(task); + runnerFork.addEventHandlers(done); + runnerFork.run(); + } + }; + createNextRunnerFork(); + } + log_('Running ' + scheduler.countActiveTasks() + ' instances of WebDriver'); + + process.on('exit', function(code) { + if (code) { + log_('Process exited with error code ' + code); + cleanUpAndExit(code); + } else if (runnerErrorCount > 0) { + reporter.reportSummary(); + cleanUpAndExit(RUNNERS_FAILED_EXIT_CODE); + } else { + if (scheduler.numTasksRemaining() > 0) { + throw new Error('BUG: launcher exited with ' + + scheduler.numTasksRemaining() + ' tasks remaining'); + } + reporter.reportSummary(); + cleanUpAndExit(0); + } + }); + } + }); }; //###### REPORTER #######// diff --git a/lib/runner.js b/lib/runner.js index 073afcee6..ddebd011b 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -1,9 +1,9 @@ var protractor = require('./protractor'), webdriver = require('selenium-webdriver'), - path = require('path'), util = require('util'), q = require('q'), - EventEmitter = require('events').EventEmitter; + EventEmitter = require('events').EventEmitter, + helper = require('./util'); /* * Runner is responsible for starting the execution of a test run and triggering @@ -18,7 +18,7 @@ var protractor = require('./protractor'), * @constructor */ var Runner = function(config) { - this.preparers_ = []; + this.preparer_ = null; this.driverprovider_ = null; this.config_ = config; @@ -49,54 +49,24 @@ var Runner = function(config) { util.inherits(Runner, EventEmitter); -/** - * Internal helper for abstraction of polymorphic filenameOrFn properties. - * @private - * @param {object} filenameOrFn The filename or function that we will execute. - * @return {object} A number or a promise that will resolve when the test - * preparers are finished. - */ -Runner.prototype.runFilenameOrFn_ = function(filenameOrFn, args) { - if (filenameOrFn) { - if (typeof filenameOrFn === 'function') { - return filenameOrFn.apply(null, args); - } else if (typeof filenameOrFn === 'string') { - return require(path.resolve(this.config_.configDir, filenameOrFn)); - } else { - // TODO - this is not generic. - throw 'config.onPrepare and config.onCleanUp must be a string or function'; - } - } -}; - /** * Registrar for testPreparers - executed right before tests run. * @public * @param {string/Fn} filenameOrFn */ -Runner.prototype.registerTestPreparer = function(filenameOrFn) { - this.preparers_.push(filenameOrFn); +Runner.prototype.setTestPreparer = function(filenameOrFn) { + this.preparer_ = filenameOrFn; }; /** - * Executor of testPreparers + * Executor of testPreparer * @public * @return {q.Promise} A promise that will resolve when the test preparers * are finished. */ -Runner.prototype.runTestPreparers = function() { - var filenameOrFn; - var promises = []; - var returned; - for (var i = 0; i < this.preparers_.length; i++) { - filenameOrFn = this.preparers_[i]; - returned = this.runFilenameOrFn_(filenameOrFn); - if (q.isPromiseAlike(returned)) { - promises.push(returned); - } - } - return q.all(promises); +Runner.prototype.runTestPreparer = function() { + return helper.runFilenameOrFn_(this.config_.configDir, this.preparer_); }; @@ -137,12 +107,14 @@ Runner.prototype.loadDriverProvider_ = function() { * @param {int} Standard unix exit code */ Runner.prototype.exit_ = function(exitCode) { - var returned = this.runFilenameOrFn_(this.config_.onCleanUp, [exitCode]); - if (typeof returned === 'number' || q.isPromiseAlike(returned)) { - return returned; - } else { - return exitCode; - } + helper.runFilenameOrFn_(this.config_.configDir, this.config_.onCleanUp, [exitCode]). + then(function(returned) { + if (typeof returned === 'number') { + return returned; + } else { + return exitCode; + } + }); }; @@ -225,7 +197,7 @@ Runner.prototype.run = function() { throw new Error('Spec patterns did not match any files.'); } - this.registerTestPreparer(this.config_.onPrepare); + this.setTestPreparer(this.config_.onPrepare); // 1) Setup environment //noinspection JSValidateTypes @@ -280,7 +252,7 @@ Runner.prototype.run = function() { }).then(function() { var passed = testResult.failedCount === 0; var exitCode = passed ? 0 : 1; - return q.when(self.exit_(exitCode)); + return self.exit_(exitCode); }); }; diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 000000000..458ad2892 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,25 @@ +var q = require('q'), + path = require('path'); + +/** + * Internal helper for abstraction of polymorphic filenameOrFn properties. + * @param {object} filenameOrFn The filename or function that we will execute. + * @param {Array.}} args The args to pass into filenameOrFn. + * @return {q.Promise} A promise that will resolve when filenameOrFn completes. + */ +exports.runFilenameOrFn_ = function(configDir, filenameOrFn, args) { + var returned; + if (filenameOrFn) { + if (typeof filenameOrFn === 'function') { + returned = filenameOrFn.apply(null, args); + } else if (typeof filenameOrFn === 'string') { + filenameOrFn = require(path.resolve(configDir, filenameOrFn)); + if (typeof filenameOrFn === 'function') { + returned = filenameOrFn.apply(null, args); + } + } else { + throw 'filenameOrFn must be a string or function'; + } + } + return q.when(returned); +};