Support running jasmine multiple times

Add support for running jasmine multiple times.

```js
const Jasmine = require('jasmine');

async function main() {
  const jasmine = new Jasmine({ projectBaseDir: process.cwd() });
  let specId = 'spec0';
  jasmine.loadConfigFile('./spec/support/jasmine.json');
  jasmine.env.configure({
    specFilter(sp) {
      return sp.id === specId;
    },
    autoCleanClosures: false
  });
  jasmine.exit = () => {};
  await jasmine.execute();
  specId = 'spec2';
  await jasmine.execute();
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});
```

With `jasmine.env.configure({ autoCleanClosures: false })` you disable Jasmine's feature to automatically clean closures (functions) during the test run. This is a requirement to be able to rerun.

When `execute` is called more than once, the `topSuite.reset` is called, which will reset the state for the next run as well as reset any child suites.

Add a function `exclude` to the `Suite` and `Spec` clases. This functions similar to `pend`, but will allow the "pending" state to persist over multiple runs. This is useful when `xit` is used.

Revert changes to jasmine.js

fix: make sure to call hooks during second run

Remove jsdoc from private apis

Fix elint issue

Add new line
This commit is contained in:
Nico Jansen
2021-10-01 16:12:57 +02:00
parent fdad8849df
commit 1fc911e0fa
6 changed files with 438 additions and 44 deletions

View File

@@ -138,7 +138,16 @@ getJasmineRequireObj().Env = function(j$) {
* @type function
* @default undefined
*/
Promise: undefined
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
};
var currentSuite = function() {
@@ -191,7 +200,8 @@ getJasmineRequireObj().Env = function(j$) {
var booleanProps = [
'random',
'failSpecWithNoExpectations',
'hideDisabled'
'hideDisabled',
'autoCleanClosures'
];
booleanProps.forEach(function(prop) {
@@ -497,10 +507,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);
@@ -701,9 +712,9 @@ getJasmineRequireObj().Env = function(j$) {
description: 'Jasmine__TopLevel__Suite',
expectationFactory: expectationFactory,
asyncExpectationFactory: suiteAsyncExpectationFactory,
expectationResultFactory: expectationResultFactory
expectationResultFactory: expectationResultFactory,
autoCleanClosures: config.autoCleanClosures
});
defaultResourcesForRunnable(topSuite.id);
currentDeclarationSuite = topSuite;
/**
@@ -821,6 +832,11 @@ getJasmineRequireObj().Env = function(j$) {
* @return {Promise<undefined>}
*/
this.execute = function(runnablesToRun, onComplete) {
if (this._executedBefore) {
topSuite.reset();
}
this._executedBefore = true;
defaultResourcesForRunnable(topSuite.id);
installGlobalErrors();
if (!runnablesToRun) {
@@ -1107,7 +1123,8 @@ getJasmineRequireObj().Env = function(j$) {
expectationFactory: expectationFactory,
asyncExpectationFactory: suiteAsyncExpectationFactory,
expectationResultFactory: expectationResultFactory,
throwOnExpectationFailure: config.oneFailurePerSpec
throwOnExpectationFailure: config.oneFailurePerSpec,
autoCleanClosures: config.autoCleanClosures
});
return suite;
@@ -1120,8 +1137,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);
return suite;
@@ -1131,7 +1148,7 @@ getJasmineRequireObj().Env = function(j$) {
ensureIsNotNested('xdescribe');
ensureIsFunction(specDefinitions, 'xdescribe');
var suite = suiteFactory(description);
suite.pend();
suite.exclude();
addSpecsToSuite(suite, specDefinitions);
return suite;
};
@@ -1216,6 +1233,7 @@ getJasmineRequireObj().Env = function(j$) {
timeout: timeout || 0
},
throwOnExpectationFailure: config.oneFailurePerSpec,
autoCleanClosures: config.autoCleanClosures,
timer: new j$.Timer()
});
return spec;
@@ -1251,8 +1269,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);
return spec;
@@ -1266,7 +1284,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;
};

View File

@@ -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() {
@@ -53,7 +55,7 @@ getJasmineRequireObj().Spec = function(j$) {
this.timer = attrs.timer || new j$.Timer();
if (!this.queueableFn.fn) {
this.pend();
this.exclude();
}
/**
@@ -121,7 +123,9 @@ x */
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();
self.resultCallback(self.result, done);
@@ -158,6 +162,37 @@ x */
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}
* @property {TraceEntry[]|null} trace - Trace messages, if any, that were logged using {@link Env#trace} during a failing spec.
* @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));
@@ -181,6 +216,10 @@ x */
);
};
/*
* Marks state as pending
* @param {string} [message] An optional reason message
*/
Spec.prototype.pend = function(message) {
this.markedPending = true;
if (message) {
@@ -188,6 +227,19 @@ x */
}
};
/*
* 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;

View File

@@ -33,6 +33,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.beforeFns = [];
this.afterFns = [];
@@ -49,27 +51,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) {
@@ -106,10 +88,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);
};
@@ -141,10 +135,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) {