Skip to content

(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

Closed
wants to merge 3 commits into from
Closed
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
55 changes: 28 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,33 +125,34 @@ To enable this configuration use the `extends` property in your

## Supported Rules

| Rule | Description | Configurations | Fixable |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ |
| [await-async-query](docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce promises from fire event methods to be handled | ![vue-badge][] | |
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
| [no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | | |
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
| [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | |
| [no-side-effects-wait-for](docs/rules/no-side-effects-wait-for.md) | Disallow the use of side effects inside `waitFor` | | |
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | |
| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | |
| [prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] |
| [render-result-naming-convention](docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| Rule | Description | Configurations | Fixable |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------ |
| [await-async-query](docs/rules/await-async-query.md) | Enforce promises from async queries to be handled | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-async-utils](docs/rules/await-async-utils.md) | Enforce async utils to be awaited properly | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce promises from fire event methods to be handled | ![vue-badge][] | |
| [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensure `data-testid` values match a provided regex. | | |
| [no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | | |
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-container](docs/rules/no-container.md) | Disallow the use of `container` methods | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-multiple-assertions-wait-for](docs/rules/no-multiple-assertions-wait-for.md) | Disallow the use of multiple expect inside `waitFor` | | |
| [no-node-access](docs/rules/no-node-access.md) | Disallow direct Node access | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-promise-in-fire-event](docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | |
| [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | |
| [no-side-effects-wait-for](docs/rules/no-side-effects-wait-for.md) | Disallow the use of side effects inside `waitFor` | | |
| [no-unnecessary-act](docs/rules/no-unnecessary-act.md) | Disallow the use of `act` when wrapping Testing Library functions or functions with an empty body | | |
| [no-side-effects-wait-for](docs/rules/no-side-effects-wait-for.md) | Disallow the use of side effects inside `waitFor` | | |
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | | |
| [no-wait-for-snapshot](docs/rules/no-wait-for-snapshot.md) | Ensures no snapshot is generated inside of a `waitFor` call | | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [prefer-presence-queries](docs/rules/prefer-presence-queries.md) | Enforce specific queries when checking element is present or not | | |
| [prefer-user-event](docs/rules/prefer-user-event.md) | Suggest using `userEvent` library instead of `fireEvent` for simulating user interaction | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [prefer-screen-queries](docs/rules/prefer-screen-queries.md) | Suggest using screen while using queries | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [prefer-wait-for](docs/rules/prefer-wait-for.md) | Use `waitFor` instead of deprecated wait methods | | ![fixable-badge][] |
| [render-result-naming-convention](docs/rules/render-result-naming-convention.md) | Enforce a valid naming for return value from `render` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |

[build-badge]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/pipeline.yml/badge.svg
[build-url]: https://github.com/testing-library/eslint-plugin-testing-library/actions/workflows/pipeline.yml
Expand Down
26 changes: 26 additions & 0 deletions docs/rules/no-unnecessary-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Disallow the use of `act` when wrapping Testing Library functions or functions with an empty body

## 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).
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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.


Examples of **incorrect** code for this rule:

```js
import { fireEvent, act } from '@testing-library/react';
Copy link
Member

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

Suggested change
import { fireEvent, act } from '@testing-library/react';
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

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 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.


it('Should have foo', () => {
act(() => fireEvent.click(el));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
act(() => fireEvent.click(el));
act(() => userEvent.click(el));

});
```

Examples of **correct** code for this rule:

```js
import { act } from '@testing-library/react';
it('Should have foo and bar', () => {
act(() => {
stuffThatDoesNotUseRTL();
});
});
```
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import noManualCleanup from './rules/no-manual-cleanup';
import noNodeAccess from './rules/no-node-access';
import noPromiseInFireEvent from './rules/no-promise-in-fire-event';
import noRenderInSetup from './rules/no-render-in-setup';
import noUnnecessaryAct from './rules/no-unnecessary-act';
import noWaitForEmptyCallback from './rules/no-wait-for-empty-callback';
import noWaitForSnapshot from './rules/no-wait-for-snapshot';
import preferExplicitAssert from './rules/prefer-explicit-assert';
Expand Down Expand Up @@ -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,
Copy link
Member

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

Copy link
Member

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?

'no-wait-for-empty-callback': noWaitForEmptyCallback,
'no-wait-for-snapshot': noWaitForSnapshot,
'prefer-explicit-assert': preferExplicitAssert,
Expand Down
85 changes: 85 additions & 0 deletions lib/rules/no-unnecessary-act.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { TSESTree, ASTUtils } from '@typescript-eslint/experimental-utils';
import {
isBlockStatement,
isCallExpression,
isMemberExpression
} from '../node-utils';
import { createTestingLibraryRule } from '../create-testing-library-rule';

export const RULE_NAME = 'no-unnecessary-act';
export type MessageIds = 'noUnnecessaryAct';
type Options = [];

const ACT_EXPRESSION_QUERY = 'CallExpression[callee.name=/^(act)$/]';
Copy link
Member

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 😅


export default createTestingLibraryRule<Options, MessageIds>({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description:
'Disallow the use of `act` when wrapping Testing Library functions or functions with an empty body',
category: 'Best Practices',
recommended: false
},
messages: {
noUnnecessaryAct:
'Avoid wrapping Testing Library functions in `act` as they are wrapped in act automatically, and avoid wrapping noop functions in `act` as they suppress valid warnings.'
},
fixable: null,
schema: []
},
defaultOptions: [],

create(context, _, helpers) {
function reportIfUnnecessary(
node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression
) {
let isEmpty = false;
let callsTL = false;
const callsOnlyTLFns = (body: Array<TSESTree.Node>): boolean =>
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;
});
Comment on lines +41 to +51
Copy link
Collaborator

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

Suggested change
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)
);

if (
isBlockStatement(node.body) &&
node.body.body.length === 0 &&
isCallExpression(node.parent) &&
ASTUtils.isIdentifier(node.parent.callee)
) {
isEmpty = true;
}
if (
isCallExpression(node.body) &&
isMemberExpression(node.body.callee) &&
helpers.isNodeComingFromTestingLibrary(node.body.callee)
) {
callsTL = true;
}
if (
isEmpty ||
callsTL ||
(isBlockStatement(node.body) && callsOnlyTLFns(node.body.body))
) {
context.report({
node,
loc: node.body.loc.start,
messageId: 'noUnnecessaryAct'
});
}
}

return {
[`${ACT_EXPRESSION_QUERY} > ArrowFunctionExpression`]: reportIfUnnecessary,
[`${ACT_EXPRESSION_QUERY} > FunctionExpression`]: reportIfUnnecessary
};
}
});
Loading