From f941c255a1119c327cf5ebebe1d05a1547257dbe Mon Sep 17 00:00:00 2001 From: Craig Nishina Date: Wed, 20 Apr 2016 17:50:41 -0700 Subject: [PATCH] chore(typescript): runner and launcher converted to typescript --- lib/configParser.ts | 17 ++ lib/driverProviders/index.ts | 8 + lib/globals.d.ts | 25 +++ lib/launcher.js | 262 ----------------------- lib/launcher.ts | 284 +++++++++++++++++++++++++ lib/runner.js | 401 ----------------------------------- lib/runner.ts | 380 +++++++++++++++++++++++++++++++++ lib/runnerCli.js | 2 +- lib/taskRunner.ts | 26 +-- lib/util.ts | 2 +- spec/unit/runner_test.js | 6 +- 11 files changed, 732 insertions(+), 681 deletions(-) create mode 100644 lib/driverProviders/index.ts delete mode 100644 lib/launcher.js create mode 100644 lib/launcher.ts delete mode 100644 lib/runner.js create mode 100644 lib/runner.ts diff --git a/lib/configParser.ts b/lib/configParser.ts index c9a1cdb8b..a964c478a 100644 --- a/lib/configParser.ts +++ b/lib/configParser.ts @@ -57,6 +57,23 @@ export interface Config { sauceKey?: string; sauceSeleniumAddress?: string; sauceUser?: string; + v8Debug?: any; + nodeDebug?: boolean; + directConnect?: boolean; + mockSelenium?: boolean; + baseUrl?: string; + untrackOutstandingTimeouts?: any; + debuggerServerPort?: number; + useAllAngular2AppRoots?: boolean; + frameworkPath?: string; + restartBrowserBetweenTests?: boolean; + onPrepare?: any; + beforeLaunch?: any; + getMultiCapabilities?: any; + elementExplorer?: any; + afterLaunch?: any; + debug?: boolean; + resultJsonOutputFile?: any; } export class ConfigParser { diff --git a/lib/driverProviders/index.ts b/lib/driverProviders/index.ts new file mode 100644 index 000000000..db05d4b29 --- /dev/null +++ b/lib/driverProviders/index.ts @@ -0,0 +1,8 @@ +export * from './attachSession'; +export * from './browserStack'; +export * from './direct'; +export * from './driverProvider'; +export * from './hosted'; +export * from './local'; +export * from './mock'; +export * from './sauce'; diff --git a/lib/globals.d.ts b/lib/globals.d.ts index 1dd9b4e75..5cd9283d8 100644 --- a/lib/globals.d.ts +++ b/lib/globals.d.ts @@ -1,4 +1,27 @@ +// Typescript transpiling will give a warning about 'global'. This work around +// is to allow protractor to set global variables. Warning message as follows: +// +// lib/globals.d.ts(2,19): error TS2300: Duplicate identifier 'global'. +// typings/main/ambient/node/index.d.ts(33,13): error TS2300: Duplicate +// identifier 'global'. +declare namespace global { + var browser: any; + var protractor: any; + var $: any; + var $$: any; + var element: any; + var by: any; + var By: any; + var DartObject: any; +} declare var browser: any; +declare var protractor: any; +declare var $: any; +declare var $$: any; +declare var element: any; +declare var by: any; +declare var By: any; +declare var DartObject: any; declare namespace webdriver { class WebDriver { @@ -9,9 +32,11 @@ declare namespace webdriver { class Session { getId: Function; + getCapabilities: Function; } class Promise { + controlFlow: Function; then: Function; } diff --git a/lib/launcher.js b/lib/launcher.js deleted file mode 100644 index a8b5aa2c0..000000000 --- a/lib/launcher.js +++ /dev/null @@ -1,262 +0,0 @@ -/** - * The launcher is responsible for parsing the capabilities from the - * input configuration and launching test runners. - */ -'use strict'; - -var ConfigParser = require('./configParser').ConfigParser, - TaskScheduler = require('./taskScheduler').TaskScheduler, - helper = require('./util'), - log = require('./logger'), - q = require('q'), - TaskRunner = require('./taskRunner').TaskRunner; - -var logPrefix = '[launcher]'; -var RUNNERS_FAILED_EXIT_CODE = 100; - -/** - * Custom console.log proxy - * @param {*} [stuff...] Any value to log - * @private - */ -var log_ = function() { - var args = [logPrefix].concat([].slice.call(arguments)); - log.puts.apply(log, args); -}; - -/** - * Keeps track of a list of task results. Provides method to add a new - * result, aggregate the results into a summary, count failures, - * and save results into a JSON file. - */ -var taskResults_ = { - results_: [], - - add: function(result) { - this.results_.push(result); - }, - - totalSpecFailures: function() { - return this.results_.reduce(function(specFailures, result) { - return specFailures + result.failedCount; - }, 0); - }, - - totalProcessFailures: function() { - return this.results_.reduce(function(processFailures, result) { - return !result.failedCount && result.exitCode !== 0 ? processFailures + 1 : processFailures; - }, 0); - }, - - saveResults: function(filepath) { - var jsonOutput = this.results_.reduce(function(jsonOutput, result) { - return jsonOutput.concat(result.specResults); - }, []); - - var json = JSON.stringify(jsonOutput, null, ' '); - var fs = require('fs'); - fs.writeFileSync(filepath, json); - }, - - reportSummary: function() { - var specFailures = this.totalSpecFailures(); - var processFailures = this.totalProcessFailures(); - this.results_.forEach(function(result) { - var capabilities = result.capabilities; - var shortName = (capabilities.browserName) ? capabilities.browserName : ''; - shortName = (capabilities.logName) ? capabilities.logName - : (capabilities.browserName) ? capabilities.browserName : ''; - shortName += (capabilities.version) ? capabilities.version : ''; - shortName += (capabilities.logName && capabilities.count < 2) ? - '' : ' #' + result.taskId; - if (result.failedCount) { - log_(shortName + ' failed ' + result.failedCount + ' test(s)'); - } else if (result.exitCode !== 0) { - log_(shortName + ' failed with exit code: ' + result.exitCode); - } else { - log_(shortName + ' passed'); - } - }); - - if (specFailures && processFailures) { - log_('overall: ' + specFailures + ' failed spec(s) and ' + - processFailures + ' process(es) failed to complete'); - } else if (specFailures) { - log_('overall: ' + specFailures + ' failed spec(s)'); - } else if (processFailures) { - log_('overall: ' + processFailures + ' process(es) failed to complete'); - } - } -}; - -/** - * Initialize and run the tests. - * Exits with 1 on test failure, and RUNNERS_FAILED_EXIT_CODE on unexpected - * failures. - * - * @param {string=} configFile - * @param {Object=} additionalConfig - */ -var init = function(configFile, additionalConfig) { - var configParser = new ConfigParser(); - if (configFile) { - configParser.addFileConfig(configFile); - } - if (additionalConfig) { - configParser.addConfig(additionalConfig); - } - var config = configParser.getConfig(); - log.set(config); - log.debug('Running with --troubleshoot'); - log.debug('Protractor version: ' + require('../package.json').version); - log.debug('Your base url for tests is ' + config.baseUrl); - - // Run beforeLaunch - helper.runFilenameOrFn_(config.configDir, config.beforeLaunch).then(function() { - - return q.promise(function(resolve) { - // 1) If getMultiCapabilities is set, resolve that as `multiCapabilities`. - if (config.getMultiCapabilities && - typeof config.getMultiCapabilities === 'function') { - if (config.multiCapabilities.length || config.capabilities) { - log.warn('getMultiCapabilities() will override both capabilities ' + - 'and multiCapabilities'); - } - // If getMultiCapabilities is defined and a function, use this. - q.when(config.getMultiCapabilities(), function(multiCapabilities) { - config.multiCapabilities = multiCapabilities; - config.capabilities = null; - }).then(resolve); - } else { - resolve(); - } - }).then(function() { - // 2) Set `multicapabilities` using `capabilities`, `multicapabilities`, - // or default - if (config.capabilities) { - if (config.multiCapabilities.length) { - log.warn('You have specified both capabilities and ' + - 'multiCapabilities. This will result in capabilities being ' + - 'ignored'); - } else { - // Use capabilities if multiCapabilities is empty. - config.multiCapabilities = [config.capabilities]; - } - } else if (!config.multiCapabilities.length) { - // Default to chrome if no capabilities given - config.multiCapabilities = [{ - browserName: 'chrome' - }]; - } - }); - }).then(function() { - // 3) If we're in `elementExplorer` mode, run only that. - if (config.elementExplorer || config.framework === 'explorer') { - if (config.multiCapabilities.length != 1) { - throw new Error('Must specify only 1 browser while using elementExplorer'); - } else { - config.capabilities = config.multiCapabilities[0]; - } - config.framework = 'explorer'; - - var Runner = require('./runner'); - var runner = new Runner(config); - return runner.run().then(function(exitCode) { - process.exit(exitCode); - }, function(err) { - log_(err); - process.exit(1); - }); - } - }).then(function() { - // 4) Run tests. - var scheduler = new TaskScheduler(config); - - process.on('exit', function(code) { - if (code) { - log_('Process exited with error code ' + code); - } else if (scheduler.numTasksOutstanding() > 0) { - log_('BUG: launcher exited with ' + - scheduler.numTasksOutstanding() + ' tasks remaining'); - process.exit(RUNNERS_FAILED_EXIT_CODE); - } - }); - - // Run afterlaunch and exit - var cleanUpAndExit = function(exitCode) { - return 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 totalTasks = scheduler.numTasksOutstanding(); - var forkProcess = false; - if (totalTasks > 1) { // Start new processes only if there are >1 tasks. - forkProcess = true; - if (config.debug) { - throw new Error('Cannot run in debug mode with ' + - 'multiCapabilities, count > 1, or sharding'); - } - } - - var deferred = q.defer(); // Resolved when all tasks are completed - var createNextTaskRunner = function() { - var task = scheduler.nextTask(); - if (task) { - var taskRunner = new TaskRunner(configFile, additionalConfig, task, forkProcess); - taskRunner.run().then(function(result) { - if (result.exitCode && !result.failedCount) { - log_('Runner process exited unexpectedly with error code: ' + result.exitCode); - } - taskResults_.add(result); - task.done(); - createNextTaskRunner(); - // If all tasks are finished - if (scheduler.numTasksOutstanding() === 0) { - deferred.fulfill(); - } - log_(scheduler.countActiveTasks() + - ' instance(s) of WebDriver still running'); - }).catch (function(err) { - log_('Error:', err.stack || err.message || err); - cleanUpAndExit(RUNNERS_FAILED_EXIT_CODE); - }); - } - }; - // Start `scheduler.maxConcurrentTasks()` workers for handling tasks in - // the beginning. As a worker finishes a task, it will pick up the next task - // from the scheduler's queue until all tasks are gone. - for (var i = 0; i < scheduler.maxConcurrentTasks(); ++i) { - createNextTaskRunner(); - } - log_('Running ' + scheduler.countActiveTasks() + ' instances of WebDriver'); - - // By now all runners have completed. - deferred.promise.then(function() { - // Save results if desired - if (config.resultJsonOutputFile) { - taskResults_.saveResults(config.resultJsonOutputFile); - } - - taskResults_.reportSummary(); - var exitCode = 0; - if (taskResults_.totalProcessFailures() > 0) { - exitCode = RUNNERS_FAILED_EXIT_CODE; - } else if (taskResults_.totalSpecFailures() > 0) { - exitCode = 1; - } - return cleanUpAndExit(exitCode); - }).done(); - }).done(); -}; - -exports.init = init; diff --git a/lib/launcher.ts b/lib/launcher.ts new file mode 100644 index 000000000..9df8519cd --- /dev/null +++ b/lib/launcher.ts @@ -0,0 +1,284 @@ +/** + * The launcher is responsible for parsing the capabilities from the + * input configuration and launching test runners. + */ +import * as q from 'q'; +import {Config, ConfigParser} from './configParser'; +import {Logger} from './logger2'; +import {Runner} from './runner'; +import {TaskRunner} from './taskRunner'; +import {TaskScheduler} from './taskScheduler'; +import * as helper from './util'; + +let logger = new Logger('launcher'); +let RUNNERS_FAILED_EXIT_CODE = 100; + +/** + * Keeps track of a list of task results. Provides method to add a new + * result, aggregate the results into a summary, count failures, + * and save results into a JSON file. + */ +class TaskResults { + // TODO: set a type for result + results_: any[] = []; + + add(result: any): void { this.results_.push(result); } + + totalSpecFailures(): number { + return this.results_.reduce((specFailures, result) => { + return specFailures + result.failedCount; + }, 0); + } + + totalProcessFailures(): number { + return this.results_.reduce((processFailures, result) => { + return !result.failedCount && result.exitCode !== 0 ? + processFailures + 1 : + processFailures; + }, 0); + } + + saveResults(filepath: string): void { + let jsonOutput = this.results_.reduce((jsonOutput, result) => { + return jsonOutput.concat(result.specResults); + }, []); + + let json = JSON.stringify(jsonOutput, null, ' '); + let fs = require('fs'); + fs.writeFileSync(filepath, json); + } + + reportSummary(): void { + let specFailures = this.totalSpecFailures(); + let processFailures = this.totalProcessFailures(); + this.results_.forEach((result: any) => { + let capabilities = result.capabilities; + let shortName = + (capabilities.browserName) ? capabilities.browserName : ''; + shortName = (capabilities.logName) ? + capabilities.logName : + (capabilities.browserName) ? capabilities.browserName : ''; + shortName += (capabilities.version) ? capabilities.version : ''; + shortName += (capabilities.logName && capabilities.count < 2) ? + '' : + ' #' + result.taskId; + if (result.failedCount) { + logger.info(shortName + ' failed ' + result.failedCount + ' test(s)'); + } else if (result.exitCode !== 0) { + logger.info(shortName + ' failed with exit code: ' + result.exitCode); + } else { + logger.info(shortName + ' passed'); + } + }); + + if (specFailures && processFailures) { + logger.info( + 'overall: ' + specFailures + ' failed spec(s) and ' + + processFailures + ' process(es) failed to complete'); + } else if (specFailures) { + logger.info('overall: ' + specFailures + ' failed spec(s)'); + } else if (processFailures) { + logger.info( + 'overall: ' + processFailures + ' process(es) failed to complete'); + } + } +} + +let taskResults_ = new TaskResults(); + +/** + * Initialize and run the tests. + * Exits with 1 on test failure, and RUNNERS_FAILED_EXIT_CODE on unexpected + * failures. + * + * @param {string=} configFile + * @param {Object=} additionalConfig + */ +let initFn = function(configFile: string, additionalConfig: Config) { + let configParser = new ConfigParser(); + if (configFile) { + configParser.addFileConfig(configFile); + } + if (additionalConfig) { + configParser.addConfig(additionalConfig); + } + let config = configParser.getConfig(); + + logger.debug('Running with --troubleshoot'); + logger.debug('Protractor version: ' + require('../package.json').version); + logger.debug('Your base url for tests is ' + config.baseUrl); + + // Run beforeLaunch + helper.runFilenameOrFn_(config.configDir, config.beforeLaunch) + .then(() => { + + return q + .Promise((resolve: Function) => { + // 1) If getMultiCapabilities is set, resolve that as + // `multiCapabilities`. + if (config.getMultiCapabilities && + typeof config.getMultiCapabilities === 'function') { + if (config.multiCapabilities.length || config.capabilities) { + logger.warn( + 'getMultiCapabilities() will override both capabilities ' + + 'and multiCapabilities'); + } + // If getMultiCapabilities is defined and a function, use this. + q.when(config.getMultiCapabilities(), (multiCapabilities) => { + config.multiCapabilities = multiCapabilities; + config.capabilities = null; + }).then((resolve) => {}); + } else { + resolve(); + } + }) + .then(() => { + // 2) Set `multicapabilities` using `capabilities`, + // `multicapabilities`, + // or default + if (config.capabilities) { + if (config.multiCapabilities.length) { + logger.warn( + 'You have specified both capabilities and ' + + 'multiCapabilities. This will result in capabilities being ' + + 'ignored'); + } else { + // Use capabilities if multiCapabilities is empty. + config.multiCapabilities = [config.capabilities]; + } + } else if (!config.multiCapabilities.length) { + // Default to chrome if no capabilities given + config.multiCapabilities = [{browserName: 'chrome'}]; + } + }); + }) + .then(() => { + // 3) If we're in `elementExplorer` mode, run only that. + if (config.elementExplorer || config.framework === 'explorer') { + if (config.multiCapabilities.length != 1) { + throw new Error( + 'Must specify only 1 browser while using elementExplorer'); + } else { + config.capabilities = config.multiCapabilities[0]; + } + config.framework = 'explorer'; + + let runner = new Runner(config); + return runner.run().then( + (exitCode: number) => { process.exit(exitCode); }, + (err: Error) => { + logger.error(err); + process.exit(1); + }); + } + }) + .then(() => { + // 4) Run tests. + let scheduler = new TaskScheduler(config); + + process.on('exit', (code: number) => { + if (code) { + logger.error('Process exited with error code ' + code); + } else if (scheduler.numTasksOutstanding() > 0) { + logger.error( + 'BUG: launcher exited with ' + scheduler.numTasksOutstanding() + + ' tasks remaining'); + process.exit(RUNNERS_FAILED_EXIT_CODE); + } + }); + + // Run afterlaunch and exit + let cleanUpAndExit = (exitCode: number) => { + return helper + .runFilenameOrFn_( + config.configDir, config.afterLaunch, [exitCode]) + .then( + (returned) => { + if (typeof returned === 'number') { + process.exit(returned); + } else { + process.exit(exitCode); + } + }, + (err: Error) => { + logger.error('Error:', err); + process.exit(1); + }); + }; + + let totalTasks = scheduler.numTasksOutstanding(); + let forkProcess = false; + if (totalTasks > + 1) { // Start new processes only if there are >1 tasks. + forkProcess = true; + if (config.debug) { + throw new Error( + 'Cannot run in debug mode with ' + + 'multiCapabilities, count > 1, or sharding'); + } + } + + let deferred = q.defer(); // Resolved when all tasks are completed + let createNextTaskRunner = () => { + var task = scheduler.nextTask(); + if (task) { + let taskRunner = + new TaskRunner(configFile, additionalConfig, task, forkProcess); + taskRunner.run() + .then((result) => { + if (result.exitCode && !result.failedCount) { + logger.error( + 'Runner process exited unexpectedly with error code: ' + + result.exitCode); + } + taskResults_.add(result); + task.done(); + createNextTaskRunner(); + // If all tasks are finished + if (scheduler.numTasksOutstanding() === 0) { + deferred.resolve(); + } + logger.info( + scheduler.countActiveTasks() + + ' instance(s) of WebDriver still running'); + }) + .catch((err: Error) => { + logger.error('Error:', err.stack || err.message || err); + cleanUpAndExit(RUNNERS_FAILED_EXIT_CODE); + }); + } + }; + // Start `scheduler.maxConcurrentTasks()` workers for handling tasks in + // the beginning. As a worker finishes a task, it will pick up the next + // task + // from the scheduler's queue until all tasks are gone. + for (var i = 0; i < scheduler.maxConcurrentTasks(); ++i) { + createNextTaskRunner(); + } + logger.info( + 'Running ' + scheduler.countActiveTasks() + + ' instances of WebDriver'); + + // By now all runners have completed. + deferred.promise + .then(function() { + // Save results if desired + if (config.resultJsonOutputFile) { + taskResults_.saveResults(config.resultJsonOutputFile); + } + + taskResults_.reportSummary(); + let exitCode = 0; + if (taskResults_.totalProcessFailures() > 0) { + exitCode = RUNNERS_FAILED_EXIT_CODE; + } else if (taskResults_.totalSpecFailures() > 0) { + exitCode = 1; + } + return cleanUpAndExit(exitCode); + }) + .done(); + }) + .done(); +}; + +export let init = initFn; diff --git a/lib/runner.js b/lib/runner.js deleted file mode 100644 index 7061fbb3f..000000000 --- a/lib/runner.js +++ /dev/null @@ -1,401 +0,0 @@ -var protractor = require('./protractor'), - webdriver = require('selenium-webdriver'), - util = require('util'), - q = require('q'), - EventEmitter = require('events').EventEmitter, - helper = require('./util'), - log = require('./logger'), - Plugins = require('./plugins'), - AttachSession = require('./driverProviders/attachSession').AttachSession, - BrowserStack = require('./driverProviders/browserStack').BrowserStack, - Direct = require('./driverProviders/direct').Direct, - Hosted = require('./driverProviders/hosted').Hosted, - Local = require('./driverProviders/local').Local, - Mock = require('./driverProviders/mock').Mock, - Sauce = require('./driverProviders/sauce').Sauce; - - -/* - * Runner is responsible for starting the execution of a test run and triggering - * setup, teardown, managing config, etc through its various dependencies. - * - * The Protractor Runner is a node EventEmitter with the following events: - * - testPass - * - testFail - * - testsDone - * - * @param {Object} config - * @constructor - */ -var Runner = function(config) { - log.set(config); - this.preparer_ = null; - this.driverprovider_ = null; - this.config_ = config; - - if (config.v8Debug) { - // Call this private function instead of sending SIGUSR1 because Windows. - process._debugProcess(process.pid); - } - - if (config.nodeDebug) { - process._debugProcess(process.pid); - var flow = webdriver.promise.controlFlow(); - - flow.execute(function() { - var nodedebug = require('child_process').fork('debug', ['localhost:5858']); - process.on('exit', function() { - nodedebug.kill('SIGTERM'); - }); - nodedebug.on('exit', function() { - process.exit('1'); - }); - }, 'start the node debugger'); - flow.timeout(1000, 'waiting for debugger to attach'); - } - - if (config.capabilities && config.capabilities.seleniumAddress) { - config.seleniumAddress = config.capabilities.seleniumAddress; - } - this.loadDriverProvider_(config); - this.setTestPreparer(config.onPrepare); -}; - -util.inherits(Runner, EventEmitter); - - -/** - * Registrar for testPreparers - executed right before tests run. - * @public - * @param {string/Fn} filenameOrFn - */ -Runner.prototype.setTestPreparer = function(filenameOrFn) { - this.preparer_ = filenameOrFn; -}; - - -/** - * Executor of testPreparer - * @public - * @return {q.Promise} A promise that will resolve when the test preparers - * are finished. - */ -Runner.prototype.runTestPreparer = function() { - return helper.runFilenameOrFn_(this.config_.configDir, this.preparer_); -}; - - -/** - * Grab driver provider based on type - * @private - * - * Priority - * 1) if directConnect is true, use that - * 2) if seleniumAddress is given, use that - * 3) if a Sauce Labs account is given, use that - * 4) if a seleniumServerJar is specified, use that - * 5) try to find the seleniumServerJar in protractor/selenium - */ -Runner.prototype.loadDriverProvider_ = function() { - - if (this.config_.directConnect) { - this.driverprovider_ = new Direct(this.config_); - } else if (this.config_.seleniumAddress) { - if (this.config_.seleniumSessionId) { - console.log('attached the session'); - this.driverprovider_ = new AttachSession(this.config_); - } else { - this.driverprovider_ = new Hosted(this.config_); - } - } else if (this.config_.browserstackUser && this.config_.browserstackKey) { - this.driverprovider_ = new BrowserStack(this.config_); - } else if (this.config_.sauceUser && this.config_.sauceKey) { - this.driverprovider_ = new Sauce(this.config_); - } else if (this.config_.seleniumServerJar) { - this.driverprovider_ = new Local(this.config_); - } else if (this.config_.mockSelenium) { - this.driverprovider_ = new Mock(this.config_); - } else { - this.driverprovider_ = new Local(this.config_); - } -}; - - -/** - * Responsible for cleaning up test run and exiting the process. - * @private - * @param {int} Standard unix exit code - */ -Runner.prototype.exit_ = function(exitCode) { - return helper.runFilenameOrFn_( - this.config_.configDir, this.config_.onCleanUp, [exitCode]). - then(function(returned) { - if (typeof returned === 'number') { - return returned; - } else { - return exitCode; - } - }); -}; - - -/** - * Getter for the Runner config object - * @public - * @return {Object} config - */ -Runner.prototype.getConfig = function() { - return this.config_; -}; - - -/** - * Get the control flow used by this runner. - * @return {Object} WebDriver control flow. - */ -Runner.prototype.controlFlow = function() { - return webdriver.promise.controlFlow(); -}; - - -/** - * Sets up convenience globals for test specs - * @private - */ -Runner.prototype.setupGlobals_ = function(browser_) { - - // Keep $, $$, element, and by/By under the global protractor namespace - protractor.browser = browser_; - protractor.$ = browser_.$; - protractor.$$ = browser_.$$; - protractor.element = browser_.element; - protractor.by = protractor.By = protractor.By; - - if (!this.config_.noGlobals) { - // Export protractor to the global namespace to be used in tests. - global.browser = browser_; - global.$ = browser_.$; - global.$$ = browser_.$$; - global.element = browser_.element; - global.by = global.By = protractor.By; - } - - global.protractor = protractor; - - - - if (!this.config_.skipSourceMapSupport) { - // Enable sourcemap support for stack traces. - require('source-map-support').install(); - } - // Required by dart2js machinery. - // https://code.google.com/p/dart/source/browse/branches/bleeding_edge/dart/sdk/lib/js/dart2js/js_dart2js.dart?spec=svn32943&r=32943#487 - global.DartObject = function(o) { this.o = o; }; -}; - - -/** - * Create a new driver from a driverProvider. Then set up a - * new protractor instance using this driver. - * This is used to set up the initial protractor instances and any - * future ones. - * - * @param {?Plugin} The plugin functions - * - * @return {Protractor} a protractor instance. - * @public - */ -Runner.prototype.createBrowser = function(plugins) { - var config = this.config_; - var driver = this.driverprovider_.getNewDriver(); - - var browser_ = protractor.wrapDriver(driver, - config.baseUrl, config.rootElement, config.untrackOutstandingTimeouts); - - browser_.params = config.params; - if (plugins) { - browser_.plugins_ = plugins; - } - if (config.getPageTimeout) { - browser_.getPageTimeout = config.getPageTimeout; - } - if (config.allScriptsTimeout) { - browser_.allScriptsTimeout = config.allScriptsTimeout; - } - if (config.debuggerServerPort) { - browser_.debuggerServerPort_ = config.debuggerServerPort; - } - if (config.useAllAngular2AppRoots) { - browser_.useAllAngular2AppRoots(); - } - var self = this; - - browser_.ready = - driver.manage().timeouts().setScriptTimeout(config.allScriptsTimeout); - - browser_.getProcessedConfig = function() { - return webdriver.promise.fulfilled(config); - }; - - browser_.forkNewDriverInstance = function(opt_useSameUrl, opt_copyMockModules) { - var newBrowser = self.createBrowser(plugins); - if (opt_copyMockModules) { - newBrowser.mockModules_ = browser_.mockModules_; - } - if (opt_useSameUrl) { - browser_.driver.getCurrentUrl().then(function(url) { - newBrowser.get(url); - }); - } - return newBrowser; - }; - - browser_.restart = function() { - // Note: because tests are not paused at this point, any async - // calls here are not guaranteed to complete before the tests resume. - self.driverprovider_.quitDriver(browser_.driver); - // Copy mock modules, but do not navigate to previous URL. - browser_ = browser_.forkNewDriverInstance(false, true); - self.setupGlobals_(browser_); - }; - - return browser_; -}; - - -/** - * Final cleanup on exiting the runner. - * - * @return {q.Promise} A promise which resolves on finish. - * @private - */ -Runner.prototype.shutdown_ = function() { - return q.all( - this.driverprovider_.getExistingDrivers(). - map(this.driverprovider_.quitDriver.bind(this.driverprovider_))); -}; - -/** - * The primary workhorse interface. Kicks off the test running process. - * - * @return {q.Promise} A promise which resolves to the exit code of the tests. - * @public - */ -Runner.prototype.run = function() { - var self = this, - testPassed, - plugins = new Plugins(self.config_), - pluginPostTestPromises, - browser_, - results; - - if (this.config_.framework !== 'explorer' && !this.config_.specs.length) { - throw new Error('Spec patterns did not match any files.'); - } - - // 1) Setup environment - //noinspection JSValidateTypes - return this.driverprovider_.setupEnv().then(function() { - // 2) Create a browser and setup globals - browser_ = self.createBrowser(plugins); - self.setupGlobals_(browser_); - return browser_.ready.then(browser_.getSession).then(function(session) { - log.debug('WebDriver session successfully started with capabilities ' + - util.inspect(session.getCapabilities())); - }, function(err) { - log.error('Unable to start a WebDriver session.'); - throw err; - }); - // 3) Setup plugins - }).then(function() { - return plugins.setup(); - // 4) Execute test cases - }).then(function() { - // Do the framework setup here so that jasmine and mocha globals are - // available to the onPrepare function. - var frameworkPath = ''; - if (self.config_.framework === 'jasmine' || - self.config_.framework === 'jasmine2') { - frameworkPath = './frameworks/jasmine.js'; - } else if (self.config_.framework === 'mocha') { - frameworkPath = './frameworks/mocha.js'; - } else if (self.config_.framework === 'debugprint') { - // Private framework. Do not use. - frameworkPath = './frameworks/debugprint.js'; - } else if (self.config_.framework === 'explorer') { - // Private framework. Do not use. - frameworkPath = './frameworks/explorer.js'; - } else if (self.config_.framework === 'custom') { - if (!self.config_.frameworkPath) { - throw new Error('When config.framework is custom, ' + - 'config.frameworkPath is required.'); - } - frameworkPath = self.config_.frameworkPath; - } else { - throw new Error('config.framework (' + self.config_.framework + - ') is not a valid framework.'); - } - - if (self.config_.restartBrowserBetweenTests) { - var restartDriver = function() { - browser_.restart(); - }; - self.on('testPass', restartDriver); - self.on('testFail', restartDriver); - } - - // We need to save these promises to make sure they're run, but we don't - // want to delay starting the next test (because we can't, it's just - // an event emitter). - pluginPostTestPromises = []; - - self.on('testPass', function(testInfo) { - pluginPostTestPromises.push(plugins.postTest(true, testInfo)); - }); - self.on('testFail', function(testInfo) { - pluginPostTestPromises.push(plugins.postTest(false, testInfo)); - }); - - log.debug('Running with spec files ' + self.config_.specs); - - return require(frameworkPath).run(self, self.config_.specs); - // 5) Wait for postTest plugins to finish - }).then(function(testResults) { - results = testResults; - return q.all(pluginPostTestPromises); - // 6) Teardown plugins - }).then(function() { - return plugins.teardown(); - // 7) Teardown - }).then(function() { - results = helper.joinTestLogs(results, plugins.getResults()); - self.emit('testsDone', results); - testPassed = results.failedCount === 0; - if (self.driverprovider_.updateJob) { - return self.driverprovider_.updateJob({ - 'passed': testPassed - }).then(function() { - return self.driverprovider_.teardownEnv(); - }); - } else { - return self.driverprovider_.teardownEnv(); - } - // 8) Let plugins do final cleanup - }).then(function() { - return plugins.postResults(); - // 9) Exit process - }).then(function() { - var exitCode = testPassed ? 0 : 1; - return self.exit_(exitCode); - }).fin(function() { - return self.shutdown_(); - }); -}; - -/** - * Creates and returns a Runner instance - * - * @public - * @param {Object} config - */ -module.exports = Runner; diff --git a/lib/runner.ts b/lib/runner.ts new file mode 100644 index 000000000..e2cb053a9 --- /dev/null +++ b/lib/runner.ts @@ -0,0 +1,380 @@ +import * as q from 'q'; +import * as util from 'util'; +import {EventEmitter} from 'events'; +import * as helper from './util'; +import {Config} from './configParser'; +import {Logger} from './logger2'; +import {AttachSession, BrowserStack, Direct, Hosted, Local, Mock, Sauce} from './driverProviders'; + +var protractor = require('./protractor'), + webdriver = require('selenium-webdriver'), Plugins = require('./plugins'); + +let logger = new Logger('runner'); +/* + * Runner is responsible for starting the execution of a test run and triggering + * setup, teardown, managing config, etc through its various dependencies. + * + * The Protractor Runner is a node EventEmitter with the following events: + * - testPass + * - testFail + * - testsDone + * + * @param {Object} config + * @constructor + */ +export class Runner extends EventEmitter { + config_: Config; + preparer_: any; + driverprovider_: any; + o: any; + + constructor(config: Config) { + super(); + this.config_ = config; + if (config.v8Debug) { + // Call this private function instead of sending SIGUSR1 because Windows. + process['_debugProcess'](process.pid); + } + + if (config.nodeDebug) { + process['_debugProcess'](process.pid); + let flow = webdriver.promise.controlFlow(); + + flow.execute(() => { + let nodedebug = + require('child_process').fork('debug', ['localhost:5858']); + process.on('exit', function() { nodedebug.kill('SIGTERM'); }); + nodedebug.on('exit', function() { process.exit(1); }); + }, 'start the node debugger'); + flow.timeout(1000, 'waiting for debugger to attach'); + } + + if (config.capabilities && config.capabilities.seleniumAddress) { + config.seleniumAddress = config.capabilities.seleniumAddress; + } + this.loadDriverProvider_(config); + this.setTestPreparer(config.onPrepare); + } + + /** + * Registrar for testPreparers - executed right before tests run. + * @public + * @param {string/Fn} filenameOrFn + */ + setTestPreparer(filenameOrFn: string|Function): void { + this.preparer_ = filenameOrFn; + } + + /** + * Executor of testPreparer + * @public + * @return {q.Promise} A promise that will resolve when the test preparers + * are finished. + */ + runTestPreparer(): q.Promise { + return helper.runFilenameOrFn_(this.config_.configDir, this.preparer_); + } + + /** + * Grab driver provider based on type + * @private + * + * Priority + * 1) if directConnect is true, use that + * 2) if seleniumAddress is given, use that + * 3) if a Sauce Labs account is given, use that + * 4) if a seleniumServerJar is specified, use that + * 5) try to find the seleniumServerJar in protractor/selenium + */ + loadDriverProvider_(config: Config) { + this.config_ = config; + if (this.config_.directConnect) { + this.driverprovider_ = new Direct(this.config_); + } else if (this.config_.seleniumAddress) { + if (this.config_.seleniumSessionId) { + this.driverprovider_ = new AttachSession(this.config_); + } else { + this.driverprovider_ = new Hosted(this.config_); + } + } else if (this.config_.browserstackUser && this.config_.browserstackKey) { + this.driverprovider_ = new BrowserStack(this.config_); + } else if (this.config_.sauceUser && this.config_.sauceKey) { + this.driverprovider_ = new Sauce(this.config_); + } else if (this.config_.seleniumServerJar) { + this.driverprovider_ = new Local(this.config_); + } else if (this.config_.mockSelenium) { + this.driverprovider_ = new Mock(this.config_); + } else { + this.driverprovider_ = new Local(this.config_); + } + } + + /** + * Responsible for cleaning up test run and exiting the process. + * @private + * @param {int} Standard unix exit code + */ + exit_ = function(exitCode: number): + any { + console.log('ran the default exit'); + return helper + .runFilenameOrFn_( + this.config_.configDir, this.config_.onCleanUp, [exitCode]) + .then((returned): number | any => { + if (typeof returned === 'number') { + return returned; + } else { + return exitCode; + } + }); + } + + /** + * Getter for the Runner config object + * @public + * @return {Object} config + */ + getConfig(): Config { + return this.config_; + } + + /** + * Get the control flow used by this runner. + * @return {Object} WebDriver control flow. + */ + controlFlow(): any { return webdriver.promise.controlFlow(); } + + /** + * Sets up convenience globals for test specs + * @private + */ + setupGlobals_(browser_: any) { + // Keep $, $$, element, and by/By under the global protractor namespace + protractor.browser = browser_; + protractor.$ = browser_.$; + protractor.$$ = browser_.$$; + protractor.element = browser_.element; + protractor.by = protractor.By; + + if (!this.config_.noGlobals) { + // Export protractor to the global namespace to be used in tests. + global.browser = browser_; + global.$ = browser_.$; + global.$$ = browser_.$$; + global.element = browser_.element; + global.by = global.By = protractor.By; + } + global.protractor = protractor; + + if (!this.config_.skipSourceMapSupport) { + // Enable sourcemap support for stack traces. + require('source-map-support').install(); + } + // Required by dart2js machinery. + // https://code.google.com/p/dart/source/browse/branches/bleeding_edge/dart/sdk/lib/js/dart2js/js_dart2js.dart?spec=svn32943&r=32943#487 + global.DartObject = function(o: any) { this.o = o; }; + } + + /** + * Create a new driver from a driverProvider. Then set up a + * new protractor instance using this driver. + * This is used to set up the initial protractor instances and any + * future ones. + * + * @param {?Plugin} The plugin functions + * + * @return {Protractor} a protractor instance. + * @public + */ + createBrowser(plugins: any): any { + var config = this.config_; + var driver = this.driverprovider_.getNewDriver(); + + var browser_ = protractor.wrapDriver( + driver, config.baseUrl, config.rootElement, + config.untrackOutstandingTimeouts); + + browser_.params = config.params; + if (plugins) { + browser_.plugins_ = plugins; + } + if (config.getPageTimeout) { + browser_.getPageTimeout = config.getPageTimeout; + } + if (config.allScriptsTimeout) { + browser_.allScriptsTimeout = config.allScriptsTimeout; + } + if (config.debuggerServerPort) { + browser_.debuggerServerPort_ = config.debuggerServerPort; + } + if (config.useAllAngular2AppRoots) { + browser_.useAllAngular2AppRoots(); + } + + browser_.ready = + driver.manage().timeouts().setScriptTimeout(config.allScriptsTimeout); + + browser_.getProcessedConfig = + () => { return webdriver.promise.fulfilled(config); }; + + browser_.forkNewDriverInstance = + (opt_useSameUrl: boolean, opt_copyMockModules: boolean) => { + let newBrowser = this.createBrowser(plugins); + if (opt_copyMockModules) { + newBrowser.mockModules_ = browser_.mockModules_; + } + if (opt_useSameUrl) { + browser_.driver.getCurrentUrl().then( + (url: string) => { newBrowser.get(url); }); + } + return newBrowser; + }; + + browser_.restart = () => { + // Note: because tests are not paused at this point, any async + // calls here are not guaranteed to complete before the tests resume. + this.driverprovider_.quitDriver(browser_.driver); + // Copy mock modules, but do not navigate to previous URL. + browser_ = browser_.forkNewDriverInstance(false, true); + this.setupGlobals_(browser_); + }; + + return browser_; + } + + /** + * Final cleanup on exiting the runner. + * + * @return {q.Promise} A promise which resolves on finish. + * @private + */ + shutdown_(): q.Promise { + return q.all(this.driverprovider_.getExistingDrivers().map( + this.driverprovider_.quitDriver.bind(this.driverprovider_))); + } + + /** + * The primary workhorse interface. Kicks off the test running process. + * + * @return {q.Promise} A promise which resolves to the exit code of the tests. + * @public + */ + run(): q.Promise { + let testPassed: boolean; + let plugins = new Plugins(this.config_); + let pluginPostTestPromises: any; + let browser_: any; + let results: any; + + if (this.config_.framework !== 'explorer' && !this.config_.specs.length) { + throw new Error('Spec patterns did not match any files.'); + } + + // 1) Setup environment + // noinspection JSValidateTypes + return this.driverprovider_.setupEnv() + .then(() => { + // 2) Create a browser and setup globals + browser_ = this.createBrowser(plugins); + this.setupGlobals_(browser_); + return browser_.ready.then(browser_.getSession) + .then( + (session: webdriver.Session) => { + logger.debug( + 'WebDriver session successfully started with capabilities ' + + util.inspect(session.getCapabilities())); + }, + (err: Error) => { + logger.error('Unable to start a WebDriver session.'); + throw err; + }); + // 3) Setup plugins + }) + .then(() => { + return plugins.setup(); + // 4) Execute test cases + }) + .then(() => { + // Do the framework setup here so that jasmine and mocha globals are + // available to the onPrepare function. + let frameworkPath = ''; + if (this.config_.framework === 'jasmine' || + this.config_.framework === 'jasmine2') { + frameworkPath = './frameworks/jasmine.js'; + } else if (this.config_.framework === 'mocha') { + frameworkPath = './frameworks/mocha.js'; + } else if (this.config_.framework === 'debugprint') { + // Private framework. Do not use. + frameworkPath = './frameworks/debugprint.js'; + } else if (this.config_.framework === 'explorer') { + // Private framework. Do not use. + frameworkPath = './frameworks/explorer.js'; + } else if (this.config_.framework === 'custom') { + if (!this.config_.frameworkPath) { + throw new Error( + 'When config.framework is custom, ' + + 'config.frameworkPath is required.'); + } + frameworkPath = this.config_.frameworkPath; + } else { + throw new Error( + 'config.framework (' + this.config_.framework + + ') is not a valid framework.'); + } + + if (this.config_.restartBrowserBetweenTests) { + var restartDriver = () => { browser_.restart(); }; + this.on('testPass', restartDriver); + this.on('testFail', restartDriver); + } + + // We need to save these promises to make sure they're run, but we + // don't + // want to delay starting the next test (because we can't, it's just + // an event emitter). + pluginPostTestPromises = []; + + this.on('testPass', (testInfo: any) => { + pluginPostTestPromises.push(plugins.postTest(true, testInfo)); + }); + this.on('testFail', (testInfo: any) => { + pluginPostTestPromises.push(plugins.postTest(false, testInfo)); + }); + + logger.debug('Running with spec files ' + this.config_.specs); + + return require(frameworkPath).run(this, this.config_.specs); + // 5) Wait for postTest plugins to finish + }) + .then((testResults: any) => { + results = testResults; + return q.all(pluginPostTestPromises); + // 6) Teardown plugins + }) + .then(() => { + return plugins.teardown(); + // 7) Teardown + }) + .then(() => { + results = helper.joinTestLogs(results, plugins.getResults()); + this.emit('testsDone', results); + testPassed = results.failedCount === 0; + if (this.driverprovider_.updateJob) { + return this.driverprovider_.updateJob({'passed': testPassed}) + .then(() => { return this.driverprovider_.teardownEnv(); }); + } else { + return this.driverprovider_.teardownEnv(); + } + // 8) Let plugins do final cleanup + }) + .then(() => { + return plugins.postResults(); + // 9) Exit process + }) + .then(() => { + var exitCode = testPassed ? 0 : 1; + return this.exit_(exitCode); + }) + .fin(() => { return this.shutdown_(); }); + } +} diff --git a/lib/runnerCli.js b/lib/runnerCli.js index fd300d68f..32a1115ca 100644 --- a/lib/runnerCli.js +++ b/lib/runnerCli.js @@ -4,7 +4,7 @@ */ var ConfigParser = require('./configParser').ConfigParser; -var Runner = require('./runner'); +var Runner = require('./runner').Runner; var log = require('./logger'); process.on('message', function(m) { diff --git a/lib/taskRunner.ts b/lib/taskRunner.ts index 76c7906a2..a6fb020c6 100644 --- a/lib/taskRunner.ts +++ b/lib/taskRunner.ts @@ -4,6 +4,7 @@ import * as q from 'q'; import {ConfigParser, Config} from './configParser'; import * as Logger from './logger'; +import {Runner} from './runner'; import {TaskLogger} from './taskLogger'; export interface RunResults { @@ -15,19 +16,19 @@ export interface RunResults { specResults: Array; } +/** + * A runner for running a specified task (capabilities + specs). + * The TaskRunner can either run the task from the current process (via + * './runner.js') or from a new process (via './runnerCli.js'). + * + * @constructor + * @param {string} configFile Path of test configuration. + * @param {object} additionalConfig Additional configuration. + * @param {object} task Task to run. + * @param {boolean} runInFork Whether to run test in a forked process. + * @constructor + */ export class TaskRunner extends EventEmitter { - /** - * A runner for running a specified task (capabilities + specs). - * The TaskRunner can either run the task from the current process (via - * './runner.js') or from a new process (via './runnerCli.js'). - * - * @constructor - * @param {string} configFile Path of test configuration. - * @param {object} additionalConfig Additional configuration. - * @param {object} task Task to run. - * @param {boolean} runInFork Whether to run test in a forked process. - * @constructor - */ constructor( private configFile: string, private additionalConfig: Config, private task: any, private runInFork: boolean) { @@ -116,7 +117,6 @@ export class TaskRunner extends EventEmitter { config.capabilities = this.task.capabilities; config.specs = this.task.specs; - let Runner = require('./runner'); let runner = new Runner(config); runner.on('testsDone', (results: RunResults) => { diff --git a/lib/util.ts b/lib/util.ts index 92c93fb74..3887f8b26 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -36,7 +36,7 @@ export function filterStackTrace(text: string): string { * @return {q.Promise} A promise that will resolve when filenameOrFn completes. */ export function runFilenameOrFn_( - configDir: string, filenameOrFn: any, args: Array): Promise { + configDir: string, filenameOrFn: any, args?: any[]): Promise { return Promise((resolvePromise) => { if (filenameOrFn && !(typeof filenameOrFn === 'string' || diff --git a/spec/unit/runner_test.js b/spec/unit/runner_test.js index 809031c6d..e63b64dea 100644 --- a/spec/unit/runner_test.js +++ b/spec/unit/runner_test.js @@ -1,4 +1,4 @@ -var Runner = require('../../built/runner'); +var Runner = require('../../built/runner').Runner; var q = require('q'); describe('the Protractor runner', function() { @@ -19,10 +19,10 @@ describe('the Protractor runner', function() { framework: 'debugprint' }; var exitCode; - Runner.prototype.exit_ = function(exit) { + var runner = new Runner(config); + runner.exit_ = function(exit) { exitCode = exit; }; - var runner = new Runner(config); runner.run().then(function() { expect(exitCode).toEqual(0);