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

Allow dates for isBelow and isAbove assertions #990

Merged
merged 17 commits into from Jun 23, 2017
Merged

Allow dates for isBelow and isAbove assertions #990

merged 17 commits into from Jun 23, 2017

Conversation

v1adko
Copy link
Contributor

@v1adko v1adko commented Jun 6, 2017

Hi guys.

This closes #980. I took #692 as an "inspiration" and extended the code to support dates as discussed in the issue.

I have encountered some questions along the way and left comments (off course will delete them once we decided what to do at those moments) - I will expand on them within GitHub, to make them easier to notice. So it's a WIP for now, just need a bit of your guidance.

This is my first attempt at actually PR-ing anything open-source, so sorry for mistakes.
I'd really like to contribute :)

Also need to fix up Phantom tests, but wanted to get some feedback and ask these questions first, to understand the general direction and the scope of this PR better.

@v1adko
Copy link
Contributor Author

v1adko commented Jun 6, 2017

I have probably made a newbie mistake deleting /chai.js. I'm not sure how exactly that happened, the file is in gitignore... I'd blame 'make clean`, but something definitely went wrong.


if (doLength) {
new Assertion(obj, msg).to.have.property('length');
} else {
new Assertion(obj, msg).is.a('number');
// Since utils/expectTypes is missing from 4.x.x and no better way to assert multiple types
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find a better way to assert multiple types, on the original PR expectTypes was suggested, but it's no longer present in the utils

@@ -587,6 +606,7 @@ module.exports = function (chai, _) {
* @api public
*/

// Should this also be updated?
Copy link
Contributor Author

@v1adko v1adko Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally we discussed only isAbove and isBelow methods to support dates, but this seems connected as well. Same question goes for assertMost and within.

test/assert.js Outdated

err(function() {
assert.isAbove(oneSecondAgo, now);
}, 'expected ' + now.toUTCString() + ' to be above ' + oneSecondAgo.toString()); // TODO: Fix different date formatting
Copy link
Contributor Author

@v1adko v1adko Jun 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I noticed is that date formatting is not uniform. While the first argument gets printed inside the error as a UTC string, second one is not. Probably should not be this way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is created by util.getMessage inside the Assertion.prototype.assert here.

The util.getMessage uses util.objectDisplay which ends up calling util.inspect that for Dates will call toUTCString() 😄

But the .above | .below assertions are just concatenating the expected value to the message which just calls .toString().

In order to fix that you need to update the call to this.assert.

Unfortunatelly github does not allow me to comment on unchanged lines so I'll try to explain here.

Basically you need to update this:

this.assert(
  obj > n
  , 'expected #{this} to be above ' + n
  , 'expected #{this} to be at most ' + n
);

To this:

this.assert(
  obj > n
  , 'expected #{this} to be above #{exp}'
  , 'expected #{this} to be at most #{exp}'
  , n
);

I believe you will need to do the same thing for the .below assertion too

Please let me know if you have any further questions 😄

@keithamus
Copy link
Member

@v1adko thanks for this!

One general note, before I review this; could you please change the base branch to master. We need to delete the 4.x.x branch but haven't gotten round to it yet!

@v1adko v1adko changed the base branch from 4.x.x to master June 6, 2017 17:47
@v1adko v1adko changed the base branch from master to 4.x.x June 6, 2017 18:00
Copy link
Member

@lucasfcosta lucasfcosta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @v1adko, thank you for this PR!

I was thinking if we should actually call these isBefore and isAfter to make it more semantic.

Maybe an alias would be enough, but I'd prefer separing those two into separate assertions.

What do you think?

Copy link
Member

@vieiralucas vieiralucas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @v1adko, thank you so much for putting your time into this.

I think that implementing this as isBefore and isAfter looks cleaner and simpler.

The "assert multiple types" problem goes away too.

@meeber
Copy link
Contributor

meeber commented Jun 6, 2017

@lucasfcosta @vieiralucas #980 contains a discussion regarding overloading isBelow and isAbove versus creating separate assertions. As noted in that thread, my preference is to overload in this case (with the stipulation that both the target and given value must both be the same type), and perhaps open a separate PR to alias isBefore and isAfter to them. I think that offers sufficient protection against making a mistake with types, while respecting the notion that "before" and "below" can be used interchangeably when speaking of numbers (and when thinking of dates in numeric fashion), as can "above" and "after".

@vieiralucas
Copy link
Member

@meeber Having the target and given value must both be the same type looks enough to me

? 'number'
: (objType === 'date')
? 'date'
: 'number or date';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, also maybe you could extract this code into a function to avoid duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where would be a better place for a function like this? Utils?

? 'number'
: (objType === 'date')
? 'date'
: 'number or date';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather use if | else if | else

What do you think?

Copy link
Contributor Author

@v1adko v1adko Jun 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, no problem :) I know that there are plans to introduce a linter, would definitely be beneficial for such cases, no more "nits".
Also, you guys can take a look at prettier as an alternative to standard linter.

test/assert.js Outdated

err(function() {
assert.isAbove(oneSecondAgo, now);
}, 'expected ' + now.toUTCString() + ' to be above ' + oneSecondAgo.toString()); // TODO: Fix different date formatting
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is created by util.getMessage inside the Assertion.prototype.assert here.

The util.getMessage uses util.objectDisplay which ends up calling util.inspect that for Dates will call toUTCString() 😄

But the .above | .below assertions are just concatenating the expected value to the message which just calls .toString().

In order to fix that you need to update the call to this.assert.

Unfortunatelly github does not allow me to comment on unchanged lines so I'll try to explain here.

Basically you need to update this:

this.assert(
  obj > n
  , 'expected #{this} to be above ' + n
  , 'expected #{this} to be at most ' + n
);

To this:

this.assert(
  obj > n
  , 'expected #{this} to be above #{exp}'
  , 'expected #{this} to be at most #{exp}'
  , n
);

I believe you will need to do the same thing for the .below assertion too

Please let me know if you have any further questions 😄

@v1adko v1adko changed the base branch from 4.x.x to master June 7, 2017 09:50
@@ -1151,6 +1183,7 @@ module.exports = function (chai, _) {
* @api public
*/

// Should this also be updated? + assertMost and within
Copy link
Contributor Author

@v1adko v1adko Jun 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally we discussed only isAbove and isBelow methods to support dates, but assertLeast seems connected as well. Same question goes for assertMost and within. [restored]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup all of those should be updated as well.

if (typeof n !== 'number') {
flagMsg = flagMsg ? flagMsg + ': ' : '';
if (!(nType === 'number' || nType === 'date')) {
var shoudBeType = (objType === 'number' || objType === 'date' )
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vieiralucas Instead of doing else-if I got rid of the nested ternary. If you think that it contradicts a style or obscures code, let me now and I'll change this too.

Restoring the question - if I were to separate this logic elsewhere, what's the best place? Utils?

Copy link
Contributor Author

@v1adko v1adko Jun 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, these functions (isAbove and isBelow) are very similar, apart from several strings and conditions, so if you really want to make it DRY, these can be separated into a some kind of assertion constructor that would take a predicate and some other params (like the string 'below/above').

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also know that there are plans to introduce a linter, would definitely be beneficial for such cases, no more "nits".
Also, you guys can take a look at prettier as an alternative to standard linter. [restored]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this type of utility function could just be local to the assertions file.

I wouldn't be opposed to a refactoring effort to further reduce duplication among these similar functions, as long as it didn't have a big impact on readability. That'd be a discussion for a separate issue/PR though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After also updating atMost, atLeast and within there is a definite pattern and code duplication, not only in these lines. So, I'm not sure what parts exactly to separate out, I think it's mostly the same code, just different conditions, so easier would be to create a generic assertion and "plug in" the differences.

test/expect.js Outdated
// Is this right? Fails with the 'implementation frames error' (same as the 4 original ones in master)
err(function () {
expect(null).to.have.length.above(0, 'blah');
}, "Cannot read property 'length' of null");
Copy link
Contributor Author

@v1adko v1adko Jun 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails, along with 4 tests concerning instance checks (they fail locally on clean master too)
Any ideas?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .property assertion is ungracefully failing on this line when the target is null. This causes Chai's implementation frames to not be properly removed from the stack trace, which in turn causes this test using the err helper function to fail. I guess this problem needs a separate issue.

@v1adko
Copy link
Contributor Author

v1adko commented Jun 7, 2017

@keithamus Changed to the master base.
Also fixed up some of the comments you guys made (like the UTC strings, thanks @vieiralucas)
Still having an issue with deleted chai.js, several questions and one failing test that I'm not sure how to address.

@meeber
Copy link
Contributor

meeber commented Jun 7, 2017

@v1adko Thanks for your work on this! A git checkout chai.js should restore the missing file. When the test suite completes successfully, it'll replace chai.js with an updated version. We don't want that until we release a new version, so you'll need to do git checkout chai.js before pushing after a successful test.

);
}

// Should it check if before <= after?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that within method does not check weather the two given params make a valid range. Should it? Or it's assumed that it's gonna always throw and the user would see it from the generic error message?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave it as-is for now. Can always readdress later.

@@ -30,10 +30,11 @@ module.exports = function expectTypes(obj, types, ssfi) {
flagMsg = flagMsg ? flagMsg + ': ' : '';

obj = flag(obj, 'object');
// TODO: Fix - results in undefined when passed date
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if obj is a date, after a flag it returns undefined, do we care?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks to me like expectTypes can only be used on assertion objects in order to make sure the target of the assertion (i.e., the value stored in the object flag) is one of the allowed types. It can't be used to check that arguments passed to an assertion are of the correct type; the object flag would be undefined regardless of the argument type. So you're doing the correct thing by checking the argument type manually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, non-issue then, got confused about the use-case of this method.

test/assert.js Outdated
}, 'blah: expected ' + now.toUTCString() + ' to be at least ' + oneSecondAfter.toUTCString());

// These fail with 'implementation frames not properly filtered'
err(function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly I still don't know how to approach this problem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be fixed in #992

test/assert.js Outdated
}, 'blah: type mismatch, expected to most value to be a number');
});

