From 2b9185611d1e4e2db52c23d77d9f98c4e51a2e9b Mon Sep 17 00:00:00 2001 From: Philippe Daouadi Date: Tue, 20 Jul 2021 14:30:20 +0200 Subject: [PATCH] Keep exeception stacktrace when an assertion fails Fixes #268 --- lib/chai-as-promised.js | 86 ++++++++++++++++++++------------- test/should-promise-specific.js | 21 ++++++++ test/support/common.js | 6 +++ 3 files changed, 80 insertions(+), 33 deletions(-) diff --git a/lib/chai-as-promised.js b/lib/chai-as-promised.js index 0b84194..c3a57c0 100644 --- a/lib/chai-as-promised.js +++ b/lib/chai-as-promised.js @@ -59,6 +59,19 @@ module.exports = (chai, utils) => { promise.then(() => done(), done); } + function replaceExceptionStack(f, originalError) { + try { + f(); + } catch (e) { + if (originalError) + { + const message_lines = (originalError.message.match(/\n/g)||[]).length + 1; + e.stack = e.message + '\n' + originalError.stack.split('\n').slice(message_lines).join('\n'); + } + throw e; + } + } + // These are for clarity and to bypass Chai refusing to allow `undefined` as actual when used with `assert`. function assertIfNegated(assertion, message, extra) { assertion.assert(true, null, message, extra.expected, extra.actual); @@ -98,9 +111,11 @@ module.exports = (chai, utils) => { return value; }, reason => { - assertIfNotNegated(this, - "expected promise to be fulfilled but it was rejected with #{act}", - { actual: getReasonName(reason) }); + replaceExceptionStack(() => + assertIfNotNegated(this, + "expected promise to be fulfilled but it was rejected with #{act}", + { actual: getReasonName(reason) }), + reason); return reason; } ); @@ -118,9 +133,12 @@ module.exports = (chai, utils) => { return value; }, reason => { - assertIfNegated(this, - "expected promise not to be rejected but it was rejected with #{act}", - { actual: getReasonName(reason) }); + replaceExceptionStack(() => + assertIfNegated(this, + "expected promise not to be rejected but it was rejected with #{act}", + { actual: getReasonName(reason) }, + reason), + reason); // Return the reason, transforming this into a fulfillment, to allow further assertions, e.g. // `promise.should.be.rejected.and.eventually.equal("reason")`. @@ -192,34 +210,36 @@ module.exports = (chai, utils) => { const reasonName = getReasonName(reason); - if (negate && everyArgIsDefined) { - if (errorLikeCompatible && errMsgMatcherCompatible) { - this.assert(true, - null, - "expected promise not to be rejected with #{exp} but it was rejected " + - "with #{act}", - errorLikeName, - reasonName); - } - } else { - if (errorLike) { - this.assert(errorLikeCompatible, - "expected promise to be rejected with #{exp} but it was rejected with #{act}", - "expected promise not to be rejected with #{exp} but it was rejected " + - "with #{act}", - errorLikeName, - reasonName); + replaceExceptionStack(() => { + if (negate && everyArgIsDefined) { + if (errorLikeCompatible && errMsgMatcherCompatible) { + this.assert(true, + null, + "expected promise not to be rejected with #{exp} but it was rejected " + + "with #{act}", + errorLikeName, + reasonName); + } + } else { + if (errorLike) { + this.assert(errorLikeCompatible, + "expected promise to be rejected with #{exp} but it was rejected with #{act}", + "expected promise not to be rejected with #{exp} but it was rejected " + + "with #{act}", + errorLikeName, + reasonName); + } + + if (errMsgMatcher) { + this.assert(errMsgMatcherCompatible, + `expected promise to be rejected with an error ${matcherRelation} #{exp} but got ` + + `#{act}`, + `expected promise not to be rejected with an error ${matcherRelation} #{exp}`, + errMsgMatcher, + checkError.getMessage(reason)); + } } - - if (errMsgMatcher) { - this.assert(errMsgMatcherCompatible, - `expected promise to be rejected with an error ${matcherRelation} #{exp} but got ` + - `#{act}`, - `expected promise not to be rejected with an error ${matcherRelation} #{exp}`, - errMsgMatcher, - checkError.getMessage(reason)); - } - } + }, reason); return reason; } diff --git a/test/should-promise-specific.js b/test/should-promise-specific.js index 2653100..fbbb2a1 100644 --- a/test/should-promise-specific.js +++ b/test/should-promise-specific.js @@ -156,6 +156,13 @@ describe("Promise-specific extensions:", () => { }); }); + describe(".fulfilled should keep the exception stack", () => { + shouldFail({ + op: () => promise.should.be.fulfilled, + stack: "should-promise-specific.js" + }); + }); + describe(".not.fulfilled", () => { shouldPass(() => promise.should.not.be.fulfilled); }); @@ -194,6 +201,13 @@ describe("Promise-specific extensions:", () => { shouldPass(() => promise.should.be.rejectedWith(error)); }); + describe(".rejectedWith(differentError) should keep the exception stack if the assertion fails", () => { + shouldFail({ + op: () => promise.should.be.rejectedWith(new Error()), + stack: "should-promise-specific.js" + }); + }); + describe(".not.rejectedWith(theError)", () => { shouldFail({ op: () => promise.should.not.be.rejectedWith(error), @@ -201,6 +215,13 @@ describe("Promise-specific extensions:", () => { }); }); + describe(".not.rejectedWith(theError) should keep the exception stack if the assertion fails", () => { + shouldFail({ + op: () => promise.should.not.be.rejectedWith(error), + stack: "should-promise-specific.js" + }); + }); + describe(".rejectedWith(theError) should allow chaining", () => { shouldPass(() => promise.should.be.rejectedWith(error).and.eventually.have.property("myProp")); }); diff --git a/test/support/common.js b/test/support/common.js index 6b0862b..211d722 100644 --- a/test/support/common.js +++ b/test/support/common.js @@ -13,6 +13,7 @@ exports.shouldFail = options => { const promiseProducer = options.op; const desiredMessageSubstring = options.message; const nonDesiredMessageSubstring = options.notMessage; + const desiredStackSubstring = options.stack; it("should return a promise rejected with an assertion error", done => { promiseProducer().then( @@ -34,6 +35,11 @@ exports.shouldFail = options => { throw new Error(`Expected promise to be rejected with an AssertionError not containing ` + `"${nonDesiredMessageSubstring}" but it was rejected with ${reason}`); } + + if (desiredStackSubstring && !reason.stack.includes(desiredStackSubstring)) { + throw new Error(`Expected promise to be rejected with an AssertionError with a stack containing ` + + `"${desiredStackSubstring}" but it was rejected with ${reason}: ${reason.stack}`); + } } ).then(done, done); });