See #2413 and other various issues and PRs for previous discussion.
In Mocha 2.x, when a Promise is returned from an "async" test (using the done() callback), the Promise resolution is effectively ignored, though it may cause an unhandled rejection _after_ the test has finished.
In Mocha 3.x to the present, returning a Promise from an "async" test is disallowed; tests must use either done() or return a Promise, but not both.
A better solution in this case would be to wait for both resolutions (the call of done() and Promise fulfillment) before the test can end.
When a test requests done() and returns a Promise:
done() is calledPromise has been resolveddone() is called to conclude the testdone() is called with an error parameterdone() is called without an error parameterPromise has been rejecteddone() is called to conclude the testdone() is eventually called, it should be ignoreddone() was called, any error parameter would have been ignoreddone() is called before it is fulfilleddone() is not called with an error parameterPromise fulfillment to conclude the testPromise has been rejectedPromise has been resolveddone() is called with an error parameterPromise fulfillment to conclude the testPromise fulfills, it should be ignoredPromise has been rejected, any error parameter would have been ignoredNormal timeout rules apply.
Comments, suggestions, questions, concerns?
I think it looks great! I think #2511 is related, since we're talking about how Mocha will respond with done and Promise. Not sure the answer to that belongs within this, but it may effect how the code is written.
I'm tagging this major-release because it will affect the behavior of existing tests; some tests may newly fail or pass, depending.
I'm not super excited to include it as a dependency, but asCallback and/or fromCallback would likely simplify the implementation.
If we add waiting for two async operations at the same time I think we need to improve the timeout handling as well. The timeout message should tell you which one of the async blockers timed out, or if both of them did. Otherwise it gets really hard to debug
@Munter agree
Just copy/pasting this comment here:
Example of using both done and Promise when both are requested/given, catching an Error that would be missed otherwise:
it('fires change event when calling blah.doSomethingThatFiresChange', async function (done) {
const blah = await getBlah()
blah.on('change', _ => done())
blah.doSomethingThatFiresChange()
})
1) fires change event when calling blah.doSomethingThatFiresChange:
Error: the `done` callback was successfully called, but the returned `Promise`
was rejected with the following `TypeError`:
TypeError: Cannot read property 'likes' of undefined
at Blah.doSomethingThatFiresChange (lib/Blah.js:5:9)
at Context.<anonymous> (test/Blah.spec.js:4:8)
More info: https://github.com/mochajs/mocha/wiki/some-page
What happens if something other than a promise is returned?
@tandrewnichols It would get ignored, as it currently does.
I actually want to weigh in against this; all of my ever-growing experience points toward mixing of promises and callbacks being worse than either one, and I think I just figured out why.
Promise.all(basePromise.then(stuff), basePromise.then(otherStuff)) or ....then(value => Promise.all(stuff(value), otherStuff(value)))Promise.all only with more associated risks.Promise.all or as dangerous as mixing callbacks and promises, but clearly developers who are unwilling to use promises wouldn't be using the callback+promise branch approach either, so anyone considering the callback+promise branch approach as easier or safer than pure callback branching shouldn't have objections in principle to using the safe and easy promise-only branching mechanism.)(NB: to the extent that async functions are just nicer-looking promise chains, presumably the same logic applies. For instance, in the above example of passing Mocha's callback to a callback API that's used inside an async test function, I would create a promise from the callback API in order to either await it in or return it from the async function instead -- and if const wasCallback = new Promise(resolve => blah.on('change', resolve)) isn't clean enough, use a dedicated promisification tool.)
TL;DR: It's wrong except when it's "right" in the sense of being identical in effect to a superior alternative that doesn't have the high probability of being wrong. I realize that's an unusually categorical judgement for programming, art of tradeoffs, but I am fairly sure the logic above guarantees it. Strong opinions weakly held and all that -- if there's a flaw in the logic I'll gladly reconsider either the logic or my stance on mixing these paradigms. As always, just my two cents' worth. :wink:
@ScottFreeCode I've finally read through your message, but I either don't understand it or I disagree.
In my head, the solution would be like, okay, I'm the runner, I've got the callback cb that I've passed, and through executing the text I've been returned a Promise testP. The callback was defined somewhat like:
let callback
const callbackP = new Promise((resolve, reject) => {
callback = function (err) {
if (err) reject(err)
else resolve()
}
}
And then we merge both promises to know when the whole thing ends:
const wholeThingP = Promise.all(callbackP, testP)
Whenever wholeThingP is fulfilled, we're done. Whenever wholeThingP is rejected, we're done.
Whenever we are done in a failed way, or the 2000 timeout occurs, inspect both promises to log their status: pending, fulfilled, resolved.
I don't feel like the "there's risk in doing this" argument is justified. Most likely because it doesn't feel complicated to me, and/or the solution I have in mind is different.
Feels nice to be engaging in the community after so long! 馃槉
@dasilvacontin scott's long gone unfortunately