Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep exception stacktrace when an assertion fails #274

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 52 additions & 33 deletions lib/chai-as-promised.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ module.exports = (chai, utils) => {
promise.then(() => done(), done);
}

function replaceExceptionStack(f, originalError) {
try {
f();
} catch (e) {
if (originalError) {
const messageLines = (originalError.message.match(/\n/g) || []).length + 1;
e.stack = e.message + "\n" + originalError.stack.split("\n").slice(messageLines).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);
Expand Down Expand Up @@ -98,9 +110,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;
}
);
Expand All @@ -118,9 +132,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")`.
Expand Down Expand Up @@ -192,34 +209,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;
}
Expand Down
21 changes: 21 additions & 0 deletions test/should-promise-specific.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -194,13 +201,27 @@ 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),
message: "not to be rejected with 'Error: boo'"
});
});

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"));
});
Expand Down
6 changes: 6 additions & 0 deletions test/support/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
});
Expand Down