Skip to content

Commit

Permalink
Made the promise returned from Jasmine#execute usable
Browse files Browse the repository at this point in the history
* An exitOnCompletion property to control whether Jasmine should make the
 Nnode process exit directly, rather than as a side effect of calling
 onComplete
* The promise returned from execute is resolved to the overall status
* onComplete is marked deprecated.

onComplete's behavior of installing a completion handler and also preventing
exit is clever, and actually what anyone using it probably wants, but it's
also non-obvious. The two-step process of setting exitOnCompletion = false
and then using the promise returned from execute() is not as slick, but it's
probably better for an audience that increasingly (for good reason) prefers
promise- or async/await-based async.
  • Loading branch information
sgravrock committed Aug 28, 2021
1 parent 2f014c9 commit 42b10d5
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 12 deletions.
31 changes: 27 additions & 4 deletions lib/jasmine.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ function Jasmine(options) {
this.coreVersion = function() {
return jasmineCore.version();
};

/**
* Whether to cause the Node process to exit when the suite finishes executing.
*
* _Note_: If {@link Jasmine#onComplete|onComplete} is called, Jasmine will not
* exit when the suite completes even if exitOnCompletion is set to true.
* @name Jasmine#exitOnCompletion
* @type {boolean}
* @default true
*/
this.exitOnCompletion = true;
}

