Merge branch 'main' into 3.99

This commit is contained in:
Steve Gravrock
2021-08-17 17:08:34 -07:00
7 changed files with 418 additions and 184 deletions

View File

@@ -1947,11 +1947,17 @@ getJasmineRequireObj().Env = function(j$) {
* *
* execute should not be called more than once. * 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 * @name Env#execute
* @since 2.0.0 * @since 2.0.0
* @function * @function
* @param {(string[])=} runnablesToRun IDs of suites and/or specs to run * @param {(string[])=} runnablesToRun IDs of suites and/or specs to run
* @param {Function=} onComplete Function that will be called after all specs have run * @param {Function=} onComplete Function that will be called after all specs have run
* @return {Promise<undefined>}
*/ */
this.execute = function(runnablesToRun, onComplete) { this.execute = function(runnablesToRun, onComplete) {
installGlobalErrors(); installGlobalErrors();
@@ -2011,65 +2017,86 @@ getJasmineRequireObj().Env = function(j$) {
var jasmineTimer = new j$.Timer(); var jasmineTimer = new j$.Timer();
jasmineTimer.start(); jasmineTimer.start();
/** var Promise = customPromise || global.Promise;
* 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() { if (Promise) {
clearResourcesForRunnable(topSuite.id); return new Promise(function(resolve) {
currentlyExecutingSuites.pop(); runAll(function() {
var overallStatus, incompleteReason; if (onComplete) {
onComplete();
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';
} }
/** resolve();
* 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();
}
}
);
}); });
} });
); } 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
);
});
}
);
}
}; };
/** /**
@@ -4768,16 +4795,22 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
function taggedOnError(error) { function taggedOnError(error) {
var substituteMsg; var substituteMsg;
if (error) { if (j$.isError_(error)) {
error.jasmineMessage = jasmineMessage + ': ' + error; error.jasmineMessage = jasmineMessage + ': ' + error;
} else { } else {
substituteMsg = jasmineMessage + ' with no error or message'; if (error) {
substituteMsg = jasmineMessage + ': ' + error;
} else {
substituteMsg = jasmineMessage + ' with no error or message';
}
if (errorType === 'unhandledRejection') { if (errorType === 'unhandledRejection') {
substituteMsg += substituteMsg +=
'\n' + '\n' +
'(Tip: to get a useful stack trace, use ' + '(Tip: to get a useful stack trace, use ' +
'Promise.reject(new Error(...)) instead of Promise.reject().)'; 'Promise.reject(new Error(...)) instead of Promise.reject(' +
(error ? '...' : '') +
').)';
} }
error = new Error(substituteMsg); error = new Error(substituteMsg);

View File

@@ -772,4 +772,26 @@ describe('Env', function() {
expect(suiteThis).toBeInstanceOf(jasmineUnderTest.Suite); expect(suiteThis).toBeInstanceOf(jasmineUnderTest.Suite);
}); });
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() {};
spyOn(env, 'deprecated');
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

@@ -170,84 +170,118 @@ describe('GlobalErrors', function() {
); );
}); });
it('reports unhandled promise rejections in node.js', function() { describe('Reporting unhandled promise rejections in node.js', function() {
var fakeGlobal = { it('reports rejections with `Error` reasons', function() {
process: { var fakeGlobal = {
on: jasmine.createSpy('process.on'), process: {
removeListener: jasmine.createSpy('process.removeListener'), on: jasmine.createSpy('process.on'),
listeners: jasmine removeListener: jasmine.createSpy('process.removeListener'),
.createSpy('process.listeners') listeners: jasmine
.and.returnValue(['foo']), .createSpy('process.listeners')
removeAllListeners: jasmine.createSpy('process.removeAllListeners') .and.returnValue(['foo']),
} removeAllListeners: jasmine.createSpy('process.removeAllListeners')
}, }
handler = jasmine.createSpy('errorHandler'), },
errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); handler = jasmine.createSpy('errorHandler'),
errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
errors.install(); errors.install();
expect(fakeGlobal.process.on).toHaveBeenCalledWith( expect(fakeGlobal.process.on).toHaveBeenCalledWith(
'unhandledRejection', 'unhandledRejection',
jasmine.any(Function) jasmine.any(Function)
); );
expect(fakeGlobal.process.listeners).toHaveBeenCalledWith( expect(fakeGlobal.process.listeners).toHaveBeenCalledWith(
'unhandledRejection' 'unhandledRejection'
); );
expect(fakeGlobal.process.removeAllListeners).toHaveBeenCalledWith( expect(fakeGlobal.process.removeAllListeners).toHaveBeenCalledWith(
'unhandledRejection' 'unhandledRejection'
); );
errors.pushListener(handler); errors.pushListener(handler);
var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1]; var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1];
addedListener(new Error('bar')); addedListener(new Error('bar'));
expect(handler).toHaveBeenCalledWith(new Error('bar')); expect(handler).toHaveBeenCalledWith(new Error('bar'));
expect(handler.calls.argsFor(0)[0].jasmineMessage).toBe( expect(handler.calls.argsFor(0)[0].jasmineMessage).toBe(
'Unhandled promise rejection: Error: bar' 'Unhandled promise rejection: Error: bar'
); );
errors.uninstall(); errors.uninstall();
expect(fakeGlobal.process.removeListener).toHaveBeenCalledWith( expect(fakeGlobal.process.removeListener).toHaveBeenCalledWith(
'unhandledRejection', 'unhandledRejection',
addedListener addedListener
); );
expect(fakeGlobal.process.on).toHaveBeenCalledWith( expect(fakeGlobal.process.on).toHaveBeenCalledWith(
'unhandledRejection', 'unhandledRejection',
'foo' 'foo'
); );
}); });
it('reports unhandled promise rejections in node.js when no error is provided', function() { it('reports rejections with non-`Error` reasons', function() {
var fakeGlobal = { var fakeGlobal = {
process: { process: {
on: jasmine.createSpy('process.on'), on: jasmine.createSpy('process.on'),
removeListener: function() {}, removeListener: function() {},
listeners: function() { listeners: function() {
return []; return [];
}, },
removeAllListeners: function() {} removeAllListeners: function() {}
} }
}, },
handler = jasmine.createSpy('errorHandler'), handler = jasmine.createSpy('errorHandler'),
errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
errors.install(); errors.install();
errors.pushListener(handler); errors.pushListener(handler);
expect(fakeGlobal.process.on.calls.argsFor(1)[0]).toEqual( expect(fakeGlobal.process.on.calls.argsFor(1)[0]).toEqual(
'unhandledRejection' 'unhandledRejection'
); );
var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1]; var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1];
addedListener(undefined); addedListener(17);
expect(handler).toHaveBeenCalledWith( expect(handler).toHaveBeenCalledWith(
new Error( new Error(
'Unhandled promise rejection with no error or message\n' + 'Unhandled promise rejection: 17\n' +
'(Tip: to get a useful stack trace, use ' + '(Tip: to get a useful stack trace, use ' +
'Promise.reject(new Error(...)) instead of Promise.reject().)' 'Promise.reject(new Error(...)) instead of Promise.reject(...).)'
) )
); );
});
it('reports rejections with no reason provided', function() {
var fakeGlobal = {
process: {
on: jasmine.createSpy('process.on'),
removeListener: function() {},
listeners: function() {
return [];
},
removeAllListeners: function() {}
}
},
handler = jasmine.createSpy('errorHandler'),
errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
errors.install();
errors.pushListener(handler);
expect(fakeGlobal.process.on.calls.argsFor(1)[0]).toEqual(
'unhandledRejection'
);
var addedListener = fakeGlobal.process.on.calls.argsFor(1)[1];
addedListener(undefined);
expect(handler).toHaveBeenCalledWith(
new Error(
'Unhandled promise rejection with no error or message\n' +
'(Tip: to get a useful stack trace, use ' +
'Promise.reject(new Error(...)) instead of Promise.reject().)'
)
);
});
}); });
describe('Reporting unhandled promise rejections in the browser', function() { describe('Reporting unhandled promise rejections in the browser', function() {

View File

@@ -2977,6 +2977,112 @@ describe('Env integration', function() {
env.execute(null, done); 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() { describe('The optional callback argument to #execute', function() {
beforeEach(function() { beforeEach(function() {
this.savedInterval = jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL; this.savedInterval = jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL;

View File

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

View File

@@ -909,11 +909,17 @@ getJasmineRequireObj().Env = function(j$) {
* *
* execute should not be called more than once. * 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 * @name Env#execute
* @since 2.0.0 * @since 2.0.0
* @function * @function
* @param {(string[])=} runnablesToRun IDs of suites and/or specs to run * @param {(string[])=} runnablesToRun IDs of suites and/or specs to run
* @param {Function=} onComplete Function that will be called after all specs have run * @param {Function=} onComplete Function that will be called after all specs have run
* @return {Promise<undefined>}
*/ */
this.execute = function(runnablesToRun, onComplete) { this.execute = function(runnablesToRun, onComplete) {
installGlobalErrors(); installGlobalErrors();
@@ -973,65 +979,86 @@ getJasmineRequireObj().Env = function(j$) {
var jasmineTimer = new j$.Timer(); var jasmineTimer = new j$.Timer();
jasmineTimer.start(); jasmineTimer.start();
/** var Promise = customPromise || global.Promise;
* 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() { if (Promise) {
clearResourcesForRunnable(topSuite.id); return new Promise(function(resolve) {
currentlyExecutingSuites.pop(); runAll(function() {
var overallStatus, incompleteReason; if (onComplete) {
onComplete();
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';
} }
/** resolve();
* 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();
}
}
);
}); });
} });
); } 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

@@ -19,16 +19,22 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
function taggedOnError(error) { function taggedOnError(error) {
var substituteMsg; var substituteMsg;
if (error) { if (j$.isError_(error)) {
error.jasmineMessage = jasmineMessage + ': ' + error; error.jasmineMessage = jasmineMessage + ': ' + error;
} else { } else {
substituteMsg = jasmineMessage + ' with no error or message'; if (error) {
substituteMsg = jasmineMessage + ': ' + error;
} else {
substituteMsg = jasmineMessage + ' with no error or message';
}
if (errorType === 'unhandledRejection') { if (errorType === 'unhandledRejection') {
substituteMsg += substituteMsg +=
'\n' + '\n' +
'(Tip: to get a useful stack trace, use ' + '(Tip: to get a useful stack trace, use ' +
'Promise.reject(new Error(...)) instead of Promise.reject().)'; 'Promise.reject(new Error(...)) instead of Promise.reject(' +
(error ? '...' : '') +
').)';
} }
error = new Error(substituteMsg); error = new Error(substituteMsg);