// There's no assert.isWithin(n, a, b)?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While should an expect implement have this options, it looks like there's no simple assert.isWithin()
Should we add an alias?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say leave that for a separate PR.

@v1adko
Copy link
Contributor Author

v1adko commented Jun 9, 2017

@meeber Updated the other methods + tests too. And checkout chai worked, thanks 👍
Added several comments to discuss. From the previous issues there are only failing tests and a discussion on a best way to make it DRY and refactor.

Also I'm going to be out of town for the weekend without a computer, so, unfortunately, won't be able to work on this. Will gladly pick it up next week.

@meeber
Copy link
Contributor

meeber commented Jun 10, 2017

@v1adko: I opened #992 to address the issue that's causing the failing tests due to implementation frames. We'll get that one merged first, and then you can rebase this branch with the change.

@v1adko
Copy link
Contributor Author

v1adko commented Jun 12, 2017

@meeber All right, I'll wait till then

@cletusw
Copy link

cletusw commented Jun 12, 2017

If we could get this in a patch update ASAP that'd be great since this fixes an unnecessary breaking change (#980) from 3.x

@v1adko
Copy link
Contributor Author

v1adko commented Jun 19, 2017

@meeber, @vieiralucas Hi :) Don't want to rush you guys or anything, just interested in how those PRs are moving along because they look green and it's already been a week. As I understood I need to wait for them to get merged first.

