diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 4b2bd8d2..fded7360 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -91,6 +91,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { ); j$.ReportDispatcher = jRequire.ReportDispatcher(j$); j$.RunableResources = jRequire.RunableResources(j$); + j$.Runner = jRequire.Runner(j$); j$.Spec = jRequire.Spec(j$); j$.Spy = jRequire.Spy(j$); j$.SpyFactory = jRequire.SpyFactory(j$); @@ -791,7 +792,12 @@ getJasmineRequireObj().Spec = function(j$) { return this.asyncExpectationFactory(actual, this); }; - Spec.prototype.execute = function(queueRunnerFactory, onComplete, excluded, failSpecWithNoExp) { + Spec.prototype.execute = function( + queueRunnerFactory, + onComplete, + excluded, + failSpecWithNoExp + ) { const onStart = { fn: done => { this.timer.start(); @@ -1112,15 +1118,13 @@ getJasmineRequireObj().Env = function(j$) { ); const runableResources = new j$.RunableResources(function() { - const r = currentRunable(); + const r = runner.currentRunable(); return r ? r.id : null; }); - let currentSpec = null; - const currentlyExecutingSuites = []; - let hasFailures = false; let reporter; let topSuite; + let runner; /** * This represents the available options to configure Jasmine. @@ -1222,14 +1226,6 @@ getJasmineRequireObj().Env = function(j$) { verboseDeprecations: false }; - function currentSuite() { - return currentlyExecutingSuites[currentlyExecutingSuites.length - 1]; - } - - function currentRunable() { - return currentSpec || currentSuite(); - } - let globalErrors = null; function installGlobalErrors() { @@ -1401,7 +1397,7 @@ getJasmineRequireObj().Env = function(j$) { function routeLateFailure(expectationResult) { // Report the result on the nearest ancestor suite that hasn't already // been reported done. - for (let r = currentRunable(); r; r = r.parentSuite) { + for (let r = runner.currentRunable(); r; r = r.parentSuite) { if (!r.reportedDone) { if (r === topSuite) { expectationResult.globalErrorType = 'lateError'; @@ -1427,7 +1423,7 @@ getJasmineRequireObj().Env = function(j$) { }); function addExpectationResult(passed, result) { - if (currentRunable() !== spec) { + if (runner.currentRunable() !== spec) { recordLateExpectation(spec, runableType, result); } return spec.addExpectationResult(passed, result); @@ -1457,7 +1453,7 @@ getJasmineRequireObj().Env = function(j$) { * @param {Object} [options] Optional extra options, as described above */ this.deprecated = function(deprecation, options) { - const runable = currentRunable() || topSuite; + const runable = runner.currentRunable() || topSuite; deprecator.addDeprecationWarning(runable, deprecation, options); }; @@ -1487,7 +1483,7 @@ getJasmineRequireObj().Env = function(j$) { options.onException = options.onException || function(e) { - (currentRunable() || topSuite).handleException(e); + (runner.currentRunable() || topSuite).handleException(e); }; options.deprecated = self.deprecated; @@ -1594,6 +1590,17 @@ getJasmineRequireObj().Env = function(j$) { recordLateError ); + runner = new j$.Runner({ + topSuite, + totalSpecsDefined: () => suiteBuilder.totalSpecsDefined, + focusedRunables: () => suiteBuilder.focusedRunables, + runableResources, + reporter, + queueRunnerFactory, + getConfig: () => config, + reportSpecDone + }); + /** * Executes the specs. * @@ -1626,195 +1633,15 @@ getJasmineRequireObj().Env = function(j$) { * @return {Promise} */ this.execute = function(runablesToRun, onComplete) { - if (this._executedBefore) { - topSuite.reset(); - } - this._executedBefore = true; - runableResources.initForRunable(topSuite.id); installGlobalErrors(); - if (!runablesToRun) { - if (suiteBuilder.focusedRunables.length) { - runablesToRun = suiteBuilder.focusedRunables; - } else { - runablesToRun = [topSuite.id]; + return runner.execute(runablesToRun).then(function(jasmineDoneInfo) { + if (onComplete) { + onComplete(); } - } - const order = new j$.Order({ - random: config.random, - seed: config.seed + return jasmineDoneInfo; }); - - const processor = new j$.TreeProcessor({ - tree: topSuite, - runnableIds: runablesToRun, - queueRunnerFactory: queueRunnerFactory, - failSpecWithNoExpectations: config.failSpecWithNoExpectations, - nodeStart: function(suite, next) { - currentlyExecutingSuites.push(suite); - runableResources.initForRunable(suite.id, suite.parentSuite.id); - reporter.suiteStarted(suite.result, next); - suite.startTimer(); - }, - nodeComplete: function(suite, result, next) { - if (suite !== currentSuite()) { - throw new Error('Tried to complete the wrong suite'); - } - - runableResources.clearForRunable(suite.id); - currentlyExecutingSuites.pop(); - - if (result.status === 'failed') { - hasFailures = true; - } - suite.endTimer(); - - if (suite.hadBeforeAllFailure) { - reportChildrenOfBeforeAllFailure(suite).then(function() { - reportSuiteDone(suite, result, next); - }); - } else { - reportSuiteDone(suite, result, next); - } - }, - orderChildren: function(node) { - return order.sort(node.children); - }, - excludeNode: function(spec) { - return !config.specFilter(spec); - } - }); - - if (!processor.processTree().valid) { - throw new Error( - 'Invalid order: would cause a beforeAll or afterAll to be run multiple times' - ); - } - - const jasmineTimer = new j$.Timer(); - jasmineTimer.start(); - - return new Promise(function(resolve) { - runAll(function(jasmineDoneInfo) { - if (onComplete) { - onComplete(); - } - - resolve(jasmineDoneInfo); - }); - }); - - 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. - * @since 2.0.0 - */ - reporter.jasmineStarted( - { - totalSpecsDefined: suiteBuilder.totalSpecsDefined, - order: order - }, - function() { - currentlyExecutingSuites.push(topSuite); - - processor.execute(function() { - (async function() { - if (topSuite.hadBeforeAllFailure) { - await reportChildrenOfBeforeAllFailure(topSuite); - } - - runableResources.clearForRunable(topSuite.id); - currentlyExecutingSuites.pop(); - let overallStatus, incompleteReason; - - if ( - hasFailures || - topSuite.result.failedExpectations.length > 0 - ) { - overallStatus = 'failed'; - } else if (suiteBuilder.focusedRunables.length > 0) { - overallStatus = 'incomplete'; - incompleteReason = 'fit() or fdescribe() was found'; - } else if (suiteBuilder.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. - * @since 2.4.0 - */ - const jasmineDoneInfo = { - overallStatus: overallStatus, - totalTime: jasmineTimer.elapsed(), - incompleteReason: incompleteReason, - order: order, - failedExpectations: topSuite.result.failedExpectations, - deprecationWarnings: topSuite.result.deprecationWarnings - }; - topSuite.reportedDone = true; - reporter.jasmineDone(jasmineDoneInfo, function() { - done(jasmineDoneInfo); - }); - })(); - }); - } - ); - } - - async function reportChildrenOfBeforeAllFailure(suite) { - for (const child of suite.children) { - if (child instanceof j$.Suite) { - await new Promise(function(resolve) { - reporter.suiteStarted(child.result, resolve); - }); - await reportChildrenOfBeforeAllFailure(child); - - // Marking the suite passed is consistent with how suites that - // contain failed specs but no suite-level failures are reported. - child.result.status = 'passed'; - - await new Promise(function(resolve) { - reporter.suiteDone(child.result, resolve); - }); - } else { - /* a spec */ - await new Promise(function(resolve) { - reporter.specStarted(child.result, resolve); - }); - - child.addExpectationResult( - false, - { - passed: false, - message: - 'Not run because a beforeAll function failed. The ' + - 'beforeAll failure will be reported on the suite that ' + - 'caused it.' - }, - true - ); - child.result.status = 'failed'; - - await new Promise(function(resolve) { - reportSpecDone(child, child.result, resolve); - }); - } - } - } }; /** @@ -1898,7 +1725,7 @@ getJasmineRequireObj().Env = function(j$) { }; function ensureIsNotNested(method) { - const runable = currentRunable(); + const runable = runner.currentRunable(); if (runable !== null && runable !== undefined) { throw new Error( "'" + method + "' should only be used in 'describe' function" @@ -1923,17 +1750,17 @@ getJasmineRequireObj().Env = function(j$) { function specResultCallback(spec, result, next) { runableResources.clearForRunable(spec.id); - currentSpec = null; + runner.currentSpec = null; if (result.status === 'failed') { - hasFailures = true; + runner.hasFailures = true; } reportSpecDone(spec, result, next); } function specStarted(spec, suite, next) { - currentSpec = spec; + runner.currentSpec = spec; runableResources.initForRunable(spec.id, suite.id); reporter.specStarted(spec.result, next); } @@ -1943,11 +1770,6 @@ getJasmineRequireObj().Env = function(j$) { reporter.specDone(result, next); } - function reportSuiteDone(suite, result, next) { - suite.reportedDone = true; - reporter.suiteDone(result, next); - } - this.it = function(description, fn, timeout) { ensureIsNotNested('it'); return suiteBuilder.it(description, fn, timeout).metadata; @@ -1972,12 +1794,15 @@ getJasmineRequireObj().Env = function(j$) { * @param {*} value The value of the property */ this.setSpecProperty = function(key, value) { - if (!currentRunable() || currentRunable() == currentSuite()) { + if ( + !runner.currentRunable() || + runner.currentRunable() == runner.currentSuite() + ) { throw new Error( "'setSpecProperty' was used when there was no current spec" ); } - currentRunable().setSpecProperty(key, value); + runner.currentRunable().setSpecProperty(key, value); }; /** @@ -1989,16 +1814,16 @@ getJasmineRequireObj().Env = function(j$) { * @param {*} value The value of the property */ this.setSuiteProperty = function(key, value) { - if (!currentSuite()) { + if (!runner.currentSuite()) { throw new Error( "'setSuiteProperty' was used when there was no current suite" ); } - currentSuite().setSuiteProperty(key, value); + runner.currentSuite().setSuiteProperty(key, value); }; this.debugLog = function(msg) { - const maybeSpec = currentRunable(); + const maybeSpec = runner.currentRunable(); if (!maybeSpec || !maybeSpec.debugLog) { throw new Error("'debugLog' was called when there was no current spec"); @@ -2008,23 +1833,23 @@ getJasmineRequireObj().Env = function(j$) { }; this.expect = function(actual) { - if (!currentRunable()) { + if (!runner.currentRunable()) { throw new Error( "'expect' was used when there was no current spec, this could be because an asynchronous test timed out" ); } - return currentRunable().expect(actual); + return runner.currentRunable().expect(actual); }; this.expectAsync = function(actual) { - if (!currentRunable()) { + if (!runner.currentRunable()) { throw new Error( "'expectAsync' was used when there was no current spec, this could be because an asynchronous test timed out" ); } - return currentRunable().expectAsync(actual); + return runner.currentRunable().expectAsync(actual); }; this.beforeEach = function(beforeEachFunction, timeout) { @@ -2056,7 +1881,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.fail = function(error) { - if (!currentRunable()) { + if (!runner.currentRunable()) { throw new Error( "'fail' was used when there was no current spec, this could be because an asynchronous test timed out" ); @@ -2076,7 +1901,7 @@ getJasmineRequireObj().Env = function(j$) { } } - currentRunable().addExpectationResult(false, { + runner.currentRunable().addExpectationResult(false, { matcherName: '', passed: false, expected: '', @@ -8422,6 +8247,233 @@ getJasmineRequireObj().RunableResources = function(j$) { return RunableResources; }; +getJasmineRequireObj().Runner = function(j$) { + class Runner { + constructor(options) { + this.topSuite_ = options.topSuite; + this.totalSpecsDefined_ = options.totalSpecsDefined; + this.focusedRunables_ = options.focusedRunables; + this.runableResources_ = options.runableResources; + this.queueRunnerFactory_ = options.queueRunnerFactory; + this.reporter_ = options.reporter; + this.getConfig_ = options.getConfig; + this.reportSpecDone_ = options.reportSpecDone; + this.hasFailures = false; + this.executedBefore_ = false; + + this.currentlyExecutingSuites_ = []; + this.currentSpec = null; + } + + currentRunable() { + return this.currentSpec || this.currentSuite(); + } + + currentSuite() { + return this.currentlyExecutingSuites_[ + this.currentlyExecutingSuites_.length - 1 + ]; + } + + // Although execute returns a promise, it isn't async for backwards + // compatibility: The "Invalid order" exception needs to be propagated + // synchronously from Env#execute. + // TODO: make this and Env#execute async in the next major release + execute(runablesToRun) { + if (this.executedBefore_) { + this.topSuite_.reset(); + } + this.executedBefore_ = true; + + this.hasFailures = false; + const totalSpecsDefined = this.totalSpecsDefined_(); + const focusedRunables = this.focusedRunables_(); + const config = this.getConfig_(); + + if (!runablesToRun) { + if (focusedRunables.length) { + runablesToRun = focusedRunables; + } else { + runablesToRun = [this.topSuite_.id]; + } + } + + const order = new j$.Order({ + random: config.random, + seed: config.seed + }); + + const processor = new j$.TreeProcessor({ + tree: this.topSuite_, + runnableIds: runablesToRun, + queueRunnerFactory: this.queueRunnerFactory_, + failSpecWithNoExpectations: config.failSpecWithNoExpectations, + nodeStart: (suite, next) => { + this.currentlyExecutingSuites_.push(suite); + this.runableResources_.initForRunable(suite.id, suite.parentSuite.id); + this.reporter_.suiteStarted(suite.result, next); + suite.startTimer(); + }, + nodeComplete: (suite, result, next) => { + if (suite !== this.currentSuite()) { + throw new Error('Tried to complete the wrong suite'); + } + + this.runableResources_.clearForRunable(suite.id); + this.currentlyExecutingSuites_.pop(); + + if (result.status === 'failed') { + this.hasFailures = true; + } + suite.endTimer(); + + if (suite.hadBeforeAllFailure) { + this.reportChildrenOfBeforeAllFailure_(suite).then(() => { + this.reportSuiteDone_(suite, result, next); + }); + } else { + this.reportSuiteDone_(suite, result, next); + } + }, + orderChildren: function(node) { + return order.sort(node.children); + }, + excludeNode: function(spec) { + return !config.specFilter(spec); + } + }); + + if (!processor.processTree().valid) { + throw new Error( + 'Invalid order: would cause a beforeAll or afterAll to be run multiple times' + ); + } + + this.runableResources_.initForRunable(this.topSuite_.id); + const jasmineTimer = new j$.Timer(); + jasmineTimer.start(); + + return new Promise(resolve => { + /** + * 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. + * @since 2.0.0 + */ + this.reporter_.jasmineStarted( + { + totalSpecsDefined, + order: order + }, + () => { + this.currentlyExecutingSuites_.push(this.topSuite_); + + processor.execute(() => { + (async () => { + if (this.topSuite_.hadBeforeAllFailure) { + await this.reportChildrenOfBeforeAllFailure_(this.topSuite_); + } + + this.runableResources_.clearForRunable(this.topSuite_.id); + this.currentlyExecutingSuites_.pop(); + let overallStatus, incompleteReason; + + if ( + this.hasFailures || + this.topSuite_.result.failedExpectations.length > 0 + ) { + overallStatus = 'failed'; + } else if (focusedRunables.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. + * @since 2.4.0 + */ + const jasmineDoneInfo = { + overallStatus: overallStatus, + totalTime: jasmineTimer.elapsed(), + incompleteReason: incompleteReason, + order: order, + failedExpectations: this.topSuite_.result.failedExpectations, + deprecationWarnings: this.topSuite_.result.deprecationWarnings + }; + this.topSuite_.reportedDone = true; + this.reporter_.jasmineDone(jasmineDoneInfo, function() { + resolve(jasmineDoneInfo); + }); + })(); + }); + } + ); + }); + } + + reportSuiteDone_(suite, result, next) { + suite.reportedDone = true; + this.reporter_.suiteDone(result, next); + } + + async reportChildrenOfBeforeAllFailure_(suite) { + for (const child of suite.children) { + if (child instanceof j$.Suite) { + await new Promise(resolve => { + this.reporter_.suiteStarted(child.result, resolve); + }); + await this.reportChildrenOfBeforeAllFailure_(child); + + // Marking the suite passed is consistent with how suites that + // contain failed specs but no suite-level failures are reported. + child.result.status = 'passed'; + + await new Promise(resolve => { + this.reporter_.suiteDone(child.result, resolve); + }); + } else { + /* a spec */ + await new Promise(resolve => { + this.reporter_.specStarted(child.result, resolve); + }); + + child.addExpectationResult( + false, + { + passed: false, + message: + 'Not run because a beforeAll function failed. The ' + + 'beforeAll failure will be reported on the suite that ' + + 'caused it.' + }, + true + ); + child.result.status = 'failed'; + + await new Promise(resolve => { + this.reportSpecDone_(child, child.result, resolve); + }); + } + } + } + } + + return Runner; +}; + getJasmineRequireObj().SkipAfterBeforeAllErrorPolicy = function(j$) { function SkipAfterBeforeAllErrorPolicy(queueableFns) { this.queueableFns_ = queueableFns; diff --git a/src/core/Env.js b/src/core/Env.js index 3680ce8a..c5490b55 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -25,15 +25,13 @@ getJasmineRequireObj().Env = function(j$) { ); const runableResources = new j$.RunableResources(function() { - const r = currentRunable(); + const r = runner.currentRunable(); return r ? r.id : null; }); - let currentSpec = null; - const currentlyExecutingSuites = []; - let hasFailures = false; let reporter; let topSuite; + let runner; /** * This represents the available options to configure Jasmine. @@ -135,14 +133,6 @@ getJasmineRequireObj().Env = function(j$) { verboseDeprecations: false }; - function currentSuite() { - return currentlyExecutingSuites[currentlyExecutingSuites.length - 1]; - } - - function currentRunable() { - return currentSpec || currentSuite(); - } - let globalErrors = null; function installGlobalErrors() { @@ -314,7 +304,7 @@ getJasmineRequireObj().Env = function(j$) { function routeLateFailure(expectationResult) { // Report the result on the nearest ancestor suite that hasn't already // been reported done. - for (let r = currentRunable(); r; r = r.parentSuite) { + for (let r = runner.currentRunable(); r; r = r.parentSuite) { if (!r.reportedDone) { if (r === topSuite) { expectationResult.globalErrorType = 'lateError'; @@ -340,7 +330,7 @@ getJasmineRequireObj().Env = function(j$) { }); function addExpectationResult(passed, result) { - if (currentRunable() !== spec) { + if (runner.currentRunable() !== spec) { recordLateExpectation(spec, runableType, result); } return spec.addExpectationResult(passed, result); @@ -370,7 +360,7 @@ getJasmineRequireObj().Env = function(j$) { * @param {Object} [options] Optional extra options, as described above */ this.deprecated = function(deprecation, options) { - const runable = currentRunable() || topSuite; + const runable = runner.currentRunable() || topSuite; deprecator.addDeprecationWarning(runable, deprecation, options); }; @@ -400,7 +390,7 @@ getJasmineRequireObj().Env = function(j$) { options.onException = options.onException || function(e) { - (currentRunable() || topSuite).handleException(e); + (runner.currentRunable() || topSuite).handleException(e); }; options.deprecated = self.deprecated; @@ -507,6 +497,17 @@ getJasmineRequireObj().Env = function(j$) { recordLateError ); + runner = new j$.Runner({ + topSuite, + totalSpecsDefined: () => suiteBuilder.totalSpecsDefined, + focusedRunables: () => suiteBuilder.focusedRunables, + runableResources, + reporter, + queueRunnerFactory, + getConfig: () => config, + reportSpecDone + }); + /** * Executes the specs. * @@ -539,195 +540,15 @@ getJasmineRequireObj().Env = function(j$) { * @return {Promise} */ this.execute = function(runablesToRun, onComplete) { - if (this._executedBefore) { - topSuite.reset(); - } - this._executedBefore = true; - runableResources.initForRunable(topSuite.id); installGlobalErrors(); - if (!runablesToRun) { - if (suiteBuilder.focusedRunables.length) { - runablesToRun = suiteBuilder.focusedRunables; - } else { - runablesToRun = [topSuite.id]; + return runner.execute(runablesToRun).then(function(jasmineDoneInfo) { + if (onComplete) { + onComplete(); } - } - const order = new j$.Order({ - random: config.random, - seed: config.seed + return jasmineDoneInfo; }); - - const processor = new j$.TreeProcessor({ - tree: topSuite, - runnableIds: runablesToRun, - queueRunnerFactory: queueRunnerFactory, - failSpecWithNoExpectations: config.failSpecWithNoExpectations, - nodeStart: function(suite, next) { - currentlyExecutingSuites.push(suite); - runableResources.initForRunable(suite.id, suite.parentSuite.id); - reporter.suiteStarted(suite.result, next); - suite.startTimer(); - }, - nodeComplete: function(suite, result, next) { - if (suite !== currentSuite()) { - throw new Error('Tried to complete the wrong suite'); - } - - runableResources.clearForRunable(suite.id); - currentlyExecutingSuites.pop(); - - if (result.status === 'failed') { - hasFailures = true; - } - suite.endTimer(); - - if (suite.hadBeforeAllFailure) { - reportChildrenOfBeforeAllFailure(suite).then(function() { - reportSuiteDone(suite, result, next); - }); - } else { - reportSuiteDone(suite, result, next); - } - }, - orderChildren: function(node) { - return order.sort(node.children); - }, - excludeNode: function(spec) { - return !config.specFilter(spec); - } - }); - - if (!processor.processTree().valid) { - throw new Error( - 'Invalid order: would cause a beforeAll or afterAll to be run multiple times' - ); - } - - const jasmineTimer = new j$.Timer(); - jasmineTimer.start(); - - return new Promise(function(resolve) { - runAll(function(jasmineDoneInfo) { - if (onComplete) { - onComplete(); - } - - resolve(jasmineDoneInfo); - }); - }); - - 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. - * @since 2.0.0 - */ - reporter.jasmineStarted( - { - totalSpecsDefined: suiteBuilder.totalSpecsDefined, - order: order - }, - function() { - currentlyExecutingSuites.push(topSuite); - - processor.execute(function() { - (async function() { - if (topSuite.hadBeforeAllFailure) { - await reportChildrenOfBeforeAllFailure(topSuite); - } - - runableResources.clearForRunable(topSuite.id); - currentlyExecutingSuites.pop(); - let overallStatus, incompleteReason; - - if ( - hasFailures || - topSuite.result.failedExpectations.length > 0 - ) { - overallStatus = 'failed'; - } else if (suiteBuilder.focusedRunables.length > 0) { - overallStatus = 'incomplete'; - incompleteReason = 'fit() or fdescribe() was found'; - } else if (suiteBuilder.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. - * @since 2.4.0 - */ - const jasmineDoneInfo = { - overallStatus: overallStatus, - totalTime: jasmineTimer.elapsed(), - incompleteReason: incompleteReason, - order: order, - failedExpectations: topSuite.result.failedExpectations, - deprecationWarnings: topSuite.result.deprecationWarnings - }; - topSuite.reportedDone = true; - reporter.jasmineDone(jasmineDoneInfo, function() { - done(jasmineDoneInfo); - }); - })(); - }); - } - ); - } - - async function reportChildrenOfBeforeAllFailure(suite) { - for (const child of suite.children) { - if (child instanceof j$.Suite) { - await new Promise(function(resolve) { - reporter.suiteStarted(child.result, resolve); - }); - await reportChildrenOfBeforeAllFailure(child); - - // Marking the suite passed is consistent with how suites that - // contain failed specs but no suite-level failures are reported. - child.result.status = 'passed'; - - await new Promise(function(resolve) { - reporter.suiteDone(child.result, resolve); - }); - } else { - /* a spec */ - await new Promise(function(resolve) { - reporter.specStarted(child.result, resolve); - }); - - child.addExpectationResult( - false, - { - passed: false, - message: - 'Not run because a beforeAll function failed. The ' + - 'beforeAll failure will be reported on the suite that ' + - 'caused it.' - }, - true - ); - child.result.status = 'failed'; - - await new Promise(function(resolve) { - reportSpecDone(child, child.result, resolve); - }); - } - } - } }; /** @@ -811,7 +632,7 @@ getJasmineRequireObj().Env = function(j$) { }; function ensureIsNotNested(method) { - const runable = currentRunable(); + const runable = runner.currentRunable(); if (runable !== null && runable !== undefined) { throw new Error( "'" + method + "' should only be used in 'describe' function" @@ -836,17 +657,17 @@ getJasmineRequireObj().Env = function(j$) { function specResultCallback(spec, result, next) { runableResources.clearForRunable(spec.id); - currentSpec = null; + runner.currentSpec = null; if (result.status === 'failed') { - hasFailures = true; + runner.hasFailures = true; } reportSpecDone(spec, result, next); } function specStarted(spec, suite, next) { - currentSpec = spec; + runner.currentSpec = spec; runableResources.initForRunable(spec.id, suite.id); reporter.specStarted(spec.result, next); } @@ -856,11 +677,6 @@ getJasmineRequireObj().Env = function(j$) { reporter.specDone(result, next); } - function reportSuiteDone(suite, result, next) { - suite.reportedDone = true; - reporter.suiteDone(result, next); - } - this.it = function(description, fn, timeout) { ensureIsNotNested('it'); return suiteBuilder.it(description, fn, timeout).metadata; @@ -885,12 +701,15 @@ getJasmineRequireObj().Env = function(j$) { * @param {*} value The value of the property */ this.setSpecProperty = function(key, value) { - if (!currentRunable() || currentRunable() == currentSuite()) { + if ( + !runner.currentRunable() || + runner.currentRunable() == runner.currentSuite() + ) { throw new Error( "'setSpecProperty' was used when there was no current spec" ); } - currentRunable().setSpecProperty(key, value); + runner.currentRunable().setSpecProperty(key, value); }; /** @@ -902,16 +721,16 @@ getJasmineRequireObj().Env = function(j$) { * @param {*} value The value of the property */ this.setSuiteProperty = function(key, value) { - if (!currentSuite()) { + if (!runner.currentSuite()) { throw new Error( "'setSuiteProperty' was used when there was no current suite" ); } - currentSuite().setSuiteProperty(key, value); + runner.currentSuite().setSuiteProperty(key, value); }; this.debugLog = function(msg) { - const maybeSpec = currentRunable(); + const maybeSpec = runner.currentRunable(); if (!maybeSpec || !maybeSpec.debugLog) { throw new Error("'debugLog' was called when there was no current spec"); @@ -921,23 +740,23 @@ getJasmineRequireObj().Env = function(j$) { }; this.expect = function(actual) { - if (!currentRunable()) { + if (!runner.currentRunable()) { throw new Error( "'expect' was used when there was no current spec, this could be because an asynchronous test timed out" ); } - return currentRunable().expect(actual); + return runner.currentRunable().expect(actual); }; this.expectAsync = function(actual) { - if (!currentRunable()) { + if (!runner.currentRunable()) { throw new Error( "'expectAsync' was used when there was no current spec, this could be because an asynchronous test timed out" ); } - return currentRunable().expectAsync(actual); + return runner.currentRunable().expectAsync(actual); }; this.beforeEach = function(beforeEachFunction, timeout) { @@ -969,7 +788,7 @@ getJasmineRequireObj().Env = function(j$) { }; this.fail = function(error) { - if (!currentRunable()) { + if (!runner.currentRunable()) { throw new Error( "'fail' was used when there was no current spec, this could be because an asynchronous test timed out" ); @@ -989,7 +808,7 @@ getJasmineRequireObj().Env = function(j$) { } } - currentRunable().addExpectationResult(false, { + runner.currentRunable().addExpectationResult(false, { matcherName: '', passed: false, expected: '', diff --git a/src/core/Runner.js b/src/core/Runner.js new file mode 100644 index 00000000..e566d73e --- /dev/null +++ b/src/core/Runner.js @@ -0,0 +1,226 @@ +getJasmineRequireObj().Runner = function(j$) { + class Runner { + constructor(options) { + this.topSuite_ = options.topSuite; + this.totalSpecsDefined_ = options.totalSpecsDefined; + this.focusedRunables_ = options.focusedRunables; + this.runableResources_ = options.runableResources; + this.queueRunnerFactory_ = options.queueRunnerFactory; + this.reporter_ = options.reporter; + this.getConfig_ = options.getConfig; + this.reportSpecDone_ = options.reportSpecDone; + this.hasFailures = false; + this.executedBefore_ = false; + + this.currentlyExecutingSuites_ = []; + this.currentSpec = null; + } + + currentRunable() { + return this.currentSpec || this.currentSuite(); + } + + currentSuite() { + return this.currentlyExecutingSuites_[ + this.currentlyExecutingSuites_.length - 1 + ]; + } + + // Although execute returns a promise, it isn't async for backwards + // compatibility: The "Invalid order" exception needs to be propagated + // synchronously from Env#execute. + // TODO: make this and Env#execute async in the next major release + execute(runablesToRun) { + if (this.executedBefore_) { + this.topSuite_.reset(); + } + this.executedBefore_ = true; + + this.hasFailures = false; + const totalSpecsDefined = this.totalSpecsDefined_(); + const focusedRunables = this.focusedRunables_(); + const config = this.getConfig_(); + + if (!runablesToRun) { + if (focusedRunables.length) { + runablesToRun = focusedRunables; + } else { + runablesToRun = [this.topSuite_.id]; + } + } + + const order = new j$.Order({ + random: config.random, + seed: config.seed + }); + + const processor = new j$.TreeProcessor({ + tree: this.topSuite_, + runnableIds: runablesToRun, + queueRunnerFactory: this.queueRunnerFactory_, + failSpecWithNoExpectations: config.failSpecWithNoExpectations, + nodeStart: (suite, next) => { + this.currentlyExecutingSuites_.push(suite); + this.runableResources_.initForRunable(suite.id, suite.parentSuite.id); + this.reporter_.suiteStarted(suite.result, next); + suite.startTimer(); + }, + nodeComplete: (suite, result, next) => { + if (suite !== this.currentSuite()) { + throw new Error('Tried to complete the wrong suite'); + } + + this.runableResources_.clearForRunable(suite.id); + this.currentlyExecutingSuites_.pop(); + + if (result.status === 'failed') { + this.hasFailures = true; + } + suite.endTimer(); + + if (suite.hadBeforeAllFailure) { + this.reportChildrenOfBeforeAllFailure_(suite).then(() => { + this.reportSuiteDone_(suite, result, next); + }); + } else { + this.reportSuiteDone_(suite, result, next); + } + }, + orderChildren: function(node) { + return order.sort(node.children); + }, + excludeNode: function(spec) { + return !config.specFilter(spec); + } + }); + + if (!processor.processTree().valid) { + throw new Error( + 'Invalid order: would cause a beforeAll or afterAll to be run multiple times' + ); + } + + this.runableResources_.initForRunable(this.topSuite_.id); + const jasmineTimer = new j$.Timer(); + jasmineTimer.start(); + + return new Promise(resolve => { + /** + * 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. + * @since 2.0.0 + */ + this.reporter_.jasmineStarted( + { + totalSpecsDefined, + order: order + }, + () => { + this.currentlyExecutingSuites_.push(this.topSuite_); + + processor.execute(() => { + (async () => { + if (this.topSuite_.hadBeforeAllFailure) { + await this.reportChildrenOfBeforeAllFailure_(this.topSuite_); + } + + this.runableResources_.clearForRunable(this.topSuite_.id); + this.currentlyExecutingSuites_.pop(); + let overallStatus, incompleteReason; + + if ( + this.hasFailures || + this.topSuite_.result.failedExpectations.length > 0 + ) { + overallStatus = 'failed'; + } else if (focusedRunables.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. + * @since 2.4.0 + */ + const jasmineDoneInfo = { + overallStatus: overallStatus, + totalTime: jasmineTimer.elapsed(), + incompleteReason: incompleteReason, + order: order, + failedExpectations: this.topSuite_.result.failedExpectations, + deprecationWarnings: this.topSuite_.result.deprecationWarnings + }; + this.topSuite_.reportedDone = true; + this.reporter_.jasmineDone(jasmineDoneInfo, function() { + resolve(jasmineDoneInfo); + }); + })(); + }); + } + ); + }); + } + + reportSuiteDone_(suite, result, next) { + suite.reportedDone = true; + this.reporter_.suiteDone(result, next); + } + + async reportChildrenOfBeforeAllFailure_(suite) { + for (const child of suite.children) { + if (child instanceof j$.Suite) { + await new Promise(resolve => { + this.reporter_.suiteStarted(child.result, resolve); + }); + await this.reportChildrenOfBeforeAllFailure_(child); + + // Marking the suite passed is consistent with how suites that + // contain failed specs but no suite-level failures are reported. + child.result.status = 'passed'; + + await new Promise(resolve => { + this.reporter_.suiteDone(child.result, resolve); + }); + } else { + /* a spec */ + await new Promise(resolve => { + this.reporter_.specStarted(child.result, resolve); + }); + + child.addExpectationResult( + false, + { + passed: false, + message: + 'Not run because a beforeAll function failed. The ' + + 'beforeAll failure will be reported on the suite that ' + + 'caused it.' + }, + true + ); + child.result.status = 'failed'; + + await new Promise(resolve => { + this.reportSpecDone_(child, child.result, resolve); + }); + } + } + } + } + + return Runner; +}; diff --git a/src/core/requireCore.js b/src/core/requireCore.js index a628e6ea..dacd9557 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -69,6 +69,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) { ); j$.ReportDispatcher = jRequire.ReportDispatcher(j$); j$.RunableResources = jRequire.RunableResources(j$); + j$.Runner = jRequire.Runner(j$); j$.Spec = jRequire.Spec(j$); j$.Spy = jRequire.Spy(j$); j$.SpyFactory = jRequire.SpyFactory(j$);