diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 5c645b58..dd2e988a 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -1947,11 +1947,17 @@ getJasmineRequireObj().Env = function(j$) { * * execute should not be called more than once. * + * If the environment supports promises, execute will return a promise that + * is resolved after the suite finishes executing. The promise will be + * resolved (not rejected) as long as the suite runs to completion. Use a + * {@link Reporter} to determine whether or not the suite passed. + * * @name Env#execute * @since 2.0.0 * @function * @param {(string[])=} runnablesToRun IDs of suites and/or specs to run * @param {Function=} onComplete Function that will be called after all specs have run + * @return {Promise} */ this.execute = function(runnablesToRun, onComplete) { installGlobalErrors(); @@ -2011,65 +2017,86 @@ getJasmineRequireObj().Env = function(j$) { var jasmineTimer = new j$.Timer(); jasmineTimer.start(); - /** - * Information passed to the {@link Reporter#jasmineStarted} event. - * @typedef JasmineStartedInfo - * @property {Int} totalSpecsDefined - The total number of specs defined in this suite. - * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. - */ - reporter.jasmineStarted( - { - totalSpecsDefined: totalSpecsDefined, - order: order - }, - function() { - currentlyExecutingSuites.push(topSuite); + var Promise = customPromise || global.Promise; - processor.execute(function() { - clearResourcesForRunnable(topSuite.id); - currentlyExecutingSuites.pop(); - var overallStatus, incompleteReason; - - if (hasFailures || topSuite.result.failedExpectations.length > 0) { - overallStatus = 'failed'; - } else if (focusedRunnables.length > 0) { - overallStatus = 'incomplete'; - incompleteReason = 'fit() or fdescribe() was found'; - } else if (totalSpecsDefined === 0) { - overallStatus = 'incomplete'; - incompleteReason = 'No specs found'; - } else { - overallStatus = 'passed'; + if (Promise) { + return new Promise(function(resolve) { + runAll(function() { + if (onComplete) { + onComplete(); } - /** - * Information passed to the {@link Reporter#jasmineDone} event. - * @typedef JasmineDoneInfo - * @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'. - * @property {Int} totalTime - The total time (in ms) that it took to execute the suite - * @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete. - * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. - * @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level. - * @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level. - */ - reporter.jasmineDone( - { - overallStatus: overallStatus, - totalTime: jasmineTimer.elapsed(), - incompleteReason: incompleteReason, - order: order, - failedExpectations: topSuite.result.failedExpectations, - deprecationWarnings: topSuite.result.deprecationWarnings - }, - function() { - if (onComplete) { - onComplete(); - } - } - ); + resolve(); }); - } - ); + }); + } else { + runAll(function() { + if (onComplete) { + onComplete(); + } + }); + } + + function runAll(done) { + /** + * Information passed to the {@link Reporter#jasmineStarted} event. + * @typedef JasmineStartedInfo + * @property {Int} totalSpecsDefined - The total number of specs defined in this suite. + * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. + */ + reporter.jasmineStarted( + { + totalSpecsDefined: totalSpecsDefined, + order: order + }, + function() { + currentlyExecutingSuites.push(topSuite); + + processor.execute(function() { + clearResourcesForRunnable(topSuite.id); + currentlyExecutingSuites.pop(); + var overallStatus, incompleteReason; + + if ( + hasFailures || + topSuite.result.failedExpectations.length > 0 + ) { + overallStatus = 'failed'; + } else if (focusedRunnables.length > 0) { + overallStatus = 'incomplete'; + incompleteReason = 'fit() or fdescribe() was found'; + } else if (totalSpecsDefined === 0) { + overallStatus = 'incomplete'; + incompleteReason = 'No specs found'; + } else { + overallStatus = 'passed'; + } + + /** + * Information passed to the {@link Reporter#jasmineDone} event. + * @typedef JasmineDoneInfo + * @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'. + * @property {Int} totalTime - The total time (in ms) that it took to execute the suite + * @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete. + * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. + * @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level. + * @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level. + */ + reporter.jasmineDone( + { + overallStatus: overallStatus, + totalTime: jasmineTimer.elapsed(), + incompleteReason: incompleteReason, + order: order, + failedExpectations: topSuite.result.failedExpectations, + deprecationWarnings: topSuite.result.deprecationWarnings + }, + done + ); + }); + } + ); + } }; /** @@ -4768,16 +4795,22 @@ getJasmineRequireObj().GlobalErrors = function(j$) { function taggedOnError(error) { var substituteMsg; - if (error) { + if (j$.isError_(error)) { error.jasmineMessage = jasmineMessage + ': ' + error; } else { - substituteMsg = jasmineMessage + ' with no error or message'; + if (error) { + substituteMsg = jasmineMessage + ': ' + error; + } else { + substituteMsg = jasmineMessage + ' with no error or message'; + } if (errorType === 'unhandledRejection') { substituteMsg += '\n' + '(Tip: to get a useful stack trace, use ' + - 'Promise.reject(new Error(...)) instead of Promise.reject().)'; + 'Promise.reject(new Error(...)) instead of Promise.reject(' + + (error ? '...' : '') + + ').)'; } error = new Error(substituteMsg); diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index a01c7ba8..fa324558 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -772,4 +772,26 @@ describe('Env', function() { expect(suiteThis).toBeInstanceOf(jasmineUnderTest.Suite); }); + + describe('#execute', function() { + it('returns a promise when the environment supports promises', function() { + jasmine.getEnv().requirePromises(); + expect(env.execute()).toBeInstanceOf(Promise); + }); + + it('returns a promise when a custom promise constructor is provided', function() { + function CustomPromise() {} + CustomPromise.resolve = function() {}; + CustomPromise.reject = function() {}; + + spyOn(env, 'deprecated'); + env.configure({ Promise: CustomPromise }); + expect(env.execute()).toBeInstanceOf(CustomPromise); + }); + + it('returns undefined when promises are unavailable', function() { + jasmine.getEnv().requireNoPromises(); + expect(env.execute()).toBeUndefined(); + }); + }); }); diff --git a/spec/core/GlobalErrorsSpec.js b/spec/core/GlobalErrorsSpec.js index 8e3ffbc5..07e2f48c 100644 --- a/spec/core/GlobalErrorsSpec.js +++ b/spec/core/GlobalErrorsSpec.js @@ -170,84 +170,118 @@ describe('GlobalErrors', function() { ); }); - it('reports unhandled promise rejections in node.js', function() { - var fakeGlobal = { - process: { - on: jasmine.createSpy('process.on'), - removeListener: jasmine.createSpy('process.removeListener'), - listeners: jasmine - .createSpy('process.listeners') - .and.returnValue(['foo']), - removeAllListeners: jasmine.createSpy('process.removeAllListeners') - } - }, - handler = jasmine.createSpy('errorHandler'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + describe('Reporting unhandled promise rejections in node.js', function() { + it('reports rejections with `Error` reasons', function() { + var fakeGlobal = { + process: { + on: jasmine.createSpy('process.on'), + removeListener: jasmine.createSpy('process.removeListener'), + listeners: jasmine + .createSpy('process.listeners') + .and.returnValue(['foo']), + removeAllListeners: jasmine.createSpy('process.removeAllListeners') + } + }, + handler = jasmine.createSpy('errorHandler'), + errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); - errors.install(); - expect(fakeGlobal.process.on).toHaveBeenCalledWith( - 'unhandledRejection', - jasmine.any(Function) - ); - expect(fakeGlobal.process.listeners).toHaveBeenCalledWith( - 'unhandledRejection' - ); - expect(fakeGlobal.process.removeAllListeners).toHaveBeenCalledWith( - 'unhandledRejection' - ); + errors.install(); + expect(fakeGlobal.process.on).toHaveBeenCalledWith( + 'unhandledRejection', + jasmine.any(Function) + ); + expect(fakeGlobal.process.listeners).toHaveBeenCalledWith( + 'unhandledRejection' + ); + expect(fakeGlobal.process.removeAllListeners).toHaveBeenCalledWith( + 'unhandledRejection' + ); - errors.pushListener(handler); + errors.pushListener(handler); - var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1]; - addedListener(new Error('bar')); + var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1]; + addedListener(new Error('bar')); - expect(handler).toHaveBeenCalledWith(new Error('bar')); - expect(handler.calls.argsFor(0)[0].jasmineMessage).toBe( - 'Unhandled promise rejection: Error: bar' - ); + expect(handler).toHaveBeenCalledWith(new Error('bar')); + expect(handler.calls.argsFor(0)[0].jasmineMessage).toBe( + 'Unhandled promise rejection: Error: bar' + ); - errors.uninstall(); + errors.uninstall(); - expect(fakeGlobal.process.removeListener).toHaveBeenCalledWith( - 'unhandledRejection', - addedListener - ); - expect(fakeGlobal.process.on).toHaveBeenCalledWith( - 'unhandledRejection', - 'foo' - ); - }); + expect(fakeGlobal.process.removeListener).toHaveBeenCalledWith( + 'unhandledRejection', + addedListener + ); + expect(fakeGlobal.process.on).toHaveBeenCalledWith( + 'unhandledRejection', + 'foo' + ); + }); - it('reports unhandled promise rejections in node.js when no error is provided', function() { - var fakeGlobal = { - process: { - on: jasmine.createSpy('process.on'), - removeListener: function() {}, - listeners: function() { - return []; - }, - removeAllListeners: function() {} - } - }, - handler = jasmine.createSpy('errorHandler'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + it('reports rejections with non-`Error` reasons', function() { + var fakeGlobal = { + process: { + on: jasmine.createSpy('process.on'), + removeListener: function() {}, + listeners: function() { + return []; + }, + removeAllListeners: function() {} + } + }, + handler = jasmine.createSpy('errorHandler'), + errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); - errors.install(); - errors.pushListener(handler); + errors.install(); + errors.pushListener(handler); - expect(fakeGlobal.process.on.calls.argsFor(1)[0]).toEqual( - 'unhandledRejection' - ); - var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1]; - addedListener(undefined); + expect(fakeGlobal.process.on.calls.argsFor(1)[0]).toEqual( + 'unhandledRejection' + ); + var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1]; + addedListener(17); - expect(handler).toHaveBeenCalledWith( - new Error( - 'Unhandled promise rejection with no error or message\n' + - '(Tip: to get a useful stack trace, use ' + - 'Promise.reject(new Error(...)) instead of Promise.reject().)' - ) - ); + expect(handler).toHaveBeenCalledWith( + new Error( + 'Unhandled promise rejection: 17\n' + + '(Tip: to get a useful stack trace, use ' + + 'Promise.reject(new Error(...)) instead of Promise.reject(...).)' + ) + ); + }); + + it('reports rejections with no reason provided', function() { + var fakeGlobal = { + process: { + on: jasmine.createSpy('process.on'), + removeListener: function() {}, + listeners: function() { + return []; + }, + removeAllListeners: function() {} + } + }, + handler = jasmine.createSpy('errorHandler'), + errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + + errors.install(); + errors.pushListener(handler); + + expect(fakeGlobal.process.on.calls.argsFor(1)[0]).toEqual( + 'unhandledRejection' + ); + var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1]; + addedListener(undefined); + + expect(handler).toHaveBeenCalledWith( + new Error( + 'Unhandled promise rejection with no error or message\n' + + '(Tip: to get a useful stack trace, use ' + + 'Promise.reject(new Error(...)) instead of Promise.reject().)' + ) + ); + }); }); describe('Reporting unhandled promise rejections in the browser', function() { diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index 57eff7d9..afff7737 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -2977,6 +2977,112 @@ describe('Env integration', function() { env.execute(null, done); }); + describe('The promise returned by #execute', function() { + beforeEach(function() { + this.savedInterval = jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL; + }); + + afterEach(function() { + jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL = this.savedInterval; + }); + + it('is resolved after reporter events are dispatched', function() { + jasmine.getEnv().requirePromises(); + var reporter = jasmine.createSpyObj('reporter', [ + 'specDone', + 'suiteDone', + 'jasmineDone' + ]); + + env.addReporter(reporter); + env.describe('suite', function() { + env.it('spec', function() {}); + }); + + return env.execute(null).then(function() { + expect(reporter.specDone).toHaveBeenCalled(); + expect(reporter.suiteDone).toHaveBeenCalled(); + expect(reporter.jasmineDone).toHaveBeenCalled(); + }); + }); + + it('is resolved after the stack is cleared', function(done) { + jasmine.getEnv().requirePromises(); + var realClearStack = jasmineUnderTest.getClearStack( + jasmineUnderTest.getGlobal() + ), + clearStackSpy = jasmine + .createSpy('clearStack') + .and.callFake(realClearStack); + spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(clearStackSpy); + + // Create a new env that has the clearStack defined above + env.cleanup_(); + env = new jasmineUnderTest.Env(); + + env.describe('suite', function() { + env.it('spec', function() {}); + }); + + env.execute(null).then(function() { + expect(clearStackSpy).toHaveBeenCalled(); // (many times) + clearStackSpy.calls.reset(); + setTimeout(function() { + expect(clearStackSpy).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + it('is resolved after QueueRunner timeouts are cleared', function() { + jasmine.getEnv().requirePromises(); + var setTimeoutSpy = spyOn( + jasmineUnderTest.getGlobal(), + 'setTimeout' + ).and.callThrough(); + var clearTimeoutSpy = spyOn( + jasmineUnderTest.getGlobal(), + 'clearTimeout' + ).and.callThrough(); + + jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL = 123456; // a distinctive value + + env = new jasmineUnderTest.Env(); + + env.describe('suite', function() { + env.it('spec', function() {}); + }); + + return env.execute(null).then(function() { + var timeoutIds = setTimeoutSpy.calls + .all() + .filter(function(call) { + return call.args[1] === 123456; + }) + .map(function(call) { + return call.returnValue; + }); + + expect(timeoutIds.length).toBeGreaterThan(0); + + timeoutIds.forEach(function(timeoutId) { + expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutId); + }); + }); + }); + + it('is resolved even if specs fail', function() { + jasmine.getEnv().requirePromises(); + env.describe('suite', function() { + env.it('spec', function() { + env.expect(true).toBe(false); + }); + }); + + return expectAsync(env.execute(null)).toBeResolved(); + }); + }); + describe('The optional callback argument to #execute', function() { beforeEach(function() { this.savedInterval = jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL; diff --git a/spec/helpers/promises.js b/spec/helpers/promises.js index a194af0c..208a9f70 100644 --- a/spec/helpers/promises.js +++ b/spec/helpers/promises.js @@ -4,4 +4,10 @@ env.pending('Environment does not support promises'); } }; + + env.requireNoPromises = function() { + if (typeof Promise === 'function') { + env.pending('Environment supports promises'); + } + }; })(jasmine.getEnv()); diff --git a/src/core/Env.js b/src/core/Env.js index 23dd507d..5f34ac2a 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -909,11 +909,17 @@ getJasmineRequireObj().Env = function(j$) { * * execute should not be called more than once. * + * If the environment supports promises, execute will return a promise that + * is resolved after the suite finishes executing. The promise will be + * resolved (not rejected) as long as the suite runs to completion. Use a + * {@link Reporter} to determine whether or not the suite passed. + * * @name Env#execute * @since 2.0.0 * @function * @param {(string[])=} runnablesToRun IDs of suites and/or specs to run * @param {Function=} onComplete Function that will be called after all specs have run + * @return {Promise} */ this.execute = function(runnablesToRun, onComplete) { installGlobalErrors(); @@ -973,65 +979,86 @@ getJasmineRequireObj().Env = function(j$) { var jasmineTimer = new j$.Timer(); jasmineTimer.start(); - /** - * Information passed to the {@link Reporter#jasmineStarted} event. - * @typedef JasmineStartedInfo - * @property {Int} totalSpecsDefined - The total number of specs defined in this suite. - * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. - */ - reporter.jasmineStarted( - { - totalSpecsDefined: totalSpecsDefined, - order: order - }, - function() { - currentlyExecutingSuites.push(topSuite); + var Promise = customPromise || global.Promise; - processor.execute(function() { - clearResourcesForRunnable(topSuite.id); - currentlyExecutingSuites.pop(); - var overallStatus, incompleteReason; - - if (hasFailures || topSuite.result.failedExpectations.length > 0) { - overallStatus = 'failed'; - } else if (focusedRunnables.length > 0) { - overallStatus = 'incomplete'; - incompleteReason = 'fit() or fdescribe() was found'; - } else if (totalSpecsDefined === 0) { - overallStatus = 'incomplete'; - incompleteReason = 'No specs found'; - } else { - overallStatus = 'passed'; + if (Promise) { + return new Promise(function(resolve) { + runAll(function() { + if (onComplete) { + onComplete(); } - /** - * Information passed to the {@link Reporter#jasmineDone} event. - * @typedef JasmineDoneInfo - * @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'. - * @property {Int} totalTime - The total time (in ms) that it took to execute the suite - * @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete. - * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. - * @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level. - * @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level. - */ - reporter.jasmineDone( - { - overallStatus: overallStatus, - totalTime: jasmineTimer.elapsed(), - incompleteReason: incompleteReason, - order: order, - failedExpectations: topSuite.result.failedExpectations, - deprecationWarnings: topSuite.result.deprecationWarnings - }, - function() { - if (onComplete) { - onComplete(); - } - } - ); + resolve(); }); - } - ); + }); + } else { + runAll(function() { + if (onComplete) { + onComplete(); + } + }); + } + + function runAll(done) { + /** + * Information passed to the {@link Reporter#jasmineStarted} event. + * @typedef JasmineStartedInfo + * @property {Int} totalSpecsDefined - The total number of specs defined in this suite. + * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. + */ + reporter.jasmineStarted( + { + totalSpecsDefined: totalSpecsDefined, + order: order + }, + function() { + currentlyExecutingSuites.push(topSuite); + + processor.execute(function() { + clearResourcesForRunnable(topSuite.id); + currentlyExecutingSuites.pop(); + var overallStatus, incompleteReason; + + if ( + hasFailures || + topSuite.result.failedExpectations.length > 0 + ) { + overallStatus = 'failed'; + } else if (focusedRunnables.length > 0) { + overallStatus = 'incomplete'; + incompleteReason = 'fit() or fdescribe() was found'; + } else if (totalSpecsDefined === 0) { + overallStatus = 'incomplete'; + incompleteReason = 'No specs found'; + } else { + overallStatus = 'passed'; + } + + /** + * Information passed to the {@link Reporter#jasmineDone} event. + * @typedef JasmineDoneInfo + * @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'. + * @property {Int} totalTime - The total time (in ms) that it took to execute the suite + * @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete. + * @property {Order} order - Information about the ordering (random or not) of this execution of the suite. + * @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level. + * @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level. + */ + reporter.jasmineDone( + { + overallStatus: overallStatus, + totalTime: jasmineTimer.elapsed(), + incompleteReason: incompleteReason, + order: order, + failedExpectations: topSuite.result.failedExpectations, + deprecationWarnings: topSuite.result.deprecationWarnings + }, + done + ); + }); + } + ); + } }; /** diff --git a/src/core/GlobalErrors.js b/src/core/GlobalErrors.js index b0c6e20e..d96e26c8 100644 --- a/src/core/GlobalErrors.js +++ b/src/core/GlobalErrors.js @@ -19,16 +19,22 @@ getJasmineRequireObj().GlobalErrors = function(j$) { function taggedOnError(error) { var substituteMsg; - if (error) { + if (j$.isError_(error)) { error.jasmineMessage = jasmineMessage + ': ' + error; } else { - substituteMsg = jasmineMessage + ' with no error or message'; + if (error) { + substituteMsg = jasmineMessage + ': ' + error; + } else { + substituteMsg = jasmineMessage + ' with no error or message'; + } if (errorType === 'unhandledRejection') { substituteMsg += '\n' + '(Tip: to get a useful stack trace, use ' + - 'Promise.reject(new Error(...)) instead of Promise.reject().)'; + 'Promise.reject(new Error(...)) instead of Promise.reject(' + + (error ? '...' : '') + + ').)'; } error = new Error(substituteMsg);