/**
Expand Down Expand Up @@ -432,8 +443,12 @@ function addFiles(kind) {
*
* _Note_: Only one callback can be registered. The callback will be called
* after the suite has completed and the results have been finalized, but not
* necessarily before all of Jasmine's cleanup has finished.
* necessarily before all of Jasmine's cleanup has finished. Calling this
* function will also prevent Jasmine from exiting the Node process at the end
* of suite execution.
*
* @deprecated Set {@link Jasmine#exitOnCompletion|exitOnCompletion} to false
* and use the promise returned from {@link Jasmine#execute|execute} instead.
* @param {function} onCompleteCallback
*/
Jasmine.prototype.onComplete = function(onCompleteCallback) {
Expand Down Expand Up @@ -476,7 +491,7 @@ Jasmine.prototype.exitCodeCompletion = function(passed) {
});
function exitIfAllStreamsCompleted() {
writesToWait--;
if (writesToWait === 0) {
if (writesToWait === 0 && jasmineRunner.exitOnCompletion) {
if(passed) {
jasmineRunner.exit(0);
}
Expand Down Expand Up @@ -508,11 +523,15 @@ function checkForJsFileImportSupport() {

/**
* Runs the test suite.
*
* _Note_: Set {@link Jasmine#exitOnCompletion|exitOnCompletion} to false if you
* intend to use the returned promise. Otherwise, the Node process will
* ordinarily exit before the promise is settled.
* @param {Array.<string>} [files] Spec files to run instead of the previously
* configured set
* @param {string} [filterString] Regex used to filter specs. If specified, only
* specs with matching full names will be run.
* @return {Promise<void>} Promise that is resolved when the suite completes.
* @return {Promise<JasmineDoneInfo>} Promise that is resolved when the suite completes.
*/
Jasmine.prototype.execute = async function(files, filterString) {
this.completionReporter.exitHandler = this.checkExit;
Expand Down Expand Up @@ -541,8 +560,12 @@ Jasmine.prototype.execute = async function(files, filterString) {
await this.loadSpecs();

this.addReporter(this.completionReporter);

let overallResult;
this.addReporter({
jasmineDone: r => overallResult = r
});
await new Promise(resolve => {
this.env.execute(null, resolve);
});
return overallResult;
};
8 changes: 8 additions & 0 deletions spec/fixtures/defaultProgrammaticFail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const Jasmine = require('../..');
const jasmine = new Jasmine();

it('fails', function() {
expect(1).toBe(2);
});

jasmine.execute();
11 changes: 11 additions & 0 deletions spec/fixtures/dontExitOnCompletion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const Jasmine = require('../..');
const jasmine = new Jasmine();

it('a spec', function() {});

jasmine.exitOnCompletion = false;
jasmine.execute().finally(function() {
setTimeout(function() {
console.log("in setTimeout cb");
});
});
13 changes: 13 additions & 0 deletions spec/fixtures/promiseFailure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const Jasmine = require('../..');
const jasmine = new Jasmine();

it('a spec', function() {
expect(1).toBe(2);
});

jasmine.exitOnCompletion = false;
jasmine.execute().then(function(result) {
if (result.overallStatus === 'failed') {
console.log("Promise failure!");
}
});
13 changes: 13 additions & 0 deletions spec/fixtures/promiseIncomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const Jasmine = require('../..');
const jasmine = new Jasmine();

it('a spec', function() {});

fit('another spec', function() {});

jasmine.exitOnCompletion = false;
jasmine.execute().then(function(result) {
if (result.overallStatus === 'incomplete') {
console.log("Promise incomplete!");
}
});
11 changes: 11 additions & 0 deletions spec/fixtures/promiseSuccess.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const Jasmine = require('../..');
const jasmine = new Jasmine();

it('a spec', function() {});

jasmine.exitOnCompletion = false;
jasmine.execute().then(function(result) {
if (result.overallStatus === 'passed') {
console.log("Promise success!");
}
});
48 changes: 42 additions & 6 deletions spec/integration_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,54 @@ describe('Integration', function () {
expect(exitCode).toEqual(0);
expect(output).toContain('in spec 1\n.in spec 2\n.in spec 3\n.in spec 4\n.in spec 5');
});

describe('Programmatic usage', function() {
it('exits on completion by default', async function() {
const {exitCode, output} = await runCommand('node', ['spec/fixtures/defaultProgrammaticFail.js']);
expect(exitCode).toEqual(1);
expect(output).toContain('1 spec, 1 failure');
});

it('does not exit on completion when exitOnCompletion is set to false', async function() {
const {exitCode, output} = await runCommand('node', ['spec/fixtures/dontExitOnCompletion.js']);
expect(exitCode).toEqual(0);
expect(output).toContain('in setTimeout cb');
});

it('resolves the returned promise when the suite passes', async function() {
const {exitCode, output} = await runCommand('node', ['spec/fixtures/promiseSuccess.js']);
expect(exitCode).toEqual(0);
expect(output).toContain('Promise success!');
});

it('resolves the returned promise when the suite fails', async function() {
const {exitCode, output} = await runCommand('node', ['spec/fixtures/promiseFailure.js']);
expect(exitCode).toEqual(0);
expect(output).toContain('Promise failure!');
});

it('resolves the returned promise when the suite is incomplete', async function() {
const {exitCode, output} = await runCommand('node', ['spec/fixtures/promiseIncomplete.js']);
expect(exitCode).toEqual(0);
expect(output).toContain('Promise incomplete!');
});
});
});

async function runJasmine(cwd, useExperimentalModulesFlag) {
return new Promise(function(resolve) {
const args = ['../../../bin/jasmine.js', '--config=jasmine.json'];
const args = ['../../../bin/jasmine.js', '--config=jasmine.json'];

if (useExperimentalModulesFlag) {
args.unshift('--experimental-modules');
}
if (useExperimentalModulesFlag) {
args.unshift('--experimental-modules');
}

return runCommand('node', args, cwd);
}

async function runCommand(cmd, args, cwd = '.') {
return new Promise(function(resolve) {
const child = child_process.spawn(
'node',
cmd,
args,
{
cwd,
Expand Down
59 changes: 57 additions & 2 deletions spec/jasmine_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ describe('Jasmine', function() {
};

this.testJasmine = new Jasmine({ jasmineCore: this.fakeJasmineCore });
this.testJasmine.exit = function() {
// Don't actually exit the node process
};
});

describe('constructor options', function() {
Expand Down Expand Up @@ -572,13 +575,15 @@ describe('Jasmine', function() {
});
});

describe('When #onComplete has been called', function() {
describe('When exitOnCompletion is set to false', function() {
it('does not exit', async function() {
this.testJasmine.onComplete(function() {});
this.testJasmine.exitOnCompletion = false;
await this.runWithOverallStatus('anything');
expect(this.testJasmine.exit).not.toHaveBeenCalled();
});
});

describe('When #onComplete has been called', function() {
it('calls the supplied completion handler with true when the whole suite is green', async function() {
const completionHandler = jasmine.createSpy('completionHandler');
this.testJasmine.onComplete(completionHandler);
Expand All @@ -592,6 +597,56 @@ describe('Jasmine', function() {
await this.runWithOverallStatus('failed');
expect(completionHandler).toHaveBeenCalledWith(false);
});

it('does not exit', async function() {
this.testJasmine.onComplete(function() {});
await this.runWithOverallStatus('anything');
expect(this.testJasmine.exit).not.toHaveBeenCalled();
});

it('ignores exitOnCompletion', async function() {
this.testJasmine.onComplete(function() {});
this.testJasmine.exitOnCompletion = true;
await this.runWithOverallStatus('anything');
expect(this.testJasmine.exit).not.toHaveBeenCalled();
});
});
});

describe('The returned promise', function() {
beforeEach(function() {
this.autocompletingFakeEnv = function(overallStatus) {
let reporters = [];
return {
execute: function(ignored, callback) {
for (const reporter of reporters) {
reporter.jasmineDone({overallStatus});
}
callback();
},
addReporter: reporter => {
reporters.push(reporter);
},
clearReporters: function() {
reporters = [];
}
};
};
});

it('is resolved with the overall suite status', async function() {
this.testJasmine.env = this.autocompletingFakeEnv('failed');

await expectAsync(this.testJasmine.execute())
.toBeResolvedTo(jasmine.objectContaining({overallStatus: 'failed'}));
});

it('is resolved with the overall suite status even if clearReporters was called', async function() {
this.testJasmine.env = this.autocompletingFakeEnv('incomplete');
this.testJasmine.clearReporters();

await expectAsync(this.testJasmine.execute())
.toBeResolvedTo(jasmine.objectContaining({overallStatus: 'incomplete'}));
});
});
});
Expand Down

0 comments on commit 42b10d5

Please sign in to comment.