diff --git a/lib/config.ts b/lib/config.ts index 30ec8de38..c04849088 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -723,6 +723,11 @@ export interface Config { nodeDebug?: boolean; debuggerServerPort?: number; frameworkPath?: string; + + /** + * Deprecated: Element explorer depends on the WebDriver control flow, and + * thus is no longer supported. + */ elementExplorer?: any; debug?: boolean; unknownFlags_?: string[]; diff --git a/lib/launcher.ts b/lib/launcher.ts index 6a92e20ea..c7077ec74 100644 --- a/lib/launcher.ts +++ b/lib/launcher.ts @@ -3,16 +3,15 @@ * input configuration and launching test runners. */ import * as fs from 'fs'; -import * as q from 'q'; import {Config} from './config'; import {ConfigParser} from './configParser'; import {ConfigError, ErrorHandler, ProtractorError} from './exitCodes'; import {Logger} from './logger'; -import {Runner} from './runner'; import {TaskRunner} from './taskRunner'; import {TaskScheduler} from './taskScheduler'; -import * as helper from './util'; +import {runFilenameOrFn_} from './util'; + let logger = new Logger('launcher'); let RUNNERS_FAILED_EXIT_CODE = 100; @@ -93,7 +92,7 @@ let taskResults_ = new TaskResults(); * @param {string=} configFile * @param {Object=} additionalConfig */ -let initFn = function(configFile: string, additionalConfig: Config) { +let initFn = async function(configFile: string, additionalConfig: Config) { let configParser = new ConfigParser(); if (configFile) { configParser.addFileConfig(configFile); @@ -108,197 +107,159 @@ let initFn = function(configFile: string, additionalConfig: Config) { logger.debug('Your base url for tests is ' + config.baseUrl); // Run beforeLaunch - helper.runFilenameOrFn_(config.configDir, config.beforeLaunch) - .then(() => { + await runFilenameOrFn_(config.configDir, config.beforeLaunch); + // 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. + const waitMultiConfig = await config.getMultiCapabilities(); + config.multiCapabilities = waitMultiConfig; + config.capabilities = null; + } - return q - .Promise((resolve: Function, reject: 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(config.getMultiCapabilities()) - .then((multiCapabilities) => { - config.multiCapabilities = multiCapabilities; - config.capabilities = null; - }) - .then(() => { - resolve(); - }) - .catch(err => { - reject(err); - }); - } 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'; + // 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'}]; + } - 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); + // 3) If we're in `elementExplorer` mode, throw an error and exit. + if (config.elementExplorer || config.framework === 'explorer') { + const err = new Error( + 'Deprecated: Element explorer depends on the ' + + 'WebDriver control flow, and thus is no longer supported.'); + logger.error(err); + process.exit(1); + } - process.on('uncaughtException', (exc: (Error|string)) => { - let e = (exc instanceof Error) ? exc : new Error(exc); - if (config.ignoreUncaughtExceptions) { - // This can be a sign of a bug in the test framework, that it may - // not be handling WebDriver errors properly. However, we don't - // want these errors to prevent running the tests. - logger.warn('Ignoring uncaught error ' + exc); - return; - } + // 4) Run tests. + let scheduler = new TaskScheduler(config); - let errorCode = ErrorHandler.parseError(e); - if (errorCode) { - let protractorError = e as ProtractorError; - ProtractorError.log(logger, errorCode, protractorError.message, protractorError.stack); - process.exit(errorCode); - } else { - logger.error(e.message); - logger.error(e.stack); - process.exit(ProtractorError.CODE); - } - }); + process.on('uncaughtException', (exc: (Error|string)) => { + let e = (exc instanceof Error) ? exc : new Error(exc); + if (config.ignoreUncaughtExceptions) { + // This can be a sign of a bug in the test framework, that it may + // not be handling WebDriver errors properly. However, we don't + // want these errors to prevent running the tests. + logger.warn('Ignoring uncaught error ' + exc); + return; + } + logger.error(e.message); + logger.error(e.stack); + if (e instanceof ProtractorError) { + let protractorError = e as ProtractorError; + process.exit(protractorError.code); + } else { + process.exit(1); + } + }); - 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); - } - }); + process.on('unhandledRejection', (reason: Error | any, p: Promise) => { + logger.warn('Unhandled rejection at:', p, 'reason:', reason); + }); - // 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); - }); - }; + 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); + } + }); - 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 ConfigError( - logger, 'Cannot run in debug mode with multiCapabilities, count > 1, or sharding'); - } - } + // Run afterlaunch and exit + const cleanUpAndExit = async (exitCode: number) => { + try { + const returned = await runFilenameOrFn_(config.configDir, config.afterLaunch, [exitCode]); + if (typeof returned === 'number') { + process.exit(returned); + } else { + process.exit(exitCode); + } + } catch (err) { + logger.error('Error:', err); + process.exit(1); + } + }; - let deferred = q.defer(); // Resolved when all tasks are completed - let createNextTaskRunner = () => { - let 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 as any).stack || err.message || err); - cleanUpAndExit(RUNNERS_FAILED_EXIT_CODE); - }); + const 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 ConfigError( + logger, 'Cannot run in debug mode with multiCapabilities, count > 1, or sharding'); + } + } + + const createNextTaskRunner = async () => { + return new Promise(async (resolve) => { + const task = scheduler.nextTask(); + if (task) { + const taskRunner = new TaskRunner(configFile, additionalConfig, task, forkProcess); + try { + const result = await taskRunner.run(); + if (result.exitCode && !result.failedCount) { + logger.error('Runner process exited unexpectedly with error code: ' + result.exitCode); } - }; - // 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 (let i = 0; i < scheduler.maxConcurrentTasks(); ++i) { + taskResults_.add(result); + task.done(); createNextTaskRunner(); + // If all tasks are finished + if (scheduler.numTasksOutstanding() === 0) { + resolve(); + } + logger.info(scheduler.countActiveTasks() + ' instance(s) of WebDriver still running'); + } catch (err) { + const errorCode = ErrorHandler.parseError(err); + logger.error('Error:', (err as any).stack || err.message || err); + await cleanUpAndExit(errorCode ? errorCode : RUNNERS_FAILED_EXIT_CODE); } - logger.info('Running ' + scheduler.countActiveTasks() + ' instances of WebDriver'); + } + }); + }; + + const maxConcurrentTasks = scheduler.maxConcurrentTasks(); + for (let i = 0; i < maxConcurrentTasks; ++i) { + await 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); - } + // By now all runners have completed. + // 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; + } + await cleanUpAndExit(exitCode); + // Start `const 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. - 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/plugins.ts b/lib/plugins.ts index 1eaf88b1d..062249e8a 100644 --- a/lib/plugins.ts +++ b/lib/plugins.ts @@ -1,19 +1,12 @@ -import * as q from 'q'; import * as webdriver from 'selenium-webdriver'; import {ProtractorBrowser} from './browser'; import {Config} from './config'; import {ConfigParser} from './configParser'; import {Logger} from './logger'; -import {protractor} from './ptor'; let logger = new Logger('plugins'); -export enum PromiseType { - Q, - WEBDRIVER -} - export interface PluginConfig { path?: string; package?: string; @@ -420,15 +413,15 @@ export class Plugins { /** * @see docs/plugins.md#writing-plugins for information on these functions */ - setup = this.pluginFunFactory('setup', PromiseType.Q); - onPrepare = this.pluginFunFactory('onPrepare', PromiseType.Q); - teardown = this.pluginFunFactory('teardown', PromiseType.Q); - postResults = this.pluginFunFactory('postResults', PromiseType.Q); - postTest = this.pluginFunFactory('postTest', PromiseType.Q); - onPageLoad = this.pluginFunFactory('onPageLoad', PromiseType.WEBDRIVER); - onPageStable = this.pluginFunFactory('onPageStable', PromiseType.WEBDRIVER); - waitForPromise = this.pluginFunFactory('waitForPromise', PromiseType.WEBDRIVER); - waitForCondition = this.pluginFunFactory('waitForCondition', PromiseType.WEBDRIVER, true); + setup = this.pluginFunFactory('setup'); + onPrepare = this.pluginFunFactory('onPrepare'); + teardown = this.pluginFunFactory('teardown'); + postResults = this.pluginFunFactory('postResults'); + postTest = this.pluginFunFactory('postTest'); + onPageLoad = this.pluginFunFactory('onPageLoad'); + onPageStable = this.pluginFunFactory('onPageStable'); + waitForPromise = this.pluginFunFactory('waitForPromise'); + waitForCondition = this.pluginFunFactory('waitForCondition', true); /** * Calls a function from a plugin safely. If the plugin's function throws an @@ -439,18 +432,15 @@ export class Plugins { * @param {Object} pluginObj The plugin object containing the function to be run * @param {string} funName The name of the function we want to run * @param {*[]} args The arguments we want to invoke the function with - * @param {PromiseType} promiseType The type of promise (WebDriver or Q) that - * should be used * @param {boolean} resultsReported If the results have already been reported * @param {*} failReturnVal The value to return if the function fails * - * @return {webdriver.promise.Promise|Q.Promise} A promise which resolves to the + * @return {Promise} A promise which resolves to the * function's return value */ private safeCallPluginFun( - pluginObj: ProtractorPlugin, funName: string, args: any[], promiseType: PromiseType, - failReturnVal: any): q.Promise|Promise|webdriver.promise.Promise { - const resolver = (done: (result: any) => void) => { + pluginObj: ProtractorPlugin, funName: string, args: any[], failReturnVal: any): Promise { + const resolver = async (done: (result: any) => void) => { const logError = (e: any) => { if (this.resultsReported) { this.printPluginResults([{ @@ -468,47 +458,33 @@ export class Plugins { done(failReturnVal); }; try { - const result = (pluginObj as any)[funName].apply(pluginObj, args); - if (webdriver.promise.isPromise(result)) { - (result as PromiseLike).then(done, logError); - } else { - done(result); - } + const result = await(pluginObj as any)[funName].apply(pluginObj, args); + done(result); } catch (e) { logError(e); } }; - if (promiseType == PromiseType.Q) { - return q.Promise(resolver); - } else if (protractor.browser.controlFlowIsEnabled()) { - return new webdriver.promise.Promise(resolver); - } else { - return new Promise(resolver); - } + return new Promise(resolver); } /** * Generates the handler for a plugin function (e.g. the setup() function) * * @param {string} funName The name of the function to make a handler for - * @param {PromiseType} promiseType The type of promise (WebDriver or Q) that should be used * @param {boolean=} failReturnVal The value that the function should return if the plugin crashes * * @return The handler */ - private pluginFunFactory(funName: string, promiseType: PromiseType.Q, failReturnVal?: boolean): - (...args: any[]) => q.Promise; - private pluginFunFactory( - funName: string, promiseType: PromiseType.WEBDRIVER, - failReturnVal?: boolean): (...args: any[]) => webdriver.promise.Promise; - private pluginFunFactory(funName: string, promiseType: PromiseType, failReturnVal?: boolean) { + private pluginFunFactory(funName: string, failReturnVal?: boolean): + (...args: any[]) => Promise; + private pluginFunFactory(funName: string, failReturnVal?: boolean): + (...args: any[]) => webdriver.promise.Promise; + private pluginFunFactory(funName: string, failReturnVal?: boolean) { return (...args: any[]) => { const promises = this.pluginObjs.filter(pluginObj => typeof(pluginObj as any)[funName] === 'function') - .map( - pluginObj => - this.safeCallPluginFun(pluginObj, funName, args, promiseType, failReturnVal)); - return promiseType == PromiseType.Q ? q.all(promises) : webdriver.promise.all(promises); + .map(pluginObj => this.safeCallPluginFun(pluginObj, funName, args, failReturnVal)); + return Promise.all(promises); }; } } diff --git a/lib/runner.ts b/lib/runner.ts index e5a4144e8..c9aff6131 100644 --- a/lib/runner.ts +++ b/lib/runner.ts @@ -1,16 +1,15 @@ import {EventEmitter} from 'events'; -import * as q from 'q'; -import {promise as wdpromise, Session} from 'selenium-webdriver'; +// TODO(cnishina): remove when selenium webdriver is upgraded. +import {promise as wdpromise} from 'selenium-webdriver'; import * as util from 'util'; import {ProtractorBrowser} from './browser'; import {Config} from './config'; import {buildDriverProvider, DriverProvider} from './driverProviders'; -import {ConfigError} from './exitCodes'; import {Logger} from './logger'; import {Plugins} from './plugins'; import {protractor} from './ptor'; -import * as helper from './util'; +import {joinTestLogs, runFilenameOrFn_} from './util'; declare let global: any; declare let process: any; @@ -34,9 +33,9 @@ export class Runner extends EventEmitter { driverprovider_: DriverProvider; o: any; plugins_: Plugins; - restartPromise: q.Promise; + restartPromise: Promise; frameworkUsesAfterEach: boolean; - ready_?: wdpromise.Promise; + ready_?: Promise; constructor(config: Config) { super(); @@ -48,19 +47,15 @@ export class Runner extends EventEmitter { if (config.nodeDebug) { process['_debugProcess'](process.pid); - let flow = wdpromise.controlFlow(); - - this.ready_ = 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').then(() => { - return flow.timeout(1000, 'waiting for debugger to attach'); + const nodedebug = require('child_process').fork('debug', ['localhost:5858']); + process.on('exit', () => { + nodedebug.kill('SIGTERM'); + }); + nodedebug.on('exit', () => { + process.exit(1); + }); + this.ready_ = new Promise(resolve => { + setTimeout(resolve, 1000); }); } @@ -84,10 +79,10 @@ export class Runner extends EventEmitter { * Executor of testPreparer * @public * @param {string[]=} An optional list of command line arguments the framework will accept. - * @return {q.Promise} A promise that will resolve when the test preparers + * @return {Promise} A promise that will resolve when the test preparers * are finished. */ - runTestPreparer(extraFlags?: string[]): q.Promise { + runTestPreparer(extraFlags?: string[]): Promise { let unknownFlags = this.config_.unknownFlags_ || []; if (extraFlags) { unknownFlags = unknownFlags.filter((f) => extraFlags.indexOf(f) === -1); @@ -100,7 +95,7 @@ export class Runner extends EventEmitter { ' Protractor CLI flag checks. '); } return this.plugins_.onPrepare().then(() => { - return helper.runFilenameOrFn_(this.config_.configDir, this.preparer_); + return runFilenameOrFn_(this.config_.configDir, this.preparer_); }); } @@ -110,17 +105,17 @@ export class Runner extends EventEmitter { * Responsible for `restartBrowserBetweenTests` * * @public - * @return {q.Promise} A promise that will resolve when the work here is done + * @return {Promise} A promise that will resolve when the work here is done */ - afterEach(): q.Promise { - let ret: q.Promise; + afterEach(): Promise { + let ret: Promise; this.frameworkUsesAfterEach = true; if (this.config_.restartBrowserBetweenTests) { - this.restartPromise = this.restartPromise || q(protractor.browser.restart()); + this.restartPromise = this.restartPromise || Promise.resolve(protractor.browser.restart()); ret = this.restartPromise; this.restartPromise = undefined; } - return ret || q(); + return ret || Promise.resolve(); } /** @@ -144,16 +139,15 @@ export class Runner extends EventEmitter { * @private * @param {int} Standard unix exit code */ - exit_ = function(exitCode: number): any { - return helper.runFilenameOrFn_(this.config_.configDir, this.config_.onCleanUp, [exitCode]) - .then((returned): number | any => { - if (typeof returned === 'number') { - return returned; - } else { - return exitCode; - } - }); - }; + async exit_(exitCode: number): Promise { + const returned = + await runFilenameOrFn_(this.config_.configDir, this.config_.onCleanUp, [exitCode]); + if (typeof returned === 'number') { + return returned; + } else { + return exitCode; + } + } /** * Getter for the Runner config object @@ -164,14 +158,6 @@ export class Runner extends EventEmitter { return this.config_; } - /** - * Get the control flow used by this runner. - * @return {Object} WebDriver control flow. - */ - controlFlow(): any { - return wdpromise.controlFlow(); - } - /** * Sets up convenience globals for test specs * @private @@ -231,14 +217,14 @@ export class Runner extends EventEmitter { let initProperties = { baseUrl: config.baseUrl, - rootElement: config.rootElement as string | wdpromise.Promise, + rootElement: config.rootElement as string | Promise, untrackOutstandingTimeouts: config.untrackOutstandingTimeouts, params: config.params, getPageTimeout: config.getPageTimeout, allScriptsTimeout: config.allScriptsTimeout, debuggerServerPort: config.debuggerServerPort, ng12Hybrid: config.ng12Hybrid, - waitForAngularEnabled: true as boolean | wdpromise.Promise + waitForAngularEnabled: true as boolean | Promise }; if (parentBrowser) { @@ -286,7 +272,7 @@ export class Runner extends EventEmitter { }); browser_.getProcessedConfig = () => { - return wdpromise.when(config); + return Promise.resolve(config); }; browser_.forkNewDriverInstance = @@ -321,24 +307,9 @@ export class Runner extends EventEmitter { browser_.restart = () => { // Note: because tests are not paused at this point, any async // calls here are not guaranteed to complete before the tests resume. - - // Seperate solutions depending on if the control flow is enabled (see lib/browser.ts) - if (browser_.controlFlowIsEnabled()) { - return browser_.restartSync().ready; - } else { - return this.driverprovider_.quitDriver(browser_.driver) - .then(replaceBrowser) - .then(newBrowser => newBrowser.ready); - } - }; - - browser_.restartSync = () => { - if (!browser_.controlFlowIsEnabled()) { - throw TypeError('Unable to use `browser.restartSync()` when the control flow is disabled'); - } - - this.driverprovider_.quitDriver(browser_.driver); - return replaceBrowser(); + return this.driverprovider_.quitDriver(browser_.driver) + .then(replaceBrowser) + .then(newBrowser => newBrowser.ready); }; return browser_; @@ -358,10 +329,10 @@ export class Runner extends EventEmitter { /** * The primary workhorse interface. Kicks off the test running process. * - * @return {q.Promise} A promise which resolves to the exit code of the tests. + * @return {Promise} A promise which resolves to the exit code of the tests. * @public */ - run(): q.Promise { + async run(): Promise { let testPassed: boolean; let plugins = this.plugins_ = new Plugins(this.config_); let pluginPostTestPromises: any; @@ -372,6 +343,7 @@ export class Runner extends EventEmitter { throw new Error('Spec patterns did not match any files.'); } + // TODO(selenium4): Remove when selenium is upgraded. if (this.config_.SELENIUM_PROMISE_MANAGER != null) { (wdpromise as any).USE_PROMISE_MANAGER = this.config_.SELENIUM_PROMISE_MANAGER; } @@ -381,120 +353,104 @@ export class Runner extends EventEmitter { } // 0) Wait for debugger - return q(this.ready_) - .then(() => { - // 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: 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.'); - } + await Promise.resolve(this.ready_); + + // 1) Setup environment + // noinspection JSValidateTypes + await this.driverprovider_.setupEnv(); + + // 2) Create a browser and setup globals + browser_ = this.createBrowser(plugins); + this.setupGlobals_(browser_); + try { + const session = await browser_.ready.then(browser_.getSession); + logger.debug( + 'WebDriver session successfully started with capabilities ' + + util.inspect(session.getCapabilities())); + } catch (err) { + logger.error('Unable to start a WebDriver session.'); + throw err; + } - if (this.config_.restartBrowserBetweenTests) { - // TODO(sjelin): replace with warnings once `afterEach` support is required - let restartDriver = () => { - if (!this.frameworkUsesAfterEach) { - this.restartPromise = q(browser_.restart()); - } - }; - this.on('testPass', restartDriver); - this.on('testFail', restartDriver); - } + // 3) Setup plugins + await plugins.setup(); + + // 4) Execute test cases + // 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.'); + } - // 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(() => { - let exitCode = testPassed ? 0 : 1; - return this.exit_(exitCode); - }) - .fin(() => { - return this.shutdown_(); - }); + if (this.config_.restartBrowserBetweenTests) { + // TODO(sjelin): replace with warnings once `afterEach` support is required + let restartDriver = () => { + if (!this.frameworkUsesAfterEach) { + this.restartPromise = Promise.resolve(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); + let testResults = await require(frameworkPath).run(this, this.config_.specs); + + // 5) Wait for postTest plugins to finish + results = testResults; + await Promise.all(pluginPostTestPromises); + + // 6) Teardown plugins + await plugins.teardown(); + + // 7) Teardown + results = joinTestLogs(results, plugins.getResults()); + this.emit('testsDone', results); + testPassed = results.failedCount === 0; + if (this.driverprovider_.updateJob) { + await this.driverprovider_.updateJob({'passed': testPassed}); + } + await this.driverprovider_.teardownEnv(); + + // 8) Let plugins do final cleanup + await plugins.postResults(); + + // 9) Exit process + const exitCode = testPassed ? 0 : 1; + + await this.shutdown_(); + + return this.exit_(exitCode); } } diff --git a/lib/taskRunner.ts b/lib/taskRunner.ts index bdef6f953..c1402411c 100644 --- a/lib/taskRunner.ts +++ b/lib/taskRunner.ts @@ -1,6 +1,5 @@ import * as child_process from 'child_process'; import {EventEmitter} from 'events'; -import * as q from 'q'; import {Config} from './config'; import {ConfigParser} from './configParser'; @@ -37,12 +36,12 @@ export class TaskRunner extends EventEmitter { /** * Sends the run command. - * @return {q.Promise} A promise that will resolve when the task finishes + * @return {Promise} A promise that will resolve when the task finishes * running. The promise contains the following parameters representing the * result of the run: * taskId, specs, capabilities, failedCount, exitCode, specResults */ - public run(): q.Promise { + public async run(): Promise { let runResults: RunResults = { taskId: this.task.taskId, specs: this.task.specs, @@ -65,61 +64,59 @@ export class TaskRunner extends EventEmitter { config.specs = this.task.specs; if (this.runInFork) { - let deferred = q.defer(); + return new Promise((resolve, reject) => { + let childProcess = child_process.fork( + __dirname + '/runnerCli.js', process.argv.slice(2), {cwd: process.cwd(), silent: true}); + let taskLogger = new TaskLogger(this.task, childProcess.pid); - let childProcess = child_process.fork( - __dirname + '/runnerCli.js', process.argv.slice(2), {cwd: process.cwd(), silent: true}); - let taskLogger = new TaskLogger(this.task, childProcess.pid); + // stdout pipe + childProcess.stdout.on('data', (data: string) => { + taskLogger.log(data); + }); - // stdout pipe - childProcess.stdout.on('data', (data: string) => { - taskLogger.log(data); - }); - - // stderr pipe - childProcess.stderr.on('data', (data: string) => { - taskLogger.log(data); - }); + // stderr pipe + childProcess.stderr.on('data', (data: string) => { + taskLogger.log(data); + }); - childProcess - .on('message', - (m: any) => { - if (config.verboseMultiSessions) { - taskLogger.peek(); - } - switch (m.event) { - case 'testPass': - process.stdout.write('.'); - break; - case 'testFail': - process.stdout.write('F'); - break; - case 'testsDone': - runResults.failedCount = m.results.failedCount; - runResults.specResults = m.results.specResults; - break; - } - }) - .on('error', - (err: any) => { - taskLogger.flush(); - deferred.reject(err); - }) - .on('exit', (code: number) => { - taskLogger.flush(); - runResults.exitCode = code; - deferred.resolve(runResults); - }); + childProcess + .on('message', + (m: any) => { + if (config.verboseMultiSessions) { + taskLogger.peek(); + } + switch (m.event) { + case 'testPass': + process.stdout.write('.'); + break; + case 'testFail': + process.stdout.write('F'); + break; + case 'testsDone': + runResults.failedCount = m.results.failedCount; + runResults.specResults = m.results.specResults; + break; + } + }) + .on('error', + (err: any) => { + taskLogger.flush(); + reject(err); + }) + .on('exit', (code: number) => { + taskLogger.flush(); + runResults.exitCode = code; + resolve(runResults); + }); - childProcess.send({ - command: 'run', - configFile: this.configFile, - additionalConfig: this.additionalConfig, - capabilities: this.task.capabilities, - specs: this.task.specs + childProcess.send({ + command: 'run', + configFile: this.configFile, + additionalConfig: this.additionalConfig, + capabilities: this.task.capabilities, + specs: this.task.specs + }); }); - - return deferred.promise; } else { let runner = new Runner(config); @@ -128,10 +125,9 @@ export class TaskRunner extends EventEmitter { runResults.specResults = results.specResults; }); - return runner.run().then((exitCode: number) => { - runResults.exitCode = exitCode; - return runResults; - }); + const exitCode = await runner.run(); + runResults.exitCode = exitCode; + return runResults; } } } diff --git a/spec/ciFullConf.js b/spec/ciFullConf.js index 3bc7e5de3..f9fae839c 100644 --- a/spec/ciFullConf.js +++ b/spec/ciFullConf.js @@ -33,16 +33,13 @@ exports.config = { 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, 'build': process.env.TRAVIS_BUILD_NUMBER, 'name': 'Protractor suite tests', - 'version': '54', - 'selenium-version': '2.53.1', - 'chromedriver-version': '2.26', - 'platform': 'OS X 10.11' + 'version': '70' }, { 'browserName': 'firefox', 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, 'build': process.env.TRAVIS_BUILD_NUMBER, 'name': 'Protractor suite tests', - 'version': '47', + 'version': '60', }], baseUrl: env.baseUrl + '/ng1/', diff --git a/spec/ciNg2Conf.js b/spec/ciNg2Conf.js index 9b6dac66f..4dc249482 100644 --- a/spec/ciNg2Conf.js +++ b/spec/ciNg2Conf.js @@ -1,21 +1,3 @@ -exports.config = require('./angular2Conf.js').config; - -exports.config.sauceUser = process.env.SAUCE_USERNAME; -exports.config.sauceKey = process.env.SAUCE_ACCESS_KEY; -exports.config.seleniumAddress = undefined; - -// TODO: add in firefox when issue #2784 is fixed -exports.config.multiCapabilities = [{ - 'browserName': 'chrome', - 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, - 'build': process.env.TRAVIS_BUILD_NUMBER, - 'name': 'Protractor suite tests', - 'version': '54', - 'selenium-version': '2.53.1', - 'chromedriver-version': '2.26', - 'platform': 'OS X 10.11' - }]; -exports.config.capabilities = undefined; -exports.config.allScriptsTimeout = 120000; -exports.config.getPageTimeout = 120000; -exports.config.jasmineNodeOpts.defaultTimeoutInterval = 120000; +exports.config = require('./ciFullConf.js').config; +exports.config.specs = require('./angular2Conf.js').config.specs; +exports.config.exclude = undefined; \ No newline at end of file