@keithamus
Copy link
Member

I think I could live with that refactor 😄. Hopefully #585 will eventually get us to a point where this kind of manual type checking isn't necessary any more.

test/expect.js Outdated
}, "blah: expected [ 1, 2, 3 ] to have a length within 5..7");

err(function () {
expect(null).to.be.within(0, 1, 'blah');
}, "blah: expected null to be a number");
}, "blah: the arguments to within must be numbers or dates");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@keithamus This doesn't seem right to me. Technically here both arguments to within are numbers, argument to expect isn't so the message is not very accurate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this message is a little vague. Can we add an extra if to just throw saying something like "blah: expected null to be a number or date"

@v1adko
Copy link
Contributor Author

v1adko commented Jun 21, 2017

@keithamus Updated all methods. I also feel like L1082-L1094 can be extracted into a separate function (like checkNumberOrDate), what's your take on that?
It's gonna reduce duplication in 4 assertion functions (within is custom).
What's your take on this?

UPD: I've managed to take out all of the common logic into one generic function, please take a look here.

@keithamus
Copy link
Member

Thanks @v1adko. While the simplicity of creating assertions using the helper is definitely somewhere we want to be in future, I think for now having the repetitive yet easily understandable code in the PR as it stands now works well. In addition, #585 grants us the scope for these kinds of optimisations, where we can truly focus on getting chai assertions to be very easy to write.

Let's just fix up the small issue you mentioned above, then get this PR merged!

@keithamus
Copy link
Member

Perfect! Thanks @v1adko this looks good to me!

@meeber, @lucasfcosta @vieiralucas as you've commented, do you all want to approve before we move this to merge?

Copy link
Member

@vieiralucas vieiralucas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Awesome work @v1adko, thank you ❤️

Copy link
Contributor

@meeber meeber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@v1adko Thanks again for your diligence on this one. I like this a lot, and it should be an easy transition to refactoring a bit later on in a separate PR. I do have a few nitpicks that I'd like to see cleaned up before we merge this.

Also, let's make sure all of these commits get rebased into a single one (either manually or via github) before merging.

