Return a promise from Env#execute in environments that support promises

[#178373231]
This commit is contained in:
Steve Gravrock
2021-08-07 13:05:55 -07:00
parent dcaac62a6c
commit e72d161fab
5 changed files with 297 additions and 110 deletions

View File

@@ -1817,11 +1817,17 @@ getJasmineRequireObj().Env = function(j$) {
*
* execute should not be called more than once.
*
* If the environment supports promises, execute will return a promise that
* is resolved after the suite finishes executing. The promise will be
* resolved (not rejected) as long as the suite runs to completion. Use a
* {@link Reporter} to determine whether or not the suite passed.
*
* @name Env#execute
* @since 2.0.0
* @function
* @param {(string[])=} runnablesToRun IDs of suites and/or specs to run
* @param {Function=} onComplete Function that will be called after all specs have run
* @return {Promise<undefined>}
*/
this.execute = function(runnablesToRun, onComplete) {
installGlobalErrors();
@@ -1881,65 +1887,86 @@ getJasmineRequireObj().Env = function(j$) {
var jasmineTimer = new j$.Timer();
jasmineTimer.start();
/**
* Information passed to the {@link Reporter#jasmineStarted} event.
* @typedef JasmineStartedInfo
* @property {Int} totalSpecsDefined - The total number of specs defined in this suite.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
*/
reporter.jasmineStarted(
{
totalSpecsDefined: totalSpecsDefined,
order: order
},
function() {
currentlyExecutingSuites.push(topSuite);
var Promise = customPromise || global.Promise;
processor.execute(function() {
clearResourcesForRunnable(topSuite.id);
currentlyExecutingSuites.pop();
var overallStatus, incompleteReason;
if (hasFailures || topSuite.result.failedExpectations.length > 0) {
overallStatus = 'failed';
} else if (focusedRunnables.length > 0) {
overallStatus = 'incomplete';
incompleteReason = 'fit() or fdescribe() was found';
} else if (totalSpecsDefined === 0) {
overallStatus = 'incomplete';
incompleteReason = 'No specs found';
} else {
overallStatus = 'passed';
if (Promise) {
return new Promise(function(resolve) {
runAll(function() {
if (onComplete) {
onComplete();
}
/**
* Information passed to the {@link Reporter#jasmineDone} event.
* @typedef JasmineDoneInfo
* @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'.
* @property {Int} totalTime - The total time (in ms) that it took to execute the suite
* @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level.
* @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level.
*/
reporter.jasmineDone(
{
overallStatus: overallStatus,
totalTime: jasmineTimer.elapsed(),
incompleteReason: incompleteReason,
order: order,
failedExpectations: topSuite.result.failedExpectations,
deprecationWarnings: topSuite.result.deprecationWarnings
},
function() {
if (onComplete) {
onComplete();
}
}
);
resolve();
});
}
);
});
} else {
runAll(function() {
if (onComplete) {
onComplete();
}
});
}
function runAll(done) {
/**
* Information passed to the {@link Reporter#jasmineStarted} event.
* @typedef JasmineStartedInfo
* @property {Int} totalSpecsDefined - The total number of specs defined in this suite.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
*/
reporter.jasmineStarted(
{
totalSpecsDefined: totalSpecsDefined,
order: order
},
function() {
currentlyExecutingSuites.push(topSuite);
processor.execute(function() {
clearResourcesForRunnable(topSuite.id);
currentlyExecutingSuites.pop();
var overallStatus, incompleteReason;
if (
hasFailures ||
topSuite.result.failedExpectations.length > 0
) {
overallStatus = 'failed';
} else if (focusedRunnables.length > 0) {
overallStatus = 'incomplete';
incompleteReason = 'fit() or fdescribe() was found';
} else if (totalSpecsDefined === 0) {
overallStatus = 'incomplete';
incompleteReason = 'No specs found';
} else {
overallStatus = 'passed';
}
/**
* Information passed to the {@link Reporter#jasmineDone} event.
* @typedef JasmineDoneInfo
* @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'.
* @property {Int} totalTime - The total time (in ms) that it took to execute the suite
* @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level.
* @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level.
*/
reporter.jasmineDone(
{
overallStatus: overallStatus,
totalTime: jasmineTimer.elapsed(),
incompleteReason: incompleteReason,
order: order,
failedExpectations: topSuite.result.failedExpectations,
deprecationWarnings: topSuite.result.deprecationWarnings
},
done
);
});
}
);
}
};
/**

View File

@@ -465,4 +465,25 @@ describe('Env', function() {
done();
});
});
describe('#execute', function() {
it('returns a promise when the environment supports promises', function() {
jasmine.getEnv().requirePromises();
expect(env.execute()).toBeInstanceOf(Promise);
});
it('returns a promise when a custom promise constructor is provided', function() {
function CustomPromise() {}
CustomPromise.resolve = function() {};
CustomPromise.reject = function() {};
env.configure({ Promise: CustomPromise });
expect(env.execute()).toBeInstanceOf(CustomPromise);
});
it('returns undefined when promises are unavailable', function() {
jasmine.getEnv().requireNoPromises();
expect(env.execute()).toBeUndefined();
});
});
});

View File

@@ -3038,6 +3038,112 @@ describe('Env integration', function() {
env.execute(null, done);
});
describe('The promise returned by #execute', function() {
beforeEach(function() {
this.savedInterval = jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL;
});
afterEach(function() {
jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL = this.savedInterval;
});
it('is resolved after reporter events are dispatched', function() {
jasmine.getEnv().requirePromises();
var reporter = jasmine.createSpyObj('reporter', [
'specDone',
'suiteDone',
'jasmineDone'
]);
env.addReporter(reporter);
env.describe('suite', function() {
env.it('spec', function() {});
});
return env.execute(null).then(function() {
expect(reporter.specDone).toHaveBeenCalled();
expect(reporter.suiteDone).toHaveBeenCalled();
expect(reporter.jasmineDone).toHaveBeenCalled();
});
});
it('is resolved after the stack is cleared', function(done) {
jasmine.getEnv().requirePromises();
var realClearStack = jasmineUnderTest.getClearStack(
jasmineUnderTest.getGlobal()
),
clearStackSpy = jasmine
.createSpy('clearStack')
.and.callFake(realClearStack);
spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(clearStackSpy);
// Create a new env that has the clearStack defined above
env.cleanup_();
env = new jasmineUnderTest.Env();
env.describe('suite', function() {
env.it('spec', function() {});
});
env.execute(null).then(function() {
expect(clearStackSpy).toHaveBeenCalled(); // (many times)
clearStackSpy.calls.reset();
setTimeout(function() {
expect(clearStackSpy).not.toHaveBeenCalled();
done();
});
});
});
it('is resolved after QueueRunner timeouts are cleared', function() {
jasmine.getEnv().requirePromises();
var setTimeoutSpy = spyOn(
jasmineUnderTest.getGlobal(),
'setTimeout'
).and.callThrough();
var clearTimeoutSpy = spyOn(
jasmineUnderTest.getGlobal(),
'clearTimeout'
).and.callThrough();
jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL = 123456; // a distinctive value
env = new jasmineUnderTest.Env();
env.describe('suite', function() {
env.it('spec', function() {});
});
return env.execute(null).then(function() {
var timeoutIds = setTimeoutSpy.calls
.all()
.filter(function(call) {
return call.args[1] === 123456;
})
.map(function(call) {
return call.returnValue;
});
expect(timeoutIds.length).toBeGreaterThan(0);
timeoutIds.forEach(function(timeoutId) {
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutId);
});
});
});
it('is resolved even if specs fail', function() {
jasmine.getEnv().requirePromises();
env.describe('suite', function() {
env.it('spec', function() {
env.expect(true).toBe(false);
});
});
return expectAsync(env.execute(null)).toBeResolved();
});
});
describe('The optional callback argument to #execute', function() {
beforeEach(function() {
this.savedInterval = jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL;

View File

@@ -4,4 +4,10 @@
env.pending('Environment does not support promises');
}
};
env.requireNoPromises = function() {
if (typeof Promise === 'function') {
env.pending('Environment supports promises');
}
};
})(jasmine.getEnv());

View File

@@ -807,11 +807,17 @@ getJasmineRequireObj().Env = function(j$) {
*
* execute should not be called more than once.
*
* If the environment supports promises, execute will return a promise that
* is resolved after the suite finishes executing. The promise will be
* resolved (not rejected) as long as the suite runs to completion. Use a
* {@link Reporter} to determine whether or not the suite passed.
*
* @name Env#execute
* @since 2.0.0
* @function
* @param {(string[])=} runnablesToRun IDs of suites and/or specs to run
* @param {Function=} onComplete Function that will be called after all specs have run
* @return {Promise<undefined>}
*/
this.execute = function(runnablesToRun, onComplete) {
installGlobalErrors();
@@ -871,65 +877,86 @@ getJasmineRequireObj().Env = function(j$) {
var jasmineTimer = new j$.Timer();
jasmineTimer.start();
/**
* Information passed to the {@link Reporter#jasmineStarted} event.
* @typedef JasmineStartedInfo
* @property {Int} totalSpecsDefined - The total number of specs defined in this suite.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
*/
reporter.jasmineStarted(
{
totalSpecsDefined: totalSpecsDefined,
order: order
},
function() {
currentlyExecutingSuites.push(topSuite);
var Promise = customPromise || global.Promise;
processor.execute(function() {
clearResourcesForRunnable(topSuite.id);
currentlyExecutingSuites.pop();
var overallStatus, incompleteReason;
if (hasFailures || topSuite.result.failedExpectations.length > 0) {
overallStatus = 'failed';
} else if (focusedRunnables.length > 0) {
overallStatus = 'incomplete';
incompleteReason = 'fit() or fdescribe() was found';
} else if (totalSpecsDefined === 0) {
overallStatus = 'incomplete';
incompleteReason = 'No specs found';
} else {
overallStatus = 'passed';
if (Promise) {
return new Promise(function(resolve) {
runAll(function() {
if (onComplete) {
onComplete();
}
/**
* Information passed to the {@link Reporter#jasmineDone} event.
* @typedef JasmineDoneInfo
* @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'.
* @property {Int} totalTime - The total time (in ms) that it took to execute the suite
* @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level.
* @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level.
*/
reporter.jasmineDone(
{
overallStatus: overallStatus,
totalTime: jasmineTimer.elapsed(),
incompleteReason: incompleteReason,
order: order,
failedExpectations: topSuite.result.failedExpectations,
deprecationWarnings: topSuite.result.deprecationWarnings
},
function() {
if (onComplete) {
onComplete();
}
}
);
resolve();
});
}
);
});
} else {
runAll(function() {
if (onComplete) {
onComplete();
}
});
}
function runAll(done) {
/**
* Information passed to the {@link Reporter#jasmineStarted} event.
* @typedef JasmineStartedInfo
* @property {Int} totalSpecsDefined - The total number of specs defined in this suite.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
*/
reporter.jasmineStarted(
{
totalSpecsDefined: totalSpecsDefined,
order: order
},
function() {
currentlyExecutingSuites.push(topSuite);
processor.execute(function() {
clearResourcesForRunnable(topSuite.id);
currentlyExecutingSuites.pop();
var overallStatus, incompleteReason;
if (
hasFailures ||
topSuite.result.failedExpectations.length > 0
) {
overallStatus = 'failed';
} else if (focusedRunnables.length > 0) {
overallStatus = 'incomplete';
incompleteReason = 'fit() or fdescribe() was found';
} else if (totalSpecsDefined === 0) {
overallStatus = 'incomplete';
incompleteReason = 'No specs found';
} else {
overallStatus = 'passed';
}
/**
* Information passed to the {@link Reporter#jasmineDone} event.
* @typedef JasmineDoneInfo
* @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'.
* @property {Int} totalTime - The total time (in ms) that it took to execute the suite
* @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level.
* @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level.
*/
reporter.jasmineDone(
{
overallStatus: overallStatus,
totalTime: jasmineTimer.elapsed(),
incompleteReason: incompleteReason,
order: order,
failedExpectations: topSuite.result.failedExpectations,
deprecationWarnings: topSuite.result.deprecationWarnings
},
done
);
});
}
);
}
};
/**