Skip to content

Commit 2b91856

Browse files
committed
Keep exeception stacktrace when an assertion fails
Fixes chaijs#268
1 parent 39bfd34 commit 2b91856

File tree

3 files changed

+80
-33
lines changed

3 files changed

+80
-33
lines changed

lib/chai-as-promised.js

+53-33
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ module.exports = (chai, utils) => {
5959
promise.then(() => done(), done);
6060
}
6161

62+
function replaceExceptionStack(f, originalError) {
63+
try {
64+
f();
65+
} catch (e) {
66+
if (originalError)
67+
{
68+
const message_lines = (originalError.message.match(/\n/g)||[]).length + 1;
69+
e.stack = e.message + '\n' + originalError.stack.split('\n').slice(message_lines).join('\n');
70+
}
71+
throw e;
72+
}
73+
}
74+
6275
// These are for clarity and to bypass Chai refusing to allow `undefined` as actual when used with `assert`.
6376
function assertIfNegated(assertion, message, extra) {
6477
assertion.assert(true, null, message, extra.expected, extra.actual);
@@ -98,9 +111,11 @@ module.exports = (chai, utils) => {
98111
return value;
99112
},
100113
reason => {
101-
assertIfNotNegated(this,
102-
"expected promise to be fulfilled but it was rejected with #{act}",
103-
{ actual: getReasonName(reason) });
114+
replaceExceptionStack(() =>
115+
assertIfNotNegated(this,
116+
"expected promise to be fulfilled but it was rejected with #{act}",
117+
{ actual: getReasonName(reason) }),
118+
reason);
104119
return reason;
105120
}
106121
);
@@ -118,9 +133,12 @@ module.exports = (chai, utils) => {
118133
return value;
119134
},
120135
reason => {
121-
assertIfNegated(this,
122-
"expected promise not to be rejected but it was rejected with #{act}",
123-
{ actual: getReasonName(reason) });
136+
replaceExceptionStack(() =>
137+
assertIfNegated(this,
138+
"expected promise not to be rejected but it was rejected with #{act}",
139+
{ actual: getReasonName(reason) },
140+
reason),
141+
reason);
124142

125143
// Return the reason, transforming this into a fulfillment, to allow further assertions, e.g.
126144
// `promise.should.be.rejected.and.eventually.equal("reason")`.
@@ -192,34 +210,36 @@ module.exports = (chai, utils) => {
192210

193211
const reasonName = getReasonName(reason);
194212

195-
if (negate && everyArgIsDefined) {
196-
if (errorLikeCompatible && errMsgMatcherCompatible) {
197-
this.assert(true,
198-
null,
199-
"expected promise not to be rejected with #{exp} but it was rejected " +
200-
"with #{act}",
201-
errorLikeName,
202-
reasonName);
203-
}
204-
} else {
205-
if (errorLike) {
206-
this.assert(errorLikeCompatible,
207-
"expected promise to be rejected with #{exp} but it was rejected with #{act}",
208-
"expected promise not to be rejected with #{exp} but it was rejected " +
209-
"with #{act}",
210-
errorLikeName,
211-
reasonName);
213+
replaceExceptionStack(() => {
214+
if (negate && everyArgIsDefined) {
215+
if (errorLikeCompatible && errMsgMatcherCompatible) {
216+
this.assert(true,
217+
null,
218+
"expected promise not to be rejected with #{exp} but it was rejected " +
219+
"with #{act}",
220+
errorLikeName,
221+
reasonName);
222+
}
223+
} else {
224+
if (errorLike) {
225+
this.assert(errorLikeCompatible,
226+
"expected promise to be rejected with #{exp} but it was rejected with #{act}",
227+
"expected promise not to be rejected with #{exp} but it was rejected " +
228+
"with #{act}",
229+
errorLikeName,
230+
reasonName);
231+
}
232+
233+
if (errMsgMatcher) {
234+
this.assert(errMsgMatcherCompatible,
235+
`expected promise to be rejected with an error ${matcherRelation} #{exp} but got ` +
236+
`#{act}`,
237+
`expected promise not to be rejected with an error ${matcherRelation} #{exp}`,
238+
errMsgMatcher,
239+
checkError.getMessage(reason));
240+
}
212241
}
213-
214-
if (errMsgMatcher) {
215-
this.assert(errMsgMatcherCompatible,
216-
`expected promise to be rejected with an error ${matcherRelation} #{exp} but got ` +
217-
`#{act}`,
218-
`expected promise not to be rejected with an error ${matcherRelation} #{exp}`,
219-
errMsgMatcher,
220-
checkError.getMessage(reason));
221-
}
222-
}
242+
}, reason);
223243

224244
return reason;
225245
}

test/should-promise-specific.js

+21
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ describe("Promise-specific extensions:", () => {
156156
});
157157
});
158158

159+
describe(".fulfilled should keep the exception stack", () => {
160+
shouldFail({
161+
op: () => promise.should.be.fulfilled,
162+
stack: "should-promise-specific.js"
163+
});
164+
});
165+
159166
describe(".not.fulfilled", () => {
160167
shouldPass(() => promise.should.not.be.fulfilled);
161168
});
@@ -194,13 +201,27 @@ describe("Promise-specific extensions:", () => {
194201
shouldPass(() => promise.should.be.rejectedWith(error));
195202
});
196203

204+
describe(".rejectedWith(differentError) should keep the exception stack if the assertion fails", () => {
205+
shouldFail({
206+
op: () => promise.should.be.rejectedWith(new Error()),
207+
stack: "should-promise-specific.js"
208+
});
209+
});
210+
197211
describe(".not.rejectedWith(theError)", () => {
198212
shouldFail({
199213
op: () => promise.should.not.be.rejectedWith(error),
200214
message: "not to be rejected with 'Error: boo'"
201215
});
202216
});
203217

218+
describe(".not.rejectedWith(theError) should keep the exception stack if the assertion fails", () => {
219+
shouldFail({
220+
op: () => promise.should.not.be.rejectedWith(error),
221+
stack: "should-promise-specific.js"
222+
});
223+
});
224+
204225
describe(".rejectedWith(theError) should allow chaining", () => {
205226
shouldPass(() => promise.should.be.rejectedWith(error).and.eventually.have.property("myProp"));
206227
});

test/support/common.js

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ exports.shouldFail = options => {
1313
const promiseProducer = options.op;
1414
const desiredMessageSubstring = options.message;
1515
const nonDesiredMessageSubstring = options.notMessage;
16+
const desiredStackSubstring = options.stack;
1617

1718
it("should return a promise rejected with an assertion error", done => {
1819
promiseProducer().then(
@@ -34,6 +35,11 @@ exports.shouldFail = options => {
3435
throw new Error(`Expected promise to be rejected with an AssertionError not containing ` +
3536
`"${nonDesiredMessageSubstring}" but it was rejected with ${reason}`);
3637
}
38+
39+
if (desiredStackSubstring && !reason.stack.includes(desiredStackSubstring)) {
40+
throw new Error(`Expected promise to be rejected with an AssertionError with a stack containing ` +
41+
`"${desiredStackSubstring}" but it was rejected with ${reason}: ${reason.stack}`);
42+
}
3743
}
3844
).then(done, done);
3945
});

0 commit comments

Comments
 (0)