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

fix(valid-expect): validate async expect calls #78

Closed

Conversation

macklinu
Copy link
Collaborator

Resolves #54

I first tried working my way up the tree - to find a parent node of an expect() call that was a ReturnStatement or AwaitStatement and then finding the function that contained the expect() call, but I had trouble thinking through how to do that with the current valid-expect rule code. I ended up checking for a test case CallExpression and working my way down the tree - into the body of the callback function:

test("foo", () => {
  // validate each `expect().resolves` and `expect().rejects` in this block
})

One downside is that this results in some really deeply nested property accessing (like node.expression.callee.object.object.arguments[0].type).

Am I approaching this the right way? Feedback is certainly welcome on how to improve this.

Copy link
Collaborator Author

@macklinu macklinu left a comment

Choose a reason for hiding this comment

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

It would also be a good idea for me to update the valid-expect documentation to explain these cases. 👍

return false;
}
const callee = node.expression.callee;
return (
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This could be improved with something like _.get or get-value.

Copy link
Member

Choose a reason for hiding this comment

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

I'm down with that. Will make coverage a bit easier as well 🙂

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do you have preferences over which dependency to add? get-value is smaller, although I don't know if that's as big a deal with ESLint plugins (for example, I would be more concerned about adding the entire Lodash dependency for a web app than a command line tool).

Copy link
Member

Choose a reason for hiding this comment

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

no preference 🙂

},
{
code:
'test("foo", async () => expect(Promise.reject(2)).rejects.toBeDefined());',
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is technically valid, since this arrow function implicitly returns the expect() statement. However, the test() callback function is async, so I figured we could warn when you're using async without await in this case.

Thoughts on this?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, I can remove that check from this rule then, and we can open an issue for another rule.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Created #81 so this wouldn't get lost

'test("foo", () => expect(Promise.resolve(2)).resolves.toBeDefined());',
'test("foo", async () => await expect(Promise.reject(2)).rejects.toBeDefined());',
'test("foo", async () => { expect(await Promise.resolve(2)).resolves.toBeDefined(); });',
'test("foo", async () => { expect(await Promise.reject(2)).rejects.toBeDefined(); });',
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Based on the comments in #54 (comment), I might remove the logic surrounding these test cases, as it's not valid to await the Promise inside expect() and also use resolves or rejects.

Copy link
Member

Choose a reason for hiding this comment

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

It'll work, it just doesn't make any sense. So I think we should warn in that case as you're doing currently

Copy link
Member

Choose a reason for hiding this comment

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

Actually, I was wrong, jest'll throw.

image

Copy link
Member

Choose a reason for hiding this comment

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

so if anything it should be added to invalid 🙂

@SimenB
Copy link
Member

SimenB commented Feb 16, 2018

conflicts again, sorry

@macklinu
Copy link
Collaborator Author

Thank you for the review, @SimenB - I'll fix the conflicts and make the necessary changes today.

Copy link
Collaborator Author

@macklinu macklinu left a comment

Choose a reason for hiding this comment

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

Updated the test cases and documentation. This is now ready for review.

Please let me know if I should refactor anything for naming or extract any functions (specifically the filter() callback functions). I inlined everything while I was addressing PR feedback to help me better see the flow of the code I wrote 😂

return {
CallExpression(node) {
if (util.isTestCase(node)) {
validateAsyncExpects(node);
return;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If we are in a test case function, there is no need to proceed, since the node.callee.name will never be expect.

Copy link
Member

@SimenB SimenB left a comment

Choose a reason for hiding this comment

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

This is awesome, thank you so much for tackling it!

return (
node.expression.type === 'CallExpression' &&
objectName === 'expect' &&
(propertyName === 'resolves' || propertyName === 'rejects')
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we can reuse the logic somehow with the other resolves/rejects checks

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea. I'll look into that while resolving the failing test case you reported below.

{
code:
'test("foo", () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });',
errors: [{ message: "Must return or await 'expect.resolves' statement" }],
Copy link
Member

Choose a reason for hiding this comment

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

can you assert on the locations as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, will make those changes 👍

Copy link
Member

@SimenB SimenB left a comment

Choose a reason for hiding this comment

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

Actually, this should be expanded to support .not.

This test fails:

    {
      code:
        'test("foo", async () => { expect(await Promise.reject(undefined)).rejects.not.toBeDefined(); });',
      errors: [
        { message: "Cannot use 'rejects' with an awaited expect expression" },
      ],
    },

@SimenB
Copy link
Member

SimenB commented Feb 25, 2018

I don't get emails when people push to PRs I'm reviewing for some reason 🙁 So please ping me when this is ready

@macklinu
Copy link
Collaborator Author

macklinu commented Feb 25, 2018 via email

@macklinu
Copy link
Collaborator Author

@SimenB I'm in-between jobs and moving right now and probably won't be available to contribute again until late March/early April. Thoughts on what to do with this PR? Just leave it open for now?

@SimenB
Copy link
Member

SimenB commented Mar 1, 2018

@macklinu there is no rush, I think it's fine to leave it open.

@macklinu
Copy link
Collaborator Author

macklinu commented Mar 9, 2018

I may be able to re-approach this PR by using context.getAncestors() instead of traversing the nodes within the callback of a test() function. I think this could help us reuse the current logic for checking the usage of .not, .resolves, and .rejects.

Here is a simplified code example to show how to check if an expect() CallExpression was returned:

const rule = {
  CallExpression(node) {
    if (
      node.callee.name === "expect" &&
      context
        .getAncestors()
        .some(
          n =>
            n.type === "ReturnStatement" &&
            n.argument.callee.object.object === node
        )
    ) {
      // expect() was returned
    }
  }
};

I think I'll be able to hack on this later today / this weekend - will see where I end up. 😄

@macklinu
Copy link
Collaborator Author

I'm not sure this is the right approach, and I haven't been able to take a look for quite some time. Closing for now.

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.

Assert that async expects are awaited or returned
2 participants