-
Notifications
You must be signed in to change notification settings - Fork 147
(feat:no-unnecessary-act): adds no-unnecessary-act rule, closes #259 #292
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
Conversation
Nice! I'll take a look during the weekend. It would be nice if you can update your branch again with |
…-library into pr/no-unnecessary-act
Examples of **incorrect** code for this rule: | ||
|
||
```js | ||
import { fireEvent, act } from '@testing-library/react'; |
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.
Use @testing-library/user-event
in order to draw attention to the act
problem and not have separate problems in the example
import { fireEvent, act } from '@testing-library/react'; | |
import { act } from '@testing-library/react'; | |
import userEvent from '@testing-library/user-event'; |
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 would actually use both examples: with fireEvent
to clarify it's a Testing Library util already wrapped with act
, and userEvent
to clarify the same since it's imported from a different package.
import { fireEvent, act } from '@testing-library/react'; | ||
|
||
it('Should have foo', () => { | ||
act(() => fireEvent.click(el)); |
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.
act(() => fireEvent.click(el)); | |
act(() => userEvent.click(el)); |
|
||
## Rule Details | ||
|
||
This rule disallows the usage of `act` when using it to wrap Testing Library functions, or functions with an empty body, which suppresses valid warnings. For more information, see [https://kcd.im/react-act](https://kcd.im/react-act). |
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 rule disallows the usage of `act` when using it to wrap Testing Library functions, or functions with an empty body, which suppresses valid warnings. For more information, see [https://kcd.im/react-act](https://kcd.im/react-act). | |
This rule disallows the usage of `act` when using it to wrap Testing Library | |
functions, or functions with an empty body, which suppresses valid warnings. For | |
more information, see https://kcd.im/react-act. |
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.
Thanks for your PR @alessbell ! I'm flagging it as WIP until you finish it.
It looks good so far! However, I've left some comments about how to detect renamed act
properly. You may need to change the implementation of the CallExpression
listener a bit to get this properly.
Regarding the detection of empty functions, you can get some inspiration from no-wait-for-empty-callback
where we actually report empty functions :)
I don't know if it could be interesting to report the act
here in two different ways:
- for
act
with empty functions: look for aCallExpression
, then check it's anact
to be reported, and finally check if its callback is empty - for
act
containing non Testing Library calls: look for anyCallExpression
, then check it's not coming from Testing Library (you have a helper available for it), and if you find one then flag the closest parentact
called as valid. I'm not sure if this approach is easier or not than the opposite one (look forCallExpression
that areact
to be reported and then check for calls inside its callback) you are following to be honest, it's just an idea in case it helps. Remember eslint-utils is part of@typescript-eslint/experimental-utils
we use, in case there is any function there it helps you to detect something.
Apart from the comments I've added throughout the PR, I think you should add more tests (specially without aggressive reporting, you can see the no-debug
PR that I mentioned in one comment) and some extra examples in the rule doc.
@@ -39,6 +40,7 @@ const rules = { | |||
'no-promise-in-fire-event': noPromiseInFireEvent, | |||
'no-render-in-setup': noRenderInSetup, | |||
'no-side-effects-wait-for': noSideEffectsWaitFor, | |||
'no-unnecessary-act': noUnnecessaryAct, |
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.
There is a snapshot that will fail because of this new rule. You can update it just running npm run test:update
@@ -39,6 +40,7 @@ const rules = { | |||
'no-promise-in-fire-event': noPromiseInFireEvent, | |||
'no-render-in-setup': noRenderInSetup, | |||
'no-side-effects-wait-for': noSideEffectsWaitFor, | |||
'no-unnecessary-act': noUnnecessaryAct, |
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 think we should enable this one for react
rules preset. What do you think?
export type MessageIds = 'noUnnecessaryAct'; | ||
type Options = []; | ||
|
||
const ACT_EXPRESSION_QUERY = 'CallExpression[callee.name=/^(act)$/]'; |
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 realized after migrating most of the rules to v4 that this limits the way we try to find the corresponding utils. This won't report act
usages if the user renames the import like import { act as renamedAct } from '@testing-library/react'
Instead, it's better to just create a listener for CallExpression
nodes and inside check if it's named act
with the available helpers. You'll probably have to add a new isActUtil
helper, in the same way I did it for debug
one here when refactoring no-debug
: https://github.com/testing-library/eslint-plugin-testing-library/pull/293/files#diff-bac511bc63f362d7483df411edf8aba96b1ae5949b4c15843a02d44804b59376
You can create this new isActUtil
copy&pasting that method and replacing "debug"
by "act"
in that detect-testing-library-utils.ts
file, and then use it inside your rule in a CallExpression
listener callback to check if that node is a valid act
. Then you can basically follow the same pattern I followed in no-debug
about finding the deepest identifier node so you get act
no matter if called like act()
or rtl.act()
.
The isTestingLibraryUtil
called inside isActUtil
, isDebugUtil
and the other helpers will detect automatically for you if such node is imported from Testing Library and gets its renamed value if some.
Not sure if I'm explaining myself here really well, probably easier if you check the idea in that PR and let me know if you want me to go over any particular point 😅
`, | ||
errors: [ | ||
{ | ||
messageId: 'noUnnecessaryAct' |
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 forgot to mention: can you check line and column for reported errors?
|
||
const ruleTester = createRuleTester({ | ||
ecmaFeatures: { | ||
jsx: 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.
Our createRuleTester
already enables jsx
so you don't need to set it here :)
body.every((node: TSESTree.ExpressionStatement) => { | ||
if ( | ||
isCallExpression(node.expression) && | ||
isMemberExpression(node.expression.callee) && | ||
isCallExpression(node.expression.callee.object) && | ||
helpers.isNodeComingFromTestingLibrary(node.expression.callee) | ||
) { | ||
return true; | ||
} | ||
return 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.
really minor suggestion, but you can drop the if clause if you want and just return the expression
body.every((node: TSESTree.ExpressionStatement) => { | |
if ( | |
isCallExpression(node.expression) && | |
isMemberExpression(node.expression.callee) && | |
isCallExpression(node.expression.callee.object) && | |
helpers.isNodeComingFromTestingLibrary(node.expression.callee) | |
) { | |
return true; | |
} | |
return false; | |
}); | |
body.every((node: TSESTree.ExpressionStatement) => | |
isCallExpression(node.expression) && | |
isMemberExpression(node.expression.callee) && | |
isCallExpression(node.expression.callee.object) && | |
helpers.isNodeComingFromTestingLibrary(node.expression.callee) | |
); |
@alessbell I just realized this PR has been closed because it pointed to We have already a beta released, so I have more spare time now. Can I help with something here? After having to migrate 20+ rules from the plugin, I really feel into writing ESLint rules so ping me if you want me to give you a hand here! |
@Belco90 thanks for your patience, and offer to help! It's not letting me edit the base branch for some reason, so I think I'll open a new PR this weekend and will let you know if I have any questions once I've addressed the comments left here 👍 |
I was starting to spin my wheels a bit so I wanted to open the PR to get some feedback 🙂 Closes #259.
I'm having trouble detecting all three of these cases:
My interpretation of the business logic is:
Find all instances of
act
(regardless of howact
is imported,ReactTestUtils.act
should be treated the same asact
) and check whether the function passed toact
:Five test cases are still failing. Will circle back around to this later today but wanted to open it up for any and all advice, thanks @Belco90 !