-
Notifications
You must be signed in to change notification settings - Fork 7
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
Major rework #18
Major rework #18
Conversation
BREAKING CHANGE: Remove `.compatibleInstance` in preparation for the upcoming addition of a `.checkError` method that'll require the thrown object to be an `Error` instance. With that requirement in place, a simple strict equality check takes the place of `.compatibleInstance`.
BREAKING CHANGE: Previously, `.getMessage` and `.compatibleMessage` accepted both `Error` instances and strings. This commit removes support for strings. This is being done in preparation for the upcoming addition of a `.checkError` method that'll require the thrown object to be an `Error` instance. With that requirement in place, it's no longer necessary to support strings in `.getMessage` and `.compatibleMessage`.
BREAKING CHANGE: Remove `.compatibleConstructor` in preparation for the upcoming addition of a `.checkError` function that'll require `errLike` to be either an `Error` instance or an `Error` constructor. With that requirement in place, a simple strict equality check followed by an `instanceof` check takes the place of `.compatibleConstructor`.
package.json
Outdated
@@ -19,6 +19,7 @@ | |||
"check-error.js" | |||
], | |||
"main": "./index.js", | |||
"version": "2.0.0-wip", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could change the version to something like 0.0.0-development
which is a very loud signal that its not a valid practical version, but it is still would hopefully solve the issues you're seeing.
README.md
Outdated
criteria = checkError.createCriteria(errObj); | ||
checkError.describeExpectedError(criteria); // "[Error: I like waffles]" | ||
checkError.checkError(new Error('i like waffles'), criteria); // false | ||
checkError.checkError(errObj, criteria); // true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have time to give this the full review, but just from glancing at the readme I'm a little bit concerned over the notion of a criteria
object. My initial questions are:
- Why do I, as a consumer of this API, need to create an object before asserting on my error.
- Why can't I just pass some arguments in place of
criteria
. E.g.checkError.checkError(errObj, new Error('I like waffles')
orcheckError.checkError(errObj, TypeError, /waffles/)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The .createCriteria
method performs quite a bit of type validation and argument handling (since two method signatures are supported) that is needed by both .checkError
and .describeExpectedError
. So the motivation behind the criteria object is only performing that work once. This may be over-engineered. It may be better overall to just bite the bullet and perform the work twice for the purposes of a simpler API. Easy change but will wait for more feedback before taking action.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figured this was the case; it just feels a little un-ergonomic to have to create arguments using this API, to then give that argument to another part of the API (although not unprecedented, see DataView
).
Perhaps we could make the functions that take a criteria
polymorphic enough to either accept a criteria, or a criteria signature? This way we get the benefits of both.
We could do something like:
// Each method that wants a `criteria` will also take a criteria signature and coerce as necessary:
function checkError(errObj, ...criteriaLike) {
const criteria = createCriteria(...criteriaLike)
// ...
}
// `createCriteria(<Criteria>)` is essentially `identity`
function createCriteria(errLike, errMsgMatcher) {
if (arguments.length === 1 && isCriteria(errLike) return errLike
// ...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems reasonable to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@keithamus Node v4 is still supported until April. Do we need to implement that logic with ES5?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, the arity semantics are likely of no concern so some sensible variant of function checkError(errObj, criteriaLikeA, criteriaLikeB)
will be fine, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent idea.
I totally agree with @keithamus. This makes the API a lot more straightforward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now this looks good, my only concern is with handling "non-Error
" objects that might be thrown, such as strings.
According to our core docs we should support throwing anything.
Please correct me if I'm wrong, but if I'm not mistaken I think this line, for example, could be triggered and end up throwing an AssertionError
if a String
was thrown even though it matches the expected matcher.
README.md
Outdated
criteria = checkError.createCriteria(errObj); | ||
checkError.describeExpectedError(criteria); // "[Error: I like waffles]" | ||
checkError.checkError(new Error('i like waffles'), criteria); // false | ||
checkError.checkError(errObj, criteria); // true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent idea.
I totally agree with @keithamus. This makes the API a lot more straightforward.
// If `errorLike` is a constructor that inherits from Error, we compare `thrown` to `errorLike` directly | ||
return thrown.constructor === errorLike || thrown instanceof errorLike; | ||
function getMessage(errorLike) { | ||
if (errorLike && errorLike.message) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the thrown error is not an object but a String
instead?
Will we handle that within chai-core
?
index.js
Outdated
|
||
if ((criteria.errMsgMatcherType === 'string' || | ||
criteria.errMsgMatcherType === 'regexp') && | ||
!compatibleMessage(errObj, criteria.errMsgMatcher)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I've stated before, what happens if errObj
is a string?
IMO if we plan on moving most of the logic to this module instead of keeping it in the core I think it would be suitable for us to deal with throwing Strings here too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm basically making the argument with this PR and chaijs/chai#1021 that there shouldn't be direct support for throwing non-Error objects, as the logic required to support that bad practice is complex and dangerous. Dangerous because it allows the same assertion to pass in different situations. For example, expect(fn).to.throw('blah')
would pass when fn
is an Error
object with the .message
property containing "blah", or a string containing "blah", or just a regular object with a .message
property containing "blah". As noted in the updated docs in chaijs/chai#1021, I believe people wishing to assert on non-Error
objects should use assertion chaining instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a good reason to not support those.
Let's just not forget about updating our docs on the core then.
test/index.js
Outdated
var criteria = checkError.createCriteria(); | ||
assert(checkError.checkError(notErrObj, criteria) === false); | ||
}); | ||
/***************/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*Nitpick: can't we just use a linebreak instead of these separators?
@keithamus @lucasfcosta Just updated this PR based on prior comments |
Bump! |
README.md
Outdated
* `compatibleMessage(err, errMatcher)` - Checks if an error message is compatible with an `errMatcher` RegExp or String (we check if the message contains the String). | ||
* `checkError(errObj[, errLike[, errMsgMatcher]])` or `checkError(errObj[, errMsgMatcher])` - Checks if an `Error` instance matches criteria defined by `errLike` and/or `errMsgMatcher`. | ||
* If `errLike` is an `Error` constructor, then validate that `errObj` is an instance of `errLike`. | ||
* If `errLike` is an `Error` instance, then validate that `errObj` is strictly (`===`) equal to `errLike`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 Given some of the talk in the chai issue tracker, and around deep-eql, should we losen this restriction to say
If
errLike
is anError
instance, then validate thaterrObj
has the sameconstructor
,message
and any other enumerable properties (excludingstack
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm in favor of this change. But I think it'd need to be implemented in deep-eql
and imported here. If one of the enumerable properties is an object, then I believe the normal deep equality algorithm should apply to that property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, well said, let's leave it out of scope here, and work on putting this in deep-eql
and bubbling up through the codebase.
* If `errLike` is an `Error` constructor, then validate that `errObj` is an instance of `errLike`. | ||
* If `errLike` is an `Error` instance, then validate that `errObj` is strictly (`===`) equal to `errLike`. | ||
* If `errLike` is a criteria object, then validate that `errObj` matches the criteria that was originally passed to `.createCriteria` in order to create the criteria object. | ||
* If `errLike` is omitted or is explicitly `null` or `undefined`, then it defaults to the built-in `Error` constructor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I'd like this at the top of this list, so the logic can fall down naturally - more reflective of the steps in the algorithm.
README.md
Outdated
* `checkError(errObj[, errLike[, errMsgMatcher]])` or `checkError(errObj[, errMsgMatcher])` - Checks if an `Error` instance matches criteria defined by `errLike` and/or `errMsgMatcher`. | ||
* If `errLike` is an `Error` constructor, then validate that `errObj` is an instance of `errLike`. | ||
* If `errLike` is an `Error` instance, then validate that `errObj` is strictly (`===`) equal to `errLike`. | ||
* If `errLike` is a criteria object, then validate that `errObj` matches the criteria that was originally passed to `.createCriteria` in order to create the criteria object. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This step should be moved to the top - and maybe criteria object
can be changed to criteria object (such as that made by `createCriteria`)
index.js
Outdated
@@ -76,10 +56,11 @@ function compatibleConstructor(thrown, errorLike) { | |||
*/ | |||
|
|||
function compatibleMessage(thrown, errMatcher) { | |||
var comparisonString = typeof thrown === 'string' ? thrown : thrown.message; | |||
if (errMatcher instanceof RegExp) { | |||
var errMatcherType = type(errMatcher).toLowerCase(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are literally just comparing regexp
and string
, a typeof
/instanceof
check would be better (faster) here.
index.js
Outdated
@@ -101,7 +82,7 @@ function getConstructorName(errorLike) { | |||
var constructorName = errorLike; | |||
if (errorLike instanceof Error) { | |||
constructorName = getFunctionName(errorLike.constructor); | |||
} else if (typeof errorLike === 'function') { | |||
} else if (type(errorLike) === 'function') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly here - typeof
would be better, as we closely control the flow here - type-detect
is better put to use where we want to check object types. If we're only ever checking primitives, it's worth considering avoiding using type-detect
entirely IMO
index.js
Outdated
var errLikeType = type(errLike).toLowerCase(); | ||
var errMsgMatcherType = type(errMsgMatcher).toLowerCase(); | ||
// Handle a criteria object being passed by just returning the same object. | ||
if (errLikeType === 'object' && isCriteria(errLike)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems isCriteria
could add the additional typeof errLike === 'object'
check. Additionally - might want to avoid type-detect
here.
// Handle `errLike` being omitted and `errMsgMatcher` being passed as the | ||
// second argument. | ||
if (errLikeType === 'string' || errLikeType === 'regexp') { | ||
if (errMsgMatcherType !== 'undefined' && errMsgMatcherType !== 'null') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More type stuff here could use typeof
/instanceof
.
index.js
Outdated
var desc = ''; | ||
var constructor = getConstructorName(criteria.errLike); | ||
var first = constructor.charAt(0).toLowerCase(); | ||
var vowels = [ 'a', 'e', 'i', 'o', 'u' ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a premature optimisation, but this array doesn't need to be created each time. Could have it as a const
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally a great improvement. Super well done 👍! I really like how chai is shaping up these days.
I think we should drop type-detect
- I don't think it brings much value here, and adds to the complexity/performance characteristics of this codebase and would also introduce maintainence burdens if chai ends up shipping multiple versions of type-detect (like it did with deep-eql for a while).
Additionally, something to think about (maybe not for this PR) is ES6ifying this codebase. I'd like us to move to rollup so chai can ultimately ship as a single compiled file (for performance reasons) and ES6 will get us there.
- Add `.createCriteria` - Add `.checkError` - Add `.describeExpectedError`
The initial implemention of `.checkError` and `.describeExpectedError` only accepted criteria objects for their criteria, which means that `.createCriteria` had to be manually called first. This update allows `errLike` and/or `errMsgMatcher` to be passed directly to those methods instead, in which case a criteria object is created internally.
@keithamus I just updated this PR based on your review. (I'll need to remove the "wip version num" commit before we merge this though.) |
I'll try to get a review done this week @meeber - but I might be too busy. Rest assured it's in my todo list to pick up as soon as I can. |
BREAKING CHANGE: Previously, `Error` instances were compared using strict equality. This commit changes the comparison algorithm to deep equality. Users wishing to compare two `Error` instances via strict equality should just use `===` directly instead of this module.
BREAKING CHANGE: This commit drops support for versions of Node below v6, and updates the Travis config accordingly.
Previously, Travis was configured to cache `node_modules`, which was causing the build to break.
This is related to chaijs/chai#1021. The purpose of these changes is to simplify the existing Chai
.throw
assertion and move a lot of the logic into this module for easier reuse. These changes also help Chai and its plugins to provide more consistent failed assertion messages related to errors.NOTE: I added a temporary version number to the
package.json
file to allow other modules to install this module from github for the purposes of demonstrating how these changes impact chai and chai-as-promised. It should be removed before merging. (But in the long term, I don't think we should continue excluding theversion
field frompackage.json
... it makes development harder.)Also related: chaijs/chai-as-promised#235