Deprecate multiple calls to done callbacks

This commit is contained in:
Steve Gravrock
2021-09-08 20:44:27 -07:00
parent 7944250290
commit be23836c9d
12 changed files with 553 additions and 74 deletions

View File

@@ -772,6 +772,7 @@ getJasmineRequireObj().Spec = function(j$) {
}; };
this.expectationResultFactory = this.expectationResultFactory =
attrs.expectationResultFactory || function() {}; attrs.expectationResultFactory || function() {};
this.deprecated = attrs.deprecated || function() {};
this.queueRunnerFactory = attrs.queueRunnerFactory || function() {}; this.queueRunnerFactory = attrs.queueRunnerFactory || function() {};
this.catchingExceptions = this.catchingExceptions =
attrs.catchingExceptions || attrs.catchingExceptions ||
@@ -866,6 +867,21 @@ getJasmineRequireObj().Spec = function(j$) {
onException: function() { onException: function() {
self.onException.apply(self, arguments); self.onException.apply(self, arguments);
}, },
onMultipleDone: function() {
// Issue a deprecation. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
self.deprecated(
"An asynchronous function called its 'done' " +
'callback more than once. This is a bug in the spec, beforeAll, ' +
'beforeEach, afterAll, or afterEach function in question. This will ' +
'be treated as an error in a future version.\n' +
'(in spec: ' +
self.getFullName() +
')',
{ ignoreRunnable: true }
);
},
onComplete: function() { onComplete: function() {
if (self.result.status === 'failed') { if (self.result.status === 'failed') {
onComplete(new j$.StopExecutionError('spec failed')); onComplete(new j$.StopExecutionError('spec failed'));
@@ -873,7 +889,8 @@ getJasmineRequireObj().Spec = function(j$) {
onComplete(); onComplete();
} }
}, },
userContext: this.userContext() userContext: this.userContext(),
runnableName: this.getFullName.bind(this)
}; };
if (this.markedPending || excluded === true) { if (this.markedPending || excluded === true) {
@@ -1929,7 +1946,8 @@ getJasmineRequireObj().Env = function(j$) {
*/ */
'specDone' 'specDone'
], ],
queueRunnerFactory queueRunnerFactory,
self.deprecated
); );
/** /**
@@ -2344,6 +2362,7 @@ getJasmineRequireObj().Env = function(j$) {
beforeAndAfterFns: beforeAndAfterFns(suite), beforeAndAfterFns: beforeAndAfterFns(suite),
expectationFactory: expectationFactory, expectationFactory: expectationFactory,
asyncExpectationFactory: specAsyncExpectationFactory, asyncExpectationFactory: specAsyncExpectationFactory,
deprecated: self.deprecated,
resultCallback: specResultCallback, resultCallback: specResultCallback,
getSpecName: function(spec) { getSpecName: function(spec) {
return getSpecName(spec, suite); return getSpecName(spec, suite);
@@ -8201,10 +8220,14 @@ getJasmineRequireObj().QueueRunner = function(j$) {
StopExecutionError.prototype = new Error(); StopExecutionError.prototype = new Error();
j$.StopExecutionError = StopExecutionError; j$.StopExecutionError = StopExecutionError;
function once(fn) { function once(fn, onTwice) {
var called = false; var called = false;
return function(arg) { return function(arg) {
if (!called) { if (called) {
if (onTwice) {
onTwice();
}
} else {
called = true; called = true;
// Direct call using single parameter, because cleanup/next does not need more // Direct call using single parameter, because cleanup/next does not need more
fn(arg); fn(arg);
@@ -8213,6 +8236,16 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}; };
} }
function fallbackOnMultipleDone() {
console.error(
new Error(
"An asynchronous function called its 'done' " +
'callback more than once, in a QueueRunner without a onMultipleDone ' +
'handler.'
)
);
}
function emptyFn() {} function emptyFn() {}
function QueueRunner(attrs) { function QueueRunner(attrs) {
@@ -8227,6 +8260,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
fn(); fn();
}; };
this.onException = attrs.onException || emptyFn; this.onException = attrs.onException || emptyFn;
this.onMultipleDone = attrs.onMultipleDone || fallbackOnMultipleDone;
this.userContext = attrs.userContext || new j$.UserContext(); this.userContext = attrs.userContext || new j$.UserContext();
this.timeout = attrs.timeout || { this.timeout = attrs.timeout || {
setTimeout: setTimeout, setTimeout: setTimeout,
@@ -8284,6 +8318,8 @@ getJasmineRequireObj().QueueRunner = function(j$) {
var self = this, var self = this,
completedSynchronously = true, completedSynchronously = true,
handleError = function handleError(error) { handleError = function handleError(error) {
// TODO probably shouldn't next() right away here.
// That makes debugging async failures much more confusing.
onException(error); onException(error);
next(error); next(error);
}, },
@@ -8293,37 +8329,52 @@ getJasmineRequireObj().QueueRunner = function(j$) {
} }
self.globalErrors.popListener(handleError); self.globalErrors.popListener(handleError);
}), }),
next = once(function next(err) { next = once(
cleanup(); function next(err) {
cleanup();
if (j$.isError_(err)) { if (j$.isError_(err)) {
if (!(err instanceof StopExecutionError) && !err.jasmineMessage) { if (!(err instanceof StopExecutionError) && !err.jasmineMessage) {
self.fail(err); self.fail(err);
}
self.errored = errored = true;
} else if (typeof err !== 'undefined' && !self.errored) {
self.deprecated(
'Any argument passed to a done callback will be treated as an ' +
'error in a future release. Call the done callback without ' +
"arguments if you don't want to trigger a spec failure."
);
} }
self.errored = errored = true;
} else if (typeof err !== 'undefined' && !self.errored) {
self.deprecated(
'Any argument passed to a done callback will be treated as an ' +
'error in a future release. Call the done callback without ' +
"arguments if you don't want to trigger a spec failure."
);
}
function runNext() { function runNext() {
if (self.completeOnFirstError && errored) { if (self.completeOnFirstError && errored) {
self.skipToCleanup(iterativeIndex); self.skipToCleanup(iterativeIndex);
} else {
self.run(iterativeIndex + 1);
}
}
if (completedSynchronously) {
self.setTimeout(runNext);
} else { } else {
self.run(iterativeIndex + 1); runNext();
}
},
function() {
try {
if (!timedOut) {
self.onMultipleDone();
}
} catch (error) {
// Any error we catch here is probably due to a bug in Jasmine,
// and it's not likely to end up anywhere useful if we let it
// propagate. Log it so it can at least show up when debugging.
console.error(error);
} }
} }
),
if (completedSynchronously) {
self.setTimeout(runNext);
} else {
runNext();
}
}),
errored = false, errored = false,
timedOut = false,
queueableFn = self.queueableFns[iterativeIndex], queueableFn = self.queueableFns[iterativeIndex],
timeoutId, timeoutId,
maybeThenable; maybeThenable;
@@ -8339,6 +8390,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
if (queueableFn.timeout !== undefined) { if (queueableFn.timeout !== undefined) {
var timeoutInterval = queueableFn.timeout || j$.DEFAULT_TIMEOUT_INTERVAL; var timeoutInterval = queueableFn.timeout || j$.DEFAULT_TIMEOUT_INTERVAL;
timeoutId = self.setTimeout(function() { timeoutId = self.setTimeout(function() {
timedOut = true;
var error = new Error( var error = new Error(
'Timeout - Async function did not complete within ' + 'Timeout - Async function did not complete within ' +
timeoutInterval + timeoutInterval +
@@ -8347,6 +8399,9 @@ getJasmineRequireObj().QueueRunner = function(j$) {
? '(custom timeout)' ? '(custom timeout)'
: '(set by jasmine.DEFAULT_TIMEOUT_INTERVAL)') : '(set by jasmine.DEFAULT_TIMEOUT_INTERVAL)')
); );
// TODO Need to decide what to do about a successful completion after a
// timeout. That should probably not be a deprecation, and maybe not
// an error in 4.0. (But a diagnostic of some sort might be helpful.)
onException(error); onException(error);
next(); next();
}, timeoutInterval); }, timeoutInterval);
@@ -8452,7 +8507,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}; };
getJasmineRequireObj().ReportDispatcher = function(j$) { getJasmineRequireObj().ReportDispatcher = function(j$) {
function ReportDispatcher(methods, queueRunnerFactory) { function ReportDispatcher(methods, queueRunnerFactory, deprecated) {
var dispatchedMethods = methods || []; var dispatchedMethods = methods || [];
for (var i = 0; i < dispatchedMethods.length; i++) { for (var i = 0; i < dispatchedMethods.length; i++) {
@@ -8496,7 +8551,15 @@ getJasmineRequireObj().ReportDispatcher = function(j$) {
queueRunnerFactory({ queueRunnerFactory({
queueableFns: fns, queueableFns: fns,
onComplete: onComplete, onComplete: onComplete,
isReporter: true isReporter: true,
onMultipleDone: function() {
deprecated(
"An asynchronous reporter callback called its 'done' callback " +
'more than once. This is a bug in the reporter callback in ' +
'question. This will be treated as an error in a future version.',
{ ignoreRunnable: true }
);
}
}); });
} }
@@ -10042,6 +10105,32 @@ getJasmineRequireObj().Suite = function(j$) {
this.result.failedExpectations.push(failedExpectation); this.result.failedExpectations.push(failedExpectation);
}; };
Suite.prototype.onMultipleDone = function() {
var msg;
// Issue a deprecation. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
if (this.parentSuite) {
msg =
"An asynchronous function called its 'done' callback more than " +
'once. This is a bug in the spec, beforeAll, beforeEach, afterAll, ' +
'or afterEach function in question. This will be treated as an error ' +
'in a future version.\n' +
'(in suite: ' +
this.getFullName() +
')';
} else {
msg =
'A top-level beforeAll or afterAll function called its ' +
"'done' callback more than once. This is a bug in the beforeAll " +
'or afterAll function in question. This will be treated as an ' +
'error in a future version.';
}
this.env.deprecated(msg, { ignoreRunnable: true });
};
Suite.prototype.addExpectationResult = function() { Suite.prototype.addExpectationResult = function() {
if (isFailure(arguments)) { if (isFailure(arguments)) {
var data = arguments[1]; var data = arguments[1];
@@ -10144,7 +10233,10 @@ getJasmineRequireObj().TreeProcessor = function() {
onException: function() { onException: function() {
tree.onException.apply(tree, arguments); tree.onException.apply(tree, arguments);
}, },
onComplete: done onComplete: done,
onMultipleDone: tree.onMultipleDone
? tree.onMultipleDone.bind(tree)
: null
}); });
}; };
@@ -10316,7 +10408,10 @@ getJasmineRequireObj().TreeProcessor = function() {
userContext: node.sharedUserContext(), userContext: node.sharedUserContext(),
onException: function() { onException: function() {
node.onException.apply(node, arguments); node.onException.apply(node, arguments);
} },
onMultipleDone: node.onMultipleDone
? node.onMultipleDone.bind(node)
: null
}); });
} }
}; };

View File

@@ -305,6 +305,32 @@ describe('QueueRunner', function() {
expect(onComplete).toHaveBeenCalled(); expect(onComplete).toHaveBeenCalled();
}); });
it('does not call onMultipleDone if an asynchrnous function completes after timing out', function() {
var timeout = 3,
queueableFn = {
fn: function(done) {
queueableFnDone = done;
},
type: 'queueable',
timeout: timeout
},
onComplete = jasmine.createSpy('onComplete'),
onMultipleDone = jasmine.createSpy('onMultipleDone'),
queueRunner = new jasmineUnderTest.QueueRunner({
queueableFns: [queueableFn],
onComplete: onComplete,
onMultipleDone: onMultipleDone
}),
queueableFnDone;
queueRunner.execute();
jasmine.clock().tick(timeout);
queueableFnDone();
expect(onComplete).toHaveBeenCalled();
expect(onMultipleDone).not.toHaveBeenCalled();
});
it('by default does not set a timeout for asynchronous functions', function() { it('by default does not set a timeout for asynchronous functions', function() {
var beforeFn = { fn: function(done) {} }, var beforeFn = { fn: function(done) {} },
queueableFn = { fn: jasmine.createSpy('fn') }, queueableFn = { fn: jasmine.createSpy('fn') },
@@ -380,13 +406,16 @@ describe('QueueRunner', function() {
} }
}, },
nextQueueableFn = { fn: jasmine.createSpy('nextFn') }, nextQueueableFn = { fn: jasmine.createSpy('nextFn') },
onMultipleDone = jasmine.createSpy('onMultipleDone'),
queueRunner = new jasmineUnderTest.QueueRunner({ queueRunner = new jasmineUnderTest.QueueRunner({
queueableFns: [queueableFn, nextQueueableFn] queueableFns: [queueableFn, nextQueueableFn],
onMultipleDone: onMultipleDone
}); });
queueRunner.execute(); queueRunner.execute();
jasmine.clock().tick(1); jasmine.clock().tick(1);
expect(nextQueueableFn.fn.calls.count()).toEqual(1); expect(nextQueueableFn.fn.calls.count()).toEqual(1);
expect(onMultipleDone).toHaveBeenCalled();
}); });
it('does not move to the next spec if done is called after an exception has ended the spec', function() { it('does not move to the next spec if done is called after an exception has ended the spec', function() {
@@ -397,13 +426,17 @@ describe('QueueRunner', function() {
} }
}, },
nextQueueableFn = { fn: jasmine.createSpy('nextFn') }, nextQueueableFn = { fn: jasmine.createSpy('nextFn') },
deprecated = jasmine.createSpy('deprecated'),
queueRunner = new jasmineUnderTest.QueueRunner({ queueRunner = new jasmineUnderTest.QueueRunner({
deprecated: deprecated,
queueableFns: [queueableFn, nextQueueableFn] queueableFns: [queueableFn, nextQueueableFn]
}); });
queueRunner.execute(); queueRunner.execute();
jasmine.clock().tick(1); jasmine.clock().tick(1);
expect(nextQueueableFn.fn.calls.count()).toEqual(1); expect(nextQueueableFn.fn.calls.count()).toEqual(1);
// Don't issue a deprecation. The error already tells the user that
// something went wrong.
expect(deprecated).not.toHaveBeenCalled();
}); });
it('should return a null when you call done', function() { it('should return a null when you call done', function() {

View File

@@ -516,4 +516,31 @@ describe('Spec', function() {
args.cleanupFns[0].fn(); args.cleanupFns[0].fn();
expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([]); expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([]);
}); });
it('passes an onMultipleDone that logs a deprecation', function() {
var queueRunnerFactory = jasmine.createSpy('queueRunnerFactory'),
deprecated = jasmine.createSpy('depredated'),
spec = new jasmineUnderTest.Spec({
deprecated: deprecated,
queueableFn: { fn: function() {} },
queueRunnerFactory: queueRunnerFactory,
getSpecName: function() {
return 'a spec';
}
});
spec.execute();
expect(queueRunnerFactory).toHaveBeenCalled();
queueRunnerFactory.calls.argsFor(0)[0].onMultipleDone();
expect(deprecated).toHaveBeenCalledWith(
"An asynchronous function called its 'done' " +
'callback more than once. This is a bug in the spec, beforeAll, ' +
'beforeEach, afterAll, or afterEach function in question. This will ' +
'be treated as an error in a future version.\n' +
'(in spec: a spec)',
{ ignoreRunnable: true }
);
});
}); });

View File

@@ -142,4 +142,44 @@ describe('Suite', function() {
); );
}); });
}); });
describe('#onMultipleDone', function() {
it('logs a special deprecation when it is the top suite', function() {
var env = jasmine.createSpyObj('env', ['deprecated']);
var suite = new jasmineUnderTest.Suite({ env: env, parentSuite: null });
suite.onMultipleDone();
expect(env.deprecated).toHaveBeenCalledWith(
'A top-level beforeAll or afterAll function called its ' +
"'done' callback more than once. This is a bug in the beforeAll " +
'or afterAll function in question. This will be treated as an ' +
'error in a future version.',
{ ignoreRunnable: true }
);
});
it('logs a deprecation including the suite name when it is a normal suite', function() {
var env = jasmine.createSpyObj('env', ['deprecated']);
var suite = new jasmineUnderTest.Suite({
env: env,
description: 'the suite',
parentSuite: {
description: 'the parent suite',
parentSuite: {}
}
});
suite.onMultipleDone();
expect(env.deprecated).toHaveBeenCalledWith(
"An asynchronous function called its 'done' callback more than " +
'once. This is a bug in the spec, beforeAll, beforeEach, afterAll, ' +
'or afterEach function in question. This will be treated as an error ' +
'in a future version.\n' +
'(in suite: the parent suite the suite)',
{ ignoreRunnable: true }
);
});
});
}); });

View File

@@ -291,7 +291,8 @@ describe('TreeProcessor', function() {
onComplete: treeComplete, onComplete: treeComplete,
onException: jasmine.any(Function), onException: jasmine.any(Function),
userContext: { root: 'context' }, userContext: { root: 'context' },
queueableFns: [{ fn: jasmine.any(Function) }] queueableFns: [{ fn: jasmine.any(Function) }],
onMultipleDone: null
}); });
queueRunner.calls.mostRecent().args[0].queueableFns[0].fn('foo'); queueRunner.calls.mostRecent().args[0].queueableFns[0].fn('foo');
@@ -321,16 +322,19 @@ describe('TreeProcessor', function() {
onComplete: treeComplete, onComplete: treeComplete,
onException: jasmine.any(Function), onException: jasmine.any(Function),
userContext: { root: 'context' }, userContext: { root: 'context' },
queueableFns: [{ fn: jasmine.any(Function) }] queueableFns: [{ fn: jasmine.any(Function) }],
onMultipleDone: null
}); });
queueRunner.calls.mostRecent().args[0].queueableFns[0].fn(nodeDone); queueRunner.calls.mostRecent().args[0].queueableFns[0].fn(nodeDone);
expect(queueRunner).toHaveBeenCalledWith({ expect(queueRunner).toHaveBeenCalledWith({
onComplete: jasmine.any(Function), onComplete: jasmine.any(Function),
onMultipleDone: null,
queueableFns: [{ fn: jasmine.any(Function) }], queueableFns: [{ fn: jasmine.any(Function) }],
userContext: { node: 'context' }, userContext: { node: 'context' },
onException: jasmine.any(Function) onException: jasmine.any(Function),
onMultipleDone: null
}); });
queueRunner.calls.mostRecent().args[0].queueableFns[0].fn('foo'); queueRunner.calls.mostRecent().args[0].queueableFns[0].fn('foo');

View File

@@ -513,6 +513,191 @@ describe('Env integration', function() {
env.execute(null, assertions); env.execute(null, assertions);
}); });
it('deprecates multiple calls to done in the top suite', function(done) {
var reporter = jasmine.createSpyObj('fakeReporter', ['jasmineDone']);
var message =
'A top-level beforeAll or afterAll function called its ' +
"'done' callback more than once. This is a bug in the beforeAll " +
'or afterAll function in question. This will be treated as an ' +
'error in a future version.';
spyOn(console, 'error');
env.addReporter(reporter);
env.configure({ verboseDeprecations: true });
env.beforeAll(function(innerDone) {
innerDone();
innerDone();
});
env.it('a spec, so the beforeAll runs', function() {});
env.afterAll(function(innerDone) {
innerDone();
innerDone();
});
env.execute(null, function() {
var warnings;
expect(reporter.jasmineDone).toHaveBeenCalled();
warnings = reporter.jasmineDone.calls.argsFor(0)[0].deprecationWarnings;
expect(warnings.length).toEqual(2);
expect(warnings[0])
.withContext('top beforeAll')
.toEqual(jasmine.objectContaining({ message: message }));
expect(warnings[1])
.withContext('top afterAll')
.toEqual(jasmine.objectContaining({ message: message }));
done();
});
});
it('deprecates multiple calls to done in a non-top suite', function(done) {
var reporter = jasmine.createSpyObj('fakeReporter', ['jasmineDone']);
var message =
"An asynchronous function called its 'done' " +
'callback more than once. This is a bug in the spec, beforeAll, ' +
'beforeEach, afterAll, or afterEach function in question. This will ' +
'be treated as an error in a future version.';
spyOn(console, 'error');
env.addReporter(reporter);
env.configure({ verboseDeprecations: true });
env.describe('a suite', function() {
env.beforeAll(function(innerDone) {
innerDone();
innerDone();
});
env.it('a spec, so that before/afters run', function() {});
env.afterAll(function(innerDone) {
innerDone();
innerDone();
});
});
env.execute(null, function() {
var warnings;
expect(reporter.jasmineDone).toHaveBeenCalled();
warnings = reporter.jasmineDone.calls.argsFor(0)[0].deprecationWarnings;
expect(warnings.length).toEqual(2);
expect(warnings[0])
.withContext('suite beforeAll')
.toEqual(
jasmine.objectContaining({
message: message + '\n(in suite: a suite)'
})
);
expect(warnings[1])
.withContext('suite afterAll')
.toEqual(
jasmine.objectContaining({
message: message + '\n(in suite: a suite)'
})
);
done();
});
});
it('deprecates multiple calls to done in a spec', function(done) {
var reporter = jasmine.createSpyObj('fakeReporter', ['jasmineDone']);
var message =
"An asynchronous function called its 'done' " +
'callback more than once. This is a bug in the spec, beforeAll, ' +
'beforeEach, afterAll, or afterEach function in question. This will ' +
'be treated as an error in a future version.\n' +
'(in spec: a suite a spec)';
spyOn(console, 'error');
env.addReporter(reporter);
env.configure({ verboseDeprecations: true });
env.describe('a suite', function() {
env.beforeEach(function(innerDone) {
innerDone();
innerDone();
});
env.it('a spec', function(innerDone) {
innerDone();
innerDone();
});
env.afterEach(function(innerDone) {
innerDone();
innerDone();
});
});
env.execute(null, function() {
var warnings;
expect(reporter.jasmineDone).toHaveBeenCalled();
warnings = reporter.jasmineDone.calls.argsFor(0)[0].deprecationWarnings;
expect(warnings.length).toEqual(3);
expect(warnings[0])
.withContext('warning caused by beforeEach')
.toEqual(jasmine.objectContaining({ message: message }));
expect(warnings[1])
.withContext('warning caused by it')
.toEqual(jasmine.objectContaining({ message: message }));
expect(warnings[2])
.withContext('warning caused by afterEach')
.toEqual(jasmine.objectContaining({ message: message }));
done();
});
});
it('deprecates multiple calls to done in reporters', function(done) {
var message =
"An asynchronous reporter callback called its 'done' callback more " +
'than once. This is a bug in the reporter callback in question. This ' +
'will be treated as an error in a future version.\nNote: This message ' +
'will be shown only once. Set config.verboseDeprecations to true to ' +
'see every occurrence.';
var reporter = jasmine.createSpyObj('fakeReport', ['jasmineDone']);
reporter.specDone = function(result, done) {
done();
done();
};
env.addReporter(reporter);
env.it('a spec', function() {});
spyOn(console, 'error');
env.execute(null, function() {
expect(reporter.jasmineDone).toHaveBeenCalled();
warnings = reporter.jasmineDone.calls.argsFor(0)[0].deprecationWarnings;
expect(warnings.length).toEqual(1);
expect(warnings[0]).toEqual(
jasmine.objectContaining({ message: message })
);
done();
});
});
it('does not deprecate a call to done that comes after a timeout', function(done) {
var reporter = jasmine.createSpyObj('fakeReporter', ['jasmineDone']),
firstSpecDone;
reporter.specDone = function(result, reporterDone) {
setTimeout(function() {
firstSpecDone();
reporterDone();
});
};
env.addReporter(reporter);
env.it(
'a spec',
function(innerDone) {
firstSpecDone = innerDone;
},
1
);
env.execute(null, function() {
expect(reporter.jasmineDone).toHaveBeenCalledWith(
jasmine.objectContaining({
deprecationWarnings: []
})
);
done();
});
});
describe('suiteDone reporting', function() { describe('suiteDone reporting', function() {
it('reports when an afterAll fails an expectation', function(done) { it('reports when an afterAll fails an expectation', function(done) {
var reporter = jasmine.createSpyObj('fakeReport', ['suiteDone']); var reporter = jasmine.createSpyObj('fakeReport', ['suiteDone']);
@@ -1197,10 +1382,6 @@ describe('Env integration', function() {
}); });
it('should wait a custom interval before reporting async functions that fail to complete', function(done) { it('should wait a custom interval before reporting async functions that fail to complete', function(done) {
if (jasmine.getEnv().skipBrowserFlake) {
jasmine.getEnv().skipBrowserFlake();
}
createMockedEnv(); createMockedEnv();
var reporter = jasmine.createSpyObj('fakeReport', [ var reporter = jasmine.createSpyObj('fakeReport', [
'jasmineDone', 'jasmineDone',
@@ -2519,7 +2700,7 @@ describe('Env integration', function() {
return setTimeout(fn, delay); return setTimeout(fn, delay);
}, },
clearTimeout: function(fn, delay) { clearTimeout: function(fn, delay) {
clearTimeout(fn, delay); return clearTimeout(fn, delay);
} }
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -2774,6 +2955,10 @@ describe('Env integration', function() {
}); });
it('provides custom equality testers to async matchers', function(done) { it('provides custom equality testers to async matchers', function(done) {
if (jasmine.getEnv().skipBrowserFlake) {
jasmine.getEnv().skipBrowserFlake();
}
jasmine.getEnv().requirePromises(); jasmine.getEnv().requirePromises();
var specDone = jasmine.createSpy('specDone'); var specDone = jasmine.createSpy('specDone');

View File

@@ -891,7 +891,8 @@ getJasmineRequireObj().Env = function(j$) {
*/ */
'specDone' 'specDone'
], ],
queueRunnerFactory queueRunnerFactory,
self.deprecated
); );
/** /**
@@ -1306,6 +1307,7 @@ getJasmineRequireObj().Env = function(j$) {
beforeAndAfterFns: beforeAndAfterFns(suite), beforeAndAfterFns: beforeAndAfterFns(suite),
expectationFactory: expectationFactory, expectationFactory: expectationFactory,
asyncExpectationFactory: specAsyncExpectationFactory, asyncExpectationFactory: specAsyncExpectationFactory,
deprecated: self.deprecated,
resultCallback: specResultCallback, resultCallback: specResultCallback,
getSpecName: function(spec) { getSpecName: function(spec) {
return getSpecName(spec, suite); return getSpecName(spec, suite);

View File

@@ -5,10 +5,14 @@ getJasmineRequireObj().QueueRunner = function(j$) {
StopExecutionError.prototype = new Error(); StopExecutionError.prototype = new Error();
j$.StopExecutionError = StopExecutionError; j$.StopExecutionError = StopExecutionError;
function once(fn) { function once(fn, onTwice) {
var called = false; var called = false;
return function(arg) { return function(arg) {
if (!called) { if (called) {
if (onTwice) {
onTwice();
}
} else {
called = true; called = true;
// Direct call using single parameter, because cleanup/next does not need more // Direct call using single parameter, because cleanup/next does not need more
fn(arg); fn(arg);
@@ -17,6 +21,16 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}; };
} }
function fallbackOnMultipleDone() {
console.error(
new Error(
"An asynchronous function called its 'done' " +
'callback more than once, in a QueueRunner without a onMultipleDone ' +
'handler.'
)
);
}
function emptyFn() {} function emptyFn() {}
function QueueRunner(attrs) { function QueueRunner(attrs) {
@@ -31,6 +45,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
fn(); fn();
}; };
this.onException = attrs.onException || emptyFn; this.onException = attrs.onException || emptyFn;
this.onMultipleDone = attrs.onMultipleDone || fallbackOnMultipleDone;
this.userContext = attrs.userContext || new j$.UserContext(); this.userContext = attrs.userContext || new j$.UserContext();
this.timeout = attrs.timeout || { this.timeout = attrs.timeout || {
setTimeout: setTimeout, setTimeout: setTimeout,
@@ -88,6 +103,8 @@ getJasmineRequireObj().QueueRunner = function(j$) {
var self = this, var self = this,
completedSynchronously = true, completedSynchronously = true,
handleError = function handleError(error) { handleError = function handleError(error) {
// TODO probably shouldn't next() right away here.
// That makes debugging async failures much more confusing.
onException(error); onException(error);
next(error); next(error);
}, },
@@ -97,37 +114,52 @@ getJasmineRequireObj().QueueRunner = function(j$) {
} }
self.globalErrors.popListener(handleError); self.globalErrors.popListener(handleError);
}), }),
next = once(function next(err) { next = once(
cleanup(); function next(err) {
cleanup();
if (j$.isError_(err)) { if (j$.isError_(err)) {
if (!(err instanceof StopExecutionError) && !err.jasmineMessage) { if (!(err instanceof StopExecutionError) && !err.jasmineMessage) {
self.fail(err); self.fail(err);
}
self.errored = errored = true;
} else if (typeof err !== 'undefined' && !self.errored) {
self.deprecated(
'Any argument passed to a done callback will be treated as an ' +
'error in a future release. Call the done callback without ' +
"arguments if you don't want to trigger a spec failure."
);
} }
self.errored = errored = true;
} else if (typeof err !== 'undefined' && !self.errored) {
self.deprecated(
'Any argument passed to a done callback will be treated as an ' +
'error in a future release. Call the done callback without ' +
"arguments if you don't want to trigger a spec failure."
);
}
function runNext() { function runNext() {
if (self.completeOnFirstError && errored) { if (self.completeOnFirstError && errored) {
self.skipToCleanup(iterativeIndex); self.skipToCleanup(iterativeIndex);
} else {
self.run(iterativeIndex + 1);
}
}
if (completedSynchronously) {
self.setTimeout(runNext);
} else { } else {
self.run(iterativeIndex + 1); runNext();
}
},
function() {
try {
if (!timedOut) {
self.onMultipleDone();
}
} catch (error) {
// Any error we catch here is probably due to a bug in Jasmine,
// and it's not likely to end up anywhere useful if we let it
// propagate. Log it so it can at least show up when debugging.
console.error(error);
} }
} }
),
if (completedSynchronously) {
self.setTimeout(runNext);
} else {
runNext();
}
}),
errored = false, errored = false,
timedOut = false,
queueableFn = self.queueableFns[iterativeIndex], queueableFn = self.queueableFns[iterativeIndex],
timeoutId, timeoutId,
maybeThenable; maybeThenable;
@@ -143,6 +175,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
if (queueableFn.timeout !== undefined) { if (queueableFn.timeout !== undefined) {
var timeoutInterval = queueableFn.timeout || j$.DEFAULT_TIMEOUT_INTERVAL; var timeoutInterval = queueableFn.timeout || j$.DEFAULT_TIMEOUT_INTERVAL;
timeoutId = self.setTimeout(function() { timeoutId = self.setTimeout(function() {
timedOut = true;
var error = new Error( var error = new Error(
'Timeout - Async function did not complete within ' + 'Timeout - Async function did not complete within ' +
timeoutInterval + timeoutInterval +
@@ -151,6 +184,9 @@ getJasmineRequireObj().QueueRunner = function(j$) {
? '(custom timeout)' ? '(custom timeout)'
: '(set by jasmine.DEFAULT_TIMEOUT_INTERVAL)') : '(set by jasmine.DEFAULT_TIMEOUT_INTERVAL)')
); );
// TODO Need to decide what to do about a successful completion after a
// timeout. That should probably not be a deprecation, and maybe not
// an error in 4.0. (But a diagnostic of some sort might be helpful.)
onException(error); onException(error);
next(); next();
}, timeoutInterval); }, timeoutInterval);

View File

@@ -1,5 +1,5 @@
getJasmineRequireObj().ReportDispatcher = function(j$) { getJasmineRequireObj().ReportDispatcher = function(j$) {
function ReportDispatcher(methods, queueRunnerFactory) { function ReportDispatcher(methods, queueRunnerFactory, deprecated) {
var dispatchedMethods = methods || []; var dispatchedMethods = methods || [];
for (var i = 0; i < dispatchedMethods.length; i++) { for (var i = 0; i < dispatchedMethods.length; i++) {
@@ -43,7 +43,15 @@ getJasmineRequireObj().ReportDispatcher = function(j$) {
queueRunnerFactory({ queueRunnerFactory({
queueableFns: fns, queueableFns: fns,
onComplete: onComplete, onComplete: onComplete,
isReporter: true isReporter: true,
onMultipleDone: function() {
deprecated(
"An asynchronous reporter callback called its 'done' callback " +
'more than once. This is a bug in the reporter callback in ' +
'question. This will be treated as an error in a future version.',
{ ignoreRunnable: true }
);
}
}); });
} }

View File

@@ -40,6 +40,7 @@ getJasmineRequireObj().Spec = function(j$) {
}; };
this.expectationResultFactory = this.expectationResultFactory =
attrs.expectationResultFactory || function() {}; attrs.expectationResultFactory || function() {};
this.deprecated = attrs.deprecated || function() {};
this.queueRunnerFactory = attrs.queueRunnerFactory || function() {}; this.queueRunnerFactory = attrs.queueRunnerFactory || function() {};
this.catchingExceptions = this.catchingExceptions =
attrs.catchingExceptions || attrs.catchingExceptions ||
@@ -134,6 +135,21 @@ getJasmineRequireObj().Spec = function(j$) {
onException: function() { onException: function() {
self.onException.apply(self, arguments); self.onException.apply(self, arguments);
}, },
onMultipleDone: function() {
// Issue a deprecation. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
self.deprecated(
"An asynchronous function called its 'done' " +
'callback more than once. This is a bug in the spec, beforeAll, ' +
'beforeEach, afterAll, or afterEach function in question. This will ' +
'be treated as an error in a future version.\n' +
'(in spec: ' +
self.getFullName() +
')',
{ ignoreRunnable: true }
);
},
onComplete: function() { onComplete: function() {
if (self.result.status === 'failed') { if (self.result.status === 'failed') {
onComplete(new j$.StopExecutionError('spec failed')); onComplete(new j$.StopExecutionError('spec failed'));
@@ -141,7 +157,8 @@ getJasmineRequireObj().Spec = function(j$) {
onComplete(); onComplete();
} }
}, },
userContext: this.userContext() userContext: this.userContext(),
runnableName: this.getFullName.bind(this)
}; };
if (this.markedPending || excluded === true) { if (this.markedPending || excluded === true) {

View File

@@ -201,6 +201,32 @@ getJasmineRequireObj().Suite = function(j$) {
this.result.failedExpectations.push(failedExpectation); this.result.failedExpectations.push(failedExpectation);
}; };
Suite.prototype.onMultipleDone = function() {
var msg;
// Issue a deprecation. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
if (this.parentSuite) {
msg =
"An asynchronous function called its 'done' callback more than " +
'once. This is a bug in the spec, beforeAll, beforeEach, afterAll, ' +
'or afterEach function in question. This will be treated as an error ' +
'in a future version.\n' +
'(in suite: ' +
this.getFullName() +
')';
} else {
msg =
'A top-level beforeAll or afterAll function called its ' +
"'done' callback more than once. This is a bug in the beforeAll " +
'or afterAll function in question. This will be treated as an ' +
'error in a future version.';
}
this.env.deprecated(msg, { ignoreRunnable: true });
};
Suite.prototype.addExpectationResult = function() { Suite.prototype.addExpectationResult = function() {
if (isFailure(arguments)) { if (isFailure(arguments)) {
var data = arguments[1]; var data = arguments[1];

View File

@@ -44,7 +44,10 @@ getJasmineRequireObj().TreeProcessor = function() {
onException: function() { onException: function() {
tree.onException.apply(tree, arguments); tree.onException.apply(tree, arguments);
}, },
onComplete: done onComplete: done,
onMultipleDone: tree.onMultipleDone
? tree.onMultipleDone.bind(tree)
: null
}); });
}; };
@@ -216,7 +219,10 @@ getJasmineRequireObj().TreeProcessor = function() {
userContext: node.sharedUserContext(), userContext: node.sharedUserContext(),
onException: function() { onException: function() {
node.onException.apply(node, arguments); node.onException.apply(node, arguments);
} },
onMultipleDone: node.onMultipleDone
? node.onMultipleDone.bind(node)
: null
}); });
} }
}; };