Report async expectations that complete after the runable completes

It's very easy to forget to `await` or `return` the promise returned
from `expectAsync`. When that happens, the expectation failure will
occur after the spec or suite's result has been reported to reporters,
and the failure will typically not be shown to the user. This change
adds a top-level suite failure in that case, similar to the way we
report unhandled exceptions or promise rejections that occur after the
runable completes. Adding the error at the top level gives us the best
chance of getting in before the set of failures we add it to is sent
to reporters.

See #1752.
This commit is contained in:
Steve Gravrock
2019-09-27 18:31:01 -07:00
parent 9a41154e3b
commit a497d0942a
3 changed files with 170 additions and 9 deletions

View File

@@ -1233,7 +1233,33 @@ getJasmineRequireObj().Env = function(j$) {
}
};
var asyncExpectationFactory = function(actual, spec) {
function recordLateExpectation(runable, runableType, result) {
var delayedExpectationResult = {};
Object.keys(result).forEach(function(k) {
delayedExpectationResult[k] = result[k];
});
delayedExpectationResult.passed = false;
delayedExpectationResult.globalErrorType = 'lateExpectation';
delayedExpectationResult.message =
runableType +
' "' +
runable.getFullName() +
'" ran a "' +
result.matcherName +
'" expectation after it finished.\n';
if (result.message) {
delayedExpectationResult.message +=
'Message: "' + result.message + '"\n';
}
delayedExpectationResult.message +=
'Did you forget to return or await the result of expectAsync?';
topSuite.result.failedExpectations.push(delayedExpectationResult);
}
var asyncExpectationFactory = function(actual, spec, runableType) {
return j$.Expectation.asyncFactory({
util: j$.matchersUtil,
customEqualityTesters: runnableResources[spec.id].customEqualityTesters,
@@ -1243,9 +1269,19 @@ getJasmineRequireObj().Env = function(j$) {
});
function addExpectationResult(passed, result) {
if (currentRunnable() !== spec) {
recordLateExpectation(spec, runableType, result);
}
return spec.addExpectationResult(passed, result);
}
};
var suiteAsyncExpectationFactory = function(actual, suite) {
return asyncExpectationFactory(actual, suite, 'Suite');
};
var specAsyncExpectationFactory = function(actual, suite) {
return asyncExpectationFactory(actual, suite, 'Spec');
};
var defaultResourcesForRunnable = function(id, parentRunnableId) {
var resources = {
@@ -1463,7 +1499,7 @@ getJasmineRequireObj().Env = function(j$) {
id: getNextSuiteId(),
description: 'Jasmine__TopLevel__Suite',
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
asyncExpectationFactory: suiteAsyncExpectationFactory,
expectationResultFactory: expectationResultFactory
});
defaultResourcesForRunnable(topSuite.id);
@@ -1796,7 +1832,7 @@ getJasmineRequireObj().Env = function(j$) {
description: description,
parentSuite: currentDeclarationSuite,
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
asyncExpectationFactory: suiteAsyncExpectationFactory,
expectationResultFactory: expectationResultFactory,
throwOnExpectationFailure: config.oneFailurePerSpec
});
@@ -1890,7 +1926,7 @@ getJasmineRequireObj().Env = function(j$) {
id: getNextSpecId(),
beforeAndAfterFns: beforeAndAfterFns(suite),
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
asyncExpectationFactory: specAsyncExpectationFactory,
resultCallback: specResultCallback,
getSpecName: function(spec) {
return getSpecName(spec, suite);

View File

@@ -2149,7 +2149,7 @@ describe("Env integration", function() {
env.it('is a spec without any expectations', function() {
// does nothing, just a mock spec without expectations
});
});
it('should report "failed" status if "failSpecWithNoExpectations" is enabled', function(done) {
@@ -2556,4 +2556,93 @@ describe("Env integration", function() {
env.execute();
});
it('reports an error when an async expectation occurs after the spec finishes', function(done) {
jasmine.getEnv().requirePromises();
var env = new jasmineUnderTest.Env(),
resolve,
promise = new Promise(function(res) { resolve = res; });
env.describe('a suite', function() {
env.it('does not wait', function() {
// Note: we intentionally don't return the result of each expectAsync.
// This causes the spec to finish before the expectations are evaluated.
env.expectAsync(promise).toBeResolved();
env.expectAsync(promise).toBeResolvedTo('something else');
});
});
env.addReporter({
specDone: function() {
resolve();
},
jasmineDone: function (result) {
expect(result.failedExpectations).toEqual([
jasmine.objectContaining({
passed: false,
globalErrorType: 'lateExpectation',
message: 'Spec "a suite does not wait" ran a "toBeResolved" expectation ' +
'after it finished.\n' +
'Did you forget to return or await the result of expectAsync?',
matcherName: 'toBeResolved'
}),
jasmine.objectContaining({
passed: false,
globalErrorType: 'lateExpectation',
message: 'Spec "a suite does not wait" ran a "toBeResolvedTo" expectation ' +
'after it finished.\n' +
'Message: "Expected a promise to be resolved to \'something else\' ' +
'but it was resolved to undefined."\n' +
'Did you forget to return or await the result of expectAsync?',
matcherName: 'toBeResolvedTo'
})
]);
done();
}
});
env.execute();
});
it('reports an error when an async expectation occurs after the suite finishes', function(done) {
jasmine.getEnv().requirePromises();
var env = new jasmineUnderTest.Env(),
resolve,
promise = new Promise(function(res) { resolve = res; });
env.describe('a suite', function() {
env.afterAll(function() {
// Note: we intentionally don't return the result of expectAsync.
// This causes the suite to finish before the expectations are evaluated.
env.expectAsync(promise).toBeResolved();
});
env.it('is a spec', function() {});
});
env.addReporter({
suiteDone: function() {
resolve();
},
jasmineDone: function (result) {
expect(result.failedExpectations).toEqual([
jasmine.objectContaining({
passed: false,
globalErrorType: 'lateExpectation',
message: 'Suite "a suite" ran a "toBeResolved" expectation ' +
'after it finished.\n' +
'Did you forget to return or await the result of expectAsync?',
matcherName: 'toBeResolved'
})
]);
done();
}
});
env.execute();
});
});

View File

@@ -321,7 +321,33 @@ getJasmineRequireObj().Env = function(j$) {
}
};
var asyncExpectationFactory = function(actual, spec) {
function recordLateExpectation(runable, runableType, result) {
var delayedExpectationResult = {};
Object.keys(result).forEach(function(k) {
delayedExpectationResult[k] = result[k];
});
delayedExpectationResult.passed = false;
delayedExpectationResult.globalErrorType = 'lateExpectation';
delayedExpectationResult.message =
runableType +
' "' +
runable.getFullName() +
'" ran a "' +
result.matcherName +
'" expectation after it finished.\n';
if (result.message) {
delayedExpectationResult.message +=
'Message: "' + result.message + '"\n';
}
delayedExpectationResult.message +=
'Did you forget to return or await the result of expectAsync?';
topSuite.result.failedExpectations.push(delayedExpectationResult);
}
var asyncExpectationFactory = function(actual, spec, runableType) {
return j$.Expectation.asyncFactory({
util: j$.matchersUtil,
customEqualityTesters: runnableResources[spec.id].customEqualityTesters,
@@ -331,9 +357,19 @@ getJasmineRequireObj().Env = function(j$) {
});
function addExpectationResult(passed, result) {
if (currentRunnable() !== spec) {
recordLateExpectation(spec, runableType, result);
}
return spec.addExpectationResult(passed, result);
}
};
var suiteAsyncExpectationFactory = function(actual, suite) {
return asyncExpectationFactory(actual, suite, 'Suite');
};
var specAsyncExpectationFactory = function(actual, suite) {
return asyncExpectationFactory(actual, suite, 'Spec');
};
var defaultResourcesForRunnable = function(id, parentRunnableId) {
var resources = {
@@ -551,7 +587,7 @@ getJasmineRequireObj().Env = function(j$) {
id: getNextSuiteId(),
description: 'Jasmine__TopLevel__Suite',
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
asyncExpectationFactory: suiteAsyncExpectationFactory,
expectationResultFactory: expectationResultFactory
});
defaultResourcesForRunnable(topSuite.id);
@@ -884,7 +920,7 @@ getJasmineRequireObj().Env = function(j$) {
description: description,
parentSuite: currentDeclarationSuite,
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
asyncExpectationFactory: suiteAsyncExpectationFactory,
expectationResultFactory: expectationResultFactory,
throwOnExpectationFailure: config.oneFailurePerSpec
});
@@ -978,7 +1014,7 @@ getJasmineRequireObj().Env = function(j$) {
id: getNextSpecId(),
beforeAndAfterFns: beforeAndAfterFns(suite),
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
asyncExpectationFactory: specAsyncExpectationFactory,
resultCallback: specResultCallback,
getSpecName: function(spec) {
return getSpecName(spec, suite);