diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index ef5c44b8..1d71abed 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -769,6 +769,8 @@ getJasmineRequireObj().Spec = function(j$) { return {}; }; this.onStart = attrs.onStart || function() {}; + this.autoCleanClosures = + attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures; this.getSpecName = attrs.getSpecName || function() { @@ -787,7 +789,7 @@ getJasmineRequireObj().Spec = function(j$) { this.timer = attrs.timer || new j$.Timer(); if (!this.queueableFn.fn) { - this.pend(); + this.exclude(); } /** @@ -857,7 +859,9 @@ getJasmineRequireObj().Spec = function(j$) { var complete = { fn: function(done) { - self.queueableFn.fn = null; + if (self.autoCleanClosures) { + self.queueableFn.fn = null; + } self.result.status = self.status(excluded, failSpecWithNoExp); self.result.duration = self.timer.elapsed(); @@ -914,6 +918,36 @@ getJasmineRequireObj().Spec = function(j$) { this.queueRunnerFactory(runnerConfig); }; + Spec.prototype.reset = function() { + /** + * @typedef SpecResult + * @property {Int} id - The unique id of this spec. + * @property {String} description - The description passed to the {@link it} that created this spec. + * @property {String} fullName - The full description including all ancestors of this spec. + * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec. + * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec. + * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec. + * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason. + * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec. + * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach. + * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty} + * @since 2.0.0 + */ + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [], + passedExpectations: [], + deprecationWarnings: [], + pendingReason: this.excludeMessage, + duration: null, + properties: null, + trace: null + }; + this.markedPending = this.markedExcluding; + }; + Spec.prototype.onException = function onException(e) { if (Spec.isPendingSpecException(e)) { this.pend(extractCustomPendingMessage(e)); @@ -937,6 +971,10 @@ getJasmineRequireObj().Spec = function(j$) { ); }; + /* + * Marks state as pending + * @param {string} [message] An optional reason message + */ Spec.prototype.pend = function(message) { this.markedPending = true; if (message) { @@ -944,6 +982,19 @@ getJasmineRequireObj().Spec = function(j$) { } }; + /* + * Like {@link Spec#pend}, but pending state will survive {@link Spec#reset} + * Useful for fit, xit, where pending state remains. + * @param {string} [message] An optional reason message + */ + Spec.prototype.exclude = function(message) { + this.markedExcluding = true; + if (this.message) { + this.excludeMessage = message; + } + this.pend(); + }; + Spec.prototype.getResult = function() { this.result.status = this.status(); return this.result; @@ -1235,6 +1286,15 @@ getJasmineRequireObj().Env = function(j$) { * property and always create native promises instead. */ Promise: undefined, + /** + * Clean closures when a suite is done running (done by clearing the stored function reference). + * This prevents memory leaks, but you won't be able to run jasmine multiple times. + * @name Configuration#autoCleanClosures + * @since 3.10.0 + * @type boolean + * @default true + */ + autoCleanClosures: true, /** * Whether or not to issue warnings for certain deprecated functionality * every time it's used. If not set or set to false, deprecation warnings @@ -1301,7 +1361,8 @@ getJasmineRequireObj().Env = function(j$) { 'failSpecWithNoExpectations', 'hideDisabled', 'stopOnSpecFailure', - 'stopSpecOnExpectationFailure' + 'stopSpecOnExpectationFailure', + 'autoCleanClosures' ]; booleanProps.forEach(function(prop) { @@ -1548,10 +1609,11 @@ getJasmineRequireObj().Env = function(j$) { delete runnableResources[id]; }; - var beforeAndAfterFns = function(suite) { + var beforeAndAfterFns = function(targetSuite) { return function() { var befores = [], - afters = []; + afters = [], + suite = targetSuite; while (suite) { befores = befores.concat(suite.beforeFns); @@ -1653,10 +1715,10 @@ getJasmineRequireObj().Env = function(j$) { expectationFactory: expectationFactory, asyncExpectationFactory: suiteAsyncExpectationFactory, expectationResultFactory: expectationResultFactory, + autoCleanClosures: config.autoCleanClosures, onLateError: recordLateError }); var deprecator = new j$.Deprecator(topSuite); - defaultResourcesForRunnable(topSuite.id); currentDeclarationSuite = topSuite; /** @@ -1775,6 +1837,11 @@ getJasmineRequireObj().Env = function(j$) { * @return {Promise} */ this.execute = function(runnablesToRun, onComplete) { + if (this._executedBefore) { + topSuite.reset(); + } + this._executedBefore = true; + defaultResourcesForRunnable(topSuite.id); installGlobalErrors(); if (!runnablesToRun) { @@ -2048,6 +2115,7 @@ getJasmineRequireObj().Env = function(j$) { asyncExpectationFactory: suiteAsyncExpectationFactory, expectationResultFactory: expectationResultFactory, throwOnExpectationFailure: config.stopSpecOnExpectationFailure, + autoCleanClosures: config.autoCleanClosures, onLateError: recordLateError }); @@ -2061,8 +2129,8 @@ getJasmineRequireObj().Env = function(j$) { if (specDefinitions.length > 0) { throw new Error('describe does not expect any arguments'); } - if (currentDeclarationSuite.markedPending) { - suite.pend(); + if (currentDeclarationSuite.markedExcluding) { + suite.exclude(); } addSpecsToSuite(suite, specDefinitions); if (suite.parentSuite && !suite.children.length) { @@ -2075,7 +2143,7 @@ getJasmineRequireObj().Env = function(j$) { ensureIsNotNested('xdescribe'); ensureIsFunction(specDefinitions, 'xdescribe'); var suite = suiteFactory(description); - suite.pend(); + suite.exclude(); addSpecsToSuite(suite, specDefinitions); return suite.metadata; }; @@ -2161,6 +2229,7 @@ getJasmineRequireObj().Env = function(j$) { timeout: timeout || 0 }, throwOnExpectationFailure: config.stopSpecOnExpectationFailure, + autoCleanClosures: config.autoCleanClosures, timer: new j$.Timer() }); return spec; @@ -2196,8 +2265,8 @@ getJasmineRequireObj().Env = function(j$) { } var spec = specFactory(description, fn, currentDeclarationSuite, timeout); - if (currentDeclarationSuite.markedPending) { - spec.pend(); + if (currentDeclarationSuite.markedExcluding) { + spec.exclude(); } currentDeclarationSuite.addChild(spec); @@ -2217,7 +2286,7 @@ getJasmineRequireObj().Env = function(j$) { ensureIsFunctionOrAsync(fn, 'xit'); } var spec = this.it_.apply(this, arguments); - spec.pend('Temporarily disabled with xit'); + spec.exclude('Temporarily disabled with xit'); return spec.metadata; }; @@ -9448,6 +9517,8 @@ getJasmineRequireObj().Suite = function(j$) { this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.expectationResultFactory = attrs.expectationResultFactory; this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; + this.autoCleanClosures = + attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures; this.onLateError = attrs.onLateError; this.beforeFns = []; @@ -9464,27 +9535,7 @@ getJasmineRequireObj().Suite = function(j$) { */ this.children = []; - /** - * @typedef SuiteResult - * @property {Int} id - The unique id of this suite. - * @property {String} description - The description text passed to the {@link describe} that made this suite. - * @property {String} fullName - The full description including all ancestors of this suite. - * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. - * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite. - * @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite. - * @property {number} duration - The time in ms for Suite execution, including any before/afterAll, before/afterEach. - * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSuiteProperty} - * @since 2.0.0 - */ - this.result = { - id: this.id, - description: this.description, - fullName: this.getFullName(), - failedExpectations: [], - deprecationWarnings: [], - duration: null, - properties: null - }; + this.reset(); } Suite.prototype.setSuiteProperty = function(key, value) { @@ -9521,10 +9572,22 @@ getJasmineRequireObj().Suite = function(j$) { return fullName.join(' '); }; + /* + * Mark the suite with "pending" status + */ Suite.prototype.pend = function() { this.markedPending = true; }; + /* + * Like {@link Suite#pend}, but pending state will survive {@link Spec#reset} + * Useful for fdescribe, xdescribe, where pending state should remain. + */ + Suite.prototype.exclude = function() { + this.pend(); + this.markedExcluding = true; + }; + Suite.prototype.beforeEach = function(fn) { this.beforeFns.unshift({ ...fn, suite: this }); }; @@ -9556,10 +9619,40 @@ getJasmineRequireObj().Suite = function(j$) { } Suite.prototype.cleanupBeforeAfter = function() { - removeFns(this.beforeAllFns); - removeFns(this.afterAllFns); - removeFns(this.beforeFns); - removeFns(this.afterFns); + if (this.autoCleanClosures) { + removeFns(this.beforeAllFns); + removeFns(this.afterAllFns); + removeFns(this.beforeFns); + removeFns(this.afterFns); + } + }; + + Suite.prototype.reset = function() { + /** + * @typedef SuiteResult + * @property {Int} id - The unique id of this suite. + * @property {String} description - The description text passed to the {@link describe} that made this suite. + * @property {String} fullName - The full description including all ancestors of this suite. + * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. + * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite. + * @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite. + * @property {number} duration - The time in ms for Suite execution, including any before/afterAll, before/afterEach. + * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSuiteProperty} + * @since 2.0.0 + */ + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [], + deprecationWarnings: [], + duration: null, + properties: null + }; + this.markedPending = this.markedExcluding; + this.children.forEach(function(child) { + child.reset(); + }); }; Suite.prototype.addChild = function(child) { diff --git a/spec/core/EnvSpec.js b/spec/core/EnvSpec.js index 8489e956..f8d4072c 100644 --- a/spec/core/EnvSpec.js +++ b/spec/core/EnvSpec.js @@ -306,13 +306,13 @@ describe('Env', function() { describe('#xit', function() { behavesLikeIt('xit'); - it('calls spec.pend with "Temporarily disabled with xit"', function() { - var pendSpy = jasmine.createSpy(); + it('calls spec.exclude with "Temporarily disabled with xit"', function() { + var excludeSpy = jasmine.createSpy(); spyOn(env, 'it_').and.returnValue({ - pend: pendSpy + exclude: excludeSpy }); env.xit('foo', function() {}); - expect(pendSpy).toHaveBeenCalledWith('Temporarily disabled with xit'); + expect(excludeSpy).toHaveBeenCalledWith('Temporarily disabled with xit'); }); it('throws an error when it receives a non-fn argument', function() { @@ -556,5 +556,23 @@ describe('Env', function() { it('returns a promise', function() { expect(env.execute()).toBeInstanceOf(Promise); }); + + it('should reset the topSuite when run twice', function() { + spyOn(jasmineUnderTest.Suite.prototype, 'reset'); + return env + .execute() // 1 + .then(function() { + return env.execute(); // 2 + }) + .then(function() { + var id; + expect( + jasmineUnderTest.Suite.prototype.reset + ).toHaveBeenCalledOnceWith(); + id = jasmineUnderTest.Suite.prototype.reset.calls.thisFor(0).id; + expect(id).toBeTruthy(); + expect(id).toEqual(env.topSuite().id); + }); + }); }); }); diff --git a/spec/core/SuiteSpec.js b/spec/core/SuiteSpec.js index d825dd27..098c67f1 100644 --- a/spec/core/SuiteSpec.js +++ b/spec/core/SuiteSpec.js @@ -149,6 +149,88 @@ describe('Suite', function() { }); }); + describe('attr.autoCleanClosures', function() { + function arrangeSuite(attrs) { + var suite = new jasmineUnderTest.Suite(attrs); + suite.beforeAll(function() {}); + suite.beforeEach(function() {}); + suite.afterEach(function() {}); + suite.afterAll(function() {}); + return suite; + } + + it('should clean closures when "attr.autoCleanClosures" is missing', function() { + var suite = arrangeSuite({}); + suite.cleanupBeforeAfter(); + expect(suite.beforeAllFns[0].fn).toBe(null); + expect(suite.beforeFns[0].fn).toBe(null); + expect(suite.afterFns[0].fn).toBe(null); + expect(suite.afterAllFns[0].fn).toBe(null); + }); + + it('should clean closures when "attr.autoCleanClosures" is true', function() { + var suite = arrangeSuite({ autoCleanClosures: true }); + suite.cleanupBeforeAfter(); + expect(suite.beforeAllFns[0].fn).toBe(null); + expect(suite.beforeFns[0].fn).toBe(null); + expect(suite.afterFns[0].fn).toBe(null); + expect(suite.afterAllFns[0].fn).toBe(null); + }); + + it('should NOT clean closures when "attr.autoCleanClosures" is false', function() { + var suite = arrangeSuite({ autoCleanClosures: false }); + suite.cleanupBeforeAfter(); + expect(suite.beforeAllFns[0].fn).not.toBe(null); + expect(suite.beforeFns[0].fn).not.toBe(null); + expect(suite.afterFns[0].fn).not.toBe(null); + expect(suite.afterAllFns[0].fn).not.toBe(null); + }); + }); + + describe('#reset', function() { + it('should reset the "pending" status', function() { + var suite = new jasmineUnderTest.Suite({}); + suite.pend(); + suite.reset(); + expect(suite.getResult().status).toBe('passed'); + }); + + it('should not reset the "pending" status when the suite was excluded', function() { + var suite = new jasmineUnderTest.Suite({}); + suite.exclude(); + suite.reset(); + expect(suite.getResult().status).toBe('pending'); + }); + + it('should also reset the children', function() { + var suite = new jasmineUnderTest.Suite({}); + var child1 = jasmine.createSpyObj(['reset']); + var child2 = jasmine.createSpyObj(['reset']); + suite.addChild(child1); + suite.addChild(child2); + + suite.reset(); + + expect(child1.reset).toHaveBeenCalled(); + expect(child2.reset).toHaveBeenCalled(); + }); + + it('should reset the failedExpectations', function() { + var suite = new jasmineUnderTest.Suite({ + expectationResultFactory: function(error) { + return error; + } + }); + suite.onException(new Error()); + + suite.reset(); + + var result = suite.getResult(); + expect(result.status).toBe('passed'); + expect(result.failedExpectations).toHaveSize(0); + }); + }); + describe('#onMultipleDone', function() { it('reports a special error when it is the top suite', function() { const onLateError = jasmine.createSpy('onLateError'); diff --git a/spec/core/integration/SpecRunningSpec.js b/spec/core/integration/SpecRunningSpec.js index 0f0a161a..9212e88a 100644 --- a/spec/core/integration/SpecRunningSpec.js +++ b/spec/core/integration/SpecRunningSpec.js @@ -1154,4 +1154,211 @@ describe('spec running', function() { }); }); }); + + describe('run multiple times', function() { + beforeEach(function() { + env.configure({ autoCleanClosures: false, random: false }); + }); + + it('should be able to run multiple times', function(done) { + var actions = []; + + env.describe('Suite', function() { + env.it('spec1', function() { + actions.push('spec1'); + }); + env.describe('inner suite', function() { + env.it('spec2', function() { + actions.push('spec2'); + }); + }); + }); + + env.execute(null, function() { + expect(actions).toEqual(['spec1', 'spec2']); + env.execute(null, function() { + expect(actions).toEqual(['spec1', 'spec2', 'spec1', 'spec2']); + done(); + }); + }); + }); + + it('should reset results between runs', function(done) { + var specResults = {}; + var suiteResults = {}; + var firstExecution = true; + + env.addReporter({ + specDone: function(spec) { + specResults[spec.description] = spec.status; + }, + suiteDone: function(suite) { + suiteResults[suite.description] = suite.status; + }, + jasmineDone: function() { + firstExecution = false; + } + }); + + env.describe('suite0', function() { + env.it('spec1', function() { + if (firstExecution) { + env.expect(1).toBe(2); + } + }); + env.describe('suite1', function() { + env.it('spec2', function() { + if (firstExecution) { + env.pending(); + } + }); + env.xit('spec3', function() {}); // Always pending + }); + env.describe('suite2', function() { + env.it('spec4', function() { + if (firstExecution) { + throw new Error('spec 3 fails'); + } + }); + }); + env.describe('suite3', function() { + env.beforeEach(function() { + throw new Error('suite 3 fails'); + }); + env.it('spec5', function() {}); + }); + env.xdescribe('suite4', function() { + // Always pending + env.it('spec6', function() {}); + }); + env.describe('suite5', function() { + env.it('spec7'); + }); + }); + + env.execute(null, function() { + expect(specResults).toEqual({ + spec1: 'failed', + spec2: 'pending', + spec3: 'pending', + spec4: 'failed', + spec5: 'failed', + spec6: 'pending', + spec7: 'pending' + }); + expect(suiteResults).toEqual({ + suite0: 'passed', + suite1: 'passed', + suite2: 'passed', + suite3: 'passed', + suite4: 'pending', + suite5: 'passed' + }); + env.execute(null, function() { + expect(specResults).toEqual({ + spec1: 'passed', + spec2: 'passed', + spec3: 'pending', + spec4: 'passed', + spec5: 'failed', + spec6: 'pending', + spec7: 'pending' + }); + expect(suiteResults).toEqual({ + suite0: 'passed', + suite1: 'passed', + suite2: 'passed', + suite3: 'passed', + suite4: 'pending', + suite5: 'passed' + }); + done(); + }); + }); + }); + + it('should execute before and after hooks per run', function(done) { + var timeline = []; + var timelineFn = function(hookName) { + return function() { + timeline.push(hookName); + }; + }; + var expectedTimeLine = [ + 'beforeAll', + 'beforeEach', + 'spec1', + 'afterEach', + 'beforeEach', + 'spec2', + 'afterEach', + 'afterAll' + ]; + + env.describe('suite0', function() { + env.beforeAll(timelineFn('beforeAll')); + env.beforeEach(timelineFn('beforeEach')); + env.afterEach(timelineFn('afterEach')); + env.afterAll(timelineFn('afterAll')); + env.it('spec1', timelineFn('spec1')); + env.it('spec2', timelineFn('spec2')); + }); + env.execute(null, function() { + expect(timeline).toEqual(expectedTimeLine); + timeline = []; + env.execute(null, function() { + expect(timeline).toEqual(expectedTimeLine); + done(); + }); + }); + }); + + it('should be able to filter out different tests in subsequent runs', function(done) { + var specResults = {}; + var focussedSpec = 'spec1'; + + env.configure({ + specFilter: function(spec) { + return spec.description === focussedSpec; + } + }); + + env.addReporter({ + specDone: function(spec) { + specResults[spec.description] = spec.status; + } + }); + + env.describe('suite0', function() { + env.it('spec1', function() {}); + env.it('spec2', function() {}); + env.it('spec3', function() {}); + }); + + env.execute(null, function() { + expect(specResults).toEqual({ + spec1: 'passed', + spec2: 'excluded', + spec3: 'excluded' + }); + focussedSpec = 'spec2'; + env.execute(null, function() { + expect(specResults).toEqual({ + spec1: 'excluded', + spec2: 'passed', + spec3: 'excluded' + }); + focussedSpec = 'spec3'; + env.execute(null, function() { + expect(specResults).toEqual({ + spec1: 'excluded', + spec2: 'excluded', + spec3: 'passed' + }); + done(); + }); + }); + }); + }); + }); }); diff --git a/src/core/Env.js b/src/core/Env.js index ce6cf454..b9dfe13c 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -122,6 +122,15 @@ getJasmineRequireObj().Env = function(j$) { * property and always create native promises instead. */ Promise: undefined, + /** + * Clean closures when a suite is done running (done by clearing the stored function reference). + * This prevents memory leaks, but you won't be able to run jasmine multiple times. + * @name Configuration#autoCleanClosures + * @since 3.10.0 + * @type boolean + * @default true + */ + autoCleanClosures: true, /** * Whether or not to issue warnings for certain deprecated functionality * every time it's used. If not set or set to false, deprecation warnings @@ -188,7 +197,8 @@ getJasmineRequireObj().Env = function(j$) { 'failSpecWithNoExpectations', 'hideDisabled', 'stopOnSpecFailure', - 'stopSpecOnExpectationFailure' + 'stopSpecOnExpectationFailure', + 'autoCleanClosures' ]; booleanProps.forEach(function(prop) { @@ -435,10 +445,11 @@ getJasmineRequireObj().Env = function(j$) { delete runnableResources[id]; }; - var beforeAndAfterFns = function(suite) { + var beforeAndAfterFns = function(targetSuite) { return function() { var befores = [], - afters = []; + afters = [], + suite = targetSuite; while (suite) { befores = befores.concat(suite.beforeFns); @@ -540,10 +551,10 @@ getJasmineRequireObj().Env = function(j$) { expectationFactory: expectationFactory, asyncExpectationFactory: suiteAsyncExpectationFactory, expectationResultFactory: expectationResultFactory, + autoCleanClosures: config.autoCleanClosures, onLateError: recordLateError }); var deprecator = new j$.Deprecator(topSuite); - defaultResourcesForRunnable(topSuite.id); currentDeclarationSuite = topSuite; /** @@ -662,6 +673,11 @@ getJasmineRequireObj().Env = function(j$) { * @return {Promise} */ this.execute = function(runnablesToRun, onComplete) { + if (this._executedBefore) { + topSuite.reset(); + } + this._executedBefore = true; + defaultResourcesForRunnable(topSuite.id); installGlobalErrors(); if (!runnablesToRun) { @@ -935,6 +951,7 @@ getJasmineRequireObj().Env = function(j$) { asyncExpectationFactory: suiteAsyncExpectationFactory, expectationResultFactory: expectationResultFactory, throwOnExpectationFailure: config.stopSpecOnExpectationFailure, + autoCleanClosures: config.autoCleanClosures, onLateError: recordLateError }); @@ -948,8 +965,8 @@ getJasmineRequireObj().Env = function(j$) { if (specDefinitions.length > 0) { throw new Error('describe does not expect any arguments'); } - if (currentDeclarationSuite.markedPending) { - suite.pend(); + if (currentDeclarationSuite.markedExcluding) { + suite.exclude(); } addSpecsToSuite(suite, specDefinitions); if (suite.parentSuite && !suite.children.length) { @@ -962,7 +979,7 @@ getJasmineRequireObj().Env = function(j$) { ensureIsNotNested('xdescribe'); ensureIsFunction(specDefinitions, 'xdescribe'); var suite = suiteFactory(description); - suite.pend(); + suite.exclude(); addSpecsToSuite(suite, specDefinitions); return suite.metadata; }; @@ -1048,6 +1065,7 @@ getJasmineRequireObj().Env = function(j$) { timeout: timeout || 0 }, throwOnExpectationFailure: config.stopSpecOnExpectationFailure, + autoCleanClosures: config.autoCleanClosures, timer: new j$.Timer() }); return spec; @@ -1083,8 +1101,8 @@ getJasmineRequireObj().Env = function(j$) { } var spec = specFactory(description, fn, currentDeclarationSuite, timeout); - if (currentDeclarationSuite.markedPending) { - spec.pend(); + if (currentDeclarationSuite.markedExcluding) { + spec.exclude(); } currentDeclarationSuite.addChild(spec); @@ -1104,7 +1122,7 @@ getJasmineRequireObj().Env = function(j$) { ensureIsFunctionOrAsync(fn, 'xit'); } var spec = this.it_.apply(this, arguments); - spec.pend('Temporarily disabled with xit'); + spec.exclude('Temporarily disabled with xit'); return spec.metadata; }; diff --git a/src/core/Spec.js b/src/core/Spec.js index 6198d24f..fc7b935e 100644 --- a/src/core/Spec.js +++ b/src/core/Spec.js @@ -36,6 +36,8 @@ getJasmineRequireObj().Spec = function(j$) { return {}; }; this.onStart = attrs.onStart || function() {}; + this.autoCleanClosures = + attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures; this.getSpecName = attrs.getSpecName || function() { @@ -54,7 +56,7 @@ getJasmineRequireObj().Spec = function(j$) { this.timer = attrs.timer || new j$.Timer(); if (!this.queueableFn.fn) { - this.pend(); + this.exclude(); } /** @@ -124,7 +126,9 @@ getJasmineRequireObj().Spec = function(j$) { var complete = { fn: function(done) { - self.queueableFn.fn = null; + if (self.autoCleanClosures) { + self.queueableFn.fn = null; + } self.result.status = self.status(excluded, failSpecWithNoExp); self.result.duration = self.timer.elapsed(); @@ -181,6 +185,36 @@ getJasmineRequireObj().Spec = function(j$) { this.queueRunnerFactory(runnerConfig); }; + Spec.prototype.reset = function() { + /** + * @typedef SpecResult + * @property {Int} id - The unique id of this spec. + * @property {String} description - The description passed to the {@link it} that created this spec. + * @property {String} fullName - The full description including all ancestors of this spec. + * @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec. + * @property {Expectation[]} passedExpectations - The list of expectations that passed during execution of this spec. + * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec. + * @property {String} pendingReason - If the spec is {@link pending}, this will be the reason. + * @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec. + * @property {number} duration - The time in ms used by the spec execution, including any before/afterEach. + * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty} + * @since 2.0.0 + */ + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [], + passedExpectations: [], + deprecationWarnings: [], + pendingReason: this.excludeMessage, + duration: null, + properties: null, + trace: null + }; + this.markedPending = this.markedExcluding; + }; + Spec.prototype.onException = function onException(e) { if (Spec.isPendingSpecException(e)) { this.pend(extractCustomPendingMessage(e)); @@ -204,6 +238,10 @@ getJasmineRequireObj().Spec = function(j$) { ); }; + /* + * Marks state as pending + * @param {string} [message] An optional reason message + */ Spec.prototype.pend = function(message) { this.markedPending = true; if (message) { @@ -211,6 +249,19 @@ getJasmineRequireObj().Spec = function(j$) { } }; + /* + * Like {@link Spec#pend}, but pending state will survive {@link Spec#reset} + * Useful for fit, xit, where pending state remains. + * @param {string} [message] An optional reason message + */ + Spec.prototype.exclude = function(message) { + this.markedExcluding = true; + if (this.message) { + this.excludeMessage = message; + } + this.pend(); + }; + Spec.prototype.getResult = function() { this.result.status = this.status(); return this.result; diff --git a/src/core/Suite.js b/src/core/Suite.js index 5e9dba66..672bcb7e 100644 --- a/src/core/Suite.js +++ b/src/core/Suite.js @@ -27,6 +27,8 @@ getJasmineRequireObj().Suite = function(j$) { this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.expectationResultFactory = attrs.expectationResultFactory; this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; + this.autoCleanClosures = + attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures; this.onLateError = attrs.onLateError; this.beforeFns = []; @@ -43,27 +45,7 @@ getJasmineRequireObj().Suite = function(j$) { */ this.children = []; - /** - * @typedef SuiteResult - * @property {Int} id - The unique id of this suite. - * @property {String} description - The description text passed to the {@link describe} that made this suite. - * @property {String} fullName - The full description including all ancestors of this suite. - * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. - * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite. - * @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite. - * @property {number} duration - The time in ms for Suite execution, including any before/afterAll, before/afterEach. - * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSuiteProperty} - * @since 2.0.0 - */ - this.result = { - id: this.id, - description: this.description, - fullName: this.getFullName(), - failedExpectations: [], - deprecationWarnings: [], - duration: null, - properties: null - }; + this.reset(); } Suite.prototype.setSuiteProperty = function(key, value) { @@ -100,10 +82,22 @@ getJasmineRequireObj().Suite = function(j$) { return fullName.join(' '); }; + /* + * Mark the suite with "pending" status + */ Suite.prototype.pend = function() { this.markedPending = true; }; + /* + * Like {@link Suite#pend}, but pending state will survive {@link Spec#reset} + * Useful for fdescribe, xdescribe, where pending state should remain. + */ + Suite.prototype.exclude = function() { + this.pend(); + this.markedExcluding = true; + }; + Suite.prototype.beforeEach = function(fn) { this.beforeFns.unshift({ ...fn, suite: this }); }; @@ -135,10 +129,40 @@ getJasmineRequireObj().Suite = function(j$) { } Suite.prototype.cleanupBeforeAfter = function() { - removeFns(this.beforeAllFns); - removeFns(this.afterAllFns); - removeFns(this.beforeFns); - removeFns(this.afterFns); + if (this.autoCleanClosures) { + removeFns(this.beforeAllFns); + removeFns(this.afterAllFns); + removeFns(this.beforeFns); + removeFns(this.afterFns); + } + }; + + Suite.prototype.reset = function() { + /** + * @typedef SuiteResult + * @property {Int} id - The unique id of this suite. + * @property {String} description - The description text passed to the {@link describe} that made this suite. + * @property {String} fullName - The full description including all ancestors of this suite. + * @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite. + * @property {Expectation[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite. + * @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite. + * @property {number} duration - The time in ms for Suite execution, including any before/afterAll, before/afterEach. + * @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSuiteProperty} + * @since 2.0.0 + */ + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [], + deprecationWarnings: [], + duration: null, + properties: null + }; + this.markedPending = this.markedExcluding; + this.children.forEach(function(child) { + child.reset(); + }); }; Suite.prototype.addChild = function(child) {