@@ -1024,7 +1024,7 @@ module.exports = function (chai, _) {
/**
* ### .above(n[, msg])
*
* Asserts that the target is a number greater than the given number `n`.
* Asserts that the target is number or a date greater than the given number or a date `n` respectively.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't read exactly right to me. I think: "Asserts that the target is a number or a date greater than the given number or date n respectively."

* Asserts that the target is a number greater than or equal to the given
* number `n`. However, it's often best to assert that the target is equal to
* Asserts that the target is a number or a date greater than or equal to the given
* number or a date `n` respectively. However, it's often best to assert that the target is equal to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't read exactly right to me. I think: "Asserts that the target is a number or a date greater than or equal to the given number or date n respectively."

@@ -1197,7 +1215,7 @@ module.exports = function (chai, _) {
/**
* ### .below(n[, msg])
*
* Asserts that the target is a number less than the given number `n`.
* Asserts that the target is a number or a date less than the given number or a date `n` respectively.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't read exactly right to me. I think: "Asserts that the target is a number or a date less than the given number or date n respectively."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@meeber I don't see how what you wrote is different from what's already there (green).
Or is it supposed to be the same? Sorry, I'm a bit confused. What's would be the suggestions to make it read better?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@v1adko In almost every case, the suggestion is "given number or date" instead of "given number or a date". In addition to that, the one for .above is also missing an "a" at the start.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@meeber So no articles then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Articles for the first half (either "the target is a number or a date", or, "the target is a number or date"). But no articles for the second half ("than the given number or date").

* Asserts that the target is a number less than or equal to the given number
* `n`. However, it's often best to assert that the target is equal to its
* Asserts that the target is a number or a date less than or equal to the given number
* or a date `n` respectively. However, it's often best to assert that the target is equal to its
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't read exactly right to me. I think: "Asserts that the target is a number or a date less than or equal to the given number or date n respectively."

* Asserts that the target is a number greater than or equal to the given
* number `start`, and less than or equal to the given number `finish`.
* Asserts that the target is a number or a date greater than or equal to the given
* number or a date `start`, and less than or equal to the given number or a date `finish` respectively.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't read exactly right to me. I think: "Asserts that the target is a number or a date greater than or equal to the given number or date start, and less than or equal to the given number or date finish respectively."

assert.isBelow(1, now, 'blah');
}, 'blah: the argument to below must be a number');
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add the custom message (blah) testing to the other failure cases here.

test/should.js Outdated
(now).should.not.be.within(now, null, 'blah');
}, "blah: the arguments to within must be dates");
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need parenthesis around <var name> whenever it's (<var name>).should; still need parenthesis for the values plugged directly, like (0).should

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, if I understood correctly, just omit parenthesis and do now.should instead of (now).should?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup!

([]).should.have.length.above(now, 'blah');
}, "blah: the argument to above must be a number");
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need parenthesis around <var name> whenever it's (<var name>).should; still need parenthesis for the values plugged directly, like (0).should

('string').should.have.length.below(now, 'blah');
}, "blah: the argument to below must be a number");
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need parenthesis around <var name> whenever it's (<var name>).should; still need parenthesis for the values plugged directly, like (0).should

test/should.js Outdated
(now).should.not.be.at.most(undefined);
}, "the argument to most must be a date");
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need parenthesis around <var name> whenever it's (<var name>).should; still need parenthesis for the values plugged directly, like (0).should

@v1adko
Copy link
Contributor Author

v1adko commented Jun 22, 2017

@meeber Done. As for the commits - I think it's better to squash them via GitHub.

@meeber
Copy link
Contributor

meeber commented Jun 22, 2017

@v1adko The tests for assert.isAbove need that same custom message loving, but otherwise this looks good to go!

@v1adko
Copy link
Contributor Author

v1adko commented Jun 22, 2017

@meeber Missed that, thanks :)
Looks like we're waiting for @lucasfcosta to sign off and it's finally done. 🎉

Copy link
Contributor

@meeber meeber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! (To whomever merges: remember to squash into single commit!)

@keithamus keithamus merged commit 3c932e2 into chaijs:master Jun 23, 2017
@keithamus
Copy link
Member

🎉 ✨

@v1adko v1adko deleted the allow-below-dates-assertion branch June 23, 2017 12:12
@cletusw
Copy link

cletusw commented Jun 23, 2017

Awesome! Can we get a release for this? Thanks.

@lucasfcosta
Copy link
Member

@cletusw Let's see what other people think, but IMO we should.

In order to avoid having to wait for too long for a release like we did last time I think it would be awesome for us to keep releasing new versions with patch and minor changes as soon as possible. This also improves feedback time and we can fix things more quickly.

What do you think?

@keithamus
Copy link
Member

Eventually we'll hopefully end up adding semantic-release to this repo like the others; in the meantime anyone is welcome to raise a PR of the output of make release, and we can merge & tag it ✨

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error or not updated docs on assert.isAbove and isBelow
7 participants