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

[WIP] feat: update .rejectedWith with new .checkError #235

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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ return assert.isRejected(promise, Error, "optional message");
return assert.isRejected(promise, /error message matcher/, "optional message");
```

### Asserting on a Promise Rejected with a Non-Error Object

Like Chai's `throw` assertion, `rejectedWith` can only be used to assert on the constructor and message of `Error` instances. This includes `Error` instances created from subclassed `Error` constructors such as `ReferenceError`, `TypeError`, and user-defined constructors that extend `Error`. No other type of value will generate a stack trace when initialized. With that said, it's still possible to assert on non-`Error` objects by using `rejected` and then performing additional assertions later in the chain, like so:

```js
it("should work when a promise is rejected with a non-`Error` object", function () {
function NonErrorConstructor(message) {
this.message = message;
}

return expect(Promise.reject(new NonErrorConstructor("waffles")))
.to.be.rejected
.and.eventually.be.an.instanceof(NonErrorConstructor)
.with.property("message", "waffles");
});

it("should work when a promise is rejected with a primitive value", function () {
return expect(Promise.reject(42)).to.be.rejected.and.eventually.equal(42);
});
```

### Progress Callbacks

Chai as Promised does not have any intrinsic support for testing promise progress callbacks. The properties you would want to test are probably much better suited to a library like [Sinon.JS](http://sinonjs.org/), perhaps in conjunction with [Sinon–Chai](https://github.com/domenic/sinon-chai):
Expand Down
122 changes: 34 additions & 88 deletions lib/chai-as-promised.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
"use strict";
/* eslint-disable no-invalid-this */
let checkError = require("check-error");
const checkError = require("check-error");

module.exports = (chai, utils) => {
const Assertion = chai.Assertion;
const assert = chai.assert;
const proxify = utils.proxify;

// If we are using a version of Chai that has checkError on it,
// we want to use that version to be consistent. Otherwise, we use
// what was passed to the factory.
if (utils.checkError) {
checkError = utils.checkError;
}

function isLegacyJQueryPromise(thenable) {
// jQuery promises are Promises/A+-compatible since 3.0.0. jQuery 3.0.0 is also the first version
// to define the catch method.
Expand Down Expand Up @@ -76,10 +69,6 @@ module.exports = (chai, utils) => {
return typeof assertion.then === "function" ? assertion : assertion._obj;
}

function getReasonName(reason) {
return reason instanceof Error ? reason.toString() : checkError.getConstructorName(reason);
}

// Grab these first, before we modify `Assertion.prototype`.

const propertyNames = Object.getOwnPropertyNames(Assertion.prototype);
Expand All @@ -100,7 +89,7 @@ module.exports = (chai, utils) => {
reason => {
assertIfNotNegated(this,
"expected promise to be fulfilled but it was rejected with #{act}",
{ actual: getReasonName(reason) });
{ actual: reason });
return reason;
}
);
Expand All @@ -120,7 +109,7 @@ module.exports = (chai, utils) => {
reason => {
assertIfNegated(this,
"expected promise not to be rejected but it was rejected with #{act}",
{ actual: getReasonName(reason) });
{ actual: reason });

// Return the reason, transforming this into a fulfillment, to allow further assertions, e.g.
// `promise.should.be.rejected.and.eventually.equal("reason")`.
Expand All @@ -132,95 +121,52 @@ module.exports = (chai, utils) => {
return this;
});

method("rejectedWith", function (errorLike, errMsgMatcher, message) {
let errorLikeName = null;
const negate = utils.flag(this, "negate") || false;
method("rejectedWith", function (errLike, errMsgMatcher, message) {
if (message !== undefined) {
utils.flag(this, "message", message);
}

// rejectedWith with that is called without arguments is
// rejectedWith that is called without arguments is
// the same as a plain ".rejected" use.
if (errorLike === undefined && errMsgMatcher === undefined &&
if (errLike === undefined && errMsgMatcher === undefined &&
message === undefined) {
/* eslint-disable no-unused-expressions */
return this.rejected;
/* eslint-enable no-unused-expressions */
}

if (message !== undefined) {
utils.flag(this, "message", message);
}

if (errorLike instanceof RegExp || typeof errorLike === "string") {
errMsgMatcher = errorLike;
errorLike = null;
} else if (errorLike && errorLike instanceof Error) {
errorLikeName = errorLike.toString();
} else if (typeof errorLike === "function") {
errorLikeName = checkError.getConstructorName(errorLike);
} else {
errorLike = null;
}
const everyArgIsDefined = Boolean(errorLike && errMsgMatcher);
const criteria = checkError.createCriteria(errLike, errMsgMatcher);
const expectedDesc = checkError.describeExpectedError(criteria);

let matcherRelation = "including";
if (errMsgMatcher instanceof RegExp) {
matcherRelation = "matching";
}
// We append the expected value instead of using #{exp} because #{exp}
// adds single quotes around the expected value, even when it doesn't
// make sense (e.g., "expected promise to be rejected with 'a TypeError'").
const failMsg = "expected promise to be rejected with " + expectedDesc;
let negatedFailMsg = "expected promised not to be rejected with " + expectedDesc;

const derivedPromise = getBasePromise(this).then(
value => {
let assertionMessage = null;
let expected = null;

if (errorLike) {
assertionMessage = "expected promise to be rejected with #{exp} but it was fulfilled with #{act}";
expected = errorLikeName;
} else if (errMsgMatcher) {
assertionMessage = `expected promise to be rejected with an error ${matcherRelation} #{exp} but ` +
`it was fulfilled with #{act}`;
expected = errMsgMatcher;
}

assertIfNotNegated(this, assertionMessage, { expected, actual: value });
this.assert(
false,
failMsg + " but it was fulfilled with #{act}",
negatedFailMsg,
errLike || errMsgMatcher,
value
);
return value;
},
reason => {
const errorLikeCompatible = errorLike && (errorLike instanceof Error ?
checkError.compatibleInstance(reason, errorLike) :
checkError.compatibleConstructor(reason, errorLike));

const errMsgMatcherCompatible = errMsgMatcher && checkError.compatibleMessage(reason, errMsgMatcher);

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);
}

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 (reason !== errLike) {
negatedFailMsg += " but it was rejected with #{act}";
} // Else, it's implicit and doesn't need to be added.

this.assert(
checkError.checkError(reason, criteria),
failMsg + " but it was rejected with #{act}",
negatedFailMsg,
errLike || errMsgMatcher,
reason
);
return reason;
}
);
Expand Down
Loading