diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index bed18d89..4502009a 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -1817,11 +1817,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(); @@ -1881,65 +1887,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/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index d8a3d699..2134dd17 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -465,4 +465,25 @@ describe('Env', function() { done(); }); }); + + 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() {}; + + 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/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index c90a19c1..8224d3a7 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -3038,6 +3038,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 18d1157d..063c4607 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -807,11 +807,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(); @@ -871,65 +877,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 + ); + }); + } + ); + } }; /**